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;
};