@loadsmart/loadsmart-ui 5.4.0 → 5.6.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/Dropdown/Dropdown.types.d.ts +2 -2
- package/dist/components/Dropdown/useDropdown.d.ts +5 -1
- package/dist/components/Select/Select.types.d.ts +4 -4
- package/dist/components/Table/Table.d.ts +9 -1
- package/dist/components/Table/Table.stories.d.ts +6 -0
- package/dist/components/Table/Table.types.d.ts +8 -0
- package/dist/components/VisuallyHidden/VisuallyHidden.d.ts +1 -0
- package/dist/components/VisuallyHidden/VisuallyHidden.stories.d.ts +14 -0
- package/dist/components/VisuallyHidden/VisuallyHidden.test.d.ts +1 -0
- package/dist/components/VisuallyHidden/index.d.ts +1 -0
- package/dist/hooks/useClickOutside/useClickOutside.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +82 -71
- package/dist/index.js.map +1 -1
- package/dist/loadsmart.theme-37a60d56.js +2 -0
- package/dist/{loadsmart.theme-63c13988.js.map → loadsmart.theme-37a60d56.js.map} +1 -1
- package/dist/{prop-0c635ee9.js → prop-06c02f6d.js} +2 -2
- package/dist/{prop-0c635ee9.js.map → prop-06c02f6d.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 +3 -2
- package/src/components/DragDropFile/components/DropZone.test.tsx +8 -0
- package/src/components/DragDropFile/components/DropZone.tsx +11 -9
- package/src/components/Dropdown/Dropdown.tsx +8 -11
- package/src/components/Dropdown/Dropdown.types.ts +2 -2
- package/src/components/Dropdown/useDropdown.ts +6 -1
- package/src/components/Select/Select.test.tsx +3 -25
- package/src/components/Select/Select.types.ts +4 -4
- package/src/components/Select/useSelect.ts +9 -11
- package/src/components/Table/Table.stories.tsx +61 -0
- package/src/components/Table/Table.test.tsx +128 -0
- package/src/components/Table/Table.tsx +89 -12
- package/src/components/Table/Table.types.ts +9 -0
- package/src/components/VisuallyHidden/VisuallyHidden.mdx +26 -0
- package/src/components/VisuallyHidden/VisuallyHidden.stories.tsx +32 -0
- package/src/components/VisuallyHidden/VisuallyHidden.test.tsx +18 -0
- package/src/components/VisuallyHidden/VisuallyHidden.tsx +6 -0
- package/src/components/VisuallyHidden/index.ts +1 -0
- package/src/hooks/useClickOutside/useClickOutside.ts +6 -6
- package/src/index.ts +2 -0
- package/src/testing/SelectEvent/SelectEvent.ts +7 -8
- package/dist/loadsmart.theme-63c13988.js +0 -2
|
@@ -39,11 +39,11 @@ const DropZone = ({
|
|
|
39
39
|
|
|
40
40
|
const onKeyPress = useCallback(
|
|
41
41
|
(event: React.KeyboardEvent) => {
|
|
42
|
-
if (inputRef.current && KeyboardKey(event).is('ENTER')) {
|
|
42
|
+
if (!disabled && inputRef.current && KeyboardKey(event).is('ENTER')) {
|
|
43
43
|
inputRef.current.click()
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
|
-
[inputRef]
|
|
46
|
+
[disabled, inputRef]
|
|
47
47
|
)
|
|
48
48
|
|
|
49
49
|
const onDrop = useCallback(
|
|
@@ -51,13 +51,15 @@ const DropZone = ({
|
|
|
51
51
|
event.preventDefault()
|
|
52
52
|
event.stopPropagation()
|
|
53
53
|
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
if (!disabled) {
|
|
55
|
+
if (isDragging) {
|
|
56
|
+
setIsDragging(false)
|
|
57
|
+
}
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
onFilesAdded(Array.from(event.dataTransfer.files || []))
|
|
60
|
+
}
|
|
59
61
|
},
|
|
60
|
-
[isDragging, onFilesAdded]
|
|
62
|
+
[disabled, isDragging, onFilesAdded]
|
|
61
63
|
)
|
|
62
64
|
|
|
63
65
|
const onDragStart = useCallback((event: React.DragEvent<HTMLDivElement>) => {
|
|
@@ -68,11 +70,11 @@ const DropZone = ({
|
|
|
68
70
|
(event: React.DragEvent<HTMLDivElement>) => {
|
|
69
71
|
event.preventDefault()
|
|
70
72
|
|
|
71
|
-
if (!isDragging) {
|
|
73
|
+
if (!disabled && !isDragging) {
|
|
72
74
|
setIsDragging(true)
|
|
73
75
|
}
|
|
74
76
|
},
|
|
75
|
-
[isDragging]
|
|
77
|
+
[disabled, isDragging]
|
|
76
78
|
)
|
|
77
79
|
|
|
78
80
|
const onDragLeave = useCallback(
|
|
@@ -43,18 +43,15 @@ export function GenericDropdown(props: GenericDropdownProps): JSX.Element {
|
|
|
43
43
|
const [contextValue, setContextValue] = useState({ expanded, toggle, disabled })
|
|
44
44
|
const ref = useRef(null)
|
|
45
45
|
|
|
46
|
-
useClickOutside(
|
|
47
|
-
|
|
48
|
-
function handleClickOutside(event?: MouseEvent | TouchEvent | KeyboardEvent) {
|
|
49
|
-
onBlur?.(event)
|
|
46
|
+
useClickOutside(ref, function handleClickOutside() {
|
|
47
|
+
onBlur?.()
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
toggle()
|
|
49
|
+
if (!expanded) {
|
|
50
|
+
return
|
|
56
51
|
}
|
|
57
|
-
|
|
52
|
+
|
|
53
|
+
toggle()
|
|
54
|
+
})
|
|
58
55
|
|
|
59
56
|
useEffect(
|
|
60
57
|
function updateContextValue() {
|
|
@@ -76,7 +73,7 @@ export function GenericDropdown(props: GenericDropdownProps): JSX.Element {
|
|
|
76
73
|
}
|
|
77
74
|
|
|
78
75
|
/**
|
|
79
|
-
* @example
|
|
76
|
+
* @example
|
|
80
77
|
<Dropdown>
|
|
81
78
|
<Dropdown.Trigger>Download</Dropdown.Trigger>
|
|
82
79
|
<Dropdown.Menu
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ButtonProps } from 'components/Button'
|
|
2
1
|
import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
import type { ButtonProps } from 'components/Button'
|
|
3
3
|
|
|
4
4
|
export interface useDropdownProps {
|
|
5
5
|
expanded: boolean
|
|
@@ -15,7 +15,7 @@ export interface useDropdownReturn {
|
|
|
15
15
|
|
|
16
16
|
export interface DropdownProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'onBlur'> {
|
|
17
17
|
disabled?: boolean
|
|
18
|
-
onBlur?: (
|
|
18
|
+
onBlur?: () => void
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface GenericDropdownProps extends DropdownProps, useDropdownProps {}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { HTMLAttributes } from 'react'
|
|
4
|
+
|
|
5
|
+
export interface DropdownProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'onBlur'> {
|
|
6
|
+
disabled?: boolean
|
|
7
|
+
onBlur?: () => void
|
|
8
|
+
}
|
|
4
9
|
|
|
5
10
|
export interface useDropdownProps {
|
|
6
11
|
expanded: boolean
|
|
@@ -11,7 +11,6 @@ import selectEvent from '../../testing/SelectEvent'
|
|
|
11
11
|
|
|
12
12
|
import type { SelectProps, Option, GenericOption } from './Select.types'
|
|
13
13
|
import Select from './Select'
|
|
14
|
-
import userEvent from '@testing-library/user-event'
|
|
15
14
|
|
|
16
15
|
const {
|
|
17
16
|
Playground,
|
|
@@ -160,27 +159,6 @@ describe('Select', () => {
|
|
|
160
159
|
expect(items.length).toBeGreaterThanOrEqual(1)
|
|
161
160
|
expect(items[0]).toHaveTextContent(FRUITS[indexToSearch].label)
|
|
162
161
|
})
|
|
163
|
-
|
|
164
|
-
it('calls blur when focus is lost', async () => {
|
|
165
|
-
const onBlur = jest.fn((event?: MouseEvent | TouchEvent | KeyboardEvent) => event)
|
|
166
|
-
|
|
167
|
-
setup({ onBlur })
|
|
168
|
-
|
|
169
|
-
const selectInput = screen.getByLabelText('Select your favorite fruit')
|
|
170
|
-
|
|
171
|
-
await selectEvent.expand(selectInput)
|
|
172
|
-
|
|
173
|
-
userEvent.click(document.body)
|
|
174
|
-
|
|
175
|
-
await selectEvent.expand(selectInput)
|
|
176
|
-
|
|
177
|
-
userEvent.keyboard('{Escape}{/Escape}')
|
|
178
|
-
|
|
179
|
-
expect(onBlur).toHaveBeenCalledTimes(2)
|
|
180
|
-
|
|
181
|
-
expect(onBlur).toHaveBeenNthCalledWith(1, new MouseEvent('mousedown'))
|
|
182
|
-
expect(onBlur).toHaveBeenNthCalledWith(2, new KeyboardEvent('keyup'))
|
|
183
|
-
})
|
|
184
162
|
})
|
|
185
163
|
|
|
186
164
|
describe('Single selection', () => {
|
|
@@ -517,7 +495,7 @@ describe('Select', () => {
|
|
|
517
495
|
|
|
518
496
|
it.each([[{ multiple: true }], [{ multiple: false }]])(
|
|
519
497
|
'overrides the empty component with %s',
|
|
520
|
-
(props) => {
|
|
498
|
+
async (props) => {
|
|
521
499
|
const Empty = () => {
|
|
522
500
|
return <Select.Empty data-testid="custom-empty">No fruit found.</Select.Empty>
|
|
523
501
|
}
|
|
@@ -530,7 +508,7 @@ describe('Select', () => {
|
|
|
530
508
|
target: { value: 'foo' },
|
|
531
509
|
})
|
|
532
510
|
|
|
533
|
-
expect(screen.
|
|
511
|
+
expect(await screen.findByTestId('custom-empty')).toHaveTextContent('No fruit found.')
|
|
534
512
|
}
|
|
535
513
|
)
|
|
536
514
|
|
|
@@ -861,7 +839,7 @@ describe('Select', () => {
|
|
|
861
839
|
const setup = (overrides: Partial<SelectProps>) =>
|
|
862
840
|
renderer(<Playground {...overrides} options={SelectableKeyTypeOptions} />).render()
|
|
863
841
|
|
|
864
|
-
const expectedValue = (value:
|
|
842
|
+
const expectedValue = (value: any) => ({
|
|
865
843
|
target: {
|
|
866
844
|
id: 'select-playground',
|
|
867
845
|
name: 'select-playground',
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import EventLike from 'utils/types/EventLike'
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { ChangeEvent, FocusEvent, ComponentType, HTMLAttributes } from 'react'
|
|
5
|
+
import type { DropdownProps, DropdownMenuItemProps } from 'components/Dropdown'
|
|
5
6
|
import type { TextFieldProps } from 'components/TextField'
|
|
6
7
|
import type {
|
|
7
|
-
Selectable,
|
|
8
8
|
SelectableAdapter,
|
|
9
|
+
Selectable,
|
|
9
10
|
SelectableKeyType,
|
|
10
11
|
SelectableState,
|
|
11
12
|
} from 'hooks/useSelectable'
|
|
12
13
|
import { useSelectableReturn } from 'hooks/useSelectable/useSelectable.types'
|
|
13
|
-
import type { ChangeEvent, ComponentType, FocusEvent, HTMLAttributes } from 'react'
|
|
14
14
|
|
|
15
15
|
export type Option = Selectable
|
|
16
16
|
export interface GenericOption {
|
|
@@ -119,7 +119,7 @@ export type useSelectReturn = {
|
|
|
119
119
|
getDropdownProps: () => {
|
|
120
120
|
toggle: () => void
|
|
121
121
|
expanded: boolean
|
|
122
|
-
onBlur: (
|
|
122
|
+
onBlur: () => void
|
|
123
123
|
}
|
|
124
124
|
getCreatebleProps: () => CreatableProps
|
|
125
125
|
isCreatable: () => boolean
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
3
|
import { isFunction } from '@loadsmart/utils-function'
|
|
3
4
|
import { isNil } from '@loadsmart/utils-object'
|
|
4
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
5
5
|
|
|
6
|
+
import { GenericAdapter } from './Select.constants'
|
|
7
|
+
import { getValue, getDisplayValue, escapeRegExp, getAdapter } from './useSelect.helpers'
|
|
6
8
|
import { useDropdown } from 'components/Dropdown'
|
|
7
|
-
import { useDidMount } from 'hooks/useDidMount'
|
|
8
9
|
import { useFocusTrap } from 'hooks/useFocusTrap'
|
|
9
10
|
import { SelectableKeyType } from 'hooks/useSelectable'
|
|
11
|
+
import { useSelectable } from './Select.context'
|
|
10
12
|
import to from 'utils/toolset/awaitTo'
|
|
11
|
-
import { isThenable } from 'utils/toolset/isThenable'
|
|
12
13
|
import toArray from 'utils/toolset/toArray'
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { escapeRegExp, getAdapter, getDisplayValue, getValue } from './useSelect.helpers'
|
|
14
|
+
import { isThenable } from 'utils/toolset/isThenable'
|
|
15
|
+
import { useDidMount } from 'hooks/useDidMount'
|
|
16
16
|
|
|
17
17
|
import type { ChangeEvent, FocusEvent } from 'react'
|
|
18
18
|
import type {
|
|
@@ -191,7 +191,7 @@ function useOptions<T = any>(props: { datasources: SelectDatasource<T>[]; adapte
|
|
|
191
191
|
*/
|
|
192
192
|
function useSelect(props: SelectProps): useSelectReturn {
|
|
193
193
|
const didMount = useDidMount()
|
|
194
|
-
const { multiple, onQueryChange, onChange, onCreate, id, name, disabled = false
|
|
194
|
+
const { multiple, onQueryChange, onChange, onCreate, id, name, disabled = false } = props
|
|
195
195
|
const dropdown = useDropdown(props)
|
|
196
196
|
|
|
197
197
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -264,19 +264,17 @@ function useSelect(props: SelectProps): useSelectReturn {
|
|
|
264
264
|
return {
|
|
265
265
|
toggle: dropdown.toggle,
|
|
266
266
|
expanded: dropdown.expanded,
|
|
267
|
-
onBlur(
|
|
267
|
+
onBlur() {
|
|
268
268
|
if (!multiple) {
|
|
269
269
|
setQuery(getDisplayValue(adapters, selectable.selected, multiple))
|
|
270
270
|
} else {
|
|
271
271
|
setQuery('')
|
|
272
272
|
}
|
|
273
273
|
options.reset()
|
|
274
|
-
|
|
275
|
-
onBlur?.(event)
|
|
276
274
|
},
|
|
277
275
|
}
|
|
278
276
|
},
|
|
279
|
-
[adapters, dropdown.expanded, dropdown.toggle, multiple, options, selectable.selected
|
|
277
|
+
[adapters, dropdown.expanded, dropdown.toggle, multiple, options, selectable.selected]
|
|
280
278
|
)
|
|
281
279
|
|
|
282
280
|
const getTriggerProps = useCallback(
|
|
@@ -508,3 +508,64 @@ This function is passed a boolean \`selected\` and a function \`toggle\`.
|
|
|
508
508
|
},
|
|
509
509
|
},
|
|
510
510
|
}
|
|
511
|
+
|
|
512
|
+
export function WithExpandableRow(args: TableProps): JSX.Element {
|
|
513
|
+
const leading = (expanded: boolean) => {
|
|
514
|
+
if (expanded) {
|
|
515
|
+
return <div style={{ fontWeight: 'bold' }}>Open</div>
|
|
516
|
+
}
|
|
517
|
+
return <div style={{ fontWeight: 'bold' }}>Closed</div>
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return (
|
|
521
|
+
<div className="p-4 bg-neutral-white">
|
|
522
|
+
<Table {...args}>
|
|
523
|
+
<Table.Head>
|
|
524
|
+
<Table.Row>
|
|
525
|
+
<Table.Expandable.HeadCell />
|
|
526
|
+
<Table.Expandable.HeadCell>Reference</Table.Expandable.HeadCell>
|
|
527
|
+
<Table.Expandable.HeadCell>Product</Table.Expandable.HeadCell>
|
|
528
|
+
<Table.Expandable.HeadCell>Price</Table.Expandable.HeadCell>
|
|
529
|
+
</Table.Row>
|
|
530
|
+
</Table.Head>
|
|
531
|
+
|
|
532
|
+
<Table.Body>
|
|
533
|
+
<Table.Expandable.Row
|
|
534
|
+
index={0}
|
|
535
|
+
leading={leading}
|
|
536
|
+
expandableContent={<div>Forced expanded with custom leading</div>}
|
|
537
|
+
expanded
|
|
538
|
+
>
|
|
539
|
+
<Table.Expandable.Cell>123456</Table.Expandable.Cell>
|
|
540
|
+
<Table.Expandable.Cell>Body 1</Table.Expandable.Cell>
|
|
541
|
+
<Table.Expandable.Cell>12345</Table.Expandable.Cell>
|
|
542
|
+
</Table.Expandable.Row>
|
|
543
|
+
<Table.Expandable.Row
|
|
544
|
+
index={1}
|
|
545
|
+
leading={leading}
|
|
546
|
+
expandableContent={<div>Initial expanded row with custom leading</div>}
|
|
547
|
+
initialExpanded
|
|
548
|
+
>
|
|
549
|
+
<Table.Expandable.Cell>123456</Table.Expandable.Cell>
|
|
550
|
+
<Table.Expandable.Cell>Body 2</Table.Expandable.Cell>
|
|
551
|
+
<Table.Expandable.Cell>12345</Table.Expandable.Cell>
|
|
552
|
+
</Table.Expandable.Row>
|
|
553
|
+
<Table.Expandable.Row index={2} expandableContent={<div>Expandable row</div>}>
|
|
554
|
+
<Table.Expandable.Cell>123456</Table.Expandable.Cell>
|
|
555
|
+
<Table.Expandable.Cell>Body 3</Table.Expandable.Cell>
|
|
556
|
+
<Table.Expandable.Cell>12345</Table.Expandable.Cell>
|
|
557
|
+
</Table.Expandable.Row>
|
|
558
|
+
<Table.Expandable.Row index={3} expanded>
|
|
559
|
+
<Table.Expandable.Cell>123456</Table.Expandable.Cell>
|
|
560
|
+
<Table.Expandable.Cell>Body 5</Table.Expandable.Cell>
|
|
561
|
+
<Table.Expandable.Cell>12345</Table.Expandable.Cell>
|
|
562
|
+
</Table.Expandable.Row>
|
|
563
|
+
</Table.Body>
|
|
564
|
+
</Table>
|
|
565
|
+
</div>
|
|
566
|
+
)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
WithExpandableRow.args = {
|
|
570
|
+
scale: 'default',
|
|
571
|
+
}
|
|
@@ -182,4 +182,132 @@ describe('<Table />', () => {
|
|
|
182
182
|
expect(checkboxes[0]).toBeChecked()
|
|
183
183
|
expect(checkboxes[1]).toBeChecked()
|
|
184
184
|
})
|
|
185
|
+
|
|
186
|
+
it('Table.Expandable renders correclty', () => {
|
|
187
|
+
const onExpandedChange = jest.fn()
|
|
188
|
+
|
|
189
|
+
const expandableTable = ({ controlledExpanded4 }: { controlledExpanded4: boolean }) => (
|
|
190
|
+
<React.Fragment>
|
|
191
|
+
<Table.Head>
|
|
192
|
+
<Table.Row>
|
|
193
|
+
<Table.Expandable.HeadCell />
|
|
194
|
+
<Table.HeadCell># Number</Table.HeadCell>
|
|
195
|
+
<Table.HeadCell>Company</Table.HeadCell>
|
|
196
|
+
<Table.HeadCell alignment="right">Price $</Table.HeadCell>
|
|
197
|
+
</Table.Row>
|
|
198
|
+
</Table.Head>
|
|
199
|
+
<Table.Body>
|
|
200
|
+
<Table.Expandable.Row
|
|
201
|
+
index={0}
|
|
202
|
+
expandableContent={<div>This is an expandable content on index 0</div>}
|
|
203
|
+
expanded
|
|
204
|
+
>
|
|
205
|
+
<Table.Cell format="number">#000000-1</Table.Cell>
|
|
206
|
+
<Table.Cell>Modal X 21</Table.Cell>
|
|
207
|
+
<Table.Cell format="currency" alignment="right">
|
|
208
|
+
$9876,50
|
|
209
|
+
</Table.Cell>
|
|
210
|
+
</Table.Expandable.Row>
|
|
211
|
+
<Table.Expandable.Row index={1}>
|
|
212
|
+
<Table.Cell format="number">#000000-2</Table.Cell>
|
|
213
|
+
<Table.Cell>Modal X 21</Table.Cell>
|
|
214
|
+
<Table.Cell format="currency" alignment="right">
|
|
215
|
+
$9876,50
|
|
216
|
+
</Table.Cell>
|
|
217
|
+
</Table.Expandable.Row>
|
|
218
|
+
<Table.Expandable.Row
|
|
219
|
+
index={2}
|
|
220
|
+
expandableContent={<div>This is an expandable content on index 2</div>}
|
|
221
|
+
leading={(expanded) => (expanded ? 'open' : 'closed')}
|
|
222
|
+
onExpandedChange={onExpandedChange}
|
|
223
|
+
>
|
|
224
|
+
<Table.Cell format="number">#000000-3</Table.Cell>
|
|
225
|
+
<Table.Cell>Modal X 21</Table.Cell>
|
|
226
|
+
<Table.Cell format="currency" alignment="right">
|
|
227
|
+
$9876,50
|
|
228
|
+
</Table.Cell>
|
|
229
|
+
</Table.Expandable.Row>
|
|
230
|
+
<Table.Expandable.Row
|
|
231
|
+
index={3}
|
|
232
|
+
expandableContent={<div>This is an expandable content on index 3</div>}
|
|
233
|
+
expanded={controlledExpanded4}
|
|
234
|
+
>
|
|
235
|
+
<Table.Cell format="number">#000000-4</Table.Cell>
|
|
236
|
+
<Table.Cell>Modal X 21</Table.Cell>
|
|
237
|
+
<Table.Cell format="currency" alignment="right">
|
|
238
|
+
$9876,50
|
|
239
|
+
</Table.Cell>
|
|
240
|
+
</Table.Expandable.Row>
|
|
241
|
+
</Table.Body>
|
|
242
|
+
</React.Fragment>
|
|
243
|
+
)
|
|
244
|
+
const props = {
|
|
245
|
+
children: expandableTable({ controlledExpanded4: true }),
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const { rerender } = setup(props)
|
|
249
|
+
|
|
250
|
+
// Check if table is displaying 3 rows
|
|
251
|
+
const row1 = screen.getByText(/000000-1/i).closest('tr')
|
|
252
|
+
expect(row1).toBeInTheDocument()
|
|
253
|
+
|
|
254
|
+
const row2 = screen.getByText(/000000-2/i).closest('tr')
|
|
255
|
+
expect(row2).toBeInTheDocument()
|
|
256
|
+
|
|
257
|
+
const row3 = screen.getByText(/000000-3/i).closest('tr')
|
|
258
|
+
expect(row3).toBeInTheDocument()
|
|
259
|
+
|
|
260
|
+
const row4 = screen.getByText(/000000-4/i).closest('tr')
|
|
261
|
+
expect(row4).toBeInTheDocument()
|
|
262
|
+
|
|
263
|
+
// Row 1 should be forced to be expanded
|
|
264
|
+
expect(screen.getByText(/This is an expandable content on index 0/i)).toBeInTheDocument()
|
|
265
|
+
|
|
266
|
+
// Clicking row1
|
|
267
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
268
|
+
user.click(row1!)
|
|
269
|
+
|
|
270
|
+
// Row 1 can't be collapsed
|
|
271
|
+
expect(screen.getByText(/This is an expandable content on index 0/i)).toBeInTheDocument()
|
|
272
|
+
|
|
273
|
+
// Checking if Row 3 has custom trailing (row is collapsed)
|
|
274
|
+
expect(screen.getByText(/close/i)).toBeInTheDocument()
|
|
275
|
+
|
|
276
|
+
// Expanding row3
|
|
277
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
278
|
+
user.click(row3!)
|
|
279
|
+
expect(screen.getByText(/This is an expandable content on index 2/i)).toBeInTheDocument()
|
|
280
|
+
|
|
281
|
+
// Check if onExpandedChange is triggered
|
|
282
|
+
expect(onExpandedChange).toHaveBeenCalledTimes(1)
|
|
283
|
+
expect(onExpandedChange).toHaveBeenCalledWith(true)
|
|
284
|
+
|
|
285
|
+
// Checking if Row 3 has custom trailing (row is expanded)
|
|
286
|
+
expect(screen.getByText(/open/i)).toBeInTheDocument()
|
|
287
|
+
|
|
288
|
+
// Collapsing row3
|
|
289
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
290
|
+
user.click(row3!)
|
|
291
|
+
expect(screen.queryByText(/This is an expandable content on index 2/i)).not.toBeInTheDocument()
|
|
292
|
+
|
|
293
|
+
// Check if onExpandedChange is triggered
|
|
294
|
+
expect(onExpandedChange).toHaveBeenCalledTimes(2)
|
|
295
|
+
expect(onExpandedChange).toHaveBeenCalledWith(false)
|
|
296
|
+
|
|
297
|
+
// Checking controlled expanded (row4), it's opened by default
|
|
298
|
+
expect(screen.queryByText(/This is an expandable content on index 4/i)).not.toBeInTheDocument()
|
|
299
|
+
|
|
300
|
+
// Clicking on row4 shouldn't change the open state because it's controlled externally
|
|
301
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
302
|
+
user.click(row4!)
|
|
303
|
+
|
|
304
|
+
// row4 should be still opened
|
|
305
|
+
expect(screen.queryByText(/This is an expandable content on index 4/i)).not.toBeInTheDocument()
|
|
306
|
+
|
|
307
|
+
// Re-render to change controlled state
|
|
308
|
+
rerender(expandableTable({ controlledExpanded4: false }))
|
|
309
|
+
|
|
310
|
+
// row4 should be collapsed because externally controlled state is changed
|
|
311
|
+
expect(screen.queryByText(/This is an expandable content on index 4/i)).not.toBeInTheDocument()
|
|
312
|
+
})
|
|
185
313
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { Children, Fragment, isValidElement, useEffect } from 'react'
|
|
1
|
+
import React, { Children, Fragment, isValidElement, useCallback, useEffect, useState } from 'react'
|
|
2
2
|
import styled, { css } from 'styled-components'
|
|
3
3
|
import type { ReactNode } from 'react'
|
|
4
4
|
import { isFunction } from '@loadsmart/utils-function'
|
|
@@ -35,6 +35,7 @@ import type {
|
|
|
35
35
|
SelectionCellProps,
|
|
36
36
|
TablePickerItemProps,
|
|
37
37
|
TablePickerProps,
|
|
38
|
+
ExpandableTableRowProps,
|
|
38
39
|
} from './Table.types'
|
|
39
40
|
|
|
40
41
|
const StyledTableBody = styled.tbody`
|
|
@@ -104,27 +105,31 @@ const StyledTableHead = styled.thead`
|
|
|
104
105
|
}
|
|
105
106
|
`
|
|
106
107
|
|
|
107
|
-
const
|
|
108
|
+
const BaseStyledTableRow = styled.tr`
|
|
108
109
|
${StyledTableHead} > & {
|
|
109
110
|
height: 36px;
|
|
110
111
|
|
|
111
112
|
background-color: ${token('color-neutral-white')};
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
${StyledTableBody} > & {
|
|
116
|
+
${hoverable`
|
|
117
|
+
outline: 1px solid ${token('color-accent')};
|
|
118
|
+
`}
|
|
119
|
+
|
|
120
|
+
${focusable`
|
|
121
|
+
box-shadow: inset ${token('shadow-glow-primary')};
|
|
122
|
+
`}
|
|
123
|
+
}
|
|
124
|
+
`
|
|
125
|
+
|
|
126
|
+
const StyledTableRow = styled(BaseStyledTableRow)<{ selected?: boolean }>`
|
|
114
127
|
background-color: ${conditional({
|
|
115
128
|
'table-row-selected-color': whenProps({ selected: true }),
|
|
116
129
|
'color-transparent': whenProps({ selected: false }),
|
|
117
130
|
})};
|
|
118
131
|
|
|
119
132
|
${StyledTableBody} > & {
|
|
120
|
-
${hoverable`
|
|
121
|
-
outline: 1px solid ${token('color-accent')};
|
|
122
|
-
`}
|
|
123
|
-
|
|
124
|
-
${focusable`
|
|
125
|
-
box-shadow: inset ${token('shadow-glow-primary')};
|
|
126
|
-
`}
|
|
127
|
-
|
|
128
133
|
&:nth-child(odd) ${StyledTableCell} {
|
|
129
134
|
background: ${token('table-row-variant-color')};
|
|
130
135
|
}
|
|
@@ -135,6 +140,13 @@ const StyledTableRow = styled.tr<{ selected?: boolean }>`
|
|
|
135
140
|
}
|
|
136
141
|
`
|
|
137
142
|
|
|
143
|
+
const StyledExpandableTableRow = styled(BaseStyledTableRow)<{ $even: boolean }>`
|
|
144
|
+
background-color: ${conditional({
|
|
145
|
+
'table-row-variant-color': whenProps({ $even: true }),
|
|
146
|
+
'color-transparent': whenProps({ $even: false }),
|
|
147
|
+
})};
|
|
148
|
+
`
|
|
149
|
+
|
|
138
150
|
const StyledTable = styled.table<{ scale?: string }>`
|
|
139
151
|
width: 100%;
|
|
140
152
|
|
|
@@ -144,7 +156,8 @@ const StyledTable = styled.table<{ scale?: string }>`
|
|
|
144
156
|
|
|
145
157
|
border-collapse: collapse;
|
|
146
158
|
|
|
147
|
-
${StyledTableBody} ${StyledTableRow}
|
|
159
|
+
${StyledTableBody} ${StyledTableRow},
|
|
160
|
+
${StyledTableBody} ${StyledExpandableTableRow} {
|
|
148
161
|
height: ${conditional({
|
|
149
162
|
'24px': whenProps({ scale: 'small' }),
|
|
150
163
|
'48px': whenProps({ scale: 'default' }),
|
|
@@ -153,7 +166,9 @@ const StyledTable = styled.table<{ scale?: string }>`
|
|
|
153
166
|
}
|
|
154
167
|
|
|
155
168
|
${StyledTableBody} ${StyledTableRow} ${StyledTableCell},
|
|
156
|
-
${StyledTableHead} ${StyledTableRow} ${StyledTableHeadCell}
|
|
169
|
+
${StyledTableHead} ${StyledTableRow} ${StyledTableHeadCell},
|
|
170
|
+
${StyledTableBody} ${StyledExpandableTableRow} ${StyledTableCell},
|
|
171
|
+
${StyledTableHead} ${StyledExpandableTableRow} ${StyledTableHeadCell} {
|
|
157
172
|
padding: ${conditional({
|
|
158
173
|
'space-xs': whenProps({ scale: 'small' }),
|
|
159
174
|
'space-s': whenProps({ scale: ['default', 'large'] }),
|
|
@@ -166,6 +181,12 @@ const StyledTable = styled.table<{ scale?: string }>`
|
|
|
166
181
|
}
|
|
167
182
|
`
|
|
168
183
|
|
|
184
|
+
const RotatableIcon = styled(Icon)<{ $rotate: boolean }>`
|
|
185
|
+
${conditional({
|
|
186
|
+
'transform: rotate(90deg);': whenProps({ $rotate: true }),
|
|
187
|
+
})}
|
|
188
|
+
`
|
|
189
|
+
|
|
169
190
|
function Table<T>({
|
|
170
191
|
children,
|
|
171
192
|
selection,
|
|
@@ -269,6 +290,14 @@ function SelectionHeadCell<T>(props: SelectionCellProps<T>): JSX.Element {
|
|
|
269
290
|
)
|
|
270
291
|
}
|
|
271
292
|
|
|
293
|
+
function ExpandableHeadCell(props: TableCellProps): JSX.Element {
|
|
294
|
+
return <TableHeadCell {...props} />
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function ExpandableCell(props: TableCellProps): JSX.Element {
|
|
298
|
+
return <TableCell {...props} />
|
|
299
|
+
}
|
|
300
|
+
|
|
272
301
|
function TableRow({ children, ...others }: TableRowProps): JSX.Element {
|
|
273
302
|
const selected = useIsRowSelected(children)
|
|
274
303
|
|
|
@@ -279,6 +308,49 @@ function TableRow({ children, ...others }: TableRowProps): JSX.Element {
|
|
|
279
308
|
)
|
|
280
309
|
}
|
|
281
310
|
|
|
311
|
+
function ExpandableTableRow({
|
|
312
|
+
index,
|
|
313
|
+
expandableContent,
|
|
314
|
+
expanded,
|
|
315
|
+
leading: propsLeading,
|
|
316
|
+
children,
|
|
317
|
+
onExpandedChange,
|
|
318
|
+
initialExpanded = false,
|
|
319
|
+
...others
|
|
320
|
+
}: ExpandableTableRowProps): JSX.Element {
|
|
321
|
+
const [openState, setOpenState] = useState(initialExpanded)
|
|
322
|
+
|
|
323
|
+
const open = expanded ?? openState
|
|
324
|
+
const colSpan = Array.isArray(children) ? children.length + 1 : 1
|
|
325
|
+
const isEven = index % 2 === 0
|
|
326
|
+
|
|
327
|
+
let leading: ReactNode = <RotatableIcon name="caret-right" $rotate={open} />
|
|
328
|
+
if (propsLeading) {
|
|
329
|
+
leading = isFunction(propsLeading) ? propsLeading(open) : propsLeading
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function toggle() {
|
|
333
|
+
if (!expandableContent) return
|
|
334
|
+
|
|
335
|
+
onExpandedChange?.(!open)
|
|
336
|
+
setOpenState(!open)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<>
|
|
341
|
+
<StyledExpandableTableRow {...others} onClick={toggle} $even={isEven}>
|
|
342
|
+
<ExpandableCell>{expandableContent && leading}</ExpandableCell>
|
|
343
|
+
{children}
|
|
344
|
+
</StyledExpandableTableRow>
|
|
345
|
+
{open && expandableContent && (
|
|
346
|
+
<StyledExpandableTableRow $even={isEven}>
|
|
347
|
+
<StyledTableCell colSpan={colSpan}>{expandableContent}</StyledTableCell>
|
|
348
|
+
</StyledExpandableTableRow>
|
|
349
|
+
)}
|
|
350
|
+
</>
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
282
354
|
function TableHeadCell({
|
|
283
355
|
alignment = 'left',
|
|
284
356
|
children,
|
|
@@ -436,6 +508,11 @@ Table.Selection = {
|
|
|
436
508
|
Cell: SelectionCell,
|
|
437
509
|
HeadCell: SelectionHeadCell,
|
|
438
510
|
}
|
|
511
|
+
Table.Expandable = {
|
|
512
|
+
HeadCell: ExpandableHeadCell,
|
|
513
|
+
Cell: ExpandableCell,
|
|
514
|
+
Row: ExpandableTableRow,
|
|
515
|
+
}
|
|
439
516
|
Table.SortHandle = TableSortHandle
|
|
440
517
|
TablePicker.Item = TablePickerItem
|
|
441
518
|
Table.Picker = TablePicker
|
|
@@ -35,6 +35,15 @@ export interface TableSectionProps extends HTMLAttributes<HTMLTableSectionElemen
|
|
|
35
35
|
|
|
36
36
|
export type TableRowProps = HTMLAttributes<HTMLTableRowElement>
|
|
37
37
|
|
|
38
|
+
export type ExpandableTableRowProps = TableRowProps & {
|
|
39
|
+
index: number
|
|
40
|
+
expandableContent?: ReactNode
|
|
41
|
+
leading?: ReactNode | ((expanded: boolean) => ReactNode)
|
|
42
|
+
initialExpanded?: boolean
|
|
43
|
+
expanded?: boolean
|
|
44
|
+
onExpandedChange?: (expanded: boolean) => void
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
export interface TableCellProps extends HTMLAttributes<HTMLTableCellElement> {
|
|
39
48
|
alignment?: 'left' | 'right'
|
|
40
49
|
format?: 'default' | 'number' | 'currency'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# VisuallyHidden
|
|
2
|
+
|
|
3
|
+
Use this component to visually hide elements but keep them accessible to assistive technologies.
|
|
4
|
+
|
|
5
|
+
Visually hide an element while still allowing it to be exposed to assistive technologies (such as screen readers) with `VisuallyHidden`.
|
|
6
|
+
This would typically be used when you want to take advantage of the behavior and semantics of a native element like a checkbox or radio button, but replace it with a custom styled element visually.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { VisuallyHidden } from '@loadsmart/loadsmart-ui'
|
|
12
|
+
|
|
13
|
+
const Example = () => (
|
|
14
|
+
<Layout.Stack>
|
|
15
|
+
<Text as="p">
|
|
16
|
+
Bellow you can't see the text, but assistive technologies can. Try to use devtools to
|
|
17
|
+
inspect this element.
|
|
18
|
+
</Text>
|
|
19
|
+
<Text as="p">
|
|
20
|
+
<VisuallyHidden>
|
|
21
|
+
This text is invisible but accessible by assistive technologies
|
|
22
|
+
</VisuallyHidden>
|
|
23
|
+
</Text>
|
|
24
|
+
</Layout.Stack>
|
|
25
|
+
)
|
|
26
|
+
```
|