Skip to content

Commit abd4469

Browse files
committed
Allow to provide initial order per DataTable column
1 parent 2a2c6ab commit abd4469

File tree

4 files changed

+110
-40
lines changed

4 files changed

+110
-40
lines changed

src/components/data/DataTable.tsx

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useCallback, useContext, useEffect, useMemo } from 'preact/hooks';
44
import { useArrowKeyNavigation } from '../../hooks/use-arrow-key-navigation';
55
import { useStableCallback } from '../../hooks/use-stable-callback';
66
import { useSyncedRef } from '../../hooks/use-synced-ref';
7-
import type { CompositeProps, Order } from '../../types';
7+
import type { CompositeProps, Order, OrderDirection } from '../../types';
88
import { downcastRef } from '../../util/typing';
99
import { ArrowDownIcon, ArrowUpIcon, SpinnerSpokesIcon } from '../icons';
1010
import { Button } from '../input';
@@ -80,8 +80,13 @@ type ComponentProps<Row> = Pick<
8080
* Columns that can be used to order the table. Ignored if `onOrderChange` is
8181
* not provided.
8282
* No columns will be orderable if this is not provided.
83+
*
84+
* This can be a map of columns and order directions, to indicate the initial
85+
* direction to use when a column becomes the ordered one
8386
*/
84-
orderableColumns?: Array<keyof Row>;
87+
orderableColumns?:
88+
| Array<keyof Row>
89+
| Partial<Record<keyof Row, OrderDirection>>;
8590

8691
/** Callback to render an individual table cell */
8792
renderItem?: (r: Row, field: keyof Row) => ComponentChildren;
@@ -98,9 +103,13 @@ function defaultRenderItem<Row>(r: Row, field: keyof Row): ComponentChildren {
98103
function calculateNewOrder<T extends string | number | symbol>(
99104
newField: T,
100105
prevOrder?: Order<T>,
106+
initialOrderForColumn?: Partial<Record<T, OrderDirection>>,
101107
): Order<T> {
102108
if (newField !== prevOrder?.field) {
103-
return { field: newField, direction: 'ascending' };
109+
return {
110+
field: newField,
111+
direction: initialOrderForColumn?.[newField] ?? 'ascending',
112+
};
104113
}
105114

106115
const newDirection =
@@ -162,12 +171,23 @@ export default function DataTable<Row>({
162171
}: DataTableProps<Row>) {
163172
const tableRef = useSyncedRef(elementRef);
164173
const scrollContext = useContext(ScrollContext);
174+
const [orderableColumnsList, initialOrderForColumn] = useMemo(
175+
() =>
176+
Array.isArray(orderableColumns)
177+
? [orderableColumns, {}]
178+
: [Object.keys(orderableColumns) as Array<keyof Row>, orderableColumns],
179+
[orderableColumns],
180+
);
165181
const updateOrder = useCallback(
166182
(newField: keyof Row) => {
167-
const newOrder = calculateNewOrder(newField, order);
183+
const newOrder = calculateNewOrder(
184+
newField,
185+
order,
186+
initialOrderForColumn,
187+
);
168188
onOrderChange?.(newOrder);
169189
},
170-
[onOrderChange, order],
190+
[initialOrderForColumn, onOrderChange, order],
171191
);
172192

173193
const noContent = loading || (!rows.length && emptyMessage);
@@ -321,7 +341,7 @@ export default function DataTable<Row>({
321341
<TableRow>
322342
{columns.map(column => {
323343
const isOrderable =
324-
!!onOrderChange && orderableColumns.includes(column.field);
344+
!!onOrderChange && orderableColumnsList.includes(column.field);
325345
const isActiveOrder = order?.field === column.field;
326346

327347
return (

src/components/data/test/DataTable-test.js

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ describe('DataTable', () => {
7474
);
7575
}
7676

77-
const createComponent = (Component, props = {}) => {
77+
const createComponent = ({ Component = DataTable, ...props } = {}) => {
7878
const wrapper = mount(
7979
<Component columns={fakeColumns} rows={fakeRows} {...props} />,
8080

@@ -87,8 +87,8 @@ describe('DataTable', () => {
8787
};
8888

8989
it('sets appropriate table attributes', () => {
90-
const wrapper = createComponent(DataTable);
91-
const interactiveWrapper = createComponent(DataTable, {
90+
const wrapper = createComponent();
91+
const interactiveWrapper = createComponent({
9292
onSelectRow: sinon.stub(),
9393
});
9494
const outer = wrapper.find('Table');
@@ -105,7 +105,7 @@ describe('DataTable', () => {
105105

106106
describe('table columns', () => {
107107
it('renders a column header for each column', () => {
108-
const wrapper = createComponent(DataTable);
108+
const wrapper = createComponent();
109109
const tableHead = wrapper.find('TableHead');
110110

111111
assert.equal(tableHead.find('th').length, 3);
@@ -115,7 +115,7 @@ describe('DataTable', () => {
115115
});
116116

117117
it('applies extra column classes', () => {
118-
const wrapper = createComponent(DataTable);
118+
const wrapper = createComponent();
119119
const tableHead = wrapper.find('TableHead');
120120

121121
assert.isTrue(tableHead.find('th').at(0).hasClass('w-[50%]'));
@@ -124,15 +124,15 @@ describe('DataTable', () => {
124124

125125
describe('table rows', () => {
126126
it('renders one table row per row provided', () => {
127-
const wrapper = createComponent(DataTable);
127+
const wrapper = createComponent();
128128
assert.equal(
129129
wrapper.find('TableBody').find('TableRow').length,
130130
fakeRows.length,
131131
);
132132
});
133133

134134
it('renders fields that are defined by columns', () => {
135-
const wrapper = createComponent(DataTable);
135+
const wrapper = createComponent();
136136
const firstRow = wrapper.find('TableBody').find('TableRow').first();
137137

138138
assert.equal(firstRow.find('td').at(0).text(), 'Chocolate Cake');
@@ -157,7 +157,7 @@ describe('DataTable', () => {
157157

158158
const rows = [fakeRows[3]];
159159

160-
const wrapper = createComponent(DataTable, {
160+
const wrapper = createComponent({
161161
renderItem: formatCell,
162162
rows,
163163
columns,
@@ -177,7 +177,7 @@ describe('DataTable', () => {
177177
describe('interacting with row data', () => {
178178
it('invokes `onSelectRow` callback when row is clicked', () => {
179179
const onSelectRow = sinon.stub();
180-
const wrapper = createComponent(DataTable, {
180+
const wrapper = createComponent({
181181
onSelectRow,
182182
});
183183

@@ -188,7 +188,8 @@ describe('DataTable', () => {
188188

189189
it('invokes `onSelectRows` callback when rows are clicked', () => {
190190
const onSelectRows = sinon.stub();
191-
const wrapper = createComponent(MultiSelectDataTable, {
191+
const wrapper = createComponent({
192+
Component: MultiSelectDataTable,
192193
onSelectRows,
193194
});
194195

@@ -212,7 +213,7 @@ describe('DataTable', () => {
212213

213214
it('invokes `onSelectRow` when row is selected with arrow keys', () => {
214215
const onSelectRow = sinon.stub();
215-
const wrapper = createComponent(DataTable, {
216+
const wrapper = createComponent({
216217
onSelectRow,
217218
});
218219

@@ -229,7 +230,8 @@ describe('DataTable', () => {
229230

230231
it('invokes `onSelectRows` callback when rows are selected with arrow keys', () => {
231232
const onSelectRows = sinon.stub();
232-
const wrapper = createComponent(MultiSelectDataTable, {
233+
const wrapper = createComponent({
234+
Component: MultiSelectDataTable,
233235
onSelectRows,
234236
});
235237

@@ -252,7 +254,7 @@ describe('DataTable', () => {
252254

253255
it('invokes `onConfirmRow` callback when row is double-clicked', () => {
254256
const onConfirmRow = sinon.stub();
255-
const wrapper = createComponent(DataTable, {
257+
const wrapper = createComponent({
256258
onConfirmRow,
257259
});
258260

@@ -263,7 +265,7 @@ describe('DataTable', () => {
263265

264266
it('invokes `onConfirmRow` callback when `Enter` is pressed on a row', () => {
265267
const onConfirmRow = sinon.stub();
266-
const wrapper = createComponent(DataTable, {
268+
const wrapper = createComponent({
267269
onConfirmRow,
268270
});
269271

@@ -275,46 +277,46 @@ describe('DataTable', () => {
275277

276278
context('when loading', () => {
277279
it('renders a loading spinner', () => {
278-
const wrapper = createComponent(DataTable, { loading: true });
280+
const wrapper = createComponent({ loading: true });
279281
assert.isTrue(wrapper.find('SpinnerSpokesIcon').exists());
280282
});
281283

282284
it('does not render any data', () => {
283-
const wrapper = createComponent(DataTable, { loading: true });
285+
const wrapper = createComponent({ loading: true });
284286
// One row, which holds the spinner
285287
assert.equal(wrapper.find('tbody tr').length, 1);
286288
});
287289

288290
it('still renders headings', () => {
289-
const wrapper = createComponent(DataTable, { loading: true });
291+
const wrapper = createComponent({ loading: true });
290292
assert.equal(wrapper.find('thead tr th').length, 3);
291293
});
292294

293295
it('does not render a TableFoot', () => {
294-
const wrapper = createComponent(DataTable, { loading: true });
296+
const wrapper = createComponent({ loading: true });
295297
assert.isFalse(wrapper.find('[data-component="TableFoot"]').exists());
296298
});
297299
});
298300

299301
context('when empty', () => {
300302
it('shows an empty message if provided', () => {
301-
const wrapper = createComponent(DataTable, {
303+
const wrapper = createComponent({
302304
emptyMessage: <strong>Nope</strong>,
303305
rows: [],
304306
});
305307
assert.equal(wrapper.find('tbody tr td').at(0).text(), 'Nope');
306308
});
307309

308310
it('still renders headings', () => {
309-
const wrapper = createComponent(DataTable, {
311+
const wrapper = createComponent({
310312
emptyMessage: <strong>Nope</strong>,
311313
rows: [],
312314
});
313315
assert.equal(wrapper.find('thead tr th').length, 3);
314316
});
315317

316318
it('does not render a TableFoot', () => {
317-
const wrapper = createComponent(DataTable, {
319+
const wrapper = createComponent({
318320
rows: [],
319321
});
320322
assert.isFalse(wrapper.find('[data-component="TableFoot"]').exists());
@@ -412,7 +414,7 @@ describe('DataTable', () => {
412414
{ direction: 'descending', expectedArrow: 'ArrowDownIcon' },
413415
].forEach(({ direction, expectedArrow }) => {
414416
it('shows initial active order', () => {
415-
const wrapper = createComponent(DataTable, {
417+
const wrapper = createComponent({
416418
order: { field: 'color', direction },
417419
});
418420
const colorTableCell = wrapper.find('TableCell').at(1);
@@ -425,7 +427,7 @@ describe('DataTable', () => {
425427
[
426428
// Clicking the same column when initially ascending, transitions to descending
427429
{
428-
initialOrder: { field: 'name', direction: 'ascending' },
430+
startingOrder: { field: 'name', direction: 'ascending' },
429431
clickedColumn: 'name',
430432
expectedNewOrder: {
431433
field: 'name',
@@ -434,26 +436,57 @@ describe('DataTable', () => {
434436
},
435437
// Clicking the same column when initially descending, transitions to ascending
436438
{
437-
initialOrder: { field: 'name', direction: 'descending' },
439+
startingOrder: { field: 'name', direction: 'descending' },
438440
clickedColumn: 'name',
439441
expectedNewOrder: { field: 'name', direction: 'ascending' },
440442
},
441443
// Clicking another column sets direction as ascending
442444
{
443-
initialOrder: { field: 'name', direction: 'ascending' },
445+
startingOrder: { field: 'name', direction: 'ascending' },
444446
clickedColumn: 'consistency',
445447
expectedNewOrder: {
446448
field: 'consistency',
447449
direction: 'ascending',
448450
},
449451
},
450-
].forEach(({ initialOrder, clickedColumn, expectedNewOrder }) => {
452+
// Change sort column to a column with no initial order specified
453+
{
454+
startingOrder: { field: 'name', direction: 'ascending' },
455+
clickedColumn: 'color',
456+
expectedNewOrder: {
457+
field: 'color',
458+
direction: 'descending',
459+
},
460+
},
461+
// Change sort column to a column with no initial order specified
462+
{
463+
startingOrder: undefined,
464+
clickedColumn: 'consistency',
465+
expectedNewOrder: {
466+
field: 'consistency',
467+
direction: 'ascending',
468+
},
469+
},
470+
// Change sort column to a column with an initial order specified
471+
{
472+
startingOrder: undefined,
473+
clickedColumn: 'color',
474+
expectedNewOrder: {
475+
field: 'color',
476+
direction: 'descending',
477+
},
478+
},
479+
].forEach(({ startingOrder, clickedColumn, expectedNewOrder }) => {
451480
it('can update order by clicking columns', () => {
452481
const onOrderChange = sinon.stub();
453-
const wrapper = createComponent(DataTable, {
482+
const wrapper = createComponent({
454483
onOrderChange,
455-
order: initialOrder,
456-
orderableColumns: ['name', 'color', 'consistency'],
484+
order: startingOrder,
485+
orderableColumns: {
486+
name: 'ascending',
487+
color: 'descending',
488+
consistency: 'ascending',
489+
},
457490
});
458491

459492
wrapper
@@ -471,7 +504,7 @@ describe('DataTable', () => {
471504
undefined,
472505
].forEach(orderableColumns => {
473506
it('can restrict which columns are orderable', () => {
474-
const wrapper = createComponent(DataTable, {
507+
const wrapper = createComponent({
475508
onOrderChange: sinon.stub(),
476509
orderableColumns,
477510
});

src/pattern-library/components/patterns/data/DataTablePage.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -702,11 +702,12 @@ export default function DataTablePage() {
702702
<Library.Info>
703703
<Library.InfoItem label="description">
704704
If provided together with <code>onOrderChange</code>, it allows
705-
to restrict which columns can be used to order the table.
706-
Defaults to all columns.
705+
to restrict which columns can be used to order the table, or
706+
define the initial ordering direction for every orderable
707+
column. Defaults to no columns.
707708
</Library.InfoItem>
708709
<Library.InfoItem label="type">
709-
<code>{`Field[] | undefined`}</code>
710+
<code>{`Field[] | Partial<Record<Field, 'ascending' | 'descending'>> | undefined`}</code>
710711
</Library.InfoItem>
711712
<Library.InfoItem label="default">
712713
<code>undefined</code>
@@ -723,6 +724,20 @@ export default function DataTablePage() {
723724
/>
724725
</div>
725726
</Library.Demo>
727+
728+
<Library.Demo title="Year orders descending by default">
729+
<div className="w-full">
730+
<ClientOrderableDataTable
731+
title="Some of Nabokov's novels"
732+
rows={nabokovRows}
733+
columns={nabokovColumns}
734+
orderableColumns={{
735+
title: 'ascending',
736+
year: 'descending',
737+
}}
738+
/>
739+
</div>
740+
</Library.Demo>
726741
</Library.Example>
727742

728743
<Library.Example title="...htmlAttributes">

src/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ export type TransitionComponent = FunctionComponent<{
5959
onTransitionEnd?: (direction: 'in' | 'out') => void;
6060
}>;
6161

62+
export type OrderDirection = 'ascending' | 'descending';
63+
6264
export type Order<Field extends string | number | symbol> = {
6365
field: Field;
64-
direction: 'ascending' | 'descending';
66+
direction: OrderDirection;
6567
};

0 commit comments

Comments
 (0)