@furystack/shades-common-components 14.0.0 → 15.0.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/CHANGELOG.md +51 -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/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 +3 -3
- 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/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
|
@@ -82,6 +82,15 @@ export const Suggest: <T>(props: SuggestProps<T>, children: ChildrenList) => JSX
|
|
|
82
82
|
|
|
83
83
|
useHostProps({
|
|
84
84
|
'data-opened': isOpened ? '' : undefined,
|
|
85
|
+
tabIndex: -1,
|
|
86
|
+
'data-spatial-nav-target': '',
|
|
87
|
+
onfocus: (ev: FocusEvent) => {
|
|
88
|
+
const host = ev.currentTarget as HTMLElement
|
|
89
|
+
const input = host.querySelector('input')
|
|
90
|
+
if (input) {
|
|
91
|
+
input.focus()
|
|
92
|
+
}
|
|
93
|
+
},
|
|
85
94
|
})
|
|
86
95
|
useDisposable('isLoadingSubscription', () =>
|
|
87
96
|
manager.isLoading.subscribe((isLoading) => {
|
|
@@ -107,7 +116,10 @@ export const Suggest: <T>(props: SuggestProps<T>, children: ChildrenList) => JSX
|
|
|
107
116
|
<div
|
|
108
117
|
ref={wrapperRef}
|
|
109
118
|
className="suggest-wrapper"
|
|
110
|
-
|
|
119
|
+
onkeydown={(ev) => {
|
|
120
|
+
const hasSuggestions = manager.isOpened.getValue() && manager.currentSuggestions.getValue().length > 0
|
|
121
|
+
if (!hasSuggestions) return
|
|
122
|
+
|
|
111
123
|
if (ev.key === 'Enter') {
|
|
112
124
|
ev.preventDefault()
|
|
113
125
|
manager.selectSuggestion()
|
|
@@ -123,7 +135,8 @@ export const Suggest: <T>(props: SuggestProps<T>, children: ChildrenList) => JSX
|
|
|
123
135
|
Math.min(manager.selectedIndex.getValue() + 1, manager.currentSuggestions.getValue().length - 1),
|
|
124
136
|
)
|
|
125
137
|
}
|
|
126
|
-
|
|
138
|
+
}}
|
|
139
|
+
oninput={(ev) => {
|
|
127
140
|
void manager.getSuggestion({ injector, term: (ev.target as HTMLInputElement).value })
|
|
128
141
|
}}
|
|
129
142
|
>
|
package/src/components/tabs.tsx
CHANGED
|
@@ -115,6 +115,10 @@ export const Tabs = Shade<{
|
|
|
115
115
|
color: cssVariableTheme.text.primary,
|
|
116
116
|
boxShadow: `inset 0 -2px 0 ${cssVariableTheme.palette.primary.main}`,
|
|
117
117
|
},
|
|
118
|
+
'& .shade-tab-btn:focus-visible': {
|
|
119
|
+
outline: cssVariableTheme.action.focusOutline,
|
|
120
|
+
outlineOffset: '-2px',
|
|
121
|
+
},
|
|
118
122
|
|
|
119
123
|
// Close button (span with role="button" via event delegation)
|
|
120
124
|
'& .shade-tab-close': {
|
|
@@ -69,10 +69,20 @@ export const TreeItem: <T>(props: TreeItemProps<T>, children: ChildrenList) => J
|
|
|
69
69
|
const isSelected = selection.includes(item)
|
|
70
70
|
|
|
71
71
|
useHostProps({
|
|
72
|
+
tabIndex: isFocused ? 0 : -1,
|
|
73
|
+
'data-spatial-nav-target': '',
|
|
72
74
|
role: 'treeitem',
|
|
73
75
|
'aria-level': (level + 1).toString(),
|
|
74
76
|
'aria-selected': isSelected.toString(),
|
|
75
77
|
...(hasChildren ? { 'aria-expanded': isExpanded.toString() } : {}),
|
|
78
|
+
onfocus: () => {
|
|
79
|
+
if (treeService.focusedItem.getValue() !== item) {
|
|
80
|
+
treeService.focusedItem.setValue(item)
|
|
81
|
+
}
|
|
82
|
+
if (!treeService.hasFocus.getValue()) {
|
|
83
|
+
treeService.hasFocus.setValue(true)
|
|
84
|
+
}
|
|
85
|
+
},
|
|
76
86
|
onclick: (ev: MouseEvent) => {
|
|
77
87
|
treeService.handleItemClick(item, ev)
|
|
78
88
|
},
|
|
@@ -93,10 +103,15 @@ export const TreeItem: <T>(props: TreeItemProps<T>, children: ChildrenList) => J
|
|
|
93
103
|
queueMicrotask(() => {
|
|
94
104
|
const el = wrapperRef.current
|
|
95
105
|
if (!el) return
|
|
106
|
+
const hostEl = el.closest('shade-tree-item') as HTMLElement
|
|
107
|
+
if (!hostEl) return
|
|
108
|
+
|
|
109
|
+
if (document.activeElement !== hostEl) {
|
|
110
|
+
hostEl.focus({ preventScroll: true })
|
|
111
|
+
}
|
|
112
|
+
|
|
96
113
|
const scrollContainer = el.closest('shade-tree') as HTMLElement
|
|
97
114
|
if (scrollContainer) {
|
|
98
|
-
const hostEl = el.closest('shade-tree-item') as HTMLElement
|
|
99
|
-
if (!hostEl) return
|
|
100
115
|
const containerRect = scrollContainer.getBoundingClientRect()
|
|
101
116
|
const itemRect = hostEl.getBoundingClientRect()
|
|
102
117
|
const itemTopInContainer = itemRect.top - containerRect.top
|
|
@@ -105,12 +120,12 @@ export const TreeItem: <T>(props: TreeItemProps<T>, children: ChildrenList) => J
|
|
|
105
120
|
if (itemTopInContainer < 0) {
|
|
106
121
|
scrollContainer.scrollTo({
|
|
107
122
|
top: scrollContainer.scrollTop + itemTopInContainer,
|
|
108
|
-
behavior: '
|
|
123
|
+
behavior: 'instant',
|
|
109
124
|
})
|
|
110
125
|
} else if (itemBottomInContainer > scrollContainer.clientHeight) {
|
|
111
126
|
scrollContainer.scrollTo({
|
|
112
127
|
top: scrollContainer.scrollTop + (itemBottomInContainer - scrollContainer.clientHeight),
|
|
113
|
-
behavior: '
|
|
128
|
+
behavior: 'instant',
|
|
114
129
|
})
|
|
115
130
|
}
|
|
116
131
|
}
|
|
@@ -501,8 +501,107 @@ describe('Tree', () => {
|
|
|
501
501
|
})
|
|
502
502
|
})
|
|
503
503
|
|
|
504
|
+
describe('item spatial navigation attributes', () => {
|
|
505
|
+
it('should set data-spatial-nav-target on tree items', async () => {
|
|
506
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
507
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
508
|
+
const treeData = createTreeData()
|
|
509
|
+
const service = createTestService()
|
|
510
|
+
|
|
511
|
+
service.rootItems.setValue(treeData)
|
|
512
|
+
service.updateFlattenedNodes()
|
|
513
|
+
|
|
514
|
+
initializeShadeRoot({
|
|
515
|
+
injector,
|
|
516
|
+
rootElement,
|
|
517
|
+
jsxElement: (
|
|
518
|
+
<Tree<TestNode>
|
|
519
|
+
rootItems={treeData}
|
|
520
|
+
treeService={service}
|
|
521
|
+
renderItem={(node) => <span>{node.name}</span>}
|
|
522
|
+
/>
|
|
523
|
+
),
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
await flushUpdates()
|
|
527
|
+
|
|
528
|
+
const items = document.querySelectorAll('shade-tree-item')
|
|
529
|
+
for (const item of items) {
|
|
530
|
+
expect(item.hasAttribute('data-spatial-nav-target')).toBe(true)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
service[Symbol.dispose]()
|
|
534
|
+
})
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
it('should set tabIndex 0 on focused item and -1 on others', async () => {
|
|
538
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
539
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
540
|
+
const treeData = createTreeData()
|
|
541
|
+
const service = createTestService()
|
|
542
|
+
|
|
543
|
+
service.rootItems.setValue(treeData)
|
|
544
|
+
service.updateFlattenedNodes()
|
|
545
|
+
service.focusedItem.setValue(treeData[1])
|
|
546
|
+
|
|
547
|
+
initializeShadeRoot({
|
|
548
|
+
injector,
|
|
549
|
+
rootElement,
|
|
550
|
+
jsxElement: (
|
|
551
|
+
<Tree<TestNode>
|
|
552
|
+
rootItems={treeData}
|
|
553
|
+
treeService={service}
|
|
554
|
+
renderItem={(node) => <span>{node.name}</span>}
|
|
555
|
+
/>
|
|
556
|
+
),
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
await flushUpdates()
|
|
560
|
+
|
|
561
|
+
const items = document.querySelectorAll<HTMLDivElement>('shade-tree-item')
|
|
562
|
+
expect(items[0]?.tabIndex).toBe(-1)
|
|
563
|
+
expect(items[1]?.tabIndex).toBe(0)
|
|
564
|
+
|
|
565
|
+
service[Symbol.dispose]()
|
|
566
|
+
})
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
it('should sync focusedItem on item onfocus', async () => {
|
|
570
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
571
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
572
|
+
const treeData = createTreeData()
|
|
573
|
+
const service = createTestService()
|
|
574
|
+
|
|
575
|
+
service.rootItems.setValue(treeData)
|
|
576
|
+
service.updateFlattenedNodes()
|
|
577
|
+
|
|
578
|
+
initializeShadeRoot({
|
|
579
|
+
injector,
|
|
580
|
+
rootElement,
|
|
581
|
+
jsxElement: (
|
|
582
|
+
<Tree<TestNode>
|
|
583
|
+
rootItems={treeData}
|
|
584
|
+
treeService={service}
|
|
585
|
+
renderItem={(node) => <span>{node.name}</span>}
|
|
586
|
+
/>
|
|
587
|
+
),
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
await flushUpdates()
|
|
591
|
+
|
|
592
|
+
const items = document.querySelectorAll('shade-tree-item')
|
|
593
|
+
items[1]?.dispatchEvent(new FocusEvent('focus'))
|
|
594
|
+
|
|
595
|
+
expect(service.focusedItem.getValue()).toEqual(treeData[1])
|
|
596
|
+
expect(service.hasFocus.getValue()).toBe(true)
|
|
597
|
+
|
|
598
|
+
service[Symbol.dispose]()
|
|
599
|
+
})
|
|
600
|
+
})
|
|
601
|
+
})
|
|
602
|
+
|
|
504
603
|
describe('keyboard navigation', () => {
|
|
505
|
-
it('should handle ArrowDown
|
|
604
|
+
it('should not handle ArrowDown (delegated to spatial navigation)', async () => {
|
|
506
605
|
await usingAsync(new Injector(), async (injector) => {
|
|
507
606
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
508
607
|
const treeData = createTreeData()
|
|
@@ -529,7 +628,7 @@ describe('Tree', () => {
|
|
|
529
628
|
|
|
530
629
|
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
|
|
531
630
|
|
|
532
|
-
expect(service.focusedItem.getValue()).toEqual(treeData[
|
|
631
|
+
expect(service.focusedItem.getValue()).toEqual(treeData[0])
|
|
533
632
|
|
|
534
633
|
service[Symbol.dispose]()
|
|
535
634
|
})
|
|
@@ -5,6 +5,8 @@ import { cssVariableTheme } from '../../services/css-variable-theme.js'
|
|
|
5
5
|
import type { TreeService } from '../../services/tree-service.js'
|
|
6
6
|
import { TreeItem } from './tree-item.js'
|
|
7
7
|
|
|
8
|
+
let nextTreeId = 0
|
|
9
|
+
|
|
8
10
|
export type TreeItemState = {
|
|
9
11
|
isFocused: boolean
|
|
10
12
|
isSelected: boolean
|
|
@@ -21,6 +23,13 @@ export type TreeProps<T> = {
|
|
|
21
23
|
variant?: 'contained' | 'outlined'
|
|
22
24
|
onItemActivate?: (item: T) => void
|
|
23
25
|
onSelectionChange?: (selected: T[]) => void
|
|
26
|
+
/**
|
|
27
|
+
* Section name for spatial navigation scoping.
|
|
28
|
+
* Sets `data-nav-section` on the tree host so that SpatialNavigationService
|
|
29
|
+
* constrains arrow-key navigation within the tree.
|
|
30
|
+
* Auto-generated per instance when not provided.
|
|
31
|
+
*/
|
|
32
|
+
navSection?: string
|
|
24
33
|
} & PartialElement<HTMLDivElement>
|
|
25
34
|
|
|
26
35
|
export const Tree: <T>(props: TreeProps<T>, children: ChildrenList) => JSX.Element<any> = Shade({
|
|
@@ -31,7 +40,9 @@ export const Tree: <T>(props: TreeProps<T>, children: ChildrenList) => JSX.Eleme
|
|
|
31
40
|
width: '100%',
|
|
32
41
|
overflow: 'auto',
|
|
33
42
|
},
|
|
34
|
-
render: ({ props, useDisposable, useObservable, useHostProps }) => {
|
|
43
|
+
render: ({ props, useDisposable, useObservable, useHostProps, useState }) => {
|
|
44
|
+
const [navSectionId] = useState('navSectionId', String(nextTreeId++))
|
|
45
|
+
|
|
35
46
|
useDisposable('keydown-handler', () => {
|
|
36
47
|
const listener = (ev: KeyboardEvent) => {
|
|
37
48
|
props.treeService.handleKeyDown(ev)
|
|
@@ -43,8 +54,8 @@ export const Tree: <T>(props: TreeProps<T>, children: ChildrenList) => JSX.Eleme
|
|
|
43
54
|
}
|
|
44
55
|
}
|
|
45
56
|
}
|
|
46
|
-
window.addEventListener('keydown', listener)
|
|
47
|
-
return { [Symbol.dispose]: () => window.removeEventListener('keydown', listener) }
|
|
57
|
+
window.addEventListener('keydown', listener, true)
|
|
58
|
+
return { [Symbol.dispose]: () => window.removeEventListener('keydown', listener, true) }
|
|
48
59
|
})
|
|
49
60
|
|
|
50
61
|
if (props.treeService.rootItems.getValue() !== props.rootItems) {
|
|
@@ -82,9 +93,16 @@ export const Tree: <T>(props: TreeProps<T>, children: ChildrenList) => JSX.Eleme
|
|
|
82
93
|
useHostProps({
|
|
83
94
|
'data-variant': props.variant || undefined,
|
|
84
95
|
'data-tree-instance-id': treeInstanceId.value,
|
|
96
|
+
'data-nav-section': props.navSection ?? `tree-${navSectionId}`,
|
|
85
97
|
role: 'tree',
|
|
86
98
|
'aria-multiselectable': 'true',
|
|
87
99
|
onclick: () => props.treeService.hasFocus.setValue(true),
|
|
100
|
+
onfocusout: (ev: FocusEvent) => {
|
|
101
|
+
const hostEl = ev.currentTarget as HTMLElement
|
|
102
|
+
if (!ev.relatedTarget || !hostEl.contains(ev.relatedTarget as Node)) {
|
|
103
|
+
props.treeService.hasFocus.setValue(false)
|
|
104
|
+
}
|
|
105
|
+
},
|
|
88
106
|
})
|
|
89
107
|
|
|
90
108
|
const [flattenedNodes] = useObservable('flattenedNodes', props.treeService.flattenedNodes)
|
|
@@ -206,8 +206,8 @@ describe('CollectionService', () => {
|
|
|
206
206
|
})
|
|
207
207
|
})
|
|
208
208
|
|
|
209
|
-
describe('
|
|
210
|
-
it('Should move focus to previous entry', () => {
|
|
209
|
+
describe('Arrow keys', () => {
|
|
210
|
+
it('Should move focus to the previous entry on ArrowUp', () => {
|
|
211
211
|
const testEntries = createTestEntries()
|
|
212
212
|
using(new CollectionService<TestEntry>({}), (service) => {
|
|
213
213
|
service.data.setValue({ count: 3, entries: testEntries })
|
|
@@ -222,22 +222,22 @@ describe('CollectionService', () => {
|
|
|
222
222
|
})
|
|
223
223
|
})
|
|
224
224
|
|
|
225
|
-
it('Should not
|
|
225
|
+
it('Should not preventDefault ArrowUp at the first entry', () => {
|
|
226
226
|
const testEntries = createTestEntries()
|
|
227
227
|
using(new CollectionService<TestEntry>({}), (service) => {
|
|
228
228
|
service.data.setValue({ count: 3, entries: testEntries })
|
|
229
229
|
service.hasFocus.setValue(true)
|
|
230
230
|
service.focusedEntry.setValue(testEntries[0])
|
|
231
231
|
|
|
232
|
-
|
|
232
|
+
const ev = createKeyboardEvent('ArrowUp')
|
|
233
|
+
service.handleKeyDown(ev)
|
|
233
234
|
|
|
235
|
+
expect(ev.preventDefault).not.toHaveBeenCalled()
|
|
234
236
|
expect(service.focusedEntry.getValue()).toBe(testEntries[0])
|
|
235
237
|
})
|
|
236
238
|
})
|
|
237
|
-
})
|
|
238
239
|
|
|
239
|
-
|
|
240
|
-
it('Should move focus to next entry', () => {
|
|
240
|
+
it('Should move focus to the next entry on ArrowDown', () => {
|
|
241
241
|
const testEntries = createTestEntries()
|
|
242
242
|
using(new CollectionService<TestEntry>({}), (service) => {
|
|
243
243
|
service.data.setValue({ count: 3, entries: testEntries })
|
|
@@ -252,60 +252,69 @@ describe('CollectionService', () => {
|
|
|
252
252
|
})
|
|
253
253
|
})
|
|
254
254
|
|
|
255
|
-
it('Should not
|
|
255
|
+
it('Should not preventDefault ArrowDown at the last entry', () => {
|
|
256
256
|
const testEntries = createTestEntries()
|
|
257
257
|
using(new CollectionService<TestEntry>({}), (service) => {
|
|
258
258
|
service.data.setValue({ count: 3, entries: testEntries })
|
|
259
259
|
service.hasFocus.setValue(true)
|
|
260
260
|
service.focusedEntry.setValue(testEntries[2])
|
|
261
261
|
|
|
262
|
-
|
|
262
|
+
const ev = createKeyboardEvent('ArrowDown')
|
|
263
|
+
service.handleKeyDown(ev)
|
|
263
264
|
|
|
265
|
+
expect(ev.preventDefault).not.toHaveBeenCalled()
|
|
264
266
|
expect(service.focusedEntry.getValue()).toBe(testEntries[2])
|
|
265
267
|
})
|
|
266
268
|
})
|
|
267
|
-
})
|
|
268
269
|
|
|
269
|
-
|
|
270
|
-
it('Should focus the first entry', () => {
|
|
270
|
+
it('Should not handle arrow keys when focusedEntry is undefined', () => {
|
|
271
271
|
const testEntries = createTestEntries()
|
|
272
272
|
using(new CollectionService<TestEntry>({}), (service) => {
|
|
273
273
|
service.data.setValue({ count: 3, entries: testEntries })
|
|
274
274
|
service.hasFocus.setValue(true)
|
|
275
|
-
service.focusedEntry.setValue(
|
|
275
|
+
service.focusedEntry.setValue(undefined)
|
|
276
276
|
|
|
277
|
-
|
|
277
|
+
const evDown = createKeyboardEvent('ArrowDown')
|
|
278
|
+
service.handleKeyDown(evDown)
|
|
279
|
+
expect(evDown.preventDefault).not.toHaveBeenCalled()
|
|
278
280
|
|
|
279
|
-
|
|
281
|
+
const evUp = createKeyboardEvent('ArrowUp')
|
|
282
|
+
service.handleKeyDown(evUp)
|
|
283
|
+
expect(evUp.preventDefault).not.toHaveBeenCalled()
|
|
280
284
|
})
|
|
281
285
|
})
|
|
282
286
|
})
|
|
283
287
|
|
|
284
|
-
describe('
|
|
285
|
-
it('Should focus the
|
|
288
|
+
describe('Home key', () => {
|
|
289
|
+
it('Should focus the first entry and preventDefault', () => {
|
|
286
290
|
const testEntries = createTestEntries()
|
|
287
291
|
using(new CollectionService<TestEntry>({}), (service) => {
|
|
288
292
|
service.data.setValue({ count: 3, entries: testEntries })
|
|
289
293
|
service.hasFocus.setValue(true)
|
|
290
|
-
service.focusedEntry.setValue(testEntries[
|
|
294
|
+
service.focusedEntry.setValue(testEntries[2])
|
|
291
295
|
|
|
292
|
-
|
|
296
|
+
const ev = createKeyboardEvent('Home')
|
|
297
|
+
service.handleKeyDown(ev)
|
|
293
298
|
|
|
294
|
-
expect(
|
|
299
|
+
expect(ev.preventDefault).toHaveBeenCalled()
|
|
300
|
+
expect(service.focusedEntry.getValue()).toBe(testEntries[0])
|
|
295
301
|
})
|
|
296
302
|
})
|
|
297
303
|
})
|
|
298
304
|
|
|
299
|
-
describe('
|
|
300
|
-
it('Should
|
|
305
|
+
describe('End key', () => {
|
|
306
|
+
it('Should focus the last entry and preventDefault', () => {
|
|
301
307
|
const testEntries = createTestEntries()
|
|
302
308
|
using(new CollectionService<TestEntry>({}), (service) => {
|
|
303
309
|
service.data.setValue({ count: 3, entries: testEntries })
|
|
304
310
|
service.hasFocus.setValue(true)
|
|
311
|
+
service.focusedEntry.setValue(testEntries[0])
|
|
305
312
|
|
|
306
|
-
|
|
313
|
+
const ev = createKeyboardEvent('End')
|
|
314
|
+
service.handleKeyDown(ev)
|
|
307
315
|
|
|
308
|
-
expect(
|
|
316
|
+
expect(ev.preventDefault).toHaveBeenCalled()
|
|
317
|
+
expect(service.focusedEntry.getValue()).toBe(testEntries[2])
|
|
309
318
|
})
|
|
310
319
|
})
|
|
311
320
|
})
|
|
@@ -62,6 +62,19 @@ export class CollectionService<T>
|
|
|
62
62
|
|
|
63
63
|
public focusedEntry = new ObservableValue<T | undefined>(undefined)
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Stores the focused entry captured on pointerdown, before the focus event
|
|
67
|
+
* can update focusedEntry. Used as the anchor for SHIFT+click range selection.
|
|
68
|
+
* Call {@link setFocusAnchor} from `onpointerdown` to snapshot the anchor
|
|
69
|
+
* before focus shifts.
|
|
70
|
+
*/
|
|
71
|
+
private focusAnchor: T | undefined = undefined
|
|
72
|
+
|
|
73
|
+
/** Snapshot the current focused entry as the anchor for SHIFT+click range selection. */
|
|
74
|
+
public setFocusAnchor(): void {
|
|
75
|
+
this.focusAnchor = this.focusedEntry.getValue()
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
public selection = new ObservableValue<T[]>([])
|
|
66
79
|
|
|
67
80
|
public searchTerm = new ObservableValue('')
|
|
@@ -107,28 +120,36 @@ export class CollectionService<T>
|
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
break
|
|
110
|
-
case '
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
case 'ArrowDown': {
|
|
124
|
+
if (focusedEntry !== undefined) {
|
|
125
|
+
const currentIndex = entries.indexOf(focusedEntry)
|
|
126
|
+
if (currentIndex >= 0 && currentIndex < entries.length - 1) {
|
|
127
|
+
ev.preventDefault()
|
|
128
|
+
this.focusedEntry.setValue(entries[currentIndex + 1])
|
|
129
|
+
}
|
|
130
|
+
}
|
|
113
131
|
break
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
132
|
+
}
|
|
133
|
+
case 'ArrowUp': {
|
|
134
|
+
if (focusedEntry !== undefined) {
|
|
135
|
+
const currentIndex = entries.indexOf(focusedEntry)
|
|
136
|
+
if (currentIndex > 0) {
|
|
137
|
+
ev.preventDefault()
|
|
138
|
+
this.focusedEntry.setValue(entries[currentIndex - 1])
|
|
139
|
+
}
|
|
140
|
+
}
|
|
119
141
|
break
|
|
142
|
+
}
|
|
120
143
|
case 'Home': {
|
|
144
|
+
ev.preventDefault()
|
|
121
145
|
this.focusedEntry.setValue(entries[0])
|
|
122
146
|
break
|
|
123
147
|
}
|
|
124
148
|
case 'End': {
|
|
149
|
+
ev.preventDefault()
|
|
125
150
|
this.focusedEntry.setValue(entries[entries.length - 1])
|
|
126
151
|
break
|
|
127
152
|
}
|
|
128
|
-
case 'Tab': {
|
|
129
|
-
this.hasFocus.setValue(!hasFocus)
|
|
130
|
-
break
|
|
131
|
-
}
|
|
132
153
|
case 'Escape': {
|
|
133
154
|
this.searchTerm.setValue('')
|
|
134
155
|
this.selection.setValue([])
|
|
@@ -152,7 +173,8 @@ export class CollectionService<T>
|
|
|
152
173
|
public handleRowClick(entry: T, ev: MouseEvent) {
|
|
153
174
|
this.emit('onRowClick', entry)
|
|
154
175
|
const currentSelectionValue = this.selection.getValue()
|
|
155
|
-
const lastFocused = this.focusedEntry.getValue()
|
|
176
|
+
const lastFocused = this.focusAnchor ?? this.focusedEntry.getValue()
|
|
177
|
+
this.focusAnchor = undefined
|
|
156
178
|
if (ev.ctrlKey) {
|
|
157
179
|
if (currentSelectionValue.includes(entry)) {
|
|
158
180
|
this.selection.setValue(currentSelectionValue.filter((s) => s !== entry))
|
|
@@ -35,6 +35,7 @@ describe('css-variable-theme', () => {
|
|
|
35
35
|
expect(cssVariableTheme.action.selectedBackground).toBe('var(--shades-theme-action-selected-background)')
|
|
36
36
|
expect(cssVariableTheme.action.activeBackground).toBe('var(--shades-theme-action-active-background)')
|
|
37
37
|
expect(cssVariableTheme.action.focusRing).toBe('var(--shades-theme-action-focus-ring)')
|
|
38
|
+
expect(cssVariableTheme.action.focusOutline).toBe('var(--shades-theme-action-focus-outline)')
|
|
38
39
|
expect(cssVariableTheme.action.disabledOpacity).toBe('var(--shades-theme-action-disabled-opacity)')
|
|
39
40
|
expect(cssVariableTheme.action.backdrop).toBe('var(--shades-theme-action-backdrop)')
|
|
40
41
|
expect(cssVariableTheme.action.subtleBorder).toBe('var(--shades-theme-action-subtle-border)')
|
|
@@ -75,6 +75,7 @@ export const cssVariableTheme = {
|
|
|
75
75
|
selectedBackground: 'var(--shades-theme-action-selected-background)',
|
|
76
76
|
activeBackground: 'var(--shades-theme-action-active-background)',
|
|
77
77
|
focusRing: 'var(--shades-theme-action-focus-ring)',
|
|
78
|
+
focusOutline: 'var(--shades-theme-action-focus-outline)',
|
|
78
79
|
disabledOpacity: 'var(--shades-theme-action-disabled-opacity)',
|
|
79
80
|
backdrop: 'var(--shades-theme-action-backdrop)',
|
|
80
81
|
subtleBorder: 'var(--shades-theme-action-subtle-border)',
|
|
@@ -176,6 +177,30 @@ export const cssVariableTheme = {
|
|
|
176
177
|
export const buildTransition = (...specs: Array<[property: string, duration: string, easing: string]>): string =>
|
|
177
178
|
specs.map(([prop, dur, ease]) => `${prop} ${dur} ${ease}`).join(', ')
|
|
178
179
|
|
|
180
|
+
const FOCUS_STYLES_ID = 'shades-focus-visible-styles'
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Injects global `:focus-visible` styles using the theme's `focusOutline` CSS variable.
|
|
184
|
+
* Ensures keyboard/spatial navigation focus is visible while mouse clicks produce no outline.
|
|
185
|
+
* Safe to call multiple times — the style element is only created once.
|
|
186
|
+
*/
|
|
187
|
+
export const injectFocusVisibleStyles = (): void => {
|
|
188
|
+
if (document.getElementById(FOCUS_STYLES_ID)) return
|
|
189
|
+
|
|
190
|
+
const style = document.createElement('style')
|
|
191
|
+
style.id = FOCUS_STYLES_ID
|
|
192
|
+
style.textContent = `
|
|
193
|
+
:focus-visible {
|
|
194
|
+
outline: ${cssVariableTheme.action.focusOutline};
|
|
195
|
+
outline-offset: 2px;
|
|
196
|
+
}
|
|
197
|
+
:focus:not(:focus-visible) {
|
|
198
|
+
outline: none;
|
|
199
|
+
}
|
|
200
|
+
`
|
|
201
|
+
document.head.appendChild(style)
|
|
202
|
+
}
|
|
203
|
+
|
|
179
204
|
const extractVarName = (key: string): string => key.replace(/^var\(/, '').replace(/[,)].*/, '')
|
|
180
205
|
|
|
181
206
|
export const setCssVariable = (key: string, value: string, root: HTMLElement) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { using } from '@furystack/utils'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
3
|
import { ListService } from './list-service.js'
|
|
4
4
|
|
|
5
5
|
type TestItem = { id: number; name: string }
|
|
@@ -80,51 +80,33 @@ describe('ListService', () => {
|
|
|
80
80
|
})
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
-
it('should handle ArrowDown
|
|
83
|
+
it('should not handle ArrowDown (delegated to spatial navigation)', () => {
|
|
84
84
|
const { service, items } = createTestService()
|
|
85
85
|
using(service, () => {
|
|
86
86
|
service.hasFocus.setValue(true)
|
|
87
87
|
service.focusedItem.setValue(items[0])
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
})
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('should not move past last item on ArrowDown', () => {
|
|
96
|
-
const { service, items } = createTestService()
|
|
97
|
-
using(service, () => {
|
|
98
|
-
service.hasFocus.setValue(true)
|
|
99
|
-
service.focusedItem.setValue(items[2])
|
|
100
|
-
|
|
101
|
-
service.handleKeyDown(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
|
|
102
|
-
|
|
103
|
-
expect(service.focusedItem.getValue()).toBe(items[2])
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('should handle ArrowUp to move focus to previous item', () => {
|
|
108
|
-
const { service, items } = createTestService()
|
|
109
|
-
using(service, () => {
|
|
110
|
-
service.hasFocus.setValue(true)
|
|
111
|
-
service.focusedItem.setValue(items[1])
|
|
112
|
-
|
|
113
|
-
service.handleKeyDown(new KeyboardEvent('keydown', { key: 'ArrowUp' }))
|
|
89
|
+
const ev = new KeyboardEvent('keydown', { key: 'ArrowDown', cancelable: true })
|
|
90
|
+
const preventSpy = vi.spyOn(ev, 'preventDefault')
|
|
91
|
+
service.handleKeyDown(ev)
|
|
114
92
|
|
|
115
93
|
expect(service.focusedItem.getValue()).toBe(items[0])
|
|
94
|
+
expect(preventSpy).not.toHaveBeenCalled()
|
|
116
95
|
})
|
|
117
96
|
})
|
|
118
97
|
|
|
119
|
-
it('should not
|
|
98
|
+
it('should not handle ArrowUp (delegated to spatial navigation)', () => {
|
|
120
99
|
const { service, items } = createTestService()
|
|
121
100
|
using(service, () => {
|
|
122
101
|
service.hasFocus.setValue(true)
|
|
123
|
-
service.focusedItem.setValue(items[
|
|
102
|
+
service.focusedItem.setValue(items[1])
|
|
124
103
|
|
|
125
|
-
|
|
104
|
+
const ev = new KeyboardEvent('keydown', { key: 'ArrowUp', cancelable: true })
|
|
105
|
+
const preventSpy = vi.spyOn(ev, 'preventDefault')
|
|
106
|
+
service.handleKeyDown(ev)
|
|
126
107
|
|
|
127
|
-
expect(service.focusedItem.getValue()).toBe(items[
|
|
108
|
+
expect(service.focusedItem.getValue()).toBe(items[1])
|
|
109
|
+
expect(preventSpy).not.toHaveBeenCalled()
|
|
128
110
|
})
|
|
129
111
|
})
|
|
130
112
|
|
|
@@ -232,17 +214,6 @@ describe('ListService', () => {
|
|
|
232
214
|
})
|
|
233
215
|
})
|
|
234
216
|
|
|
235
|
-
it('should handle Tab to toggle focus', () => {
|
|
236
|
-
const { service } = createTestService()
|
|
237
|
-
using(service, () => {
|
|
238
|
-
service.hasFocus.setValue(true)
|
|
239
|
-
|
|
240
|
-
service.handleKeyDown(new KeyboardEvent('keydown', { key: 'Tab' }))
|
|
241
|
-
|
|
242
|
-
expect(service.hasFocus.getValue()).toBe(false)
|
|
243
|
-
})
|
|
244
|
-
})
|
|
245
|
-
|
|
246
217
|
it('should handle Escape to clear selection and search term', () => {
|
|
247
218
|
const { service, items } = createTestService()
|
|
248
219
|
using(service, () => {
|