From 0093d4a5c90ba1e02b78b40551bd96d4608105ea Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Tue, 25 Nov 2025 09:41:33 -0500 Subject: [PATCH 1/7] Add server-side filtering example to DataList stories --- .../DataList/DataList.stories.tsx | 264 +++++++++++++++++- 1 file changed, 263 insertions(+), 1 deletion(-) diff --git a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx index 27d0fbde385..19b72bdd6d2 100644 --- a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx +++ b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx @@ -1,8 +1,9 @@ // Added because SB and TS don't play nice with each other at the moment // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck -import { DataList, DataTable, FlexBox } from '@codecademy/gamut'; +import { DataList, DataTable, FlexBox, Text } from '@codecademy/gamut'; import type { Meta, StoryObj } from '@storybook/react'; +import { useCallback, useEffect, useState } from 'react'; import { cols, @@ -213,3 +214,264 @@ export const DisableContainerQuery: Story = { args: {}, render: () => , }; + +// Server-side filtering example component +const ServerSideFilteringExample = () => { + // Mock data for our example + const allCrewMembers = [ + { + id: 1, + name: 'Jean Luc Picard', + role: 'Captain', + ship: 'USS Enterprise', + species: 'Human', + status: 'Active', + }, + { + id: 2, + name: 'Wesley Crusher', + role: 'Deus Ex Machina', + ship: 'USS Enterprise', + species: 'Human/Traveler', + status: 'Transcended', + }, + { + id: 3, + name: 'Geordie LaForge', + role: 'Chief Engineer', + ship: 'USS Enterprise', + species: 'Human', + status: 'Active', + }, + { + id: 4, + name: 'Data', + role: 'Lt. Commander', + ship: 'USS Enterprise', + species: 'Android', + status: 'Active', + }, + { + id: 5, + name: 'William Riker', + role: 'First Officer', + ship: 'USS Titan', + species: 'Human', + status: 'Active', + }, + { + id: 6, + name: 'Worf', + role: 'Security Officer', + ship: 'DS9', + species: 'Klingon', + status: 'Active', + }, + ]; + + // State management for server-side filtering + const [rows, setRows] = useState(allCrewMembers); + const [query, setQuery] = useState({ sort: {}, filter: {} }); + const [loading, setLoading] = useState(false); + const [apiCallInfo, setApiCallInfo] = useState('No filters applied yet'); + + // Mock API call function - in real implementation, replace with your actual API + const fetchFilteredData = useCallback(async (filterQuery) => { + setLoading(true); + + // Simulate API call delay + await new Promise(resolve => setTimeout(resolve, 500)); + + // Build query params that would be sent to your API + const queryParams = new URLSearchParams(); + + // Add filters to query params + if (filterQuery.filter) { + Object.entries(filterQuery.filter).forEach(([key, values]) => { + if (values && values.length > 0) { + queryParams.append(`filter[${key}]`, values.join(',')); + } + }); + } + + // Add sorts to query params + if (filterQuery.sort) { + Object.entries(filterQuery.sort).forEach(([key, direction]) => { + if (direction && direction !== 'none') { + queryParams.append(`sort[${key}]`, direction); + } + }); + } + + // Show what would be sent to the API + const queryString = queryParams.toString(); + setApiCallInfo( + queryString + ? `API called with: ${queryString}` + : 'API called with no filters' + ); + + // In a real implementation, you would make an actual API call here: + // const response = await fetch(`/api/crew?${queryString}`); + // const data = await response.json(); + // return data.rows; + + // Mock server-side filtering logic (replace this with actual API response) + let filteredData = [...allCrewMembers]; + + // Apply filters + if (filterQuery.filter) { + Object.entries(filterQuery.filter).forEach(([key, values]) => { + if (values && values.length > 0) { + filteredData = filteredData.filter(row => !values.includes(row[key])); + } + }); + } + + // Apply sorting + if (filterQuery.sort) { + Object.entries(filterQuery.sort).forEach(([key, direction]) => { + if (direction && direction !== 'none') { + filteredData.sort((a, b) => { + const aVal = String(a[key]).toLowerCase(); + const bVal = String(b[key]).toLowerCase(); + const comparison = aVal.localeCompare(bVal); + return direction === 'asc' ? comparison : -comparison; + }); + } + }); + } + + return filteredData; + }, []); + + // Handle query changes (filters and sorts) + const handleQueryChange = useCallback(async (change) => { + let newQuery = { ...query }; + + switch (change.type) { + case 'filter': { + const { dimension, value } = change.payload; + newQuery = { + ...newQuery, + filter: { ...newQuery.filter, [dimension]: value }, + }; + break; + } + case 'sort': { + const { dimension, value } = change.payload; + newQuery = { + ...newQuery, + sort: { [dimension]: value }, + }; + break; + } + case 'reset': { + newQuery = { sort: {}, filter: {} }; + break; + } + } + + setQuery(newQuery); + + // Fetch filtered data from server + const filteredRows = await fetchFilteredData(newQuery); + setRows(filteredRows); + setLoading(false); + }, [query, fetchFilteredData]); + + // Initial load + useEffect(() => { + fetchFilteredData(query).then(data => { + setRows(data); + setLoading(false); + }); + }, []); + + // Column configuration with filterable columns + const columns = [ + { + header: 'Name', + key: 'name', + size: 'lg', + type: 'header', + sortable: true, + }, + { + header: 'Rank', + key: 'role', + size: 'lg', + sortable: true, + }, + { + header: 'Ship', + key: 'ship', + size: 'lg', + sortable: true, + // Available filter options for this column + filters: ['USS Enterprise', 'USS Titan', 'DS9'], + }, + { + header: 'Species', + key: 'species', + size: 'lg', + sortable: true, + // Available filter options for this column + filters: ['Human', 'Android', 'Klingon'], + }, + { + header: 'Status', + key: 'status', + size: 'md', + fill: true, + sortable: true, + filters: ['Active', 'Transcended'], + }, + ]; + + return ( + + Server-Side Filtering Example + + This example demonstrates how to implement server-side filtering. When you apply a filter or sort, + the component calls an API endpoint with the filter parameters instead of filtering locally. + + + {apiCallInfo} + + + + Implementation notes: +
+ • Replace fetchFilteredData with your actual API call +
+ • The onQueryChange callback receives filter/sort changes +
+ • Pass query parameters to your API endpoint +
+ • Update rows with the filtered data from the API response +
• Use the loading prop to show loading state during API calls +
+
+ ); +}; + +export const ServerSideFiltering: Story = { + render: () => , +}; From c08bf8497a5ba107ca42666283ca10c2c4915016 Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Tue, 25 Nov 2025 10:11:52 -0500 Subject: [PATCH 2/7] Add custom expand/collapse example to DataList stories --- .../DataList/DataList.stories.tsx | 178 +++++++++++++++++- 1 file changed, 169 insertions(+), 9 deletions(-) diff --git a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx index 19b72bdd6d2..ec35c430b64 100644 --- a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx +++ b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx @@ -1,9 +1,9 @@ // Added because SB and TS don't play nice with each other at the moment // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck -import { DataList, DataTable, FlexBox, Text } from '@codecademy/gamut'; +import { Anchor, DataList, DataTable, FillButton, FlexBox, Text } from '@codecademy/gamut'; import type { Meta, StoryObj } from '@storybook/react'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { cols, @@ -218,7 +218,7 @@ export const DisableContainerQuery: Story = { // Server-side filtering example component const ServerSideFilteringExample = () => { // Mock data for our example - const allCrewMembers = [ + const allCrewMembers = useMemo(() => [ { id: 1, name: 'Jean Luc Picard', @@ -267,7 +267,7 @@ const ServerSideFilteringExample = () => { species: 'Klingon', status: 'Active', }, - ]; + ], []); // State management for server-side filtering const [rows, setRows] = useState(allCrewMembers); @@ -343,7 +343,7 @@ const ServerSideFilteringExample = () => { } return filteredData; - }, []); + }, [allCrewMembers]); // Handle query changes (filters and sorts) const handleQueryChange = useCallback(async (change) => { @@ -382,10 +382,15 @@ const ServerSideFilteringExample = () => { // Initial load useEffect(() => { - fetchFilteredData(query).then(data => { - setRows(data); - setLoading(false); - }); + fetchFilteredData(query) + .then(data => { + setRows(data); + setLoading(false); + }) + .catch(() => { + setLoading(false); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Column configuration with filterable columns @@ -475,3 +480,158 @@ const ServerSideFilteringExample = () => { export const ServerSideFiltering: Story = { render: () => , }; + +// Custom expand/collapse example +const CustomExpandExample = () => { + const crew = [ + { + id: 1, + name: 'Jean Luc Picard', + role: 'Captain', + ship: 'USS Enterprise', + bio: 'An experienced Starfleet officer known for his diplomatic skills and moral integrity.', + }, + { + id: 2, + name: 'Wesley Crusher', + role: 'Acting Ensign', + ship: 'USS Enterprise', + bio: 'A young prodigy who eventually transcends to a higher plane of existence.', + }, + { + id: 3, + name: 'Geordie LaForge', + role: 'Chief Engineer', + ship: 'USS Enterprise', + bio: 'A brilliant engineer who can see with the help of his VISOR.', + }, + { + id: 4, + name: 'Data', + role: 'Lt. Commander', + ship: 'USS Enterprise', + bio: 'An android exploring what it means to be human.', + }, + ]; + + // Track which rows are expanded + const [expandedRows, setExpandedRows] = useState([]); + + // Handler to toggle expansion from anywhere + const handleToggleExpand = useCallback((rowId) => { + setExpandedRows((prev) => { + if (prev.includes(rowId)) { + // Collapse: remove from array + return prev.filter((id) => id !== rowId); + } + // Expand: add to array + return [...prev, rowId]; + }); + }, []); + + // Standard onRowExpand handler (for the built-in chevron button) + const onRowExpand = useCallback( + ({ payload: { rowId } }) => { + handleToggleExpand(rowId); + }, + [handleToggleExpand] + ); + + // Columns with custom expand trigger in the name cell + const columns = [ + { + header: 'Name', + key: 'name', + size: 'lg', + type: 'header', + render: (row) => ( + { + e.preventDefault(); + handleToggleExpand(row.id); + }} + > + {row.name} + + ), + }, + { + header: 'Rank', + key: 'role', + size: 'lg', + }, + { + header: 'Ship', + key: 'ship', + size: 'lg', + fill: true, + }, + ]; + + // Expanded content + const expandedContent = useCallback( + ({ row, onCollapse }) => ( + + + + Biography + {row.bio} + + + Close + + { + // eslint-disable-next-line no-alert + alert(`More about ${row.name}`); + }} + > + Learn More + + + + + ), + [] + ); + + return ( + + Custom Expand/Collapse Example + + This example shows how to trigger row expansion from a custom element (like an anchor in the name column). + Click on any crew member's name to expand their row, or use the chevron button on the right. + + + + Implementation notes: +
+ • Manage expanded state yourself with useState +
+ • Create a toggle handler that adds/removes row IDs from the expanded array +
+ • Use the handler in custom render functions (like the Name column) +
+ • Also connect it to onRowExpand so the built-in chevron works +
• The expandedContent callback receives an onCollapse function for custom close buttons +
+
+ ); +}; + +export const CustomExpand: Story = { + render: () => , +}; From 9c502bbe60c3bdf7102b9eed36ccdc0f85a083a6 Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Tue, 25 Nov 2025 10:17:10 -0500 Subject: [PATCH 3/7] Update custom expand example to hide chevron button --- .../DataList/DataList.stories.tsx | 584 ++++++++++-------- 1 file changed, 313 insertions(+), 271 deletions(-) diff --git a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx index ec35c430b64..4e398539a2f 100644 --- a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx +++ b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx @@ -1,7 +1,14 @@ // Added because SB and TS don't play nice with each other at the moment // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck -import { Anchor, DataList, DataTable, FillButton, FlexBox, Text } from '@codecademy/gamut'; +import { + Anchor, + DataList, + DataTable, + FillButton, + FlexBox, + Text, +} from '@codecademy/gamut'; import type { Meta, StoryObj } from '@storybook/react'; import { useCallback, useEffect, useMemo, useState } from 'react'; @@ -218,56 +225,59 @@ export const DisableContainerQuery: Story = { // Server-side filtering example component const ServerSideFilteringExample = () => { // Mock data for our example - const allCrewMembers = useMemo(() => [ - { - id: 1, - name: 'Jean Luc Picard', - role: 'Captain', - ship: 'USS Enterprise', - species: 'Human', - status: 'Active', - }, - { - id: 2, - name: 'Wesley Crusher', - role: 'Deus Ex Machina', - ship: 'USS Enterprise', - species: 'Human/Traveler', - status: 'Transcended', - }, - { - id: 3, - name: 'Geordie LaForge', - role: 'Chief Engineer', - ship: 'USS Enterprise', - species: 'Human', - status: 'Active', - }, - { - id: 4, - name: 'Data', - role: 'Lt. Commander', - ship: 'USS Enterprise', - species: 'Android', - status: 'Active', - }, - { - id: 5, - name: 'William Riker', - role: 'First Officer', - ship: 'USS Titan', - species: 'Human', - status: 'Active', - }, - { - id: 6, - name: 'Worf', - role: 'Security Officer', - ship: 'DS9', - species: 'Klingon', - status: 'Active', - }, - ], []); + const allCrewMembers = useMemo( + () => [ + { + id: 1, + name: 'Jean Luc Picard', + role: 'Captain', + ship: 'USS Enterprise', + species: 'Human', + status: 'Active', + }, + { + id: 2, + name: 'Wesley Crusher', + role: 'Deus Ex Machina', + ship: 'USS Enterprise', + species: 'Human/Traveler', + status: 'Transcended', + }, + { + id: 3, + name: 'Geordie LaForge', + role: 'Chief Engineer', + ship: 'USS Enterprise', + species: 'Human', + status: 'Active', + }, + { + id: 4, + name: 'Data', + role: 'Lt. Commander', + ship: 'USS Enterprise', + species: 'Android', + status: 'Active', + }, + { + id: 5, + name: 'William Riker', + role: 'First Officer', + ship: 'USS Titan', + species: 'Human', + status: 'Active', + }, + { + id: 6, + name: 'Worf', + role: 'Security Officer', + ship: 'DS9', + species: 'Klingon', + status: 'Active', + }, + ], + [] + ); // State management for server-side filtering const [rows, setRows] = useState(allCrewMembers); @@ -276,114 +286,122 @@ const ServerSideFilteringExample = () => { const [apiCallInfo, setApiCallInfo] = useState('No filters applied yet'); // Mock API call function - in real implementation, replace with your actual API - const fetchFilteredData = useCallback(async (filterQuery) => { - setLoading(true); - - // Simulate API call delay - await new Promise(resolve => setTimeout(resolve, 500)); - - // Build query params that would be sent to your API - const queryParams = new URLSearchParams(); - - // Add filters to query params - if (filterQuery.filter) { - Object.entries(filterQuery.filter).forEach(([key, values]) => { - if (values && values.length > 0) { - queryParams.append(`filter[${key}]`, values.join(',')); - } - }); - } - - // Add sorts to query params - if (filterQuery.sort) { - Object.entries(filterQuery.sort).forEach(([key, direction]) => { - if (direction && direction !== 'none') { - queryParams.append(`sort[${key}]`, direction); - } - }); - } - - // Show what would be sent to the API - const queryString = queryParams.toString(); - setApiCallInfo( - queryString - ? `API called with: ${queryString}` - : 'API called with no filters' - ); - - // In a real implementation, you would make an actual API call here: - // const response = await fetch(`/api/crew?${queryString}`); - // const data = await response.json(); - // return data.rows; - - // Mock server-side filtering logic (replace this with actual API response) - let filteredData = [...allCrewMembers]; - - // Apply filters - if (filterQuery.filter) { - Object.entries(filterQuery.filter).forEach(([key, values]) => { - if (values && values.length > 0) { - filteredData = filteredData.filter(row => !values.includes(row[key])); - } - }); - } - - // Apply sorting - if (filterQuery.sort) { - Object.entries(filterQuery.sort).forEach(([key, direction]) => { - if (direction && direction !== 'none') { - filteredData.sort((a, b) => { - const aVal = String(a[key]).toLowerCase(); - const bVal = String(b[key]).toLowerCase(); - const comparison = aVal.localeCompare(bVal); - return direction === 'asc' ? comparison : -comparison; - }); - } - }); - } - - return filteredData; - }, [allCrewMembers]); + const fetchFilteredData = useCallback( + async (filterQuery) => { + setLoading(true); - // Handle query changes (filters and sorts) - const handleQueryChange = useCallback(async (change) => { - let newQuery = { ...query }; - - switch (change.type) { - case 'filter': { - const { dimension, value } = change.payload; - newQuery = { - ...newQuery, - filter: { ...newQuery.filter, [dimension]: value }, - }; - break; + // Simulate API call delay + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Build query params that would be sent to your API + const queryParams = new URLSearchParams(); + + // Add filters to query params + if (filterQuery.filter) { + Object.entries(filterQuery.filter).forEach(([key, values]) => { + if (values && values.length > 0) { + queryParams.append(`filter[${key}]`, values.join(',')); + } + }); } - case 'sort': { - const { dimension, value } = change.payload; - newQuery = { - ...newQuery, - sort: { [dimension]: value }, - }; - break; + + // Add sorts to query params + if (filterQuery.sort) { + Object.entries(filterQuery.sort).forEach(([key, direction]) => { + if (direction && direction !== 'none') { + queryParams.append(`sort[${key}]`, direction); + } + }); + } + + // Show what would be sent to the API + const queryString = queryParams.toString(); + setApiCallInfo( + queryString + ? `API called with: ${queryString}` + : 'API called with no filters' + ); + + // In a real implementation, you would make an actual API call here: + // const response = await fetch(`/api/crew?${queryString}`); + // const data = await response.json(); + // return data.rows; + + // Mock server-side filtering logic (replace this with actual API response) + let filteredData = [...allCrewMembers]; + + // Apply filters + if (filterQuery.filter) { + Object.entries(filterQuery.filter).forEach(([key, values]) => { + if (values && values.length > 0) { + filteredData = filteredData.filter( + (row) => !values.includes(row[key]) + ); + } + }); } - case 'reset': { - newQuery = { sort: {}, filter: {} }; - break; + + // Apply sorting + if (filterQuery.sort) { + Object.entries(filterQuery.sort).forEach(([key, direction]) => { + if (direction && direction !== 'none') { + filteredData.sort((a, b) => { + const aVal = String(a[key]).toLowerCase(); + const bVal = String(b[key]).toLowerCase(); + const comparison = aVal.localeCompare(bVal); + return direction === 'asc' ? comparison : -comparison; + }); + } + }); } - } - - setQuery(newQuery); - - // Fetch filtered data from server - const filteredRows = await fetchFilteredData(newQuery); - setRows(filteredRows); - setLoading(false); - }, [query, fetchFilteredData]); + + return filteredData; + }, + [allCrewMembers] + ); + + // Handle query changes (filters and sorts) + const handleQueryChange = useCallback( + async (change) => { + let newQuery = { ...query }; + + switch (change.type) { + case 'filter': { + const { dimension, value } = change.payload; + newQuery = { + ...newQuery, + filter: { ...newQuery.filter, [dimension]: value }, + }; + break; + } + case 'sort': { + const { dimension, value } = change.payload; + newQuery = { + ...newQuery, + sort: { [dimension]: value }, + }; + break; + } + case 'reset': { + newQuery = { sort: {}, filter: {} }; + break; + } + } + + setQuery(newQuery); + + // Fetch filtered data from server + const filteredRows = await fetchFilteredData(newQuery); + setRows(filteredRows); + setLoading(false); + }, + [query, fetchFilteredData] + ); // Initial load useEffect(() => { fetchFilteredData(query) - .then(data => { + .then((data) => { setRows(data); setLoading(false); }) @@ -438,8 +456,9 @@ const ServerSideFilteringExample = () => { Server-Side Filtering Example - This example demonstrates how to implement server-side filtering. When you apply a filter or sort, - the component calls an API endpoint with the filter parameters instead of filtering locally. + This example demonstrates how to implement server-side filtering. When + you apply a filter or sort, the component calls an API endpoint with the + filter parameters instead of filtering locally. { /> Implementation notes: -
- • Replace fetchFilteredData with your actual API call -
- • The onQueryChange callback receives filter/sort changes +
• Replace fetchFilteredData with your actual API call +
• The onQueryChange callback receives filter/sort + changes
• Pass query parameters to your API endpoint
• Update rows with the filtered data from the API response -
• Use the loading prop to show loading state during API calls +
• Use the loading prop to show loading state during + API calls
); @@ -481,152 +500,175 @@ export const ServerSideFiltering: Story = { render: () => , }; -// Custom expand/collapse example +// Custom expand/collapse example without chevron const CustomExpandExample = () => { - const crew = [ - { - id: 1, - name: 'Jean Luc Picard', - role: 'Captain', - ship: 'USS Enterprise', - bio: 'An experienced Starfleet officer known for his diplomatic skills and moral integrity.', - }, - { - id: 2, - name: 'Wesley Crusher', - role: 'Acting Ensign', - ship: 'USS Enterprise', - bio: 'A young prodigy who eventually transcends to a higher plane of existence.', - }, - { - id: 3, - name: 'Geordie LaForge', - role: 'Chief Engineer', - ship: 'USS Enterprise', - bio: 'A brilliant engineer who can see with the help of his VISOR.', - }, - { - id: 4, - name: 'Data', - role: 'Lt. Commander', - ship: 'USS Enterprise', - bio: 'An android exploring what it means to be human.', - }, - ]; + // Expanded data with crew members + const crewData = useMemo( + () => [ + { + id: 1, + name: 'Jean Luc Picard', + role: 'Captain', + ship: 'USS Enterprise', + bio: 'An experienced Starfleet officer known for his diplomatic skills and moral integrity.', + }, + { + id: 2, + name: 'Wesley Crusher', + role: 'Acting Ensign', + ship: 'USS Enterprise', + bio: 'A young prodigy who eventually transcends to a higher plane of existence.', + }, + { + id: 3, + name: 'Geordie LaForge', + role: 'Chief Engineer', + ship: 'USS Enterprise', + bio: 'A brilliant engineer who can see with the help of his VISOR.', + }, + { + id: 4, + name: 'Data', + role: 'Lt. Commander', + ship: 'USS Enterprise', + bio: 'An android exploring what it means to be human.', + }, + ], + [] + ); // Track which rows are expanded - const [expandedRows, setExpandedRows] = useState([]); + const [expandedIds, setExpandedIds] = useState([]); - // Handler to toggle expansion from anywhere + // Handler to toggle expansion const handleToggleExpand = useCallback((rowId) => { - setExpandedRows((prev) => { + setExpandedIds((prev) => { if (prev.includes(rowId)) { - // Collapse: remove from array return prev.filter((id) => id !== rowId); } - // Expand: add to array return [...prev, rowId]; }); }, []); - // Standard onRowExpand handler (for the built-in chevron button) - const onRowExpand = useCallback( - ({ payload: { rowId } }) => { - handleToggleExpand(rowId); - }, - [handleToggleExpand] - ); + // Generate rows that include expanded content inline + const rows = useMemo(() => { + const result = []; + crewData.forEach((crew) => { + // Add the main row + result.push({ + ...crew, + rowType: 'main', + mainId: crew.id, + }); - // Columns with custom expand trigger in the name cell - const columns = [ - { - header: 'Name', - key: 'name', - size: 'lg', - type: 'header', - render: (row) => ( - { - e.preventDefault(); - handleToggleExpand(row.id); - }} - > - {row.name} - - ), - }, - { - header: 'Rank', - key: 'role', - size: 'lg', - }, - { - header: 'Ship', - key: 'ship', - size: 'lg', - fill: true, - }, - ]; + // If expanded, add an expanded content row + if (expandedIds.includes(crew.id)) { + result.push({ + id: `${crew.id}-expanded`, + name: crew.name, + bio: crew.bio, + role: '', + ship: '', + rowType: 'expanded', + mainId: crew.id, + }); + } + }); + return result; + }, [crewData, expandedIds]); - // Expanded content - const expandedContent = useCallback( - ({ row, onCollapse }) => ( - - - - Biography - {row.bio} - - - Close - - { - // eslint-disable-next-line no-alert - alert(`More about ${row.name}`); + // Columns with custom expand trigger and expanded content rendering + const columns = useMemo( + () => [ + { + header: 'Name', + key: 'name', + size: 'lg', + type: 'header', + render: (row) => { + if (row.rowType === 'expanded') { + return ( + + Biography + {row.bio} + + handleToggleExpand(row.mainId)} + > + Close + + { + // eslint-disable-next-line no-alert + alert(`More about ${row.name}`); + }} + > + Learn More + + + + ); + } + return ( + { + e.preventDefault(); + handleToggleExpand(row.id); }} > - Learn More - - - - - ), - [] + {row.name} + + ); + }, + }, + { + header: 'Rank', + key: 'role', + size: 'lg', + }, + { + header: 'Ship', + key: 'ship', + size: 'lg', + fill: true, + }, + ], + [handleToggleExpand] ); return ( - Custom Expand/Collapse Example + Custom Expand/Collapse (No Chevron) - This example shows how to trigger row expansion from a custom element (like an anchor in the name column). - Click on any crew member's name to expand their row, or use the chevron button on the right. + This example shows expand/collapse without the built-in chevron button. + Click on any crew member's name to expand their bio inline. Implementation notes: +
• Don't use expandedContent or{' '} + onRowExpand props
- • Manage expanded state yourself with useState -
- • Create a toggle handler that adds/removes row IDs from the expanded array + • Generate rows dynamically - insert "expanded" rows after + expanded items
- • Use the handler in custom render functions (like the Name column) + • Use a rowType field to distinguish main vs expanded rows
- • Also connect it to onRowExpand so the built-in chevron works -
• The expandedContent callback receives an onCollapse function for custom close buttons + • In column render functions, check{' '} + row.rowType to render differently +
• For expanded rows, span content across the first column and + leave others empty
); From 88b39f64b13020b5a027ff7aa2ee7a96c1099fcb Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Tue, 25 Nov 2025 10:20:21 -0500 Subject: [PATCH 4/7] Add nested table in expanded content example --- .../DataList/DataList.stories.tsx | 267 +++++++++++++++++- 1 file changed, 263 insertions(+), 4 deletions(-) diff --git a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx index 4e398539a2f..d6ae139593d 100644 --- a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx +++ b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx @@ -662,10 +662,9 @@ const CustomExpandExample = () => {
• Generate rows dynamically - insert "expanded" rows after expanded items -
- • Use a rowType field to distinguish main vs expanded rows -
- • In column render functions, check{' '} +
• Use a rowType field to distinguish main vs expanded + rows +
• In column render functions, check{' '} row.rowType to render differently
• For expanded rows, span content across the first column and leave others empty @@ -677,3 +676,263 @@ const CustomExpandExample = () => { export const CustomExpand: Story = { render: () => , }; + +// Nested table in expanded content example +const NestedTableExample = () => { + const crew = useMemo( + () => [ + { + id: 1, + name: 'Jean Luc Picard', + role: 'Captain', + ship: 'USS Enterprise', + }, + { + id: 2, + name: 'Wesley Crusher', + role: 'Acting Ensign', + ship: 'USS Enterprise', + }, + { + id: 3, + name: 'Geordie LaForge', + role: 'Chief Engineer', + ship: 'USS Enterprise', + }, + { + id: 4, + name: 'Data', + role: 'Lt. Commander', + ship: 'USS Enterprise', + }, + ], + [] + ); + + // Mock mission data for each crew member + const missionData = useMemo( + () => ({ + 1: [ + { + id: 'm1', + mission: 'First Contact with the Borg', + stardate: '42761.3', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm2', + mission: 'Diplomatic Mission to Romulus', + stardate: '43152.4', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm3', + mission: 'Rescue Operation at Wolf 359', + stardate: '44001.4', + status: 'Completed', + outcome: 'Partial Success', + }, + ], + 2: [ + { + id: 'm4', + mission: 'Training Exercise Alpha', + stardate: '42523.7', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm5', + mission: 'Assist in Engine Repairs', + stardate: '42901.3', + status: 'Completed', + outcome: 'Success', + }, + ], + 3: [ + { + id: 'm6', + mission: 'Engine Overhaul Project', + stardate: '42686.4', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm7', + mission: 'Holodeck Maintenance', + stardate: '43125.8', + status: 'In Progress', + outcome: 'Pending', + }, + { + id: 'm8', + mission: 'Warp Core Analysis', + stardate: '43349.2', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm9', + mission: 'Sensor Array Upgrade', + stardate: '43489.2', + status: 'Completed', + outcome: 'Success', + }, + ], + 4: [ + { + id: 'm10', + mission: 'Science Survey Mission', + stardate: '42761.9', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm11', + mission: 'Away Team Investigation', + stardate: '43125.8', + status: 'Completed', + outcome: 'Success', + }, + ], + }), + [] + ); + + const [expandedRows, setExpandedRows] = useState([]); + + const onRowExpand = useCallback( + ({ payload: { toggle, rowId } }) => { + setExpandedRows((prev) => { + if (toggle) { + return prev.filter((id) => id !== rowId); + } + return [...prev, rowId]; + }); + }, + [] + ); + + const columns = useMemo( + () => [ + { + header: 'Name', + key: 'name', + size: 'lg', + type: 'header', + }, + { + header: 'Rank', + key: 'role', + size: 'lg', + }, + { + header: 'Ship', + key: 'ship', + size: 'lg', + fill: true, + }, + ], + [] + ); + + // Mission table columns + const missionColumns = useMemo( + () => [ + { + header: 'Mission', + key: 'mission', + size: 'xl', + type: 'header', + }, + { + header: 'Stardate', + key: 'stardate', + size: 'md', + }, + { + header: 'Status', + key: 'status', + size: 'sm', + }, + { + header: 'Outcome', + key: 'outcome', + size: 'md', + fill: true, + }, + ], + [] + ); + + const expandedContent = useCallback( + ({ row }) => ( + + + Mission History for {row.name} + + + + ), + [missionColumns, missionData] + ); + + return ( + + Nested Table in Expanded Content + + This example shows how to display a DataTable inside the expanded + content. Click the chevron to expand a crew member and see their mission + history. + + + + Implementation notes: +
+ • Use DataTable component inside{' '} + expandedContent +
+ • The nested table receives its own columns,{' '} + rows, and id +
+ • Add padding/margins for visual hierarchy (e.g., pl=64 to + align with parent) +
+ • Use spacing="condensed" for nested tables to + save space +
• Consider adding a background color to distinguish nested content +
+
+ ); +}; + +export const NestedTable: Story = { + render: () => , +}; From 640139f69995000bd42eb80bca0ad761870eb847 Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Tue, 25 Nov 2025 10:22:01 -0500 Subject: [PATCH 5/7] Add List as table in expanded content example --- .../DataList/DataList.stories.tsx | 289 ++++++++++++++++-- 1 file changed, 268 insertions(+), 21 deletions(-) diff --git a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx index d6ae139593d..9f466d4c29c 100644 --- a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx +++ b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx @@ -7,6 +7,11 @@ import { DataTable, FillButton, FlexBox, + List, + ListCol, + ListHeaderCol, + ListHeaderRow, + ListRow, Text, } from '@codecademy/gamut'; import type { Meta, StoryObj } from '@storybook/react'; @@ -803,17 +808,14 @@ const NestedTableExample = () => { const [expandedRows, setExpandedRows] = useState([]); - const onRowExpand = useCallback( - ({ payload: { toggle, rowId } }) => { - setExpandedRows((prev) => { - if (toggle) { - return prev.filter((id) => id !== rowId); - } - return [...prev, rowId]; - }); - }, - [] - ); + const onRowExpand = useCallback(({ payload: { toggle, rowId } }) => { + setExpandedRows((prev) => { + if (toggle) { + return prev.filter((id) => id !== rowId); + } + return [...prev, rowId]; + }); + }, []); const columns = useMemo( () => [ @@ -915,18 +917,14 @@ const NestedTableExample = () => { /> Implementation notes: -
- • Use DataTable component inside{' '} +
• Use DataTable component inside{' '} expandedContent -
- • The nested table receives its own columns,{' '} +
• The nested table receives its own columns,{' '} rows, and id -
- • Add padding/margins for visual hierarchy (e.g., pl=64 to - align with parent) -
- • Use spacing="condensed" for nested tables to - save space +
• Add padding/margins for visual hierarchy (e.g.,{' '} + pl=64 to align with parent) +
• Use spacing="condensed" for nested tables + to save space
• Consider adding a background color to distinguish nested content
@@ -936,3 +934,252 @@ const NestedTableExample = () => { export const NestedTable: Story = { render: () => , }; + +// List component as table in expanded content example +const ListAsTableExample = () => { + const crew = useMemo( + () => [ + { + id: 1, + name: 'Jean Luc Picard', + role: 'Captain', + ship: 'USS Enterprise', + }, + { + id: 2, + name: 'Wesley Crusher', + role: 'Acting Ensign', + ship: 'USS Enterprise', + }, + { + id: 3, + name: 'Geordie LaForge', + role: 'Chief Engineer', + ship: 'USS Enterprise', + }, + { + id: 4, + name: 'Data', + role: 'Lt. Commander', + ship: 'USS Enterprise', + }, + ], + [] + ); + + // Mock mission data for each crew member + const missionData = useMemo( + () => ({ + 1: [ + { + id: 'm1', + mission: 'First Contact with the Borg', + stardate: '42761.3', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm2', + mission: 'Diplomatic Mission to Romulus', + stardate: '43152.4', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm3', + mission: 'Rescue Operation at Wolf 359', + stardate: '44001.4', + status: 'Completed', + outcome: 'Partial Success', + }, + ], + 2: [ + { + id: 'm4', + mission: 'Training Exercise Alpha', + stardate: '42523.7', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm5', + mission: 'Assist in Engine Repairs', + stardate: '42901.3', + status: 'Completed', + outcome: 'Success', + }, + ], + 3: [ + { + id: 'm6', + mission: 'Engine Overhaul Project', + stardate: '42686.4', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm7', + mission: 'Holodeck Maintenance', + stardate: '43125.8', + status: 'In Progress', + outcome: 'Pending', + }, + { + id: 'm8', + mission: 'Warp Core Analysis', + stardate: '43349.2', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm9', + mission: 'Sensor Array Upgrade', + stardate: '43489.2', + status: 'Completed', + outcome: 'Success', + }, + ], + 4: [ + { + id: 'm10', + mission: 'Science Survey Mission', + stardate: '42761.9', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm11', + mission: 'Away Team Investigation', + stardate: '43125.8', + status: 'Completed', + outcome: 'Success', + }, + ], + }), + [] + ); + + const [expandedRows, setExpandedRows] = useState([]); + + const onRowExpand = useCallback(({ payload: { toggle, rowId } }) => { + setExpandedRows((prev) => { + if (toggle) { + return prev.filter((id) => id !== rowId); + } + return [...prev, rowId]; + }); + }, []); + + const columns = useMemo( + () => [ + { + header: 'Name', + key: 'name', + size: 'lg', + type: 'header', + }, + { + header: 'Rank', + key: 'role', + size: 'lg', + }, + { + header: 'Ship', + key: 'ship', + size: 'lg', + fill: true, + }, + ], + [] + ); + + const expandedContent = useCallback( + ({ row }) => { + const missions = missionData[row.id] || []; + + return ( + + + Mission History for {row.name} + + + + + Mission + + Stardate + Status + + Outcome + + + {missions.map((mission) => ( + + + + {mission.mission} + + + + {mission.stardate} + + + {mission.status} + + + {mission.outcome} + + + ))} + + + ); + }, + [missionData] + ); + + return ( + + List Component as Table in Expanded Content + + This example shows how to use the List component with{' '} + as="table" inside expanded content. Click the + chevron to expand a crew member and see their mission history. + + + + Implementation notes: +
• Use List as="table" variant="table"{' '} + as the container +
• Add ListHeaderRow with ListHeaderCol{' '} + components for headers +
• Use ListRow and ListCol for data rows +
• Set type="header" on the first column for + row headers +
• List component gives you more flexibility than DataTable for custom + layouts +
+
+ ); +}; + +export const ListAsTable: Story = { + render: () => , +}; From 29c44c66ec3786457ae951a216988b71853dc7be Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Tue, 25 Nov 2025 10:26:17 -0500 Subject: [PATCH 6/7] Fix List as table example - use correct TableHeader component --- .../DataList/DataList.stories.tsx | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx index 9f466d4c29c..0e2201ea3b9 100644 --- a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx +++ b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx @@ -9,9 +9,8 @@ import { FlexBox, List, ListCol, - ListHeaderCol, - ListHeaderRow, ListRow, + TableHeader, Text, } from '@codecademy/gamut'; import type { Meta, StoryObj } from '@storybook/react'; @@ -1109,17 +1108,27 @@ const ListAsTableExample = () => { Mission History for {row.name} - - - - Mission - - Stardate - Status - - Outcome - - + + + Mission + + + Stardate + + + Status + + + Outcome + + + } + id={`missions-list-${row.id}`} + variant="table" + > {missions.map((mission) => ( @@ -1147,7 +1156,9 @@ const ListAsTableExample = () => { return ( - List Component as Table in Expanded Content + + List Component as Table in Expanded Content + This example shows how to use the List component with{' '} as="table" inside expanded content. Click the @@ -1166,15 +1177,17 @@ const ListAsTableExample = () => { /> Implementation notes: -
• Use List as="table" variant="table"{' '} - as the container -
• Add ListHeaderRow with ListHeaderCol{' '} - components for headers +
• Use{' '} + List as="table" variant="table" as the + container +
• Pass a header prop with TableHeader{' '} + containing ListCol components with{' '} + columnHeader prop
• Use ListRow and ListCol for data rows
• Set type="header" on the first column for row headers -
• List component gives you more flexibility than DataTable for custom - layouts +
• List component gives you more flexibility than DataTable for + custom layouts
); From 841ca5c4c96ec4a784e9000e99ec009794a27008 Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Tue, 25 Nov 2025 10:33:47 -0500 Subject: [PATCH 7/7] Remove NestedTable example, keep only ServerSideFiltering, CustomExpand, and ListAsTable stories --- .../DataList/DataList.stories.tsx | 270 +----------------- 1 file changed, 1 insertion(+), 269 deletions(-) diff --git a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx index 0e2201ea3b9..8421d5e1532 100644 --- a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx +++ b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx @@ -4,7 +4,6 @@ import { Anchor, DataList, - DataTable, FillButton, FlexBox, List, @@ -116,21 +115,7 @@ const meta: Meta = { onRowExpand: () => {}, expandedContent: ({ row }) => ( - + Expanded content for {row.name} ), }, @@ -681,259 +666,6 @@ export const CustomExpand: Story = { render: () => , }; -// Nested table in expanded content example -const NestedTableExample = () => { - const crew = useMemo( - () => [ - { - id: 1, - name: 'Jean Luc Picard', - role: 'Captain', - ship: 'USS Enterprise', - }, - { - id: 2, - name: 'Wesley Crusher', - role: 'Acting Ensign', - ship: 'USS Enterprise', - }, - { - id: 3, - name: 'Geordie LaForge', - role: 'Chief Engineer', - ship: 'USS Enterprise', - }, - { - id: 4, - name: 'Data', - role: 'Lt. Commander', - ship: 'USS Enterprise', - }, - ], - [] - ); - - // Mock mission data for each crew member - const missionData = useMemo( - () => ({ - 1: [ - { - id: 'm1', - mission: 'First Contact with the Borg', - stardate: '42761.3', - status: 'Completed', - outcome: 'Success', - }, - { - id: 'm2', - mission: 'Diplomatic Mission to Romulus', - stardate: '43152.4', - status: 'Completed', - outcome: 'Success', - }, - { - id: 'm3', - mission: 'Rescue Operation at Wolf 359', - stardate: '44001.4', - status: 'Completed', - outcome: 'Partial Success', - }, - ], - 2: [ - { - id: 'm4', - mission: 'Training Exercise Alpha', - stardate: '42523.7', - status: 'Completed', - outcome: 'Success', - }, - { - id: 'm5', - mission: 'Assist in Engine Repairs', - stardate: '42901.3', - status: 'Completed', - outcome: 'Success', - }, - ], - 3: [ - { - id: 'm6', - mission: 'Engine Overhaul Project', - stardate: '42686.4', - status: 'Completed', - outcome: 'Success', - }, - { - id: 'm7', - mission: 'Holodeck Maintenance', - stardate: '43125.8', - status: 'In Progress', - outcome: 'Pending', - }, - { - id: 'm8', - mission: 'Warp Core Analysis', - stardate: '43349.2', - status: 'Completed', - outcome: 'Success', - }, - { - id: 'm9', - mission: 'Sensor Array Upgrade', - stardate: '43489.2', - status: 'Completed', - outcome: 'Success', - }, - ], - 4: [ - { - id: 'm10', - mission: 'Science Survey Mission', - stardate: '42761.9', - status: 'Completed', - outcome: 'Success', - }, - { - id: 'm11', - mission: 'Away Team Investigation', - stardate: '43125.8', - status: 'Completed', - outcome: 'Success', - }, - ], - }), - [] - ); - - const [expandedRows, setExpandedRows] = useState([]); - - const onRowExpand = useCallback(({ payload: { toggle, rowId } }) => { - setExpandedRows((prev) => { - if (toggle) { - return prev.filter((id) => id !== rowId); - } - return [...prev, rowId]; - }); - }, []); - - const columns = useMemo( - () => [ - { - header: 'Name', - key: 'name', - size: 'lg', - type: 'header', - }, - { - header: 'Rank', - key: 'role', - size: 'lg', - }, - { - header: 'Ship', - key: 'ship', - size: 'lg', - fill: true, - }, - ], - [] - ); - - // Mission table columns - const missionColumns = useMemo( - () => [ - { - header: 'Mission', - key: 'mission', - size: 'xl', - type: 'header', - }, - { - header: 'Stardate', - key: 'stardate', - size: 'md', - }, - { - header: 'Status', - key: 'status', - size: 'sm', - }, - { - header: 'Outcome', - key: 'outcome', - size: 'md', - fill: true, - }, - ], - [] - ); - - const expandedContent = useCallback( - ({ row }) => ( - - - Mission History for {row.name} - - - - ), - [missionColumns, missionData] - ); - - return ( - - Nested Table in Expanded Content - - This example shows how to display a DataTable inside the expanded - content. Click the chevron to expand a crew member and see their mission - history. - - - - Implementation notes: -
• Use DataTable component inside{' '} - expandedContent -
• The nested table receives its own columns,{' '} - rows, and id -
• Add padding/margins for visual hierarchy (e.g.,{' '} - pl=64 to align with parent) -
• Use spacing="condensed" for nested tables - to save space -
• Consider adding a background color to distinguish nested content -
-
- ); -}; - -export const NestedTable: Story = { - render: () => , -}; - // List component as table in expanded content example const ListAsTableExample = () => { const crew = useMemo(