Skip to content

Commit

Permalink
Allow useOrderedRows to put all null elements at the bottom
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Jun 19, 2024
1 parent ea267fe commit 73a0529
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 6 deletions.
99 changes: 93 additions & 6 deletions src/hooks/test/use-ordered-rows-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
Expand Down Expand Up @@ -60,8 +60,11 @@ describe('useOrderedRows', () => {
);
}

function createComponent() {
return mount(<FakeComponent />);
function createComponent(
rows = starWarsCharacters,
initialOrder = undefined,
) {
return mount(<FakeComponent rows={rows} initialOrder={initialOrder} />);
}

function assertDefaultOrder(wrapper) {
Expand All @@ -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(),
Expand Down Expand Up @@ -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 },
]);
});
});
});
12 changes: 12 additions & 0 deletions src/hooks/use-ordered-rows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export function useOrderedRows<Row>(
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];

Expand All @@ -30,6 +33,15 @@ export function useOrderedRows<Row>(
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]);
Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,11 @@ export type OrderDirection = 'ascending' | 'descending';
export type Order<Field extends string | number | symbol> = {
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;
};

0 comments on commit 73a0529

Please sign in to comment.