@loadsmart/loadsmart-ui 5.0.1 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Button/Button.d.ts +1 -0
- package/dist/components/Calendar/Pickers/PickerButton.d.ts +1 -1
- package/dist/components/Dropdown/Dropdown.stories.d.ts +6 -7
- package/dist/components/Dropdown/Dropdown.types.d.ts +2 -2
- package/dist/components/Dropdown/DropdownTrigger.d.ts +1 -1
- package/dist/components/Dropdown/useDropdown.d.ts +1 -5
- package/dist/components/Icon/Icon.d.ts +1 -0
- package/dist/components/Select/Select.types.d.ts +4 -4
- package/dist/components/Table/Table.d.ts +7 -1
- package/dist/components/Table/Table.fixtures.d.ts +1 -0
- package/dist/components/Table/Table.stories.d.ts +20 -4
- package/dist/components/Table/Table.types.d.ts +30 -1
- package/dist/components/Table/useSortBy.d.ts +2 -2
- package/dist/components/Table/useSortBy.types.d.ts +5 -6
- package/dist/hooks/useClickOutside/useClickOutside.d.ts +1 -1
- package/dist/index.js +448 -440
- package/dist/index.js.map +1 -1
- package/dist/loadsmart.theme-63c13988.js +2 -0
- package/dist/{loadsmart.theme-37a60d56.js.map → loadsmart.theme-63c13988.js.map} +1 -1
- package/dist/{prop-06c02f6d.js → prop-0c635ee9.js} +2 -2
- package/dist/{prop-06c02f6d.js.map → prop-0c635ee9.js.map} +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/theming/index.js +1 -1
- package/dist/tools/index.js +1 -1
- package/package.json +1 -1
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/Calendar/Pickers/DayPicker.tsx +9 -1
- package/src/components/Calendar/Pickers/PickerButton.tsx +14 -6
- package/src/components/Dropdown/Dropdown.stories.tsx +26 -75
- package/src/components/Dropdown/Dropdown.tsx +11 -8
- package/src/components/Dropdown/Dropdown.types.ts +2 -2
- package/src/components/Dropdown/DropdownTrigger.tsx +1 -1
- package/src/components/Dropdown/useDropdown.ts +1 -6
- package/src/components/Icon/Icon.tsx +2 -0
- package/src/components/Icon/assets/dots-horizontal.svg +1 -0
- package/src/components/Select/Select.test.tsx +23 -1
- package/src/components/Select/Select.types.ts +4 -4
- package/src/components/Select/useSelect.ts +11 -9
- package/src/components/Table/Table.fixtures.ts +26 -16
- package/src/components/Table/Table.stories.tsx +156 -25
- package/src/components/Table/Table.test.tsx +29 -0
- package/src/components/Table/Table.tsx +85 -1
- package/src/components/Table/Table.types.ts +39 -1
- package/src/components/Table/useSortBy.ts +5 -5
- package/src/components/Table/useSortBy.types.ts +5 -5
- package/src/hooks/useClickOutside/useClickOutside.ts +6 -6
- package/src/testing/SelectEvent/SelectEvent.ts +8 -7
- package/dist/loadsmart.theme-37a60d56.js +0 -2
|
@@ -3,14 +3,16 @@ import React from 'react'
|
|
|
3
3
|
import { Meta } from '@storybook/react/types-6-0'
|
|
4
4
|
|
|
5
5
|
import Table, { useSelection } from './Table'
|
|
6
|
-
import type { TableProps } from './Table.types'
|
|
6
|
+
import type { TableColumns, TableProps } from './Table.types'
|
|
7
7
|
import useSortBy from './useSortBy'
|
|
8
8
|
|
|
9
9
|
import { TABLE_COLUMNS, TABLE_ROWS } from './Table.fixtures'
|
|
10
|
+
import type { RowFixture } from './Table.fixtures'
|
|
10
11
|
|
|
11
12
|
import { Button } from 'components/Button'
|
|
12
13
|
import { Text } from 'components/Text'
|
|
13
14
|
import { Switch } from 'components/Switch'
|
|
15
|
+
import { Icon } from 'components/Icon'
|
|
14
16
|
|
|
15
17
|
export default {
|
|
16
18
|
title: 'Components/Table',
|
|
@@ -118,7 +120,141 @@ WithSorting.args = {
|
|
|
118
120
|
scale: 'default',
|
|
119
121
|
}
|
|
120
122
|
|
|
121
|
-
export function
|
|
123
|
+
export function WithColumnPicker(args: TableProps): JSX.Element {
|
|
124
|
+
const ID_COLUMNS: TableColumns<RowFixture> = [
|
|
125
|
+
{
|
|
126
|
+
title: 'Ref',
|
|
127
|
+
option: 'Reference',
|
|
128
|
+
key: 'ref',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
title: 'Seq',
|
|
132
|
+
option: 'Sequence',
|
|
133
|
+
key: 'value',
|
|
134
|
+
},
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
const [idCol, setIdCol] = React.useState(ID_COLUMNS[0])
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<Table {...args}>
|
|
141
|
+
<Table.Head>
|
|
142
|
+
<Table.Row>
|
|
143
|
+
<Table.HeadCell>
|
|
144
|
+
{idCol.title}
|
|
145
|
+
<Table.Picker
|
|
146
|
+
value={idCol}
|
|
147
|
+
onChange={setIdCol}
|
|
148
|
+
options={ID_COLUMNS}
|
|
149
|
+
title="Pick an identifier column"
|
|
150
|
+
/>
|
|
151
|
+
</Table.HeadCell>
|
|
152
|
+
<Table.HeadCell>Product Name</Table.HeadCell>
|
|
153
|
+
<Table.HeadCell>Price</Table.HeadCell>
|
|
154
|
+
<Table.HeadCell>Owner</Table.HeadCell>
|
|
155
|
+
<Table.HeadCell>
|
|
156
|
+
Release Date
|
|
157
|
+
<Table.Picker trigger={(expanded) => (expanded ? 'close' : 'open')} align="end">
|
|
158
|
+
<Table.Picker.Item checked option={{ title: 'Release Date', key: 'release-date' }} />
|
|
159
|
+
<Table.Picker.Item disabled>Example of custom item</Table.Picker.Item>
|
|
160
|
+
</Table.Picker>
|
|
161
|
+
</Table.HeadCell>
|
|
162
|
+
</Table.Row>
|
|
163
|
+
</Table.Head>
|
|
164
|
+
|
|
165
|
+
<Table.Body>
|
|
166
|
+
{TABLE_ROWS.map((entry) => (
|
|
167
|
+
<Table.Row>
|
|
168
|
+
<Table.Cell>{entry[idCol.key]}</Table.Cell>
|
|
169
|
+
<Table.Cell>{entry.product}</Table.Cell>
|
|
170
|
+
<Table.Cell>${new Intl.NumberFormat().format(Number(entry.price))}</Table.Cell>
|
|
171
|
+
<Table.Cell>{entry.owner}</Table.Cell>
|
|
172
|
+
<Table.Cell>{entry['release-date']}</Table.Cell>
|
|
173
|
+
</Table.Row>
|
|
174
|
+
))}
|
|
175
|
+
</Table.Body>
|
|
176
|
+
</Table>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
WithColumnPicker.args = {
|
|
181
|
+
scale: 'default',
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
WithColumnPicker.parameters = {
|
|
185
|
+
docs: {
|
|
186
|
+
description: {
|
|
187
|
+
story: `
|
|
188
|
+
#### \`<Table.Picker />\`
|
|
189
|
+
|
|
190
|
+
This component renders a \`Dropdown\` menu, making it possible for users to select what column should be visible at the moment.
|
|
191
|
+
|
|
192
|
+
\`\`\`jsx
|
|
193
|
+
interface IMyRow {
|
|
194
|
+
uuid: string
|
|
195
|
+
ext_id: string
|
|
196
|
+
ref: number
|
|
197
|
+
// other properties...
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Make sure to import TableColumns generic type if you're using TypeScript */
|
|
201
|
+
const FIRST_COLUMNS: TableColumns<IMyRow> = [
|
|
202
|
+
{ key: 'uuid', title: 'Id', option: 'Identifier' },
|
|
203
|
+
{ key: 'ext_id', title: 'Ext. Id', option: 'External Id' },
|
|
204
|
+
{ key: 'ref', title: 'Ref', option: 'Reference' },
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
function MyAmazingTable() {
|
|
208
|
+
const [pickedColumn, setPickedColumn] = React.useState(FIRST_COLUMNS[0])
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<Table>
|
|
212
|
+
<Table.Head>
|
|
213
|
+
<Table.Row>
|
|
214
|
+
<Table.HeadCell>
|
|
215
|
+
{/* Don't forget to render the title of the current picked column */}
|
|
216
|
+
{pickedColumn.title}
|
|
217
|
+
|
|
218
|
+
<Table.Picker
|
|
219
|
+
value={pickedColumn}
|
|
220
|
+
onChange={(option) => setPickedColumn(option)}
|
|
221
|
+
options={FIRST_COLUMNS}
|
|
222
|
+
title="Pick an identifier column"
|
|
223
|
+
/>
|
|
224
|
+
</Table.HeadCell>
|
|
225
|
+
</Table.Row>
|
|
226
|
+
</Table.Head>
|
|
227
|
+
|
|
228
|
+
<Table.Body>
|
|
229
|
+
<Table.Row>
|
|
230
|
+
<Table.Cell>
|
|
231
|
+
{/* The \`key\` property is the accessor of your data interface */}
|
|
232
|
+
{entry[pickedColumn.key]}
|
|
233
|
+
</Table.Cell>
|
|
234
|
+
</Table.Row>
|
|
235
|
+
</Table.Body>
|
|
236
|
+
</Table>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
\`\`\`
|
|
240
|
+
|
|
241
|
+
## Props
|
|
242
|
+
|
|
243
|
+
| Prop | Type | Default | Description |
|
|
244
|
+
|----------|---------------------------------------|----------------|------------------------------------------------|
|
|
245
|
+
| value | \`TableColumn<T>\` | \`undefined\` | Current picked column |
|
|
246
|
+
| onChange | \`(column: TableColumn<T>) => void\` | \`undefined\` | Callback that's invoked with the picked column |
|
|
247
|
+
| options | \`TableColumns<T>\` | \`undefined\` | List of possible columns to pick |
|
|
248
|
+
`,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
source: {
|
|
253
|
+
type: 'code',
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function WithRowSelection(args: TableProps): JSX.Element {
|
|
122
258
|
return (
|
|
123
259
|
<div className="p-4 bg-neutral-white">
|
|
124
260
|
<Table {...args} selection={{ onChange: console.log }}>
|
|
@@ -150,7 +286,7 @@ export function Selection(args: TableProps): JSX.Element {
|
|
|
150
286
|
<Table.Row key={row.value}>
|
|
151
287
|
<Table.Selection.Cell value={row} />
|
|
152
288
|
{Object.keys(row)
|
|
153
|
-
.slice(1,
|
|
289
|
+
.slice(1, 6)
|
|
154
290
|
.map((key, index) => (
|
|
155
291
|
<Table.Cell key={index}>{row[key]}</Table.Cell>
|
|
156
292
|
))}
|
|
@@ -162,7 +298,7 @@ export function Selection(args: TableProps): JSX.Element {
|
|
|
162
298
|
)
|
|
163
299
|
}
|
|
164
300
|
|
|
165
|
-
|
|
301
|
+
WithRowSelection.parameters = {
|
|
166
302
|
docs: {
|
|
167
303
|
description: {
|
|
168
304
|
story: `
|
|
@@ -214,7 +350,7 @@ It also accepts \`children\`, serving as a placeholder for when there are no sel
|
|
|
214
350
|
},
|
|
215
351
|
}
|
|
216
352
|
|
|
217
|
-
|
|
353
|
+
WithRowSelection.args = {
|
|
218
354
|
scale: 'default',
|
|
219
355
|
}
|
|
220
356
|
|
|
@@ -235,14 +371,14 @@ function MyCustomSelection() {
|
|
|
235
371
|
)
|
|
236
372
|
}
|
|
237
373
|
|
|
238
|
-
const
|
|
374
|
+
const CUSTOM_SELECTION_ROWS = TABLE_ROWS.map((row) => {
|
|
239
375
|
const advancedRow = { ...row }
|
|
240
376
|
advancedRow._type = 'myRow'
|
|
241
377
|
advancedRow.rowId = advancedRow.value
|
|
242
378
|
return advancedRow
|
|
243
379
|
})
|
|
244
380
|
|
|
245
|
-
export function
|
|
381
|
+
export function WithCustomRowSelection(): JSX.Element {
|
|
246
382
|
return (
|
|
247
383
|
<div className="p-4 bg-neutral-white">
|
|
248
384
|
<Table<MyRow>
|
|
@@ -272,13 +408,13 @@ export function AdvancedSelection(): JSX.Element {
|
|
|
272
408
|
</Table.Head>
|
|
273
409
|
|
|
274
410
|
<Table.Body>
|
|
275
|
-
{
|
|
411
|
+
{CUSTOM_SELECTION_ROWS.map((row) => (
|
|
276
412
|
<Table.Row key={row.value}>
|
|
277
413
|
<Table.Selection.Cell value={row}>
|
|
278
414
|
{({ selected, toggle }) => <Switch onToggle={toggle} active={selected} />}
|
|
279
415
|
</Table.Selection.Cell>
|
|
280
416
|
{Object.keys(row)
|
|
281
|
-
.slice(1,
|
|
417
|
+
.slice(1, 6)
|
|
282
418
|
.map((key, index) => (
|
|
283
419
|
<Table.Cell key={index}>{row[key]}</Table.Cell>
|
|
284
420
|
))}
|
|
@@ -291,29 +427,24 @@ export function AdvancedSelection(): JSX.Element {
|
|
|
291
427
|
)
|
|
292
428
|
}
|
|
293
429
|
|
|
294
|
-
|
|
430
|
+
WithCustomRowSelection.parameters = {
|
|
295
431
|
docs: {
|
|
296
432
|
description: {
|
|
297
433
|
story: `
|
|
298
|
-
The \`<Table>\` component accepts a \`selection\` prop, an object for the
|
|
299
|
-
|
|
300
|
-
### \`selection.multiple\`
|
|
301
|
-
|
|
302
|
-
\`boolean\`: defaults to \`false\`, if set to \`true\` will render \`radio\` inputs and will only have one selected row.
|
|
434
|
+
The \`<Table>\` component accepts a \`selection\` prop, an object for the \`useSelectable\` hook, where you can specify if it's multiple or single, register an \`onChange\` callback, set your adapters, and even initially selected rows.
|
|
303
435
|
|
|
304
|
-
|
|
436
|
+
**\`selection\`**
|
|
305
437
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
### \`selection.adapters\`
|
|
313
|
-
|
|
314
|
-
Your object interface doesn't have a \`value\` property, you can provide an adapter for any data type, by implementing a \`getKey\` function that returns your id, like so:
|
|
438
|
+
| Prop | Type | Default | Description |
|
|
439
|
+
|----------|----------------------------------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
440
|
+
| multiple | \`boolean\` | \`true\` | If set to \`false\` renders \`radio\` inputs and will only have one selected row at maximum |
|
|
441
|
+
| onChange | \`(selected: T[]) => void\` | \`undefined\` | Callback that's invoked with the current selected rows for every change |
|
|
442
|
+
| selected | \`T[]\` | \`[]\` | Sets the initial selected rows |
|
|
443
|
+
| adapters | \`Record<T['_type'], SelectableAdapter<T>>\` | \`undefined\` | If your data interface doesn't have a \`value\` property, you can provide an adapter, implementing a \`getKey\` function that returns your id. See example below. |
|
|
315
444
|
|
|
316
445
|
\`\`\`jsx
|
|
446
|
+
// Example of selection.adapters usage
|
|
447
|
+
|
|
317
448
|
interface IMyRow {
|
|
318
449
|
myKeyName: string
|
|
319
450
|
_type: string
|
|
@@ -52,6 +52,35 @@ describe('<Table />', () => {
|
|
|
52
52
|
})
|
|
53
53
|
})
|
|
54
54
|
|
|
55
|
+
describe('Column Picker', () => {
|
|
56
|
+
it('should pick columns', () => {
|
|
57
|
+
const onChange = jest.fn()
|
|
58
|
+
const OPTION_1 = { title: 'Title1', option: 'Option1', key: 'key1' }
|
|
59
|
+
const OPTION_2 = { title: 'Title2', option: 'Option2', key: 'Key2' }
|
|
60
|
+
|
|
61
|
+
setup({
|
|
62
|
+
children: (
|
|
63
|
+
<Table.Picker
|
|
64
|
+
options={[OPTION_1, OPTION_2]}
|
|
65
|
+
onChange={onChange}
|
|
66
|
+
value={OPTION_1}
|
|
67
|
+
title="Pick a column"
|
|
68
|
+
/>
|
|
69
|
+
),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
user.click(screen.getByRole('button', { name: 'Pick a column' }))
|
|
73
|
+
|
|
74
|
+
expect(screen.getAllByRole('option')).toHaveLength(2)
|
|
75
|
+
|
|
76
|
+
expect(screen.getByRole('option', { selected: true })).toHaveTextContent('Option1')
|
|
77
|
+
|
|
78
|
+
user.click(screen.getByRole('option', { name: 'Option2' }))
|
|
79
|
+
|
|
80
|
+
expect(onChange).toHaveBeenCalledWith(OPTION_2)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
55
84
|
describe('Selection', () => {
|
|
56
85
|
it('should accept selections and allow bulk actions', () => {
|
|
57
86
|
const action = jest.fn()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { Children, Fragment, isValidElement, useEffect } from 'react'
|
|
2
2
|
import styled, { css } from 'styled-components'
|
|
3
3
|
import type { ReactNode } from 'react'
|
|
4
|
+
import { isFunction } from '@loadsmart/utils-function'
|
|
4
5
|
|
|
5
6
|
import { getToken as token } from 'theming'
|
|
6
7
|
import { conditional, whenProps, prop } from 'tools/index'
|
|
@@ -10,6 +11,10 @@ import { Checkbox } from 'components/Checkbox'
|
|
|
10
11
|
import { Radio } from 'components/Radio'
|
|
11
12
|
import { Link } from 'components/Link'
|
|
12
13
|
import { Text } from 'components/Text'
|
|
14
|
+
import { Dropdown } from 'components/Dropdown'
|
|
15
|
+
import { Layout } from 'components/Layout'
|
|
16
|
+
import { Icon } from 'components/Icon'
|
|
17
|
+
import { Children as InternalButton } from 'components/Button/Button'
|
|
13
18
|
|
|
14
19
|
import {
|
|
15
20
|
isCellSelected,
|
|
@@ -28,6 +33,8 @@ import type {
|
|
|
28
33
|
TableCaptionProps,
|
|
29
34
|
TableSelectionProps,
|
|
30
35
|
SelectionCellProps,
|
|
36
|
+
TablePickerItemProps,
|
|
37
|
+
TablePickerProps,
|
|
31
38
|
} from './Table.types'
|
|
32
39
|
|
|
33
40
|
const StyledTableBody = styled.tbody`
|
|
@@ -285,7 +292,9 @@ function TableHeadCell({
|
|
|
285
292
|
onClick={onClick}
|
|
286
293
|
{...others}
|
|
287
294
|
>
|
|
288
|
-
|
|
295
|
+
<Layout.Group space="xs" align="center">
|
|
296
|
+
{children}
|
|
297
|
+
</Layout.Group>
|
|
289
298
|
</StyledTableHeadCell>
|
|
290
299
|
)
|
|
291
300
|
}
|
|
@@ -322,6 +331,79 @@ function TableSelectionActions({ buttons, children, ...others }: TableSelectionP
|
|
|
322
331
|
)
|
|
323
332
|
}
|
|
324
333
|
|
|
334
|
+
const StyledPickerTrigger = styled(Dropdown.Trigger.Button)`
|
|
335
|
+
padding: 0;
|
|
336
|
+
|
|
337
|
+
${InternalButton} {
|
|
338
|
+
margin: ${token('space-2xs')};
|
|
339
|
+
}
|
|
340
|
+
`
|
|
341
|
+
|
|
342
|
+
function TablePickerItem<T>({
|
|
343
|
+
option,
|
|
344
|
+
checked,
|
|
345
|
+
children,
|
|
346
|
+
...props
|
|
347
|
+
}: TablePickerItemProps<T>): JSX.Element {
|
|
348
|
+
return (
|
|
349
|
+
<Dropdown.Item
|
|
350
|
+
leading={
|
|
351
|
+
checked ? (
|
|
352
|
+
<Icon name="check" size="16" color="neutral-darker" />
|
|
353
|
+
) : (
|
|
354
|
+
<Layout.Box padding="s" />
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
role="option"
|
|
358
|
+
aria-selected={checked ? 'true' : 'false'}
|
|
359
|
+
{...props}
|
|
360
|
+
>
|
|
361
|
+
{children || (option && (option.option || option.title || option.key)) || 'Column'}
|
|
362
|
+
</Dropdown.Item>
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const TriggerIcon = styled(Icon).attrs({
|
|
367
|
+
name: 'dots-horizontal',
|
|
368
|
+
size: '12',
|
|
369
|
+
color: 'neutral-darker',
|
|
370
|
+
})`
|
|
371
|
+
transform: rotate(90deg);
|
|
372
|
+
`
|
|
373
|
+
|
|
374
|
+
function TablePicker<T>({
|
|
375
|
+
value,
|
|
376
|
+
onChange,
|
|
377
|
+
options,
|
|
378
|
+
align,
|
|
379
|
+
children,
|
|
380
|
+
trigger: propsTrigger,
|
|
381
|
+
...props
|
|
382
|
+
}: TablePickerProps<T>): JSX.Element {
|
|
383
|
+
return (
|
|
384
|
+
<Dropdown>
|
|
385
|
+
<StyledPickerTrigger trailing={null} scale="small" {...props}>
|
|
386
|
+
{propsTrigger !== undefined ? (
|
|
387
|
+
({ expanded }) => (isFunction(propsTrigger) ? propsTrigger(expanded) : propsTrigger)
|
|
388
|
+
) : (
|
|
389
|
+
<TriggerIcon />
|
|
390
|
+
)}
|
|
391
|
+
</StyledPickerTrigger>
|
|
392
|
+
<Dropdown.Menu align={align} role="listbox">
|
|
393
|
+
{children ||
|
|
394
|
+
options?.map((option, i) => (
|
|
395
|
+
<TablePickerItem
|
|
396
|
+
key={i}
|
|
397
|
+
option={option}
|
|
398
|
+
checked={option.key === value?.key}
|
|
399
|
+
onClick={() => onChange?.(option)}
|
|
400
|
+
/>
|
|
401
|
+
))}
|
|
402
|
+
</Dropdown.Menu>
|
|
403
|
+
</Dropdown>
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
|
|
325
407
|
function useIsRowSelected(children: ReactNode): boolean {
|
|
326
408
|
const { selected, config } = useTableSelection()
|
|
327
409
|
|
|
@@ -355,6 +437,8 @@ Table.Selection = {
|
|
|
355
437
|
HeadCell: SelectionHeadCell,
|
|
356
438
|
}
|
|
357
439
|
Table.SortHandle = TableSortHandle
|
|
440
|
+
TablePicker.Item = TablePickerItem
|
|
441
|
+
Table.Picker = TablePicker
|
|
358
442
|
|
|
359
443
|
export { useSelection }
|
|
360
444
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Selectable } from 'hooks/useSelectable'
|
|
2
|
-
import type { ComponentType, HTMLAttributes } from 'react'
|
|
2
|
+
import type { ComponentType, HTMLAttributes, PropsWithChildren, ReactNode } from 'react'
|
|
3
3
|
import React from 'react'
|
|
4
|
+
import type { DropdownMenuItemProps, DropdownMenuProps } from 'components/Dropdown'
|
|
5
|
+
import type { DropdownTriggerButtonProps } from 'components/Dropdown/DropdownTrigger'
|
|
4
6
|
|
|
5
7
|
import type { TableSelectableRow, TableSelectionConfig } from './Selection'
|
|
6
8
|
|
|
@@ -40,3 +42,39 @@ export interface TableCellProps extends HTMLAttributes<HTMLTableCellElement> {
|
|
|
40
42
|
scale?: Scale
|
|
41
43
|
selected?: boolean
|
|
42
44
|
}
|
|
45
|
+
|
|
46
|
+
export type TableColumn<T = Record<string, unknown>> = {
|
|
47
|
+
/**
|
|
48
|
+
* The accessor of you entry interface
|
|
49
|
+
*/
|
|
50
|
+
key: keyof T
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The label that should go in the Header cell
|
|
54
|
+
*/
|
|
55
|
+
title: ReactNode
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The text of the Dropdown.Item
|
|
59
|
+
* If omitted, `title` is used
|
|
60
|
+
*/
|
|
61
|
+
option?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type TableColumns<T> = Array<TableColumn<T>>
|
|
65
|
+
|
|
66
|
+
export interface TablePickerItemProps<T> extends DropdownMenuItemProps {
|
|
67
|
+
option?: TableColumn<T>
|
|
68
|
+
checked?: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface TablePickerProps<T>
|
|
72
|
+
extends PropsWithChildren<
|
|
73
|
+
Partial<Omit<DropdownTriggerButtonProps, 'onChange' | 'value' | 'children'>>
|
|
74
|
+
> {
|
|
75
|
+
value?: TableColumn<T>
|
|
76
|
+
onChange?: (value: TableColumn<T>) => void
|
|
77
|
+
options?: TableColumns<T>
|
|
78
|
+
trigger?: ReactNode | ((expanded: boolean) => ReactNode)
|
|
79
|
+
align?: DropdownMenuProps['align']
|
|
80
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { useCallback, useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import get from 'utils/toolset/get'
|
|
4
|
-
import type { TableRowGroup,
|
|
4
|
+
import type { TableRowGroup, SortableColumn, ColumnGroup, SortState } from './useSortBy.types'
|
|
5
5
|
import { SortDirection } from './useSortBy.types'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Sort table rows using column keys with Array.prototype.sort() function
|
|
9
9
|
*
|
|
10
|
-
* @param {
|
|
10
|
+
* @param {SortableColumn} sortConfig - sorting config including `key` and `isSortedDesc`
|
|
11
11
|
* @param {TableRowGroup} tableRowGroup - a group of table rows to be sorted
|
|
12
12
|
*
|
|
13
13
|
* @returns {TableRowGroup} sorted table rows
|
|
14
14
|
*/
|
|
15
|
-
function sortRows(sortConfig:
|
|
15
|
+
function sortRows(sortConfig: SortableColumn, tableRowGroup: TableRowGroup): TableRowGroup {
|
|
16
16
|
return [...tableRowGroup].sort((object1, object2) => {
|
|
17
17
|
if (get(object1, sortConfig.key) > get(object2, sortConfig.key)) {
|
|
18
18
|
return sortConfig.order === SortDirection.DESC ? -1 : 1
|
|
@@ -41,7 +41,7 @@ function useSortBy(
|
|
|
41
41
|
): {
|
|
42
42
|
tableData: TableRowGroup
|
|
43
43
|
columnData: ColumnGroup
|
|
44
|
-
sortByColumn: (column:
|
|
44
|
+
sortByColumn: (column: SortableColumn) => void
|
|
45
45
|
} {
|
|
46
46
|
const [state, setState] = useState<SortState>({
|
|
47
47
|
sortConfig: { key: '', title: '' },
|
|
@@ -51,7 +51,7 @@ function useSortBy(
|
|
|
51
51
|
|
|
52
52
|
const { sortConfig, columnGroup: columns, tableRowGroup: tableRows } = state
|
|
53
53
|
|
|
54
|
-
const sortByColumn = useCallback((column:
|
|
54
|
+
const sortByColumn = useCallback((column: SortableColumn) => {
|
|
55
55
|
setState(({ columnGroup: columns, ...rest }) => {
|
|
56
56
|
const updatedColumns: ColumnGroup = columns.map((current) => {
|
|
57
57
|
if (!column.isSortable || current.key !== column.key) {
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
+
import type { TableColumn } from './Table.types'
|
|
2
|
+
|
|
1
3
|
export type TableRowGroup = Array<Record<string, string | number>>
|
|
2
4
|
|
|
3
|
-
export
|
|
4
|
-
key: string
|
|
5
|
-
title: string
|
|
5
|
+
export interface SortableColumn extends TableColumn {
|
|
6
6
|
isSortable?: boolean
|
|
7
7
|
order?: SortDirection | null
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export type ColumnGroup = Array<
|
|
10
|
+
export type ColumnGroup = Array<SortableColumn>
|
|
11
11
|
|
|
12
12
|
export interface SortState {
|
|
13
|
-
sortConfig:
|
|
13
|
+
sortConfig: SortableColumn
|
|
14
14
|
columnGroup: ColumnGroup
|
|
15
15
|
tableRowGroup: TableRowGroup
|
|
16
16
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from 'react'
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import KeyboardKey from 'utils/toolset/keyboard'
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ import type { RefObject } from 'react'
|
|
|
6
6
|
|
|
7
7
|
function useClickOutside<T extends HTMLElement>(
|
|
8
8
|
container: RefObject<T>,
|
|
9
|
-
callback: () => void,
|
|
9
|
+
callback: (event?: MouseEvent | TouchEvent | KeyboardEvent) => void,
|
|
10
10
|
disabled = false
|
|
11
11
|
): void {
|
|
12
12
|
const [active, setActive] = useState(false)
|
|
@@ -19,23 +19,23 @@ function useClickOutside<T extends HTMLElement>(
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
const handleEvent = useCallback(
|
|
22
|
-
function handleEvent(
|
|
22
|
+
function handleEvent(event: MouseEvent | TouchEvent | KeyboardEvent) {
|
|
23
23
|
function hasPressedEsc() {
|
|
24
|
-
return KeyboardKey((
|
|
24
|
+
return KeyboardKey((event as unknown) as React.KeyboardEvent).is('ESCAPE')
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
if (!getContainer() || disabled) {
|
|
28
28
|
return
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const target =
|
|
31
|
+
const target = event.target as Node
|
|
32
32
|
const hasTarget = getContainer()?.contains(target)
|
|
33
33
|
|
|
34
34
|
if (!active && hasTarget) {
|
|
35
35
|
setActive(true)
|
|
36
36
|
} else if (active && (!hasTarget || hasPressedEsc())) {
|
|
37
37
|
setActive(false)
|
|
38
|
-
callback()
|
|
38
|
+
callback(event)
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
[active, callback, disabled, getContainer]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
|
-
import { waitFor, within
|
|
2
|
+
import { queries, waitFor, within } from '@testing-library/dom'
|
|
3
3
|
import { act } from '@testing-library/react'
|
|
4
|
+
import userEvent from '@testing-library/user-event'
|
|
4
5
|
|
|
5
6
|
// based on https://github.com/romgain/react-select-event/blob/master/src/index.ts
|
|
6
7
|
|
|
@@ -42,7 +43,7 @@ async function expand(input: HTMLElement): Promise<void> {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
await act(async () => {
|
|
45
|
-
|
|
46
|
+
userEvent.click(within(selectContainer).getByTestId('select-trigger-handle'))
|
|
46
47
|
|
|
47
48
|
expect(await queries.findByRole(selectContainer, 'listbox')).toBeInTheDocument()
|
|
48
49
|
})
|
|
@@ -62,7 +63,7 @@ async function collapse(input: HTMLElement): Promise<void> {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
await act(async () => {
|
|
65
|
-
|
|
66
|
+
userEvent.click(within(selectContainer).getByTestId('select-trigger-handle'))
|
|
66
67
|
|
|
67
68
|
await waitFor(() => {
|
|
68
69
|
expect(queries.queryByRole(selectContainer, 'listbox')).not.toBeInTheDocument()
|
|
@@ -88,7 +89,7 @@ async function select(option: string, input: HTMLElement): Promise<void> {
|
|
|
88
89
|
|
|
89
90
|
// click the option if exists; Select currently closes when an item is clicked.
|
|
90
91
|
if (optionElement && optionElement.getAttribute('aria-selected') == 'false') {
|
|
91
|
-
|
|
92
|
+
userEvent.click(optionElement)
|
|
92
93
|
}
|
|
93
94
|
})
|
|
94
95
|
|
|
@@ -114,7 +115,7 @@ async function unselect(option: string, input: HTMLElement): Promise<void> {
|
|
|
114
115
|
|
|
115
116
|
// ensures that the option exists and IS selected
|
|
116
117
|
if (optionElement && optionElement.getAttribute('aria-selected') == 'true') {
|
|
117
|
-
|
|
118
|
+
userEvent.click(optionElement)
|
|
118
119
|
}
|
|
119
120
|
})
|
|
120
121
|
|
|
@@ -136,7 +137,7 @@ async function clear(input: HTMLElement): Promise<void> {
|
|
|
136
137
|
const clearButton = within(selectContainer).getByTestId('select-trigger-clear')
|
|
137
138
|
|
|
138
139
|
if (clearButton) {
|
|
139
|
-
|
|
140
|
+
userEvent.click(clearButton)
|
|
140
141
|
}
|
|
141
142
|
})
|
|
142
143
|
}
|
|
@@ -151,7 +152,7 @@ async function search(query: string, input: HTMLElement): Promise<void> {
|
|
|
151
152
|
const selectContainer = getReactSelectContainerFromInput(input)
|
|
152
153
|
|
|
153
154
|
await act(async () => {
|
|
154
|
-
|
|
155
|
+
userEvent.type(input, query)
|
|
155
156
|
|
|
156
157
|
await queries.findAllByRole(selectContainer, 'option')
|
|
157
158
|
})
|