@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.
Files changed (49) hide show
  1. package/dist/components/Button/Button.d.ts +1 -0
  2. package/dist/components/Calendar/Pickers/PickerButton.d.ts +1 -1
  3. package/dist/components/Dropdown/Dropdown.stories.d.ts +6 -7
  4. package/dist/components/Dropdown/Dropdown.types.d.ts +2 -2
  5. package/dist/components/Dropdown/DropdownTrigger.d.ts +1 -1
  6. package/dist/components/Dropdown/useDropdown.d.ts +1 -5
  7. package/dist/components/Icon/Icon.d.ts +1 -0
  8. package/dist/components/Select/Select.types.d.ts +4 -4
  9. package/dist/components/Table/Table.d.ts +7 -1
  10. package/dist/components/Table/Table.fixtures.d.ts +1 -0
  11. package/dist/components/Table/Table.stories.d.ts +20 -4
  12. package/dist/components/Table/Table.types.d.ts +30 -1
  13. package/dist/components/Table/useSortBy.d.ts +2 -2
  14. package/dist/components/Table/useSortBy.types.d.ts +5 -6
  15. package/dist/hooks/useClickOutside/useClickOutside.d.ts +1 -1
  16. package/dist/index.js +448 -440
  17. package/dist/index.js.map +1 -1
  18. package/dist/loadsmart.theme-63c13988.js +2 -0
  19. package/dist/{loadsmart.theme-37a60d56.js.map → loadsmart.theme-63c13988.js.map} +1 -1
  20. package/dist/{prop-06c02f6d.js → prop-0c635ee9.js} +2 -2
  21. package/dist/{prop-06c02f6d.js.map → prop-0c635ee9.js.map} +1 -1
  22. package/dist/testing/index.js +1 -1
  23. package/dist/testing/index.js.map +1 -1
  24. package/dist/theming/index.js +1 -1
  25. package/dist/tools/index.js +1 -1
  26. package/package.json +1 -1
  27. package/src/components/Button/Button.tsx +1 -1
  28. package/src/components/Calendar/Pickers/DayPicker.tsx +9 -1
  29. package/src/components/Calendar/Pickers/PickerButton.tsx +14 -6
  30. package/src/components/Dropdown/Dropdown.stories.tsx +26 -75
  31. package/src/components/Dropdown/Dropdown.tsx +11 -8
  32. package/src/components/Dropdown/Dropdown.types.ts +2 -2
  33. package/src/components/Dropdown/DropdownTrigger.tsx +1 -1
  34. package/src/components/Dropdown/useDropdown.ts +1 -6
  35. package/src/components/Icon/Icon.tsx +2 -0
  36. package/src/components/Icon/assets/dots-horizontal.svg +1 -0
  37. package/src/components/Select/Select.test.tsx +23 -1
  38. package/src/components/Select/Select.types.ts +4 -4
  39. package/src/components/Select/useSelect.ts +11 -9
  40. package/src/components/Table/Table.fixtures.ts +26 -16
  41. package/src/components/Table/Table.stories.tsx +156 -25
  42. package/src/components/Table/Table.test.tsx +29 -0
  43. package/src/components/Table/Table.tsx +85 -1
  44. package/src/components/Table/Table.types.ts +39 -1
  45. package/src/components/Table/useSortBy.ts +5 -5
  46. package/src/components/Table/useSortBy.types.ts +5 -5
  47. package/src/hooks/useClickOutside/useClickOutside.ts +6 -6
  48. package/src/testing/SelectEvent/SelectEvent.ts +8 -7
  49. 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 Selection(args: TableProps): JSX.Element {
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, 5)
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
- Selection.parameters = {
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
- Selection.args = {
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 ADVANCED_ROWS = TABLE_ROWS.map((row) => {
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 AdvancedSelection(): JSX.Element {
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
- {ADVANCED_ROWS.map((row) => (
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, 5)
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
- AdvancedSelection.parameters = {
430
+ WithCustomRowSelection.parameters = {
295
431
  docs: {
296
432
  description: {
297
433
  story: `
298
- The \`<Table>\` component accepts a \`selection\` prop, an object for the [\`useSelectable\`*(read more)*](#) hook, where you can specify if it's multiple or single, register an \`onChange\` callback, set your adapters, and even initially selected rows.
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
- ### \`selection.onChange\`
436
+ **\`selection\`**
305
437
 
306
- \`function\`: defaults to \`undefined\`, callback that's invoked with the current selected rows for every change.
307
-
308
- ### \`selection.selected\`
309
-
310
- \`array\`: defaults to \`[]\`, set the initial selected rows.
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
- {children}
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, Column, ColumnGroup, SortState } from './useSortBy.types'
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 {Column} sortConfig - sorting config including `key` and `isSortedDesc`
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: Column, tableRowGroup: TableRowGroup): TableRowGroup {
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: Column) => void
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: 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 type Column = {
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<Column>
10
+ export type ColumnGroup = Array<SortableColumn>
11
11
 
12
12
  export interface SortState {
13
- sortConfig: Column
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(e: Event) {
22
+ function handleEvent(event: MouseEvent | TouchEvent | KeyboardEvent) {
23
23
  function hasPressedEsc() {
24
- return KeyboardKey((e as unknown) as React.KeyboardEvent).is('ESCAPE')
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 = e.target as Node
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, queries, fireEvent } from '@testing-library/dom'
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
- fireEvent.click(within(selectContainer).getByTestId('select-trigger-handle'))
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
- fireEvent.click(within(selectContainer).getByTestId('select-trigger-handle'))
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
- fireEvent.click(optionElement)
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
- fireEvent.click(optionElement)
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
- fireEvent.click(clearButton)
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
- fireEvent.change(input, { target: { value: query } })
155
+ userEvent.type(input, query)
155
156
 
156
157
  await queries.findAllByRole(selectContainer, 'option')
157
158
  })