@furystack/shades-common-components 14.0.0 → 15.0.1
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/CHANGELOG.md +71 -0
- package/esm/components/accordion/accordion-item.d.ts.map +1 -1
- package/esm/components/accordion/accordion-item.js +6 -9
- package/esm/components/accordion/accordion-item.js.map +1 -1
- package/esm/components/accordion/accordion.d.ts +7 -0
- package/esm/components/accordion/accordion.d.ts.map +1 -1
- package/esm/components/accordion/accordion.js +4 -1
- package/esm/components/accordion/accordion.js.map +1 -1
- package/esm/components/accordion/accordion.spec.js +91 -50
- package/esm/components/accordion/accordion.spec.js.map +1 -1
- package/esm/components/carousel.js +1 -1
- package/esm/components/carousel.js.map +1 -1
- package/esm/components/chip.d.ts.map +1 -1
- package/esm/components/chip.js +4 -2
- package/esm/components/chip.js.map +1 -1
- package/esm/components/chip.spec.js +42 -0
- package/esm/components/chip.spec.js.map +1 -1
- package/esm/components/command-palette/index.d.ts.map +1 -1
- package/esm/components/command-palette/index.js +14 -1
- package/esm/components/command-palette/index.js.map +1 -1
- package/esm/components/command-palette/index.spec.js +78 -33
- package/esm/components/command-palette/index.spec.js.map +1 -1
- package/esm/components/data-grid/data-grid-row.d.ts.map +1 -1
- package/esm/components/data-grid/data-grid-row.js +18 -2
- package/esm/components/data-grid/data-grid-row.js.map +1 -1
- package/esm/components/data-grid/data-grid.d.ts +7 -0
- package/esm/components/data-grid/data-grid.d.ts.map +1 -1
- package/esm/components/data-grid/data-grid.js +28 -10
- package/esm/components/data-grid/data-grid.js.map +1 -1
- package/esm/components/data-grid/data-grid.spec.js +114 -34
- package/esm/components/data-grid/data-grid.spec.js.map +1 -1
- package/esm/components/data-grid/selection-cell.d.ts.map +1 -1
- package/esm/components/data-grid/selection-cell.js +1 -1
- package/esm/components/data-grid/selection-cell.js.map +1 -1
- package/esm/components/dialog.d.ts +11 -0
- package/esm/components/dialog.d.ts.map +1 -1
- package/esm/components/dialog.js +2 -2
- package/esm/components/dialog.js.map +1 -1
- package/esm/components/dialog.spec.js +54 -2
- package/esm/components/dialog.spec.js.map +1 -1
- package/esm/components/dropdown.d.ts.map +1 -1
- package/esm/components/dropdown.js +1 -1
- package/esm/components/dropdown.js.map +1 -1
- package/esm/components/dropdown.spec.js +8 -0
- package/esm/components/dropdown.spec.js.map +1 -1
- package/esm/components/image.d.ts.map +1 -1
- package/esm/components/image.js +15 -6
- package/esm/components/image.js.map +1 -1
- package/esm/components/image.spec.js +60 -0
- package/esm/components/image.spec.js.map +1 -1
- package/esm/components/inputs/checkbox.d.ts.map +1 -1
- package/esm/components/inputs/checkbox.js +1 -0
- package/esm/components/inputs/checkbox.js.map +1 -1
- package/esm/components/inputs/radio.d.ts.map +1 -1
- package/esm/components/inputs/radio.js +1 -0
- package/esm/components/inputs/radio.js.map +1 -1
- package/esm/components/inputs/slider.d.ts.map +1 -1
- package/esm/components/inputs/slider.js +1 -0
- package/esm/components/inputs/slider.js.map +1 -1
- package/esm/components/inputs/switch.d.ts.map +1 -1
- package/esm/components/inputs/switch.js +1 -0
- package/esm/components/inputs/switch.js.map +1 -1
- package/esm/components/list/list-item.d.ts.map +1 -1
- package/esm/components/list/list-item.js +21 -5
- package/esm/components/list/list-item.js.map +1 -1
- package/esm/components/list/list.d.ts +7 -0
- package/esm/components/list/list.d.ts.map +1 -1
- package/esm/components/list/list.js +28 -8
- package/esm/components/list/list.js.map +1 -1
- package/esm/components/list/list.spec.js +117 -23
- package/esm/components/list/list.spec.js.map +1 -1
- package/esm/components/markdown/markdown-display.d.ts.map +1 -1
- package/esm/components/markdown/markdown-display.js +11 -1
- package/esm/components/markdown/markdown-display.js.map +1 -1
- package/esm/components/markdown/markdown-display.spec.js +97 -0
- package/esm/components/markdown/markdown-display.spec.js.map +1 -1
- package/esm/components/markdown/markdown-editor.spec.js +87 -0
- package/esm/components/markdown/markdown-editor.spec.js.map +1 -1
- package/esm/components/menu/menu.js +1 -1
- package/esm/components/menu/menu.js.map +1 -1
- package/esm/components/modal.d.ts +10 -0
- package/esm/components/modal.d.ts.map +1 -1
- package/esm/components/modal.js +24 -4
- package/esm/components/modal.js.map +1 -1
- package/esm/components/modal.spec.js +86 -1
- package/esm/components/modal.spec.js.map +1 -1
- package/esm/components/page-layout/index.js +1 -1
- package/esm/components/page-layout/index.js.map +1 -1
- package/esm/components/page-layout/index.spec.js +14 -0
- package/esm/components/page-layout/index.spec.js.map +1 -1
- package/esm/components/rating.d.ts.map +1 -1
- package/esm/components/rating.js +28 -21
- package/esm/components/rating.js.map +1 -1
- package/esm/components/rating.spec.js +151 -4
- package/esm/components/rating.spec.js.map +1 -1
- package/esm/components/suggest/index.d.ts.map +1 -1
- package/esm/components/suggest/index.js +14 -1
- package/esm/components/suggest/index.js.map +1 -1
- package/esm/components/suggest/index.spec.js +98 -43
- package/esm/components/suggest/index.spec.js.map +1 -1
- package/esm/components/suggest/suggest-manager.js +2 -2
- package/esm/components/suggest/suggest-manager.js.map +1 -1
- package/esm/components/tabs.d.ts.map +1 -1
- package/esm/components/tabs.js +4 -0
- package/esm/components/tabs.js.map +1 -1
- package/esm/components/tree/tree-item.d.ts.map +1 -1
- package/esm/components/tree/tree-item.js +18 -5
- package/esm/components/tree/tree-item.js.map +1 -1
- package/esm/components/tree/tree.d.ts +7 -0
- package/esm/components/tree/tree.d.ts.map +1 -1
- package/esm/components/tree/tree.js +12 -3
- package/esm/components/tree/tree.js.map +1 -1
- package/esm/components/tree/tree.spec.js +64 -2
- package/esm/components/tree/tree.spec.js.map +1 -1
- package/esm/services/collection-service.d.ts +9 -0
- package/esm/services/collection-service.d.ts.map +1 -1
- package/esm/services/collection-service.js +33 -11
- package/esm/services/collection-service.js.map +1 -1
- package/esm/services/collection-service.spec.js +33 -24
- package/esm/services/collection-service.spec.js.map +1 -1
- package/esm/services/css-variable-theme.d.ts +7 -0
- package/esm/services/css-variable-theme.d.ts.map +1 -1
- package/esm/services/css-variable-theme.js +23 -0
- package/esm/services/css-variable-theme.js.map +1 -1
- package/esm/services/css-variable-theme.spec.js +1 -0
- package/esm/services/css-variable-theme.spec.js.map +1 -1
- package/esm/services/list-service.d.ts +9 -0
- package/esm/services/list-service.d.ts.map +1 -1
- package/esm/services/list-service.js +13 -13
- package/esm/services/list-service.js.map +1 -1
- package/esm/services/list-service.spec.js +13 -33
- package/esm/services/list-service.spec.js.map +1 -1
- package/esm/services/theme-provider-service.d.ts +3 -0
- package/esm/services/theme-provider-service.d.ts.map +1 -1
- package/esm/services/theme-provider-service.js.map +1 -1
- package/esm/services/tree-service.d.ts.map +1 -1
- package/esm/services/tree-service.js +5 -9
- package/esm/services/tree-service.js.map +1 -1
- package/esm/services/tree-service.spec.js +12 -9
- package/esm/services/tree-service.spec.js.map +1 -1
- package/esm/themes/architect-theme.d.ts +1 -0
- package/esm/themes/architect-theme.d.ts.map +1 -1
- package/esm/themes/architect-theme.js +1 -0
- package/esm/themes/architect-theme.js.map +1 -1
- package/esm/themes/auditore-theme.d.ts +1 -0
- package/esm/themes/auditore-theme.d.ts.map +1 -1
- package/esm/themes/auditore-theme.js +1 -0
- package/esm/themes/auditore-theme.js.map +1 -1
- package/esm/themes/black-mesa-theme.d.ts +1 -0
- package/esm/themes/black-mesa-theme.d.ts.map +1 -1
- package/esm/themes/black-mesa-theme.js +1 -0
- package/esm/themes/black-mesa-theme.js.map +1 -1
- package/esm/themes/chieftain-theme.d.ts +1 -0
- package/esm/themes/chieftain-theme.d.ts.map +1 -1
- package/esm/themes/chieftain-theme.js +1 -0
- package/esm/themes/chieftain-theme.js.map +1 -1
- package/esm/themes/default-dark-theme.d.ts +1 -0
- package/esm/themes/default-dark-theme.d.ts.map +1 -1
- package/esm/themes/default-dark-theme.js +1 -0
- package/esm/themes/default-dark-theme.js.map +1 -1
- package/esm/themes/default-light-theme.d.ts +1 -0
- package/esm/themes/default-light-theme.d.ts.map +1 -1
- package/esm/themes/default-light-theme.js +1 -0
- package/esm/themes/default-light-theme.js.map +1 -1
- package/esm/themes/dragonborn-theme.d.ts +1 -0
- package/esm/themes/dragonborn-theme.d.ts.map +1 -1
- package/esm/themes/dragonborn-theme.js +1 -0
- package/esm/themes/dragonborn-theme.js.map +1 -1
- package/esm/themes/hawkins-theme.d.ts +1 -0
- package/esm/themes/hawkins-theme.d.ts.map +1 -1
- package/esm/themes/hawkins-theme.js +1 -0
- package/esm/themes/hawkins-theme.js.map +1 -1
- package/esm/themes/jedi-theme.d.ts +1 -0
- package/esm/themes/jedi-theme.d.ts.map +1 -1
- package/esm/themes/jedi-theme.js +1 -0
- package/esm/themes/jedi-theme.js.map +1 -1
- package/esm/themes/neon-runner-theme.d.ts +1 -0
- package/esm/themes/neon-runner-theme.d.ts.map +1 -1
- package/esm/themes/neon-runner-theme.js +1 -0
- package/esm/themes/neon-runner-theme.js.map +1 -1
- package/esm/themes/paladin-theme.d.ts +1 -0
- package/esm/themes/paladin-theme.d.ts.map +1 -1
- package/esm/themes/paladin-theme.js +1 -0
- package/esm/themes/paladin-theme.js.map +1 -1
- package/esm/themes/plumber-theme.d.ts +1 -0
- package/esm/themes/plumber-theme.d.ts.map +1 -1
- package/esm/themes/plumber-theme.js +1 -0
- package/esm/themes/plumber-theme.js.map +1 -1
- package/esm/themes/replicant-theme.d.ts +1 -0
- package/esm/themes/replicant-theme.d.ts.map +1 -1
- package/esm/themes/replicant-theme.js +1 -0
- package/esm/themes/replicant-theme.js.map +1 -1
- package/esm/themes/sandworm-theme.d.ts +1 -0
- package/esm/themes/sandworm-theme.d.ts.map +1 -1
- package/esm/themes/sandworm-theme.js +1 -0
- package/esm/themes/sandworm-theme.js.map +1 -1
- package/esm/themes/shadow-broker-theme.d.ts +1 -0
- package/esm/themes/shadow-broker-theme.d.ts.map +1 -1
- package/esm/themes/shadow-broker-theme.js +1 -0
- package/esm/themes/shadow-broker-theme.js.map +1 -1
- package/esm/themes/sith-theme.d.ts +1 -0
- package/esm/themes/sith-theme.d.ts.map +1 -1
- package/esm/themes/sith-theme.js +1 -0
- package/esm/themes/sith-theme.js.map +1 -1
- package/esm/themes/vault-dweller-theme.d.ts +1 -0
- package/esm/themes/vault-dweller-theme.d.ts.map +1 -1
- package/esm/themes/vault-dweller-theme.js +1 -0
- package/esm/themes/vault-dweller-theme.js.map +1 -1
- package/esm/themes/wild-hunt-theme.d.ts +1 -0
- package/esm/themes/wild-hunt-theme.d.ts.map +1 -1
- package/esm/themes/wild-hunt-theme.js +1 -0
- package/esm/themes/wild-hunt-theme.js.map +1 -1
- package/esm/themes/xenomorph-theme.d.ts +1 -0
- package/esm/themes/xenomorph-theme.d.ts.map +1 -1
- package/esm/themes/xenomorph-theme.js +1 -0
- package/esm/themes/xenomorph-theme.js.map +1 -1
- package/package.json +7 -7
- package/src/components/accordion/accordion-item.tsx +9 -14
- package/src/components/accordion/accordion.spec.tsx +134 -79
- package/src/components/accordion/accordion.tsx +13 -1
- package/src/components/carousel.tsx +1 -1
- package/src/components/chip.spec.tsx +64 -0
- package/src/components/chip.tsx +4 -1
- package/src/components/command-palette/index.spec.tsx +95 -33
- package/src/components/command-palette/index.tsx +15 -3
- package/src/components/data-grid/data-grid-row.tsx +20 -2
- package/src/components/data-grid/data-grid.spec.tsx +185 -57
- package/src/components/data-grid/data-grid.tsx +38 -13
- package/src/components/data-grid/selection-cell.tsx +1 -0
- package/src/components/dialog.spec.tsx +77 -2
- package/src/components/dialog.tsx +14 -1
- package/src/components/dropdown.spec.tsx +9 -0
- package/src/components/dropdown.tsx +1 -0
- package/src/components/image.spec.tsx +82 -0
- package/src/components/image.tsx +16 -7
- package/src/components/inputs/checkbox.tsx +1 -0
- package/src/components/inputs/radio.tsx +1 -0
- package/src/components/inputs/slider.tsx +1 -0
- package/src/components/inputs/switch.tsx +1 -0
- package/src/components/list/list-item.tsx +22 -4
- package/src/components/list/list.spec.tsx +165 -32
- package/src/components/list/list.tsx +37 -10
- package/src/components/markdown/markdown-display.spec.tsx +132 -0
- package/src/components/markdown/markdown-display.tsx +12 -1
- package/src/components/markdown/markdown-editor.spec.tsx +123 -0
- package/src/components/menu/menu.tsx +1 -1
- package/src/components/modal.spec.tsx +124 -1
- package/src/components/modal.tsx +41 -3
- package/src/components/page-layout/index.spec.tsx +20 -0
- package/src/components/page-layout/index.tsx +1 -1
- package/src/components/rating.spec.tsx +199 -4
- package/src/components/rating.tsx +28 -22
- package/src/components/suggest/index.spec.tsx +147 -43
- package/src/components/suggest/index.tsx +15 -2
- package/src/components/suggest/suggest-manager.ts +2 -2
- package/src/components/tabs.tsx +4 -0
- package/src/components/tree/tree-item.tsx +19 -4
- package/src/components/tree/tree.spec.tsx +101 -2
- package/src/components/tree/tree.tsx +21 -3
- package/src/services/collection-service.spec.ts +33 -24
- package/src/services/collection-service.ts +35 -13
- package/src/services/css-variable-theme.spec.ts +1 -0
- package/src/services/css-variable-theme.ts +25 -0
- package/src/services/list-service.spec.ts +13 -42
- package/src/services/list-service.ts +15 -13
- package/src/services/theme-provider-service.ts +2 -0
- package/src/services/tree-service.spec.ts +12 -9
- package/src/services/tree-service.ts +5 -8
- package/src/themes/architect-theme.ts +1 -0
- package/src/themes/auditore-theme.ts +1 -0
- package/src/themes/black-mesa-theme.ts +1 -0
- package/src/themes/chieftain-theme.ts +1 -0
- package/src/themes/default-dark-theme.ts +1 -0
- package/src/themes/default-light-theme.ts +1 -0
- package/src/themes/dragonborn-theme.ts +1 -0
- package/src/themes/hawkins-theme.ts +1 -0
- package/src/themes/jedi-theme.ts +1 -0
- package/src/themes/neon-runner-theme.ts +1 -0
- package/src/themes/paladin-theme.ts +1 -0
- package/src/themes/plumber-theme.ts +1 -0
- package/src/themes/replicant-theme.ts +1 -0
- package/src/themes/sandworm-theme.ts +1 -0
- package/src/themes/shadow-broker-theme.ts +1 -0
- package/src/themes/sith-theme.ts +1 -0
- package/src/themes/vault-dweller-theme.ts +1 -0
- package/src/themes/wild-hunt-theme.ts +1 -0
- package/src/themes/xenomorph-theme.ts +1 -0
|
@@ -13,6 +13,17 @@ export type DialogProps = {
|
|
|
13
13
|
actions?: JSX.Element
|
|
14
14
|
maxWidth?: string
|
|
15
15
|
fullWidth?: boolean
|
|
16
|
+
/**
|
|
17
|
+
* When true, traps spatial navigation within the dialog's bounds.
|
|
18
|
+
* Forwarded to the underlying Modal component.
|
|
19
|
+
*/
|
|
20
|
+
trapFocus?: boolean
|
|
21
|
+
/**
|
|
22
|
+
* Section name for spatial navigation scoping.
|
|
23
|
+
* Forwarded to the underlying Modal component.
|
|
24
|
+
* @default 'modal'
|
|
25
|
+
*/
|
|
26
|
+
navSection?: string
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
const showAnimation = async (el: Element | null) => {
|
|
@@ -127,7 +138,7 @@ export const Dialog = Shade<DialogProps>({
|
|
|
127
138
|
},
|
|
128
139
|
},
|
|
129
140
|
render: ({ props, children }) => {
|
|
130
|
-
const { isVisible, title, onClose, actions, maxWidth = '560px', fullWidth } = props
|
|
141
|
+
const { isVisible, title, onClose, actions, maxWidth = '560px', fullWidth, trapFocus = true, navSection } = props
|
|
131
142
|
|
|
132
143
|
const handleClose = () => {
|
|
133
144
|
onClose?.()
|
|
@@ -139,6 +150,8 @@ export const Dialog = Shade<DialogProps>({
|
|
|
139
150
|
onClose={handleClose}
|
|
140
151
|
showAnimation={showAnimation}
|
|
141
152
|
hideAnimation={hideAnimation}
|
|
153
|
+
trapFocus={trapFocus}
|
|
154
|
+
navSection={navSection}
|
|
142
155
|
backdropStyle={{
|
|
143
156
|
display: 'flex',
|
|
144
157
|
alignItems: 'center',
|
|
@@ -416,6 +416,15 @@ describe('Dropdown', () => {
|
|
|
416
416
|
})
|
|
417
417
|
})
|
|
418
418
|
|
|
419
|
+
describe('spatial navigation', () => {
|
|
420
|
+
it('should have data-spatial-nav-passthrough on the backdrop', async () => {
|
|
421
|
+
await usingAsync(await renderDropdown({}), async ({ dropdown }) => {
|
|
422
|
+
const backdrop = dropdown.querySelector('.dropdown-backdrop') as HTMLElement
|
|
423
|
+
expect(backdrop.hasAttribute('data-spatial-nav-passthrough')).toBe(true)
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
|
|
419
428
|
describe('placement', () => {
|
|
420
429
|
it('should accept bottomRight placement', async () => {
|
|
421
430
|
await usingAsync(await renderDropdown({ placement: 'bottomRight' }), async ({ dropdown }) => {
|
|
@@ -345,6 +345,88 @@ describe('Image component', () => {
|
|
|
345
345
|
})
|
|
346
346
|
})
|
|
347
347
|
|
|
348
|
+
it('should be focusable when preview is enabled', async () => {
|
|
349
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
350
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
351
|
+
|
|
352
|
+
initializeShadeRoot({
|
|
353
|
+
injector,
|
|
354
|
+
rootElement,
|
|
355
|
+
jsxElement: <Image src="https://example.com/photo.jpg" preview />,
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
await flushUpdates()
|
|
359
|
+
|
|
360
|
+
const imageComponent = document.querySelector('shade-image') as HTMLElement
|
|
361
|
+
expect(imageComponent.getAttribute('tabindex')).toBe('0')
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('should not be focusable when preview is disabled', async () => {
|
|
366
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
367
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
368
|
+
|
|
369
|
+
initializeShadeRoot({
|
|
370
|
+
injector,
|
|
371
|
+
rootElement,
|
|
372
|
+
jsxElement: <Image src="https://example.com/photo.jpg" />,
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
await flushUpdates()
|
|
376
|
+
|
|
377
|
+
const imageComponent = document.querySelector('shade-image') as HTMLElement
|
|
378
|
+
expect(imageComponent.hasAttribute('tabindex')).toBe(false)
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it('should open lightbox when pressing Enter on a preview-enabled image', async () => {
|
|
383
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
384
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
385
|
+
|
|
386
|
+
initializeShadeRoot({
|
|
387
|
+
injector,
|
|
388
|
+
rootElement,
|
|
389
|
+
jsxElement: <Image src="https://example.com/photo.jpg" alt="My photo" preview />,
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
await flushUpdates()
|
|
393
|
+
|
|
394
|
+
const imageComponent = document.querySelector('shade-image') as HTMLElement
|
|
395
|
+
imageComponent.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
|
|
396
|
+
|
|
397
|
+
await flushUpdates()
|
|
398
|
+
|
|
399
|
+
const lightbox = document.querySelector('.lightbox-backdrop')
|
|
400
|
+
expect(lightbox).not.toBeNull()
|
|
401
|
+
|
|
402
|
+
lightbox?.remove()
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('should open lightbox when pressing Space on a preview-enabled image', async () => {
|
|
407
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
408
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
409
|
+
|
|
410
|
+
initializeShadeRoot({
|
|
411
|
+
injector,
|
|
412
|
+
rootElement,
|
|
413
|
+
jsxElement: <Image src="https://example.com/photo.jpg" alt="My photo" preview />,
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
await flushUpdates()
|
|
417
|
+
|
|
418
|
+
const imageComponent = document.querySelector('shade-image') as HTMLElement
|
|
419
|
+
imageComponent.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }))
|
|
420
|
+
|
|
421
|
+
await flushUpdates()
|
|
422
|
+
|
|
423
|
+
const lightbox = document.querySelector('.lightbox-backdrop')
|
|
424
|
+
expect(lightbox).not.toBeNull()
|
|
425
|
+
|
|
426
|
+
lightbox?.remove()
|
|
427
|
+
})
|
|
428
|
+
})
|
|
429
|
+
|
|
348
430
|
it('should store src and alt as data attributes', async () => {
|
|
349
431
|
await usingAsync(new Injector(), async (injector) => {
|
|
350
432
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
package/src/components/image.tsx
CHANGED
|
@@ -438,13 +438,6 @@ export const Image = Shade<ImageProps>({
|
|
|
438
438
|
style: styleOverrides,
|
|
439
439
|
} = props
|
|
440
440
|
|
|
441
|
-
useHostProps({
|
|
442
|
-
'data-preview': preview ? '' : undefined,
|
|
443
|
-
'data-src': src,
|
|
444
|
-
'data-alt': alt,
|
|
445
|
-
...(styleOverrides ? { style: styleOverrides as Record<string, string> } : {}),
|
|
446
|
-
})
|
|
447
|
-
|
|
448
441
|
const [hasError, setHasError] = useState('hasError', false)
|
|
449
442
|
|
|
450
443
|
const handleClick = () => {
|
|
@@ -466,6 +459,22 @@ export const Image = Shade<ImageProps>({
|
|
|
466
459
|
createLightbox(src, alt)
|
|
467
460
|
}
|
|
468
461
|
|
|
462
|
+
useHostProps({
|
|
463
|
+
'data-preview': preview ? '' : undefined,
|
|
464
|
+
'data-src': src,
|
|
465
|
+
'data-alt': alt,
|
|
466
|
+
tabIndex: preview ? 0 : undefined,
|
|
467
|
+
onkeydown: preview
|
|
468
|
+
? (ev: KeyboardEvent) => {
|
|
469
|
+
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
470
|
+
ev.preventDefault()
|
|
471
|
+
handleClick()
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
: undefined,
|
|
475
|
+
...(styleOverrides ? { style: styleOverrides as Record<string, string> } : {}),
|
|
476
|
+
})
|
|
477
|
+
|
|
469
478
|
return (
|
|
470
479
|
<div ref={imageHostRef} style={{ display: 'contents' }}>
|
|
471
480
|
<img
|
|
@@ -46,8 +46,21 @@ export const ListItem: <T>(props: ListItemProps<T>, children: ChildrenList) => J
|
|
|
46
46
|
const isSelected = selection.includes(item)
|
|
47
47
|
|
|
48
48
|
useHostProps({
|
|
49
|
+
tabIndex: isFocused ? 0 : -1,
|
|
50
|
+
'data-spatial-nav-target': '',
|
|
49
51
|
role: 'option',
|
|
50
52
|
'aria-selected': isSelected.toString(),
|
|
53
|
+
onpointerdown: () => {
|
|
54
|
+
listService.setFocusAnchor()
|
|
55
|
+
},
|
|
56
|
+
onfocus: () => {
|
|
57
|
+
if (listService.focusedItem.getValue() !== item) {
|
|
58
|
+
listService.focusedItem.setValue(item)
|
|
59
|
+
}
|
|
60
|
+
if (!listService.hasFocus.getValue()) {
|
|
61
|
+
listService.hasFocus.setValue(true)
|
|
62
|
+
}
|
|
63
|
+
},
|
|
51
64
|
onclick: (ev: MouseEvent) => {
|
|
52
65
|
listService.handleItemClick(item, ev)
|
|
53
66
|
},
|
|
@@ -65,11 +78,16 @@ export const ListItem: <T>(props: ListItemProps<T>, children: ChildrenList) => J
|
|
|
65
78
|
queueMicrotask(() => {
|
|
66
79
|
const el = wrapperRef.current
|
|
67
80
|
if (!el) return
|
|
81
|
+
const hostEl = el.closest('shade-list-item') as HTMLElement
|
|
82
|
+
if (!hostEl) return
|
|
83
|
+
|
|
84
|
+
if (document.activeElement !== hostEl) {
|
|
85
|
+
hostEl.focus({ preventScroll: true })
|
|
86
|
+
}
|
|
87
|
+
|
|
68
88
|
const scrollContainer = el.closest('shade-list') as HTMLElement
|
|
69
89
|
if (scrollContainer) {
|
|
70
90
|
const containerRect = scrollContainer.getBoundingClientRect()
|
|
71
|
-
const hostEl = el.closest('shade-list-item') as HTMLElement
|
|
72
|
-
if (!hostEl) return
|
|
73
91
|
const itemRect = hostEl.getBoundingClientRect()
|
|
74
92
|
const itemTopInContainer = itemRect.top - containerRect.top
|
|
75
93
|
const itemBottomInContainer = itemRect.bottom - containerRect.top
|
|
@@ -77,12 +95,12 @@ export const ListItem: <T>(props: ListItemProps<T>, children: ChildrenList) => J
|
|
|
77
95
|
if (itemTopInContainer < 0) {
|
|
78
96
|
scrollContainer.scrollTo({
|
|
79
97
|
top: scrollContainer.scrollTop + itemTopInContainer,
|
|
80
|
-
behavior: '
|
|
98
|
+
behavior: 'instant',
|
|
81
99
|
})
|
|
82
100
|
} else if (itemBottomInContainer > scrollContainer.clientHeight) {
|
|
83
101
|
scrollContainer.scrollTo({
|
|
84
102
|
top: scrollContainer.scrollTop + (itemBottomInContainer - scrollContainer.clientHeight),
|
|
85
|
-
behavior: '
|
|
103
|
+
behavior: 'instant',
|
|
86
104
|
})
|
|
87
105
|
}
|
|
88
106
|
}
|
|
@@ -252,7 +252,7 @@ describe('List', () => {
|
|
|
252
252
|
})
|
|
253
253
|
})
|
|
254
254
|
|
|
255
|
-
it('should lose focus on
|
|
255
|
+
it('should lose focus on focusout to an outside element', async () => {
|
|
256
256
|
await usingAsync(new Injector(), async (injector) => {
|
|
257
257
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
258
258
|
const service = createTestService()
|
|
@@ -262,7 +262,7 @@ describe('List', () => {
|
|
|
262
262
|
rootElement,
|
|
263
263
|
jsxElement: (
|
|
264
264
|
<>
|
|
265
|
-
<
|
|
265
|
+
<button data-testid="outside">Outside</button>
|
|
266
266
|
<List<TestItem> items={testItems} listService={service} renderItem={(item) => <span>{item.name}</span>} />
|
|
267
267
|
</>
|
|
268
268
|
),
|
|
@@ -277,7 +277,7 @@ describe('List', () => {
|
|
|
277
277
|
expect(service.hasFocus.getValue()).toBe(true)
|
|
278
278
|
|
|
279
279
|
const outside = document.querySelector('[data-testid="outside"]') as HTMLElement
|
|
280
|
-
|
|
280
|
+
wrapper?.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: outside }))
|
|
281
281
|
|
|
282
282
|
expect(service.hasFocus.getValue()).toBe(false)
|
|
283
283
|
|
|
@@ -336,6 +336,87 @@ describe('List', () => {
|
|
|
336
336
|
service[Symbol.dispose]()
|
|
337
337
|
})
|
|
338
338
|
})
|
|
339
|
+
|
|
340
|
+
it('should not initialize focusedItem on wrapper focusin (items handle focus individually)', async () => {
|
|
341
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
342
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
343
|
+
const service = createTestService()
|
|
344
|
+
|
|
345
|
+
initializeShadeRoot({
|
|
346
|
+
injector,
|
|
347
|
+
rootElement,
|
|
348
|
+
jsxElement: (
|
|
349
|
+
<List<TestItem> items={testItems} listService={service} renderItem={(item) => <span>{item.name}</span>} />
|
|
350
|
+
),
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
await flushUpdates()
|
|
354
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
355
|
+
|
|
356
|
+
expect(service.hasFocus.getValue()).toBe(false)
|
|
357
|
+
expect(service.focusedItem.getValue()).toBeUndefined()
|
|
358
|
+
|
|
359
|
+
service[Symbol.dispose]()
|
|
360
|
+
})
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('should clear hasFocus on focusout when focus moves outside', async () => {
|
|
364
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
365
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
366
|
+
const service = createTestService()
|
|
367
|
+
const outsideEl = document.createElement('button')
|
|
368
|
+
outsideEl.textContent = 'Outside'
|
|
369
|
+
document.body.appendChild(outsideEl)
|
|
370
|
+
|
|
371
|
+
initializeShadeRoot({
|
|
372
|
+
injector,
|
|
373
|
+
rootElement,
|
|
374
|
+
jsxElement: (
|
|
375
|
+
<List<TestItem> items={testItems} listService={service} renderItem={(item) => <span>{item.name}</span>} />
|
|
376
|
+
),
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
await flushUpdates()
|
|
380
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
381
|
+
|
|
382
|
+
service.hasFocus.setValue(true)
|
|
383
|
+
|
|
384
|
+
const wrapper = document.querySelector('.shade-list-wrapper') as HTMLElement
|
|
385
|
+
wrapper?.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: outsideEl }))
|
|
386
|
+
|
|
387
|
+
expect(service.hasFocus.getValue()).toBe(false)
|
|
388
|
+
|
|
389
|
+
outsideEl.remove()
|
|
390
|
+
service[Symbol.dispose]()
|
|
391
|
+
})
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('should clear hasFocus on focusout when relatedTarget is null', async () => {
|
|
395
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
396
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
397
|
+
const service = createTestService()
|
|
398
|
+
|
|
399
|
+
initializeShadeRoot({
|
|
400
|
+
injector,
|
|
401
|
+
rootElement,
|
|
402
|
+
jsxElement: (
|
|
403
|
+
<List<TestItem> items={testItems} listService={service} renderItem={(item) => <span>{item.name}</span>} />
|
|
404
|
+
),
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
await flushUpdates()
|
|
408
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
409
|
+
|
|
410
|
+
service.hasFocus.setValue(true)
|
|
411
|
+
|
|
412
|
+
const wrapper = document.querySelector('.shade-list-wrapper') as HTMLElement
|
|
413
|
+
wrapper?.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: null }))
|
|
414
|
+
|
|
415
|
+
expect(service.hasFocus.getValue()).toBe(false)
|
|
416
|
+
|
|
417
|
+
service[Symbol.dispose]()
|
|
418
|
+
})
|
|
419
|
+
})
|
|
339
420
|
})
|
|
340
421
|
|
|
341
422
|
describe('selection', () => {
|
|
@@ -426,7 +507,7 @@ describe('List', () => {
|
|
|
426
507
|
})
|
|
427
508
|
|
|
428
509
|
describe('keyboard navigation', () => {
|
|
429
|
-
it('should handle ArrowDown
|
|
510
|
+
it('should not handle ArrowDown (delegated to spatial navigation)', async () => {
|
|
430
511
|
await usingAsync(new Injector(), async (injector) => {
|
|
431
512
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
432
513
|
const service = createTestService()
|
|
@@ -446,13 +527,13 @@ describe('List', () => {
|
|
|
446
527
|
|
|
447
528
|
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
|
|
448
529
|
|
|
449
|
-
expect(service.focusedItem.getValue()).toEqual(testItems[
|
|
530
|
+
expect(service.focusedItem.getValue()).toEqual(testItems[0])
|
|
450
531
|
|
|
451
532
|
service[Symbol.dispose]()
|
|
452
533
|
})
|
|
453
534
|
})
|
|
454
535
|
|
|
455
|
-
it('should handle ArrowUp
|
|
536
|
+
it('should not handle ArrowUp (delegated to spatial navigation)', async () => {
|
|
456
537
|
await usingAsync(new Injector(), async (injector) => {
|
|
457
538
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
458
539
|
const service = createTestService()
|
|
@@ -472,7 +553,7 @@ describe('List', () => {
|
|
|
472
553
|
|
|
473
554
|
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }))
|
|
474
555
|
|
|
475
|
-
expect(service.focusedItem.getValue()).toEqual(testItems[
|
|
556
|
+
expect(service.focusedItem.getValue()).toEqual(testItems[1])
|
|
476
557
|
|
|
477
558
|
service[Symbol.dispose]()
|
|
478
559
|
})
|
|
@@ -666,31 +747,6 @@ describe('List', () => {
|
|
|
666
747
|
})
|
|
667
748
|
})
|
|
668
749
|
|
|
669
|
-
it('should handle Tab to toggle focus', async () => {
|
|
670
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
671
|
-
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
672
|
-
const service = createTestService()
|
|
673
|
-
|
|
674
|
-
service.hasFocus.setValue(true)
|
|
675
|
-
|
|
676
|
-
initializeShadeRoot({
|
|
677
|
-
injector,
|
|
678
|
-
rootElement,
|
|
679
|
-
jsxElement: (
|
|
680
|
-
<List<TestItem> items={testItems} listService={service} renderItem={(item) => <span>{item.name}</span>} />
|
|
681
|
-
),
|
|
682
|
-
})
|
|
683
|
-
|
|
684
|
-
await flushUpdates()
|
|
685
|
-
|
|
686
|
-
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }))
|
|
687
|
-
|
|
688
|
-
expect(service.hasFocus.getValue()).toBe(false)
|
|
689
|
-
|
|
690
|
-
service[Symbol.dispose]()
|
|
691
|
-
})
|
|
692
|
-
})
|
|
693
|
-
|
|
694
750
|
it('should not handle keyboard when not focused', async () => {
|
|
695
751
|
await usingAsync(new Injector(), async (injector) => {
|
|
696
752
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
@@ -956,6 +1012,83 @@ describe('List', () => {
|
|
|
956
1012
|
})
|
|
957
1013
|
})
|
|
958
1014
|
|
|
1015
|
+
describe('item spatial navigation attributes', () => {
|
|
1016
|
+
it('should set data-spatial-nav-target on list items', async () => {
|
|
1017
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1018
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
1019
|
+
const service = createTestService()
|
|
1020
|
+
|
|
1021
|
+
initializeShadeRoot({
|
|
1022
|
+
injector,
|
|
1023
|
+
rootElement,
|
|
1024
|
+
jsxElement: (
|
|
1025
|
+
<List<TestItem> items={testItems} listService={service} renderItem={(item) => <span>{item.name}</span>} />
|
|
1026
|
+
),
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
await flushUpdates()
|
|
1030
|
+
|
|
1031
|
+
const items = document.querySelectorAll('shade-list-item')
|
|
1032
|
+
for (const item of items) {
|
|
1033
|
+
expect(item.hasAttribute('data-spatial-nav-target')).toBe(true)
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
service[Symbol.dispose]()
|
|
1037
|
+
})
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1040
|
+
it('should set tabIndex 0 on focused item and -1 on others', async () => {
|
|
1041
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1042
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
1043
|
+
const service = createTestService()
|
|
1044
|
+
|
|
1045
|
+
service.focusedItem.setValue(testItems[1])
|
|
1046
|
+
|
|
1047
|
+
initializeShadeRoot({
|
|
1048
|
+
injector,
|
|
1049
|
+
rootElement,
|
|
1050
|
+
jsxElement: (
|
|
1051
|
+
<List<TestItem> items={testItems} listService={service} renderItem={(item) => <span>{item.name}</span>} />
|
|
1052
|
+
),
|
|
1053
|
+
})
|
|
1054
|
+
|
|
1055
|
+
await flushUpdates()
|
|
1056
|
+
|
|
1057
|
+
const items = document.querySelectorAll<HTMLDivElement>('shade-list-item')
|
|
1058
|
+
expect(items[0]?.tabIndex).toBe(-1)
|
|
1059
|
+
expect(items[1]?.tabIndex).toBe(0)
|
|
1060
|
+
expect(items[2]?.tabIndex).toBe(-1)
|
|
1061
|
+
|
|
1062
|
+
service[Symbol.dispose]()
|
|
1063
|
+
})
|
|
1064
|
+
})
|
|
1065
|
+
|
|
1066
|
+
it('should sync focusedItem on item onfocus', async () => {
|
|
1067
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1068
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
1069
|
+
const service = createTestService()
|
|
1070
|
+
|
|
1071
|
+
initializeShadeRoot({
|
|
1072
|
+
injector,
|
|
1073
|
+
rootElement,
|
|
1074
|
+
jsxElement: (
|
|
1075
|
+
<List<TestItem> items={testItems} listService={service} renderItem={(item) => <span>{item.name}</span>} />
|
|
1076
|
+
),
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
await flushUpdates()
|
|
1080
|
+
|
|
1081
|
+
const items = document.querySelectorAll('shade-list-item')
|
|
1082
|
+
items[2]?.dispatchEvent(new FocusEvent('focus'))
|
|
1083
|
+
|
|
1084
|
+
expect(service.focusedItem.getValue()).toEqual(testItems[2])
|
|
1085
|
+
expect(service.hasFocus.getValue()).toBe(true)
|
|
1086
|
+
|
|
1087
|
+
service[Symbol.dispose]()
|
|
1088
|
+
})
|
|
1089
|
+
})
|
|
1090
|
+
})
|
|
1091
|
+
|
|
959
1092
|
describe('keyboard listener cleanup', () => {
|
|
960
1093
|
it('should remove keyboard listener when component is disconnected', async () => {
|
|
961
1094
|
await usingAsync(new Injector(), async (injector) => {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { ChildrenList, PartialElement } from '@furystack/shades'
|
|
2
2
|
import { createComponent, Shade } from '@furystack/shades'
|
|
3
|
-
import { ClickAwayService } from '../../services/click-away-service.js'
|
|
4
3
|
import { cssVariableTheme } from '../../services/css-variable-theme.js'
|
|
5
4
|
import type { ListService } from '../../services/list-service.js'
|
|
6
5
|
import { Pagination } from '../pagination.js'
|
|
7
6
|
import { ListItem } from './list-item.js'
|
|
8
7
|
|
|
8
|
+
let nextListId = 0
|
|
9
|
+
|
|
9
10
|
export type ListItemState = {
|
|
10
11
|
isFocused: boolean
|
|
11
12
|
isSelected: boolean
|
|
@@ -31,6 +32,13 @@ export type ListProps<T> = {
|
|
|
31
32
|
onSelectionChange?: (selected: T[]) => void
|
|
32
33
|
/** Optional pagination configuration. When provided, items are sliced and a Pagination control is rendered. */
|
|
33
34
|
pagination?: ListPaginationProps
|
|
35
|
+
/**
|
|
36
|
+
* Section name for spatial navigation scoping.
|
|
37
|
+
* Sets `data-nav-section` on the list wrapper so that SpatialNavigationService
|
|
38
|
+
* constrains arrow-key navigation within the list.
|
|
39
|
+
* Auto-generated per instance when not provided.
|
|
40
|
+
*/
|
|
41
|
+
navSection?: string
|
|
34
42
|
} & PartialElement<HTMLDivElement>
|
|
35
43
|
|
|
36
44
|
export const List: <T>(props: ListProps<T>, children: ChildrenList) => JSX.Element<any> = Shade({
|
|
@@ -46,8 +54,9 @@ export const List: <T>(props: ListProps<T>, children: ChildrenList) => JSX.Eleme
|
|
|
46
54
|
padding: '8px 0',
|
|
47
55
|
},
|
|
48
56
|
},
|
|
49
|
-
render: ({ props, useDisposable, useHostProps, useRef }) => {
|
|
57
|
+
render: ({ props, useDisposable, useHostProps, useRef, useState }) => {
|
|
50
58
|
const wrapperRef = useRef<HTMLDivElement>('listWrapper')
|
|
59
|
+
const [navSectionId] = useState('navSectionId', String(nextListId++))
|
|
51
60
|
|
|
52
61
|
useDisposable('keydown-handler', () => {
|
|
53
62
|
const listener = (ev: KeyboardEvent) => {
|
|
@@ -60,8 +69,8 @@ export const List: <T>(props: ListProps<T>, children: ChildrenList) => JSX.Eleme
|
|
|
60
69
|
}
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
|
-
window.addEventListener('keydown', listener)
|
|
64
|
-
return { [Symbol.dispose]: () => window.removeEventListener('keydown', listener) }
|
|
72
|
+
window.addEventListener('keydown', listener, true)
|
|
73
|
+
return { [Symbol.dispose]: () => window.removeEventListener('keydown', listener, true) }
|
|
65
74
|
})
|
|
66
75
|
|
|
67
76
|
const { pagination } = props
|
|
@@ -79,13 +88,30 @@ export const List: <T>(props: ListProps<T>, children: ChildrenList) => JSX.Eleme
|
|
|
79
88
|
|
|
80
89
|
props.listService.items.setValue(visibleItems)
|
|
81
90
|
|
|
82
|
-
useDisposable(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
useDisposable('focus-coordination', () => {
|
|
92
|
+
const handleFocusOut = (ev: FocusEvent) => {
|
|
93
|
+
const wrapper = wrapperRef.current
|
|
94
|
+
if (wrapper && (!ev.relatedTarget || !wrapper.contains(ev.relatedTarget as Node))) {
|
|
86
95
|
props.listService.hasFocus.setValue(false)
|
|
87
|
-
}
|
|
88
|
-
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
queueMicrotask(() => {
|
|
100
|
+
const wrapper = wrapperRef.current
|
|
101
|
+
if (wrapper) {
|
|
102
|
+
wrapper.addEventListener('focusout', handleFocusOut)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
[Symbol.dispose]: () => {
|
|
108
|
+
const wrapper = wrapperRef.current
|
|
109
|
+
if (wrapper) {
|
|
110
|
+
wrapper.removeEventListener('focusout', handleFocusOut)
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
})
|
|
89
115
|
|
|
90
116
|
if (props.onSelectionChange) {
|
|
91
117
|
const { onSelectionChange } = props
|
|
@@ -107,6 +133,7 @@ export const List: <T>(props: ListProps<T>, children: ChildrenList) => JSX.Eleme
|
|
|
107
133
|
role="listbox"
|
|
108
134
|
ariaMultiSelectable="true"
|
|
109
135
|
className="shade-list-wrapper"
|
|
136
|
+
data-nav-section={props.navSection ?? `list-${navSectionId}`}
|
|
110
137
|
onclick={() => props.listService.hasFocus.setValue(true)}
|
|
111
138
|
>
|
|
112
139
|
{visibleItems.map((item) => (
|