Skip to content

Commit 4eb0958

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

File tree

4 files changed

+111
-34
lines changed

4 files changed

+111
-34
lines changed

src/components/data/DataTable.tsx

Lines changed: 18 additions & 4 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';
@@ -83,6 +83,11 @@ type ComponentProps<Row> = Pick<
8383
*/
8484
orderableColumns?: Array<keyof Row>;
8585

86+
/**
87+
* The initial order direction to set when a column becomes the ordered one.
88+
*/
89+
initialColumnsOrderDir?: Partial<Record<keyof Row, OrderDirection>>;
90+
8691
/** Callback to render an individual table cell */
8792
renderItem?: (r: Row, field: keyof Row) => ComponentChildren;
8893
};
@@ -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+
initialColumnsOrderDir?: 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: initialColumnsOrderDir?.[newField] ?? 'ascending',
112+
};
104113
}
105114

106115
const newDirection =
@@ -151,6 +160,7 @@ export default function DataTable<Row>({
151160
order,
152161
onOrderChange,
153162
orderableColumns = [],
163+
initialColumnsOrderDir,
154164

155165
// Forwarded to Table
156166
title,
@@ -164,10 +174,14 @@ export default function DataTable<Row>({
164174
const scrollContext = useContext(ScrollContext);
165175
const updateOrder = useCallback(
166176
(newField: keyof Row) => {
167-
const newOrder = calculateNewOrder(newField, order);
177+
const newOrder = calculateNewOrder(
178+
newField,
179+
order,
180+
initialColumnsOrderDir,
181+
);
168182
onOrderChange?.(newOrder);
169183
},
170-
[onOrderChange, order],
184+
[initialColumnsOrderDir, onOrderChange, order],
171185
);
172186

173187
const noContent = loading || (!rows.length && emptyMessage);

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

Lines changed: 59 additions & 29 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,54 @@ 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+
// Clicking another column sets direction default order when defined
453+
{
454+
startingOrder: { field: 'name', direction: 'ascending' },
455+
clickedColumn: 'color',
456+
expectedNewOrder: {
457+
field: 'color',
458+
direction: 'descending',
459+
},
460+
},
461+
// No order sets direction as ascending
462+
{
463+
startingOrder: undefined,
464+
clickedColumn: 'consistency',
465+
expectedNewOrder: {
466+
field: 'consistency',
467+
direction: 'ascending',
468+
},
469+
},
470+
// No order sets direction as default order when defined
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,
484+
order: startingOrder,
456485
orderableColumns: ['name', 'color', 'consistency'],
486+
initialColumnsOrderDir: { color: 'descending' },
457487
});
458488

459489
wrapper
@@ -471,7 +501,7 @@ describe('DataTable', () => {
471501
undefined,
472502
].forEach(orderableColumns => {
473503
it('can restrict which columns are orderable', () => {
474-
const wrapper = createComponent(DataTable, {
504+
const wrapper = createComponent({
475505
onOrderChange: sinon.stub(),
476506
orderableColumns,
477507
});

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,37 @@ export default function DataTablePage() {
725725
</Library.Demo>
726726
</Library.Example>
727727

728+
<Library.Example title="initialColumnsOrderDir">
729+
<Library.Info>
730+
<Library.InfoItem label="description">
731+
The initial order direction (<code>ascending</code> or{' '}
732+
<code>descending</code>) to set when a column becomes the
733+
ordered one. Defaults to <code>ascending</code> for any column
734+
not explicitly provided here.
735+
</Library.InfoItem>
736+
<Library.InfoItem label="type">
737+
<code>{`Partial<Record<keyof Row, 'ascending' | 'descending'>>`}</code>
738+
</Library.InfoItem>
739+
<Library.InfoItem label="default">
740+
<code>undefined</code>
741+
</Library.InfoItem>
742+
</Library.Info>
743+
744+
<Library.Demo title="Year orders descending by default">
745+
<div className="w-full">
746+
<ClientOrderableDataTable
747+
title="Some of Nabokov's novels"
748+
rows={nabokovRows}
749+
columns={nabokovColumns}
750+
orderableColumns={['title', 'year']}
751+
initialColumnsOrderDir={{
752+
year: 'descending',
753+
}}
754+
/>
755+
</div>
756+
</Library.Demo>
757+
</Library.Example>
758+
728759
<Library.Example title="...htmlAttributes">
729760
<Library.Info>
730761
<Library.InfoItem label="description">

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)