diff --git a/src/hooks/test/use-ordered-rows-test.js b/src/hooks/test/use-ordered-rows-test.js index 3fba61ac..3e28e39f 100644 --- a/src/hooks/test/use-ordered-rows-test.js +++ b/src/hooks/test/use-ordered-rows-test.js @@ -14,9 +14,9 @@ const starWarsCharacters = [ ]; describe('useOrderedRows', () => { - function FakeComponent() { - const [order, setOrder] = useState(); - const orderedRows = useOrderedRows(starWarsCharacters, order); + function FakeComponent({ rows, initialOrder }) { + const [order, setOrder] = useState(initialOrder); + const orderedRows = useOrderedRows(rows, order); return (
@@ -60,8 +60,11 @@ describe('useOrderedRows', () => { ); } - function createComponent() { - return mount(); + function createComponent( + rows = starWarsCharacters, + initialOrder = undefined, + ) { + return mount(); } function assertDefaultOrder(wrapper) { @@ -72,7 +75,7 @@ describe('useOrderedRows', () => { expectedRows.forEach((character, index) => { assert.equal( wrapper.find(`[data-testid="name-${index}"]`).text(), - character.name, + character.name ?? '', ); assert.equal( wrapper.find(`[data-testid="age-${index}"]`).text(), @@ -142,4 +145,88 @@ describe('useOrderedRows', () => { assertDefaultOrder(wrapper); }); }); + + [ + // Null/undefined element is initially last + [...starWarsCharacters, { name: null, age: 20 }], + [...starWarsCharacters, { name: undefined, age: 20 }], + + // Null/undefined element is initially first + [{ name: null, age: 20 }, ...starWarsCharacters], + [{ name: undefined, age: 20 }, ...starWarsCharacters], + + // Null/undefined element is initially somewhere in between + [ + starWarsCharacters[0], + starWarsCharacters[1], + { name: null, age: 20 }, + starWarsCharacters[2], + starWarsCharacters[3], + starWarsCharacters[4], + starWarsCharacters[5], + ], + [ + starWarsCharacters[0], + starWarsCharacters[1], + starWarsCharacters[2], + starWarsCharacters[3], + { name: undefined, age: 20 }, + starWarsCharacters[4], + starWarsCharacters[5], + ], + ].forEach(rows => { + it('orders null values last when initial order is ascending', () => { + const wrapper = createComponent(rows, { + field: 'name', + direction: 'ascending', + }); + + // All items are ordered ascending, with nulls at the bottom + assertOrder(wrapper, [ + { name: 'Baby Yoda', age: 2 }, + { name: 'baby yöda The Second', age: 2 }, + { name: 'Han Solo', age: 25 }, + { name: 'leia Organa', age: 20 }, + { name: 'Luke Skywalker', age: 20 }, + { name: 'young Anakin Skywalker', age: 10 }, + { name: null, age: 20 }, + ]); + }); + + it('orders null values last when initial order is descending', () => { + const wrapper = createComponent(rows, { + field: 'name', + direction: 'descending', + }); + + // All items are ordered descending, with nulls at the bottom + assertOrder(wrapper, [ + { name: 'young Anakin Skywalker', age: 10 }, + { name: 'Luke Skywalker', age: 20 }, + { name: 'leia Organa', age: 20 }, + { name: 'Han Solo', age: 25 }, + { name: 'baby yöda The Second', age: 2 }, + { name: 'Baby Yoda', age: 2 }, + { name: null, age: 20 }, + ]); + }); + + it('orders null values first when nullsLast is false', () => { + const wrapper = createComponent(rows, { + field: 'name', + direction: 'descending', + nullsLast: false, + }); + + assertOrder(wrapper, [ + { name: null, age: 20 }, + { name: 'young Anakin Skywalker', age: 10 }, + { name: 'Luke Skywalker', age: 20 }, + { name: 'leia Organa', age: 20 }, + { name: 'Han Solo', age: 25 }, + { name: 'baby yöda The Second', age: 2 }, + { name: 'Baby Yoda', age: 2 }, + ]); + }); + }); }); diff --git a/src/hooks/use-ordered-rows.ts b/src/hooks/use-ordered-rows.ts index 849fc85a..2f1550c9 100644 --- a/src/hooks/use-ordered-rows.ts +++ b/src/hooks/use-ordered-rows.ts @@ -19,6 +19,9 @@ export function useOrderedRows( return rows; } + // Order nulls last by default + const { nullsLast = true } = order; + return [...rows].sort(({ [order.field]: a }, { [order.field]: b }) => { const [x, y] = order.direction === 'ascending' ? [a, b] : [b, a]; @@ -30,6 +33,15 @@ export function useOrderedRows( return 0; } + // We check a/b instead of x/y because nulls should not be affected by the + // regular order direction. + if (a === null || a === undefined) { + return nullsLast ? 1 : -1; + } + if (b === null || b === undefined) { + return nullsLast ? -1 : 1; + } + return x > y ? 1 : -1; }); }, [order, rows]); diff --git a/src/types.ts b/src/types.ts index abcee104..420468dc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -64,4 +64,11 @@ export type OrderDirection = 'ascending' | 'descending'; export type Order = { field: Field; direction: OrderDirection; + + /** + * Indicates whether entries where the value for `field` is null/undefined + * should go last. Otherwise, they will go first. + * Defaults to true. + */ + nullsLast?: boolean; };