@proyecto-viviana/solidaria 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/autocomplete/createAutocomplete.d.ts +2 -2
- package/dist/autocomplete/createAutocomplete.d.ts.map +1 -1
- package/dist/index.js +233 -234
- package/dist/index.js.map +2 -2
- package/dist/index.ssr.js +233 -234
- package/dist/index.ssr.js.map +2 -2
- package/dist/interactions/PressEvent.d.ts +13 -10
- package/dist/interactions/PressEvent.d.ts.map +1 -1
- package/dist/interactions/createPress.d.ts.map +1 -1
- package/dist/interactions/index.d.ts +1 -1
- package/dist/interactions/index.d.ts.map +1 -1
- package/dist/select/createHiddenSelect.d.ts.map +1 -1
- package/dist/toolbar/createToolbar.d.ts.map +1 -1
- package/dist/tooltip/createTooltipTrigger.d.ts.map +1 -1
- package/package.json +9 -7
- package/src/autocomplete/createAutocomplete.ts +341 -0
- package/src/autocomplete/index.ts +9 -0
- package/src/breadcrumbs/createBreadcrumbs.ts +196 -0
- package/src/breadcrumbs/index.ts +8 -0
- package/src/button/createButton.ts +142 -0
- package/src/button/createToggleButton.ts +101 -0
- package/src/button/index.ts +4 -0
- package/src/button/types.ts +78 -0
- package/src/calendar/createCalendar.ts +138 -0
- package/src/calendar/createCalendarCell.ts +187 -0
- package/src/calendar/createCalendarGrid.ts +140 -0
- package/src/calendar/createRangeCalendar.ts +136 -0
- package/src/calendar/createRangeCalendarCell.ts +186 -0
- package/src/calendar/index.ts +34 -0
- package/src/checkbox/createCheckbox.ts +135 -0
- package/src/checkbox/createCheckboxGroup.ts +137 -0
- package/src/checkbox/createCheckboxGroupItem.ts +117 -0
- package/src/checkbox/createCheckboxGroupState.ts +193 -0
- package/src/checkbox/index.ts +13 -0
- package/src/color/createColorArea.ts +314 -0
- package/src/color/createColorField.ts +137 -0
- package/src/color/createColorSlider.ts +197 -0
- package/src/color/createColorSwatch.ts +40 -0
- package/src/color/createColorWheel.ts +208 -0
- package/src/color/index.ts +24 -0
- package/src/color/types.ts +116 -0
- package/src/combobox/createComboBox.ts +647 -0
- package/src/combobox/index.ts +6 -0
- package/src/combobox/intl/en-US.json +7 -0
- package/src/combobox/intl/es-ES.json +7 -0
- package/src/combobox/intl/index.ts +23 -0
- package/src/datepicker/createDateField.ts +154 -0
- package/src/datepicker/createDatePicker.ts +206 -0
- package/src/datepicker/createDateSegment.ts +229 -0
- package/src/datepicker/createTimeField.ts +154 -0
- package/src/datepicker/index.ts +28 -0
- package/src/dialog/createDialog.ts +120 -0
- package/src/dialog/index.ts +2 -0
- package/src/dialog/types.ts +19 -0
- package/src/disclosure/createDisclosure.ts +131 -0
- package/src/disclosure/createDisclosureGroup.ts +62 -0
- package/src/disclosure/index.ts +11 -0
- package/src/dnd/createDrag.ts +209 -0
- package/src/dnd/createDraggableCollection.ts +63 -0
- package/src/dnd/createDraggableItem.ts +243 -0
- package/src/dnd/createDrop.ts +321 -0
- package/src/dnd/createDroppableCollection.ts +293 -0
- package/src/dnd/createDroppableItem.ts +213 -0
- package/src/dnd/index.ts +47 -0
- package/src/dnd/types.ts +89 -0
- package/src/dnd/utils.ts +294 -0
- package/src/focus/FocusScope.tsx +408 -0
- package/src/focus/createAutoFocus.ts +321 -0
- package/src/focus/createFocusRestore.ts +313 -0
- package/src/focus/createVirtualFocus.ts +396 -0
- package/src/focus/index.ts +35 -0
- package/src/form/createFormReset.ts +51 -0
- package/src/form/createFormValidation.ts +224 -0
- package/src/form/index.ts +11 -0
- package/src/grid/GridKeyboardDelegate.ts +429 -0
- package/src/grid/createGrid.ts +261 -0
- package/src/grid/createGridCell.ts +182 -0
- package/src/grid/createGridRow.ts +153 -0
- package/src/grid/index.ts +18 -0
- package/src/grid/types.ts +133 -0
- package/src/gridlist/createGridList.ts +185 -0
- package/src/gridlist/createGridListItem.ts +180 -0
- package/src/gridlist/createGridListSelectionCheckbox.ts +59 -0
- package/src/gridlist/index.ts +16 -0
- package/src/gridlist/types.ts +81 -0
- package/src/i18n/NumberFormatter.ts +266 -0
- package/src/i18n/createCollator.ts +79 -0
- package/src/i18n/createDateFormatter.ts +83 -0
- package/src/i18n/createFilter.ts +131 -0
- package/src/i18n/createNumberFormatter.ts +52 -0
- package/src/i18n/createStringFormatter.ts +87 -0
- package/src/i18n/index.ts +40 -0
- package/src/i18n/locale.tsx +188 -0
- package/src/i18n/utils.ts +99 -0
- package/src/index.ts +670 -0
- package/src/interactions/FocusableProvider.tsx +44 -0
- package/src/interactions/PressEvent.ts +126 -0
- package/src/interactions/createFocus.ts +163 -0
- package/src/interactions/createFocusRing.ts +89 -0
- package/src/interactions/createFocusWithin.ts +206 -0
- package/src/interactions/createFocusable.ts +168 -0
- package/src/interactions/createHover.ts +254 -0
- package/src/interactions/createInteractionModality.ts +424 -0
- package/src/interactions/createKeyboard.ts +82 -0
- package/src/interactions/createLongPress.ts +174 -0
- package/src/interactions/createMove.ts +289 -0
- package/src/interactions/createPress.ts +834 -0
- package/src/interactions/index.ts +78 -0
- package/src/label/createField.ts +145 -0
- package/src/label/createLabel.ts +117 -0
- package/src/label/createLabels.ts +50 -0
- package/src/label/index.ts +19 -0
- package/src/landmark/createLandmark.ts +377 -0
- package/src/landmark/index.ts +8 -0
- package/src/link/createLink.ts +182 -0
- package/src/link/index.ts +1 -0
- package/src/listbox/createListBox.ts +269 -0
- package/src/listbox/createOption.ts +151 -0
- package/src/listbox/index.ts +12 -0
- package/src/live-announcer/announce.ts +322 -0
- package/src/live-announcer/index.ts +9 -0
- package/src/menu/createMenu.ts +396 -0
- package/src/menu/createMenuItem.ts +149 -0
- package/src/menu/createMenuTrigger.ts +88 -0
- package/src/menu/index.ts +18 -0
- package/src/meter/createMeter.ts +75 -0
- package/src/meter/index.ts +1 -0
- package/src/numberfield/createNumberField.ts +268 -0
- package/src/numberfield/index.ts +5 -0
- package/src/overlays/ariaHideOutside.ts +219 -0
- package/src/overlays/createInteractOutside.ts +149 -0
- package/src/overlays/createModal.tsx +202 -0
- package/src/overlays/createOverlay.ts +155 -0
- package/src/overlays/createOverlayTrigger.ts +85 -0
- package/src/overlays/createPreventScroll.ts +266 -0
- package/src/overlays/index.ts +44 -0
- package/src/popover/calculatePosition.ts +766 -0
- package/src/popover/createOverlayPosition.ts +356 -0
- package/src/popover/createPopover.ts +170 -0
- package/src/popover/index.ts +24 -0
- package/src/progress/createProgressBar.ts +128 -0
- package/src/progress/index.ts +5 -0
- package/src/radio/createRadio.ts +287 -0
- package/src/radio/createRadioGroup.ts +189 -0
- package/src/radio/createRadioGroupState.ts +201 -0
- package/src/radio/index.ts +23 -0
- package/src/searchfield/createSearchField.ts +186 -0
- package/src/searchfield/index.ts +2 -0
- package/src/select/createHiddenSelect.tsx +236 -0
- package/src/select/createSelect.ts +395 -0
- package/src/select/index.ts +14 -0
- package/src/selection/createTypeSelect.ts +201 -0
- package/src/selection/index.ts +6 -0
- package/src/separator/createSeparator.ts +82 -0
- package/src/separator/index.ts +6 -0
- package/src/slider/createSlider.ts +349 -0
- package/src/slider/index.ts +2 -0
- package/src/ssr/index.tsx +370 -0
- package/src/switch/createSwitch.ts +70 -0
- package/src/switch/index.ts +1 -0
- package/src/table/createTable.ts +526 -0
- package/src/table/createTableCell.ts +147 -0
- package/src/table/createTableColumnHeader.ts +115 -0
- package/src/table/createTableHeaderRow.ts +40 -0
- package/src/table/createTableRow.ts +155 -0
- package/src/table/createTableRowGroup.ts +32 -0
- package/src/table/createTableSelectAllCheckbox.ts +73 -0
- package/src/table/createTableSelectionCheckbox.ts +59 -0
- package/src/table/index.ts +30 -0
- package/src/table/types.ts +165 -0
- package/src/tabs/createTabs.ts +472 -0
- package/src/tabs/index.ts +14 -0
- package/src/tag/createTag.ts +194 -0
- package/src/tag/createTagGroup.ts +154 -0
- package/src/tag/index.ts +12 -0
- package/src/textfield/createTextField.ts +198 -0
- package/src/textfield/index.ts +5 -0
- package/src/toast/createToast.ts +118 -0
- package/src/toast/createToastRegion.ts +100 -0
- package/src/toast/index.ts +11 -0
- package/src/toggle/createToggle.ts +223 -0
- package/src/toggle/createToggleState.ts +94 -0
- package/src/toggle/index.ts +7 -0
- package/src/toolbar/createToolbar.ts +369 -0
- package/src/toolbar/index.ts +6 -0
- package/src/tooltip/createTooltip.ts +79 -0
- package/src/tooltip/createTooltipTrigger.ts +222 -0
- package/src/tooltip/index.ts +6 -0
- package/src/tree/createTree.ts +246 -0
- package/src/tree/createTreeItem.ts +233 -0
- package/src/tree/createTreeSelectionCheckbox.ts +68 -0
- package/src/tree/index.ts +16 -0
- package/src/tree/types.ts +87 -0
- package/src/utils/createDescription.ts +137 -0
- package/src/utils/dom.ts +327 -0
- package/src/utils/env.ts +54 -0
- package/src/utils/events.ts +106 -0
- package/src/utils/filterDOMProps.ts +116 -0
- package/src/utils/focus.ts +151 -0
- package/src/utils/geometry.ts +115 -0
- package/src/utils/globalListeners.ts +142 -0
- package/src/utils/index.ts +80 -0
- package/src/utils/mergeProps.ts +52 -0
- package/src/utils/platform.ts +52 -0
- package/src/utils/reactivity.ts +36 -0
- package/src/utils/textSelection.ts +114 -0
- package/src/visually-hidden/createVisuallyHidden.ts +124 -0
- package/src/visually-hidden/index.ts +6 -0
- package/dist/index.jsx +0 -15845
- package/dist/index.jsx.map +0 -7
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createToolbar - Accessibility hook for toolbar elements
|
|
3
|
+
*
|
|
4
|
+
* Provides keyboard navigation between toolbar items using arrow keys.
|
|
5
|
+
* Based on @react-aria/toolbar useToolbar.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createSignal,
|
|
10
|
+
createEffect,
|
|
11
|
+
onMount,
|
|
12
|
+
onCleanup,
|
|
13
|
+
type Accessor,
|
|
14
|
+
} from 'solid-js'
|
|
15
|
+
import { type MaybeAccessor, access } from '../utils'
|
|
16
|
+
import { useLocale } from '../i18n'
|
|
17
|
+
import { getOwnerDocument, isFocusable } from '../utils'
|
|
18
|
+
import { focusSafely } from '../utils/focus'
|
|
19
|
+
|
|
20
|
+
// ============================================
|
|
21
|
+
// TYPES
|
|
22
|
+
// ============================================
|
|
23
|
+
|
|
24
|
+
export type Orientation = 'horizontal' | 'vertical'
|
|
25
|
+
|
|
26
|
+
export interface AriaToolbarProps {
|
|
27
|
+
/** The orientation of the toolbar. @default 'horizontal' */
|
|
28
|
+
orientation?: MaybeAccessor<Orientation>
|
|
29
|
+
/** An accessibility label for the toolbar. */
|
|
30
|
+
'aria-label'?: MaybeAccessor<string>
|
|
31
|
+
/** Identifies the element (or elements) that labels the toolbar. */
|
|
32
|
+
'aria-labelledby'?: MaybeAccessor<string>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ToolbarAria {
|
|
36
|
+
/** Props for the toolbar container element. */
|
|
37
|
+
toolbarProps: {
|
|
38
|
+
role: 'toolbar' | 'group'
|
|
39
|
+
'aria-orientation': Orientation
|
|
40
|
+
'aria-label'?: string
|
|
41
|
+
'aria-labelledby'?: string
|
|
42
|
+
tabIndex?: number
|
|
43
|
+
ref: (el: HTMLElement) => void
|
|
44
|
+
}
|
|
45
|
+
/** The orientation of the toolbar. */
|
|
46
|
+
orientation: Accessor<Orientation>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================
|
|
50
|
+
// FOCUS MANAGER FOR TOOLBAR
|
|
51
|
+
// ============================================
|
|
52
|
+
|
|
53
|
+
interface FocusManagerOptions {
|
|
54
|
+
from?: Element
|
|
55
|
+
tabbable?: boolean
|
|
56
|
+
wrap?: boolean
|
|
57
|
+
accept?: (node: Element) => boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface FocusManager {
|
|
61
|
+
focusNext(opts?: FocusManagerOptions): HTMLElement | null
|
|
62
|
+
focusPrevious(opts?: FocusManagerOptions): HTMLElement | null
|
|
63
|
+
focusFirst(opts?: FocusManagerOptions): HTMLElement | null
|
|
64
|
+
focusLast(opts?: FocusManagerOptions): HTMLElement | null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isTabbable(element: Element): boolean {
|
|
68
|
+
if (!isFocusable(element)) {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
const tabIndex = element.getAttribute('tabindex')
|
|
72
|
+
if (tabIndex != null) {
|
|
73
|
+
return parseInt(tabIndex, 10) >= 0
|
|
74
|
+
}
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getFocusableElements(root: Element, tabbable = false): HTMLElement[] {
|
|
79
|
+
const elements: HTMLElement[] = []
|
|
80
|
+
const filter = tabbable ? isTabbable : isFocusable
|
|
81
|
+
|
|
82
|
+
// Check the root element itself
|
|
83
|
+
if (filter(root)) {
|
|
84
|
+
elements.push(root as HTMLElement)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check all descendants
|
|
88
|
+
const descendants = root.querySelectorAll('*')
|
|
89
|
+
for (let i = 0; i < descendants.length; i++) {
|
|
90
|
+
const el = descendants[i]
|
|
91
|
+
if (filter(el)) {
|
|
92
|
+
elements.push(el as HTMLElement)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return elements
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getActiveElement(doc: Document): Element | null {
|
|
100
|
+
let activeElement = doc.activeElement
|
|
101
|
+
while (activeElement?.shadowRoot?.activeElement) {
|
|
102
|
+
activeElement = activeElement.shadowRoot.activeElement
|
|
103
|
+
}
|
|
104
|
+
return activeElement
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createFocusManager(ref: Accessor<HTMLElement | undefined>): FocusManager {
|
|
108
|
+
return {
|
|
109
|
+
focusNext(opts: FocusManagerOptions = {}) {
|
|
110
|
+
const root = ref()
|
|
111
|
+
if (!root) return null
|
|
112
|
+
|
|
113
|
+
const { from, tabbable = true, wrap = false, accept } = opts
|
|
114
|
+
const doc = getOwnerDocument(root)
|
|
115
|
+
const current = from || getActiveElement(doc)
|
|
116
|
+
|
|
117
|
+
let elements = getFocusableElements(root, tabbable)
|
|
118
|
+
if (accept) {
|
|
119
|
+
elements = elements.filter(accept)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!current || elements.length === 0) return null
|
|
123
|
+
|
|
124
|
+
const currentIndex = elements.indexOf(current as HTMLElement)
|
|
125
|
+
let nextIndex = currentIndex + 1
|
|
126
|
+
|
|
127
|
+
if (nextIndex >= elements.length) {
|
|
128
|
+
if (wrap) {
|
|
129
|
+
nextIndex = 0
|
|
130
|
+
} else {
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const nextElement = elements[nextIndex]
|
|
136
|
+
if (nextElement) {
|
|
137
|
+
focusSafely(nextElement)
|
|
138
|
+
return nextElement
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return null
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
focusPrevious(opts: FocusManagerOptions = {}) {
|
|
145
|
+
const root = ref()
|
|
146
|
+
if (!root) return null
|
|
147
|
+
|
|
148
|
+
const { from, tabbable = true, wrap = false, accept } = opts
|
|
149
|
+
const doc = getOwnerDocument(root)
|
|
150
|
+
const current = from || getActiveElement(doc)
|
|
151
|
+
|
|
152
|
+
let elements = getFocusableElements(root, tabbable)
|
|
153
|
+
if (accept) {
|
|
154
|
+
elements = elements.filter(accept)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!current || elements.length === 0) return null
|
|
158
|
+
|
|
159
|
+
const currentIndex = elements.indexOf(current as HTMLElement)
|
|
160
|
+
let prevIndex = currentIndex - 1
|
|
161
|
+
|
|
162
|
+
if (prevIndex < 0) {
|
|
163
|
+
if (wrap) {
|
|
164
|
+
prevIndex = elements.length - 1
|
|
165
|
+
} else {
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const prevElement = elements[prevIndex]
|
|
171
|
+
if (prevElement) {
|
|
172
|
+
focusSafely(prevElement)
|
|
173
|
+
return prevElement
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return null
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
focusFirst(opts: FocusManagerOptions = {}) {
|
|
180
|
+
const root = ref()
|
|
181
|
+
if (!root) return null
|
|
182
|
+
|
|
183
|
+
const { tabbable = true, accept } = opts
|
|
184
|
+
let elements = getFocusableElements(root, tabbable)
|
|
185
|
+
if (accept) {
|
|
186
|
+
elements = elements.filter(accept)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (elements.length > 0) {
|
|
190
|
+
focusSafely(elements[0])
|
|
191
|
+
return elements[0]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return null
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
focusLast(opts: FocusManagerOptions = {}) {
|
|
198
|
+
const root = ref()
|
|
199
|
+
if (!root) return null
|
|
200
|
+
|
|
201
|
+
const { tabbable = true, accept } = opts
|
|
202
|
+
let elements = getFocusableElements(root, tabbable)
|
|
203
|
+
if (accept) {
|
|
204
|
+
elements = elements.filter(accept)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (elements.length > 0) {
|
|
208
|
+
const lastElement = elements[elements.length - 1]
|
|
209
|
+
focusSafely(lastElement)
|
|
210
|
+
return lastElement
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return null
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ============================================
|
|
219
|
+
// CREATE TOOLBAR HOOK
|
|
220
|
+
// ============================================
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Provides the behavior and accessibility implementation for a toolbar.
|
|
224
|
+
* A toolbar is a container for a set of interactive controls with arrow key navigation.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```tsx
|
|
228
|
+
* let ref;
|
|
229
|
+
* const { toolbarProps } = createToolbar({ orientation: 'horizontal' });
|
|
230
|
+
* return (
|
|
231
|
+
* <div {...toolbarProps}>
|
|
232
|
+
* <Button>Cut</Button>
|
|
233
|
+
* <Button>Copy</Button>
|
|
234
|
+
* <Button>Paste</Button>
|
|
235
|
+
* </div>
|
|
236
|
+
* );
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
export function createToolbar(props: AriaToolbarProps = {}): ToolbarAria {
|
|
240
|
+
let toolbarRef: HTMLElement | undefined
|
|
241
|
+
const [isInToolbar, setIsInToolbar] = createSignal(false)
|
|
242
|
+
let lastFocusedElement: Element | null = null
|
|
243
|
+
|
|
244
|
+
const locale = useLocale()
|
|
245
|
+
const orientation = () => access(props.orientation) ?? 'horizontal'
|
|
246
|
+
const ariaLabel = () => access(props['aria-label'])
|
|
247
|
+
const ariaLabelledby = () => access(props['aria-labelledby'])
|
|
248
|
+
|
|
249
|
+
const focusManager = createFocusManager(() => toolbarRef)
|
|
250
|
+
|
|
251
|
+
// Check if this toolbar is nested inside another toolbar
|
|
252
|
+
onMount(() => {
|
|
253
|
+
if (toolbarRef) {
|
|
254
|
+
const parentToolbar = toolbarRef.parentElement?.closest('[role="toolbar"]')
|
|
255
|
+
setIsInToolbar(!!parentToolbar)
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
// Keyboard event handler
|
|
260
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
261
|
+
// Don't handle if nested toolbar (parent handles navigation)
|
|
262
|
+
if (isInToolbar()) return
|
|
263
|
+
|
|
264
|
+
const dir = locale().direction
|
|
265
|
+
const isRTL = dir === 'rtl'
|
|
266
|
+
const isHorizontal = orientation() === 'horizontal'
|
|
267
|
+
|
|
268
|
+
let handled = false
|
|
269
|
+
|
|
270
|
+
switch (e.key) {
|
|
271
|
+
case 'ArrowRight':
|
|
272
|
+
if (isHorizontal) {
|
|
273
|
+
if (isRTL) {
|
|
274
|
+
focusManager.focusPrevious({ tabbable: true })
|
|
275
|
+
} else {
|
|
276
|
+
focusManager.focusNext({ tabbable: true })
|
|
277
|
+
}
|
|
278
|
+
handled = true
|
|
279
|
+
}
|
|
280
|
+
break
|
|
281
|
+
case 'ArrowLeft':
|
|
282
|
+
if (isHorizontal) {
|
|
283
|
+
if (isRTL) {
|
|
284
|
+
focusManager.focusNext({ tabbable: true })
|
|
285
|
+
} else {
|
|
286
|
+
focusManager.focusPrevious({ tabbable: true })
|
|
287
|
+
}
|
|
288
|
+
handled = true
|
|
289
|
+
}
|
|
290
|
+
break
|
|
291
|
+
case 'ArrowDown':
|
|
292
|
+
if (!isHorizontal) {
|
|
293
|
+
focusManager.focusNext({ tabbable: true })
|
|
294
|
+
handled = true
|
|
295
|
+
}
|
|
296
|
+
break
|
|
297
|
+
case 'ArrowUp':
|
|
298
|
+
if (!isHorizontal) {
|
|
299
|
+
focusManager.focusPrevious({ tabbable: true })
|
|
300
|
+
handled = true
|
|
301
|
+
}
|
|
302
|
+
break
|
|
303
|
+
case 'Tab':
|
|
304
|
+
// Store the last focused element for re-entry
|
|
305
|
+
lastFocusedElement = e.target as Element
|
|
306
|
+
break
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (handled) {
|
|
310
|
+
e.preventDefault()
|
|
311
|
+
e.stopPropagation()
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Focus handler - restore last focused element when re-entering
|
|
316
|
+
const onFocus = (e: FocusEvent) => {
|
|
317
|
+
if (isInToolbar()) return
|
|
318
|
+
|
|
319
|
+
// Only restore if focus is coming from outside the toolbar
|
|
320
|
+
const root = toolbarRef
|
|
321
|
+
if (!root) return
|
|
322
|
+
|
|
323
|
+
const relatedTarget = e.relatedTarget as Element | null
|
|
324
|
+
|
|
325
|
+
// If focus came from outside and we have a last focused element
|
|
326
|
+
if (
|
|
327
|
+
lastFocusedElement &&
|
|
328
|
+
root.contains(lastFocusedElement) &&
|
|
329
|
+
(!relatedTarget || !root.contains(relatedTarget))
|
|
330
|
+
) {
|
|
331
|
+
// Restore focus to the last focused element
|
|
332
|
+
focusSafely(lastFocusedElement as HTMLElement)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Set up capture event listeners
|
|
337
|
+
const setRef = (el: HTMLElement) => {
|
|
338
|
+
toolbarRef = el
|
|
339
|
+
|
|
340
|
+
// Use capture phase for keyboard events
|
|
341
|
+
el.addEventListener('keydown', onKeyDown, true)
|
|
342
|
+
el.addEventListener('focus', onFocus, true)
|
|
343
|
+
|
|
344
|
+
onCleanup(() => {
|
|
345
|
+
el.removeEventListener('keydown', onKeyDown, true)
|
|
346
|
+
el.removeEventListener('focus', onFocus, true)
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
toolbarProps: {
|
|
352
|
+
get role() {
|
|
353
|
+
return isInToolbar() ? 'group' : 'toolbar'
|
|
354
|
+
},
|
|
355
|
+
get 'aria-orientation'() {
|
|
356
|
+
return orientation()
|
|
357
|
+
},
|
|
358
|
+
get 'aria-label'() {
|
|
359
|
+
return ariaLabel()
|
|
360
|
+
},
|
|
361
|
+
get 'aria-labelledby'() {
|
|
362
|
+
// Only use aria-labelledby if no aria-label is provided
|
|
363
|
+
return ariaLabel() ? undefined : ariaLabelledby()
|
|
364
|
+
},
|
|
365
|
+
ref: setRef,
|
|
366
|
+
},
|
|
367
|
+
orientation,
|
|
368
|
+
}
|
|
369
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createTooltip hook for Solidaria
|
|
3
|
+
*
|
|
4
|
+
* Provides the accessibility implementation for a Tooltip component.
|
|
5
|
+
*
|
|
6
|
+
* Port of @react-aria/tooltip useTooltip.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type JSX } from 'solid-js';
|
|
10
|
+
import { type TooltipTriggerState } from '@proyecto-viviana/solid-stately';
|
|
11
|
+
import { createHover } from '../interactions/createHover';
|
|
12
|
+
import { filterDOMProps, mergeProps } from '../utils';
|
|
13
|
+
|
|
14
|
+
// ============================================
|
|
15
|
+
// TYPES
|
|
16
|
+
// ============================================
|
|
17
|
+
|
|
18
|
+
export interface TooltipProps {
|
|
19
|
+
/** Whether the tooltip is disabled. */
|
|
20
|
+
isDisabled?: boolean;
|
|
21
|
+
/** Custom aria-label for the tooltip. */
|
|
22
|
+
'aria-label'?: string;
|
|
23
|
+
/** ID of an element that labels the tooltip. */
|
|
24
|
+
'aria-labelledby'?: string;
|
|
25
|
+
/** ID of an element that describes the tooltip. */
|
|
26
|
+
'aria-describedby'?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TooltipAria {
|
|
30
|
+
/** Props to spread on the tooltip element. */
|
|
31
|
+
tooltipProps: JSX.HTMLAttributes<HTMLElement>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================
|
|
35
|
+
// IMPLEMENTATION
|
|
36
|
+
// ============================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Provides the accessibility implementation for a Tooltip component.
|
|
40
|
+
*
|
|
41
|
+
* When hovering over the tooltip itself, it stays open. When the mouse leaves
|
|
42
|
+
* the tooltip, it closes.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* import { createTooltip } from 'solidaria';
|
|
47
|
+
* import { createTooltipTriggerState } from 'solid-stately';
|
|
48
|
+
*
|
|
49
|
+
* function Tooltip(props) {
|
|
50
|
+
* const state = props.state;
|
|
51
|
+
* const { tooltipProps } = createTooltip(props, state);
|
|
52
|
+
*
|
|
53
|
+
* return (
|
|
54
|
+
* <div {...tooltipProps} role="tooltip">
|
|
55
|
+
* {props.children}
|
|
56
|
+
* </div>
|
|
57
|
+
* );
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function createTooltip(
|
|
62
|
+
props: TooltipProps = {},
|
|
63
|
+
state?: TooltipTriggerState
|
|
64
|
+
): TooltipAria {
|
|
65
|
+
const domProps = filterDOMProps(props, { labelable: true });
|
|
66
|
+
|
|
67
|
+
const { hoverProps } = createHover({
|
|
68
|
+
onHoverStart: () => state?.open(true),
|
|
69
|
+
onHoverEnd: () => state?.close(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
tooltipProps: mergeProps<JSX.HTMLAttributes<HTMLElement>>(
|
|
74
|
+
domProps,
|
|
75
|
+
hoverProps,
|
|
76
|
+
{ role: 'tooltip' }
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createTooltipTrigger hook for Solidaria
|
|
3
|
+
*
|
|
4
|
+
* Provides the behavior and accessibility implementation for a tooltip trigger,
|
|
5
|
+
* e.g. a button that shows a description when focused or hovered.
|
|
6
|
+
*
|
|
7
|
+
* Port of @react-aria/tooltip useTooltipTrigger.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { type JSX, createEffect, onCleanup } from 'solid-js';
|
|
11
|
+
import { type TooltipTriggerState } from '@proyecto-viviana/solid-stately';
|
|
12
|
+
import { createHover } from '../interactions/createHover';
|
|
13
|
+
import { createFocusable } from '../interactions/createFocusable';
|
|
14
|
+
import { mergeProps } from '../utils';
|
|
15
|
+
import { createId } from '../ssr';
|
|
16
|
+
|
|
17
|
+
// ============================================
|
|
18
|
+
// TYPES
|
|
19
|
+
// ============================================
|
|
20
|
+
|
|
21
|
+
export interface TooltipTriggerProps {
|
|
22
|
+
/** Whether the tooltip should be disabled. */
|
|
23
|
+
isDisabled?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The trigger mechanism for the tooltip.
|
|
26
|
+
* @default 'focus'
|
|
27
|
+
*/
|
|
28
|
+
trigger?: 'focus';
|
|
29
|
+
/**
|
|
30
|
+
* Whether the tooltip should close when the trigger is pressed.
|
|
31
|
+
* @default true
|
|
32
|
+
*/
|
|
33
|
+
shouldCloseOnPress?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TooltipTriggerAria {
|
|
37
|
+
/** Props to spread on the trigger element. */
|
|
38
|
+
triggerProps: JSX.HTMLAttributes<HTMLElement>;
|
|
39
|
+
/** Props to spread on the tooltip element (id for accessibility). */
|
|
40
|
+
tooltipProps: { id: string };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ============================================
|
|
44
|
+
// GLOBAL STATE
|
|
45
|
+
// ============================================
|
|
46
|
+
|
|
47
|
+
type Modality = 'keyboard' | 'pointer' | 'virtual';
|
|
48
|
+
let currentModality: Modality | null = null;
|
|
49
|
+
|
|
50
|
+
// Track interaction modality (pointer vs keyboard)
|
|
51
|
+
if (typeof document !== 'undefined') {
|
|
52
|
+
document.addEventListener('keydown', () => {
|
|
53
|
+
currentModality = 'keyboard';
|
|
54
|
+
}, true);
|
|
55
|
+
document.addEventListener('pointerdown', () => {
|
|
56
|
+
currentModality = 'pointer';
|
|
57
|
+
}, true);
|
|
58
|
+
document.addEventListener('pointermove', () => {
|
|
59
|
+
currentModality = 'pointer';
|
|
60
|
+
}, true);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isFocusVisible(): boolean {
|
|
64
|
+
return currentModality === 'keyboard';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================
|
|
68
|
+
// IMPLEMENTATION
|
|
69
|
+
// ============================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Provides the behavior and accessibility implementation for a tooltip trigger.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```tsx
|
|
76
|
+
* import { createTooltipTrigger } from 'solidaria';
|
|
77
|
+
* import { createTooltipTriggerState } from 'solid-stately';
|
|
78
|
+
*
|
|
79
|
+
* function TooltipButton(props) {
|
|
80
|
+
* let ref;
|
|
81
|
+
* const state = createTooltipTriggerState({ delay: 500 });
|
|
82
|
+
* const { triggerProps, tooltipProps } = createTooltipTrigger(
|
|
83
|
+
* { isDisabled: props.isDisabled },
|
|
84
|
+
* state,
|
|
85
|
+
* () => ref
|
|
86
|
+
* );
|
|
87
|
+
*
|
|
88
|
+
* return (
|
|
89
|
+
* <>
|
|
90
|
+
* <button ref={ref} {...triggerProps}>
|
|
91
|
+
* Hover me
|
|
92
|
+
* </button>
|
|
93
|
+
* <Show when={state.isOpen()}>
|
|
94
|
+
* <div {...tooltipProps}>Tooltip content</div>
|
|
95
|
+
* </Show>
|
|
96
|
+
* </>
|
|
97
|
+
* );
|
|
98
|
+
* }
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export function createTooltipTrigger(
|
|
102
|
+
props: TooltipTriggerProps,
|
|
103
|
+
state: TooltipTriggerState,
|
|
104
|
+
ref: () => HTMLElement | null | undefined
|
|
105
|
+
): TooltipTriggerAria {
|
|
106
|
+
const {
|
|
107
|
+
isDisabled = false,
|
|
108
|
+
trigger,
|
|
109
|
+
shouldCloseOnPress = true,
|
|
110
|
+
} = props;
|
|
111
|
+
|
|
112
|
+
const tooltipId = createId();
|
|
113
|
+
|
|
114
|
+
// Track hover and focus state
|
|
115
|
+
let isHovered = false;
|
|
116
|
+
let isFocused = false;
|
|
117
|
+
|
|
118
|
+
const handleShow = () => {
|
|
119
|
+
if (isHovered || isFocused) {
|
|
120
|
+
state.open(isFocused);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleHide = (immediate?: boolean) => {
|
|
125
|
+
if (!isHovered && !isFocused) {
|
|
126
|
+
state.close(immediate);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Handle Escape key to dismiss tooltip
|
|
131
|
+
createEffect(() => {
|
|
132
|
+
if (!state.isOpen()) return;
|
|
133
|
+
|
|
134
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
135
|
+
const element = ref();
|
|
136
|
+
if (element) {
|
|
137
|
+
if (e.key === 'Escape') {
|
|
138
|
+
e.stopPropagation();
|
|
139
|
+
state.close(true);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
document.addEventListener('keydown', onKeyDown, true);
|
|
145
|
+
onCleanup(() => {
|
|
146
|
+
document.removeEventListener('keydown', onKeyDown, true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const onHoverStart = () => {
|
|
151
|
+
if (trigger === 'focus') {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Hover events (onPointerEnter) only fire from pointer interactions,
|
|
155
|
+
// so we can always set isHovered to true here
|
|
156
|
+
isHovered = true;
|
|
157
|
+
handleShow();
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const onHoverEnd = () => {
|
|
161
|
+
if (trigger === 'focus') {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
isFocused = false;
|
|
165
|
+
isHovered = false;
|
|
166
|
+
handleHide();
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const onPressStart = () => {
|
|
170
|
+
if (!shouldCloseOnPress) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
isFocused = false;
|
|
174
|
+
isHovered = false;
|
|
175
|
+
handleHide(true);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const onFocus = () => {
|
|
179
|
+
const visible = isFocusVisible();
|
|
180
|
+
if (visible) {
|
|
181
|
+
isFocused = true;
|
|
182
|
+
handleShow();
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const onBlur = () => {
|
|
187
|
+
isFocused = false;
|
|
188
|
+
isHovered = false;
|
|
189
|
+
handleHide(true);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const { hoverProps } = createHover({
|
|
193
|
+
isDisabled,
|
|
194
|
+
onHoverStart,
|
|
195
|
+
onHoverEnd,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const { focusableProps } = createFocusable({
|
|
199
|
+
isDisabled,
|
|
200
|
+
onFocus,
|
|
201
|
+
onBlur,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const triggerProps = {
|
|
205
|
+
...focusableProps,
|
|
206
|
+
...hoverProps,
|
|
207
|
+
get 'aria-describedby'() {
|
|
208
|
+
return state.isOpen() ? tooltipId : undefined;
|
|
209
|
+
},
|
|
210
|
+
onPointerDown: onPressStart,
|
|
211
|
+
onKeyDown: onPressStart,
|
|
212
|
+
// Remove tabIndex set by focusableProps to avoid overriding
|
|
213
|
+
tabIndex: undefined,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
triggerProps: triggerProps as JSX.HTMLAttributes<HTMLElement>,
|
|
218
|
+
tooltipProps: {
|
|
219
|
+
id: tooltipId,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|