diff --git a/src/components/data/DataTable.tsx b/src/components/data/DataTable.tsx index d09d134ef..a3d6331cd 100644 --- a/src/components/data/DataTable.tsx +++ b/src/components/data/DataTable.tsx @@ -4,7 +4,7 @@ import { useCallback, useContext, useEffect, useMemo } from 'preact/hooks'; import { useArrowKeyNavigation } from '../../hooks/use-arrow-key-navigation'; import { useStableCallback } from '../../hooks/use-stable-callback'; import { useSyncedRef } from '../../hooks/use-synced-ref'; -import type { CompositeProps } from '../../types'; +import type { CompositeProps, Order } from '../../types'; import { downcastRef } from '../../util/typing'; import { ArrowDownIcon, ArrowUpIcon, SpinnerSpokesIcon } from '../icons'; import { Button } from '../input'; @@ -22,11 +22,6 @@ export type TableColumn = { classes?: string; }; -export type Order = { - field: Field; - direction: 'ascending' | 'descending'; -}; - type ComponentProps = { rows: Row[]; columns: TableColumn[]; @@ -100,7 +95,10 @@ function defaultRenderItem(r: Row, field: keyof Row): ComponentChildren { return r[field] as ComponentChildren; } -function calculateNewOrder(newField: T, prevOrder?: Order): Order { +function calculateNewOrder( + newField: T, + prevOrder?: Order, +): Order { if (newField !== prevOrder?.field) { return { field: newField, direction: 'ascending' }; } diff --git a/src/hooks/test/use-ordered-rows-test.js b/src/hooks/test/use-ordered-rows-test.js new file mode 100644 index 000000000..a6db9e515 --- /dev/null +++ b/src/hooks/test/use-ordered-rows-test.js @@ -0,0 +1,129 @@ +import { mount } from 'enzyme'; +import { useState } from 'preact/hooks'; + +import { useOrderedRows } from '../use-ordered-rows'; + +const starWarsCharacters = [ + { name: 'Luke Skywalker', age: 20 }, + { name: 'Princess Leia Organa', age: 20 }, + { name: 'Han Solo', age: 25 }, +]; + +describe('useOrderedRows', () => { + function FakeComponent() { + const [order, setOrder] = useState(); + const orderedRows = useOrderedRows(starWarsCharacters, order); + + return ( +
+ {orderedRows.map((character, index) => ( +
+ {character.name} + {character.age} +
+ ))} + + + + + +
+ ); + } + + function createComponent() { + return mount(); + } + + function assertDefaultOrder(wrapper) { + assertOrder(wrapper, starWarsCharacters); + } + + function assertOrder(wrapper, expectedRows) { + expectedRows.forEach((character, index) => { + assert.equal( + wrapper.find(`[data-testid="name-${index}"]`).text(), + character.name, + ); + assert.equal( + wrapper.find(`[data-testid="age-${index}"]`).text(), + character.age, + ); + }); + } + + [ + { + orderId: 'button-order-by-name-asc', + expectedRows: [ + { name: 'Han Solo', age: 25 }, + { name: 'Luke Skywalker', age: 20 }, + { name: 'Princess Leia Organa', age: 20 }, + ], + }, + { + orderId: 'button-order-by-name-desc', + expectedRows: [ + { name: 'Princess Leia Organa', age: 20 }, + { name: 'Luke Skywalker', age: 20 }, + { name: 'Han Solo', age: 25 }, + ], + }, + { + orderId: 'button-order-by-age-asc', + expectedRows: [ + { name: 'Luke Skywalker', age: 20 }, + { name: 'Princess Leia Organa', age: 20 }, + { name: 'Han Solo', age: 25 }, + ], + }, + { + orderId: 'button-order-by-age-desc', + expectedRows: [ + { name: 'Han Solo', age: 25 }, + { name: 'Luke Skywalker', age: 20 }, + { name: 'Princess Leia Organa', age: 20 }, + ], + }, + ].forEach(({ orderId, expectedRows }) => { + it('orders rows based on field and direction', () => { + const wrapper = createComponent(); + + // Rows are initially not ordered + assertDefaultOrder(wrapper); + + // Click button to order + wrapper.find(`[data-testid="${orderId}"]`).simulate('click'); + assertOrder(wrapper, expectedRows); + + // Order can be reset + wrapper.find('[data-testid="button-reset-order"]').simulate('click'); + assertDefaultOrder(wrapper); + }); + }); +}); diff --git a/src/hooks/use-ordered-rows.ts b/src/hooks/use-ordered-rows.ts new file mode 100644 index 000000000..ba8641b61 --- /dev/null +++ b/src/hooks/use-ordered-rows.ts @@ -0,0 +1,30 @@ +import { useMemo } from 'preact/hooks'; + +import type { Order } from '../types'; + +/** + * Orders a list of rows based on provided order options. + * Provided rows are not mutated, but a copy is returned instead. + */ +export function useOrderedRows( + rows: Row[], + order?: Order, +): Row[] { + return useMemo(() => { + if (!order) { + return rows; + } + + return [...rows].sort((a, b) => { + if (a[order.field] === b[order.field]) { + return 0; + } + + if (order.direction === 'ascending') { + return a[order.field] > b[order.field] ? 1 : -1; + } + + return a[order.field] > b[order.field] ? -1 : 1; + }); + }, [order, rows]); +} diff --git a/src/index.ts b/src/index.ts index 6e0d892dd..b91466e47 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ export { useArrowKeyNavigation } from './hooks/use-arrow-key-navigation'; export { useClickAway } from './hooks/use-click-away'; export { useFocusAway } from './hooks/use-focus-away'; export { useKeyPress } from './hooks/use-key-press'; +export { useOrderedRows } from './hooks/use-ordered-rows'; export { useStableCallback } from './hooks/use-stable-callback'; export { useSyncedRef } from './hooks/use-synced-ref'; export { useToastMessages } from './hooks/use-toast-messages'; @@ -74,6 +75,7 @@ export type { BaseProps, CompositeProps, IconComponent, + Order, PresentationalProps, TransitionComponent, } from './types'; diff --git a/src/pattern-library/components/patterns/data/DataTablePage.tsx b/src/pattern-library/components/patterns/data/DataTablePage.tsx index 25336c816..7777943b5 100644 --- a/src/pattern-library/components/patterns/data/DataTablePage.tsx +++ b/src/pattern-library/components/patterns/data/DataTablePage.tsx @@ -1,7 +1,8 @@ -import { useCallback, useMemo, useRef, useState } from 'preact/hooks'; +import { useCallback, useRef, useState } from 'preact/hooks'; -import { Button, DataTable, type DataTableProps, Scroll } from '../../../../'; -import type { Order } from '../../../../components/data/DataTable'; +import { Button, DataTable, Scroll } from '../../../../'; +import type { DataTableProps, Order } from '../../../../'; +import { useOrderedRows } from '../../../../hooks/use-ordered-rows'; import Library from '../../Library'; import { nabokovNovels } from '../samples'; import type { NabokovNovel } from '../samples'; @@ -15,29 +16,6 @@ const nabokovColumns = [ type SimpleNabokovNovel = Omit; -function useOrderedRows( - rows: SimpleNabokovNovel[], - order?: Order, -) { - return useMemo(() => { - if (!order) { - return rows; - } - - return [...rows].sort((a, b) => { - if (a[order.field] === b[order.field]) { - return 0; - } - - if (order.direction === 'ascending') { - return a[order.field] > b[order.field] ? 1 : -1; - } - - return a[order.field] > b[order.field] ? -1 : 1; - }); - }, [order, rows]); -} - function ClientOrderableDataTable({ rows, // By default, all columns are orderable diff --git a/src/types.ts b/src/types.ts index 5deba5f54..cd1045660 100644 --- a/src/types.ts +++ b/src/types.ts @@ -34,3 +34,8 @@ export type TransitionComponent = FunctionComponent<{ direction?: 'in' | 'out'; onTransitionEnd?: (direction: 'in' | 'out') => void; }>; + +export type Order = { + field: Field; + direction: 'ascending' | 'descending'; +};