@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.
Files changed (44) hide show
  1. package/dist/components/Dropdown/Dropdown.types.d.ts +2 -2
  2. package/dist/components/Dropdown/useDropdown.d.ts +5 -1
  3. package/dist/components/Select/Select.types.d.ts +4 -4
  4. package/dist/components/Table/Table.d.ts +9 -1
  5. package/dist/components/Table/Table.stories.d.ts +6 -0
  6. package/dist/components/Table/Table.types.d.ts +8 -0
  7. package/dist/components/VisuallyHidden/VisuallyHidden.d.ts +1 -0
  8. package/dist/components/VisuallyHidden/VisuallyHidden.stories.d.ts +14 -0
  9. package/dist/components/VisuallyHidden/VisuallyHidden.test.d.ts +1 -0
  10. package/dist/components/VisuallyHidden/index.d.ts +1 -0
  11. package/dist/hooks/useClickOutside/useClickOutside.d.ts +1 -1
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +82 -71
  14. package/dist/index.js.map +1 -1
  15. package/dist/loadsmart.theme-37a60d56.js +2 -0
  16. package/dist/{loadsmart.theme-63c13988.js.map → loadsmart.theme-37a60d56.js.map} +1 -1
  17. package/dist/{prop-0c635ee9.js → prop-06c02f6d.js} +2 -2
  18. package/dist/{prop-0c635ee9.js.map → prop-06c02f6d.js.map} +1 -1
  19. package/dist/testing/index.js +1 -1
  20. package/dist/testing/index.js.map +1 -1
  21. package/dist/theming/index.js +1 -1
  22. package/dist/tools/index.js +1 -1
  23. package/package.json +3 -2
  24. package/src/components/DragDropFile/components/DropZone.test.tsx +8 -0
  25. package/src/components/DragDropFile/components/DropZone.tsx +11 -9
  26. package/src/components/Dropdown/Dropdown.tsx +8 -11
  27. package/src/components/Dropdown/Dropdown.types.ts +2 -2
  28. package/src/components/Dropdown/useDropdown.ts +6 -1
  29. package/src/components/Select/Select.test.tsx +3 -25
  30. package/src/components/Select/Select.types.ts +4 -4
  31. package/src/components/Select/useSelect.ts +9 -11
  32. package/src/components/Table/Table.stories.tsx +61 -0
  33. package/src/components/Table/Table.test.tsx +128 -0
  34. package/src/components/Table/Table.tsx +89 -12
  35. package/src/components/Table/Table.types.ts +9 -0
  36. package/src/components/VisuallyHidden/VisuallyHidden.mdx +26 -0
  37. package/src/components/VisuallyHidden/VisuallyHidden.stories.tsx +32 -0
  38. package/src/components/VisuallyHidden/VisuallyHidden.test.tsx +18 -0
  39. package/src/components/VisuallyHidden/VisuallyHidden.tsx +6 -0
  40. package/src/components/VisuallyHidden/index.ts +1 -0
  41. package/src/hooks/useClickOutside/useClickOutside.ts +6 -6
  42. package/src/index.ts +2 -0
  43. package/src/testing/SelectEvent/SelectEvent.ts +7 -8
  44. 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 (isDragging) {
55
- setIsDragging(false)
56
- }
54
+ if (!disabled) {
55
+ if (isDragging) {
56
+ setIsDragging(false)
57
+ }
57
58
 
58
- onFilesAdded(Array.from(event.dataTransfer.files || []))
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
- ref,
48
- function handleClickOutside(event?: MouseEvent | TouchEvent | KeyboardEvent) {
49
- onBlur?.(event)
46
+ useClickOutside(ref, function handleClickOutside() {
47
+ onBlur?.()
50
48
 
51
- if (!expanded) {
52
- return
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?: (event?: MouseEvent | TouchEvent | KeyboardEvent) => void
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 { DropdownProps } from './Dropdown.types'
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.getByTestId('custom-empty')).toHaveTextContent('No fruit found.')
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: unknown) => ({
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 { DropdownMenuItemProps, DropdownProps } from 'components/Dropdown'
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: (event?: MouseEvent | TouchEvent | KeyboardEvent) => void
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 { GenericAdapter } from './Select.constants'
14
- import { useSelectable } from './Select.context'
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, onBlur } = props
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(event?: MouseEvent | TouchEvent | KeyboardEvent) {
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, onBlur]
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 StyledTableRow = styled.tr<{ selected?: boolean }>`
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&apos;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
+ ```