@proyecto-viviana/solidaria 0.2.5 → 0.2.8
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/LICENSE +21 -0
- package/dist/actiongroup/createActionGroup.d.ts +29 -0
- package/dist/actiongroup/createActionGroup.d.ts.map +1 -0
- package/dist/actiongroup/index.d.ts +2 -0
- package/dist/actiongroup/index.d.ts.map +1 -0
- package/dist/autocomplete/createAutocomplete.d.ts +6 -2
- package/dist/autocomplete/createAutocomplete.d.ts.map +1 -1
- package/dist/breadcrumbs/createBreadcrumbs.d.ts +2 -0
- package/dist/breadcrumbs/createBreadcrumbs.d.ts.map +1 -1
- package/dist/button/createToggleButtonGroup.d.ts +32 -0
- package/dist/button/createToggleButtonGroup.d.ts.map +1 -0
- package/dist/button/index.d.ts +2 -0
- package/dist/button/index.d.ts.map +1 -1
- package/dist/calendar/createCalendarCell.d.ts +2 -0
- package/dist/calendar/createCalendarCell.d.ts.map +1 -1
- package/dist/calendar/createCalendarGrid.d.ts.map +1 -1
- package/dist/calendar/createRangeCalendarCell.d.ts +3 -1
- package/dist/calendar/createRangeCalendarCell.d.ts.map +1 -1
- package/dist/checkbox/createCheckboxGroup.d.ts +5 -1
- package/dist/checkbox/createCheckboxGroup.d.ts.map +1 -1
- package/dist/collections/index.d.ts +56 -0
- package/dist/collections/index.d.ts.map +1 -0
- package/dist/color/createColorArea.d.ts.map +1 -1
- package/dist/color/createColorSlider.d.ts.map +1 -1
- package/dist/color/createColorWheel.d.ts.map +1 -1
- package/dist/combobox/createComboBox.d.ts +6 -0
- package/dist/combobox/createComboBox.d.ts.map +1 -1
- package/dist/datepicker/createDatePicker.d.ts +6 -0
- package/dist/datepicker/createDatePicker.d.ts.map +1 -1
- package/dist/datepicker/createDateRangePicker.d.ts +40 -0
- package/dist/datepicker/createDateRangePicker.d.ts.map +1 -0
- package/dist/datepicker/createDateSegment.d.ts +1 -1
- package/dist/datepicker/createDateSegment.d.ts.map +1 -1
- package/dist/datepicker/createTimeSegment.d.ts +29 -0
- package/dist/datepicker/createTimeSegment.d.ts.map +1 -0
- package/dist/datepicker/index.d.ts +2 -0
- package/dist/datepicker/index.d.ts.map +1 -1
- package/dist/disclosure/createDisclosureGroup.d.ts +2 -1
- package/dist/disclosure/createDisclosureGroup.d.ts.map +1 -1
- package/dist/dnd/createDrag.d.ts.map +1 -1
- package/dist/dnd/createDraggableCollection.d.ts +4 -0
- package/dist/dnd/createDraggableCollection.d.ts.map +1 -1
- package/dist/dnd/createDraggableItem.d.ts.map +1 -1
- package/dist/dnd/createDrop.d.ts.map +1 -1
- package/dist/dnd/createDroppableCollection.d.ts +32 -1
- package/dist/dnd/createDroppableCollection.d.ts.map +1 -1
- package/dist/dnd/createDroppableItem.d.ts.map +1 -1
- package/dist/dnd/index.d.ts +1 -1
- package/dist/dnd/index.d.ts.map +1 -1
- package/dist/grid/createGrid.d.ts.map +1 -1
- package/dist/gridlist/createGridList.d.ts.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4659 -3452
- package/dist/index.js.map +1 -7
- package/dist/index.ssr.js +4659 -3452
- package/dist/index.ssr.js.map +1 -7
- package/dist/interactions/createFocus.d.ts.map +1 -1
- package/dist/interactions/createFocusWithin.d.ts.map +1 -1
- package/dist/link/createLink.d.ts +10 -0
- package/dist/link/createLink.d.ts.map +1 -1
- package/dist/listbox/createListBox.d.ts +1 -0
- package/dist/listbox/createListBox.d.ts.map +1 -1
- package/dist/listbox/createOption.d.ts.map +1 -1
- package/dist/menu/createMenu.d.ts +1 -0
- package/dist/menu/createMenu.d.ts.map +1 -1
- package/dist/meter/createMeter.d.ts.map +1 -1
- package/dist/numberfield/createNumberField.d.ts +18 -0
- package/dist/numberfield/createNumberField.d.ts.map +1 -1
- package/dist/overlays/createModal.d.ts +16 -0
- package/dist/overlays/createModal.d.ts.map +1 -1
- package/dist/overlays/createOverlay.d.ts.map +1 -1
- package/dist/overlays/index.d.ts +1 -1
- package/dist/overlays/index.d.ts.map +1 -1
- package/dist/popover/createOverlayPosition.d.ts.map +1 -1
- package/dist/popover/createPopover.d.ts.map +1 -1
- package/dist/progress/createProgressBar.d.ts.map +1 -1
- package/dist/radio/createRadioGroup.d.ts +2 -2
- package/dist/radio/createRadioGroup.d.ts.map +1 -1
- package/dist/searchfield/createSearchField.d.ts.map +1 -1
- package/dist/select/createHiddenSelect.d.ts.map +1 -1
- package/dist/select/createSelect.d.ts.map +1 -1
- package/dist/slider/createSlider.d.ts.map +1 -1
- package/dist/table/createTable.d.ts.map +1 -1
- package/dist/tabs/createTabs.d.ts +1 -1
- package/dist/tabs/createTabs.d.ts.map +1 -1
- package/dist/tag/createTag.d.ts.map +1 -1
- package/dist/tag/createTagGroup.d.ts.map +1 -1
- package/dist/toast/createToast.d.ts +4 -0
- package/dist/toast/createToast.d.ts.map +1 -1
- package/dist/toast/createToastRegion.d.ts.map +1 -1
- package/dist/toolbar/createToolbar.d.ts.map +1 -1
- package/dist/tooltip/createTooltipTrigger.d.ts.map +1 -1
- package/dist/tree/createTree.d.ts.map +1 -1
- package/dist/tree/createTreeItem.d.ts.map +1 -1
- package/dist/tree/types.d.ts +4 -0
- package/dist/tree/types.d.ts.map +1 -1
- package/dist/utils/env.d.ts +1 -1
- package/dist/utils/env.d.ts.map +1 -1
- package/dist/utils/platform.d.ts.map +1 -1
- package/dist/visually-hidden/createVisuallyHidden.d.ts.map +1 -1
- package/package.json +8 -6
- package/src/actiongroup/createActionGroup.ts +324 -0
- package/src/actiongroup/index.ts +8 -0
- package/src/autocomplete/createAutocomplete.ts +32 -9
- package/src/breadcrumbs/createBreadcrumbs.ts +10 -15
- package/src/button/createButton.ts +1 -1
- package/src/button/createToggleButtonGroup.ts +128 -0
- package/src/button/index.ts +9 -0
- package/src/calendar/createCalendarCell.ts +6 -4
- package/src/calendar/createCalendarGrid.ts +27 -18
- package/src/calendar/createRangeCalendarCell.ts +26 -9
- package/src/checkbox/createCheckboxGroup.ts +21 -4
- package/src/collections/index.ts +242 -0
- package/src/color/createColorArea.ts +380 -314
- package/src/color/createColorField.ts +137 -137
- package/src/color/createColorSlider.ts +286 -197
- package/src/color/createColorSwatch.ts +40 -40
- package/src/color/createColorWheel.ts +218 -208
- package/src/color/index.ts +24 -24
- package/src/color/types.ts +116 -116
- package/src/combobox/createComboBox.ts +670 -647
- package/src/combobox/index.ts +6 -6
- package/src/datepicker/createDatePicker.ts +54 -16
- package/src/datepicker/createDateRangePicker.ts +246 -0
- package/src/datepicker/createDateSegment.ts +185 -31
- package/src/datepicker/createTimeSegment.ts +370 -0
- package/src/datepicker/index.ts +14 -0
- package/src/dialog/createDialog.ts +120 -120
- package/src/dialog/index.ts +2 -2
- package/src/dialog/types.ts +19 -19
- package/src/disclosure/createDisclosureGroup.ts +5 -2
- package/src/dnd/createDrag.ts +224 -209
- package/src/dnd/createDraggableCollection.ts +96 -63
- package/src/dnd/createDraggableItem.ts +259 -243
- package/src/dnd/createDrop.ts +322 -321
- package/src/dnd/createDroppableCollection.ts +682 -293
- package/src/dnd/createDroppableItem.ts +215 -213
- package/src/dnd/index.ts +55 -47
- package/src/dnd/types.ts +89 -89
- package/src/dnd/utils.ts +294 -294
- package/src/focus/createAutoFocus.ts +321 -321
- package/src/focus/createFocusRestore.ts +313 -313
- package/src/focus/createVirtualFocus.ts +396 -396
- package/src/form/createFormValidation.ts +224 -224
- package/src/form/index.ts +11 -11
- package/src/grid/createGrid.ts +3 -1
- package/src/gridlist/createGridList.ts +16 -0
- package/src/gridlist/createGridListItem.ts +1 -1
- package/src/i18n/NumberFormatter.ts +266 -266
- package/src/i18n/createCollator.ts +79 -79
- package/src/i18n/createDateFormatter.ts +83 -83
- package/src/i18n/createFilter.ts +131 -131
- package/src/i18n/createNumberFormatter.ts +52 -52
- package/src/i18n/index.ts +40 -40
- package/src/i18n/locale.tsx +188 -188
- package/src/i18n/utils.ts +99 -99
- package/src/index.ts +51 -0
- package/src/interactions/createFocus.ts +6 -5
- package/src/interactions/createFocusWithin.ts +6 -5
- package/src/interactions/createLongPress.ts +174 -174
- package/src/interactions/createMove.ts +289 -289
- package/src/interactions/createPress.ts +5 -5
- package/src/landmark/createLandmark.ts +377 -377
- package/src/landmark/index.ts +8 -8
- package/src/link/createLink.ts +23 -8
- package/src/listbox/createListBox.ts +308 -269
- package/src/listbox/createOption.ts +162 -151
- package/src/listbox/index.ts +12 -12
- package/src/live-announcer/announce.ts +322 -322
- package/src/live-announcer/index.ts +9 -9
- package/src/menu/createMenu.ts +405 -396
- package/src/menu/createMenuItem.ts +149 -149
- package/src/menu/createMenuTrigger.ts +88 -88
- package/src/menu/index.ts +18 -18
- package/src/meter/createMeter.ts +1 -6
- package/src/numberfield/createNumberField.ts +311 -268
- package/src/numberfield/index.ts +5 -5
- package/src/overlays/ariaHideOutside.ts +219 -219
- package/src/overlays/createInteractOutside.ts +149 -149
- package/src/overlays/createModal.tsx +238 -202
- package/src/overlays/createOverlay.ts +165 -155
- package/src/overlays/createOverlayTrigger.ts +85 -85
- package/src/overlays/createPreventScroll.ts +266 -266
- package/src/overlays/index.ts +48 -44
- package/src/popover/calculatePosition.ts +6 -6
- package/src/popover/createOverlayPosition.ts +7 -4
- package/src/popover/createPopover.ts +21 -7
- package/src/progress/createProgressBar.ts +6 -1
- package/src/radio/createRadioGroup.ts +88 -14
- package/src/searchfield/createSearchField.ts +241 -186
- package/src/searchfield/index.ts +2 -2
- package/src/select/createHiddenSelect.tsx +263 -236
- package/src/select/createSelect.ts +373 -395
- package/src/select/index.ts +14 -14
- package/src/slider/createSlider.ts +364 -349
- package/src/slider/index.ts +2 -2
- package/src/ssr/index.tsx +370 -370
- package/src/table/createTable.ts +3 -1
- package/src/table/createTableColumnHeader.ts +1 -1
- package/src/table/createTableRow.ts +1 -1
- package/src/tabs/createTabs.ts +80 -51
- package/src/tag/createTag.ts +135 -6
- package/src/tag/createTagGroup.ts +7 -2
- package/src/toast/createToast.ts +8 -2
- package/src/toast/createToastRegion.ts +0 -1
- package/src/toolbar/createToolbar.ts +75 -1
- package/src/tooltip/createTooltip.ts +79 -79
- package/src/tooltip/createTooltipTrigger.ts +226 -222
- package/src/tooltip/index.ts +6 -6
- package/src/tree/createTree.ts +261 -246
- package/src/tree/createTreeItem.ts +282 -233
- package/src/tree/createTreeSelectionCheckbox.ts +68 -68
- package/src/tree/index.ts +16 -16
- package/src/tree/types.ts +91 -87
- package/src/utils/env.ts +55 -54
- package/src/utils/platform.ts +16 -6
- package/src/visually-hidden/createVisuallyHidden.ts +139 -124
- package/src/visually-hidden/index.ts +6 -6
package/src/menu/createMenu.ts
CHANGED
|
@@ -1,396 +1,405 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Provides the behavior and accessibility implementation for a menu component.
|
|
3
|
-
* A menu displays a list of actions or options that a user can choose.
|
|
4
|
-
* Based on @react-aria/menu useMenu.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { createEffect, onCleanup, type JSX, type Accessor } from 'solid-js';
|
|
8
|
-
import { createFocusWithin } from '../interactions/createFocusWithin';
|
|
9
|
-
import { createLabel } from '../label/createLabel';
|
|
10
|
-
import { createTypeSelect } from '../selection/createTypeSelect';
|
|
11
|
-
import { filterDOMProps } from '../utils/filterDOMProps';
|
|
12
|
-
import { mergeProps } from '../utils/mergeProps';
|
|
13
|
-
import { createId } from '../ssr';
|
|
14
|
-
import { access, type MaybeAccessor } from '../utils/reactivity';
|
|
15
|
-
import { isDevEnv } from '../utils/env';
|
|
16
|
-
import type { MenuState, Key, Collection } from '@proyecto-viviana/solid-stately';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Default number of items to skip for page up/down when DOM measurement is not available.
|
|
20
|
-
*/
|
|
21
|
-
const DEFAULT_PAGE_SIZE = 10;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Find the next non-disabled key in a collection.
|
|
25
|
-
*/
|
|
26
|
-
function findNextNonDisabledKey<T>(
|
|
27
|
-
collection: Collection<T>,
|
|
28
|
-
currentKey: Key | null,
|
|
29
|
-
direction: 'next' | 'prev',
|
|
30
|
-
isDisabled: (key: Key) => boolean,
|
|
31
|
-
wrap: boolean
|
|
32
|
-
): Key | null {
|
|
33
|
-
const getNextKey = direction === 'next'
|
|
34
|
-
? (key: Key) => collection.getKeyAfter(key)
|
|
35
|
-
: (key: Key) => collection.getKeyBefore(key);
|
|
36
|
-
|
|
37
|
-
const getFirstKey = direction === 'next'
|
|
38
|
-
? () => collection.getFirstKey()
|
|
39
|
-
: () => collection.getLastKey();
|
|
40
|
-
|
|
41
|
-
let nextKey = currentKey != null ? getNextKey(currentKey) : getFirstKey();
|
|
42
|
-
|
|
43
|
-
// Skip disabled keys
|
|
44
|
-
while (nextKey != null && isDisabled(nextKey)) {
|
|
45
|
-
nextKey = getNextKey(nextKey);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// If we've reached the end and wrapping is enabled
|
|
49
|
-
if (nextKey == null && wrap) {
|
|
50
|
-
nextKey = getFirstKey();
|
|
51
|
-
// Skip disabled keys from the start
|
|
52
|
-
while (nextKey != null && isDisabled(nextKey)) {
|
|
53
|
-
nextKey = getNextKey(nextKey);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return nextKey;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface AriaMenuProps {
|
|
61
|
-
/** An ID for the menu. */
|
|
62
|
-
id?: string;
|
|
63
|
-
/** Whether the menu is disabled. */
|
|
64
|
-
isDisabled?: boolean;
|
|
65
|
-
/** The label for the menu. */
|
|
66
|
-
label?: JSX.Element;
|
|
67
|
-
/** An accessible label for the menu when no visible label is provided. */
|
|
68
|
-
'aria-label'?: string;
|
|
69
|
-
/** The ID of an element that labels the menu. */
|
|
70
|
-
'aria-labelledby'?: string;
|
|
71
|
-
/** The ID of an element that describes the menu. */
|
|
72
|
-
'aria-describedby'?: string;
|
|
73
|
-
/** Handler called when focus moves into the menu. */
|
|
74
|
-
onFocus?: (e: FocusEvent) => void;
|
|
75
|
-
/** Handler called when focus moves out of the menu. */
|
|
76
|
-
onBlur?: (e: FocusEvent) => void;
|
|
77
|
-
/** Handler called when the focus state changes. */
|
|
78
|
-
onFocusChange?: (isFocused: boolean) => void;
|
|
79
|
-
/** Handler called when an item is activated (pressed). */
|
|
80
|
-
onAction?: (key: Key) => void;
|
|
81
|
-
/** Handler called when the menu should close. */
|
|
82
|
-
onClose?: () => void;
|
|
83
|
-
/** Whether focus should automatically wrap around. */
|
|
84
|
-
shouldFocusWrap?: boolean;
|
|
85
|
-
/** Whether to auto-focus the first item when the menu opens. */
|
|
86
|
-
autoFocus?: boolean | 'first' | 'last';
|
|
87
|
-
/** Whether type-to-select is disabled. @default false */
|
|
88
|
-
disallowTypeAhead?: boolean;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface MenuAria {
|
|
92
|
-
/** Props for the menu element. */
|
|
93
|
-
menuProps: JSX.HTMLAttributes<HTMLElement>;
|
|
94
|
-
/** Props for the menu's label element (if any). */
|
|
95
|
-
labelProps: JSX.HTMLAttributes<HTMLElement>;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Shared data between menu and menu items
|
|
99
|
-
const menuData = new WeakMap<object, MenuData>();
|
|
100
|
-
|
|
101
|
-
interface MenuData {
|
|
102
|
-
id: string;
|
|
103
|
-
onAction?: (key: Key) => void;
|
|
104
|
-
onClose?: () => void;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
*
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const p = getProps();
|
|
139
|
-
menuData.set(state, {
|
|
140
|
-
id,
|
|
141
|
-
onAction: p.onAction,
|
|
142
|
-
onClose: p.onClose,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
get
|
|
172
|
-
return
|
|
173
|
-
},
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
case '
|
|
210
|
-
e.preventDefault();
|
|
211
|
-
const currentKey = state.focusedKey();
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
state.setFocusedKey(
|
|
215
|
-
}
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
case '
|
|
219
|
-
e.preventDefault();
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Provides the behavior and accessibility implementation for a menu component.
|
|
3
|
+
* A menu displays a list of actions or options that a user can choose.
|
|
4
|
+
* Based on @react-aria/menu useMenu.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createEffect, onCleanup, type JSX, type Accessor } from 'solid-js';
|
|
8
|
+
import { createFocusWithin } from '../interactions/createFocusWithin';
|
|
9
|
+
import { createLabel } from '../label/createLabel';
|
|
10
|
+
import { createTypeSelect } from '../selection/createTypeSelect';
|
|
11
|
+
import { filterDOMProps } from '../utils/filterDOMProps';
|
|
12
|
+
import { mergeProps } from '../utils/mergeProps';
|
|
13
|
+
import { createId } from '../ssr';
|
|
14
|
+
import { access, type MaybeAccessor } from '../utils/reactivity';
|
|
15
|
+
import { isDevEnv } from '../utils/env';
|
|
16
|
+
import type { MenuState, Key, Collection } from '@proyecto-viviana/solid-stately';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default number of items to skip for page up/down when DOM measurement is not available.
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_PAGE_SIZE = 10;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Find the next non-disabled key in a collection.
|
|
25
|
+
*/
|
|
26
|
+
function findNextNonDisabledKey<T>(
|
|
27
|
+
collection: Collection<T>,
|
|
28
|
+
currentKey: Key | null,
|
|
29
|
+
direction: 'next' | 'prev',
|
|
30
|
+
isDisabled: (key: Key) => boolean,
|
|
31
|
+
wrap: boolean
|
|
32
|
+
): Key | null {
|
|
33
|
+
const getNextKey = direction === 'next'
|
|
34
|
+
? (key: Key) => collection.getKeyAfter(key)
|
|
35
|
+
: (key: Key) => collection.getKeyBefore(key);
|
|
36
|
+
|
|
37
|
+
const getFirstKey = direction === 'next'
|
|
38
|
+
? () => collection.getFirstKey()
|
|
39
|
+
: () => collection.getLastKey();
|
|
40
|
+
|
|
41
|
+
let nextKey = currentKey != null ? getNextKey(currentKey) : getFirstKey();
|
|
42
|
+
|
|
43
|
+
// Skip disabled keys
|
|
44
|
+
while (nextKey != null && isDisabled(nextKey)) {
|
|
45
|
+
nextKey = getNextKey(nextKey);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// If we've reached the end and wrapping is enabled
|
|
49
|
+
if (nextKey == null && wrap) {
|
|
50
|
+
nextKey = getFirstKey();
|
|
51
|
+
// Skip disabled keys from the start
|
|
52
|
+
while (nextKey != null && isDisabled(nextKey)) {
|
|
53
|
+
nextKey = getNextKey(nextKey);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return nextKey;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AriaMenuProps {
|
|
61
|
+
/** An ID for the menu. */
|
|
62
|
+
id?: string;
|
|
63
|
+
/** Whether the menu is disabled. */
|
|
64
|
+
isDisabled?: boolean;
|
|
65
|
+
/** The label for the menu. */
|
|
66
|
+
label?: JSX.Element;
|
|
67
|
+
/** An accessible label for the menu when no visible label is provided. */
|
|
68
|
+
'aria-label'?: string;
|
|
69
|
+
/** The ID of an element that labels the menu. */
|
|
70
|
+
'aria-labelledby'?: string;
|
|
71
|
+
/** The ID of an element that describes the menu. */
|
|
72
|
+
'aria-describedby'?: string;
|
|
73
|
+
/** Handler called when focus moves into the menu. */
|
|
74
|
+
onFocus?: (e: FocusEvent) => void;
|
|
75
|
+
/** Handler called when focus moves out of the menu. */
|
|
76
|
+
onBlur?: (e: FocusEvent) => void;
|
|
77
|
+
/** Handler called when the focus state changes. */
|
|
78
|
+
onFocusChange?: (isFocused: boolean) => void;
|
|
79
|
+
/** Handler called when an item is activated (pressed). */
|
|
80
|
+
onAction?: (key: Key) => void;
|
|
81
|
+
/** Handler called when the menu should close. */
|
|
82
|
+
onClose?: () => void;
|
|
83
|
+
/** Whether focus should automatically wrap around. */
|
|
84
|
+
shouldFocusWrap?: boolean;
|
|
85
|
+
/** Whether to auto-focus the first item when the menu opens. */
|
|
86
|
+
autoFocus?: boolean | 'first' | 'last';
|
|
87
|
+
/** Whether type-to-select is disabled. @default false */
|
|
88
|
+
disallowTypeAhead?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface MenuAria {
|
|
92
|
+
/** Props for the menu element. */
|
|
93
|
+
menuProps: JSX.HTMLAttributes<HTMLElement>;
|
|
94
|
+
/** Props for the menu's label element (if any). */
|
|
95
|
+
labelProps: JSX.HTMLAttributes<HTMLElement>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Shared data between menu and menu items
|
|
99
|
+
const menuData = new WeakMap<object, MenuData>();
|
|
100
|
+
|
|
101
|
+
interface MenuData {
|
|
102
|
+
id: string;
|
|
103
|
+
onAction?: (key: Key) => void;
|
|
104
|
+
onClose?: () => void;
|
|
105
|
+
isDisabled?: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getMenuData(state: MenuState): MenuData | undefined {
|
|
109
|
+
return menuData.get(state);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Provides the behavior and accessibility implementation for a menu component.
|
|
114
|
+
* A menu displays a list of actions or options that a user can choose.
|
|
115
|
+
*/
|
|
116
|
+
export function createMenu<T>(
|
|
117
|
+
props: MaybeAccessor<AriaMenuProps>,
|
|
118
|
+
state: MenuState<T>,
|
|
119
|
+
ref?: Accessor<HTMLElement | null>
|
|
120
|
+
): MenuAria {
|
|
121
|
+
const getProps = () => access(props);
|
|
122
|
+
const id = createId(getProps().id);
|
|
123
|
+
|
|
124
|
+
// Development-time warning for missing accessibility labels
|
|
125
|
+
if (isDevEnv()) {
|
|
126
|
+
const p = getProps();
|
|
127
|
+
if (!p.label && !p['aria-label'] && !p['aria-labelledby']) {
|
|
128
|
+
console.warn(
|
|
129
|
+
'[solidaria] A Menu requires an aria-label or aria-labelledby attribute for accessibility.'
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Filter DOM props
|
|
135
|
+
const domProps = () => filterDOMProps(getProps() as unknown as Record<string, unknown>, { labelable: true });
|
|
136
|
+
|
|
137
|
+
const updateSharedData = () => {
|
|
138
|
+
const p = getProps();
|
|
139
|
+
menuData.set(state, {
|
|
140
|
+
id,
|
|
141
|
+
onAction: p.onAction,
|
|
142
|
+
onClose: p.onClose,
|
|
143
|
+
isDisabled: p.isDisabled,
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Ensure menu items created in the same render pass can read parent metadata.
|
|
148
|
+
updateSharedData();
|
|
149
|
+
|
|
150
|
+
// Share data with child menu items
|
|
151
|
+
createEffect(() => {
|
|
152
|
+
updateSharedData();
|
|
153
|
+
|
|
154
|
+
onCleanup(() => {
|
|
155
|
+
menuData.delete(state);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Handle focus within
|
|
160
|
+
const { focusWithinProps } = createFocusWithin({
|
|
161
|
+
onFocusWithin: (e) => getProps().onFocus?.(e),
|
|
162
|
+
onBlurWithin: (e) => getProps().onBlur?.(e),
|
|
163
|
+
onFocusWithinChange: (isFocused) => {
|
|
164
|
+
getProps().onFocusChange?.(isFocused);
|
|
165
|
+
state.setFocused(isFocused);
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Label handling
|
|
170
|
+
const { labelProps, fieldProps } = createLabel({
|
|
171
|
+
get id() {
|
|
172
|
+
return id;
|
|
173
|
+
},
|
|
174
|
+
get label() {
|
|
175
|
+
return getProps().label;
|
|
176
|
+
},
|
|
177
|
+
get 'aria-label'() {
|
|
178
|
+
return getProps()['aria-label'];
|
|
179
|
+
},
|
|
180
|
+
get 'aria-labelledby'() {
|
|
181
|
+
return getProps()['aria-labelledby'];
|
|
182
|
+
},
|
|
183
|
+
labelElementType: 'span',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Type-to-select
|
|
187
|
+
const { typeSelectProps } = createTypeSelect({
|
|
188
|
+
collection: () => state.collection(),
|
|
189
|
+
focusedKey: () => state.focusedKey(),
|
|
190
|
+
onFocusedKeyChange: (key) => state.setFocusedKey(key),
|
|
191
|
+
isKeyDisabled: (key) => state.isDisabled(key),
|
|
192
|
+
get isDisabled() {
|
|
193
|
+
return getProps().disallowTypeAhead ?? false;
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Keyboard navigation
|
|
198
|
+
const onKeyDown: JSX.EventHandler<HTMLElement, KeyboardEvent> = (e) => {
|
|
199
|
+
if (getProps().isDisabled) return;
|
|
200
|
+
|
|
201
|
+
const collection = state.collection();
|
|
202
|
+
const p = getProps();
|
|
203
|
+
const wrap = p.shouldFocusWrap ?? false;
|
|
204
|
+
|
|
205
|
+
// Use state.isDisabled which properly checks the disabledKeys accessor
|
|
206
|
+
const isDisabled = (key: Key) => state.isDisabled(key);
|
|
207
|
+
|
|
208
|
+
switch (e.key) {
|
|
209
|
+
case 'ArrowDown': {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
const currentKey = state.focusedKey();
|
|
212
|
+
const nextKey = findNextNonDisabledKey(collection, currentKey, 'next', isDisabled, wrap);
|
|
213
|
+
if (nextKey != null) {
|
|
214
|
+
state.setFocusedKey(nextKey);
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
case 'ArrowUp': {
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
const currentKey = state.focusedKey();
|
|
221
|
+
const prevKey = findNextNonDisabledKey(collection, currentKey, 'prev', isDisabled, wrap);
|
|
222
|
+
if (prevKey != null) {
|
|
223
|
+
state.setFocusedKey(prevKey);
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case 'Home': {
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
// Find first non-disabled key
|
|
230
|
+
let firstKey = collection.getFirstKey();
|
|
231
|
+
while (firstKey != null && isDisabled(firstKey)) {
|
|
232
|
+
firstKey = collection.getKeyAfter(firstKey);
|
|
233
|
+
}
|
|
234
|
+
if (firstKey != null) {
|
|
235
|
+
state.setFocusedKey(firstKey);
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case 'End': {
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
// Find last non-disabled key
|
|
242
|
+
let lastKey = collection.getLastKey();
|
|
243
|
+
while (lastKey != null && isDisabled(lastKey)) {
|
|
244
|
+
lastKey = collection.getKeyBefore(lastKey);
|
|
245
|
+
}
|
|
246
|
+
if (lastKey != null) {
|
|
247
|
+
state.setFocusedKey(lastKey);
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case ' ':
|
|
252
|
+
case 'Enter': {
|
|
253
|
+
e.preventDefault();
|
|
254
|
+
const focusedKey = state.focusedKey();
|
|
255
|
+
// Don't activate disabled items
|
|
256
|
+
if (focusedKey != null && !isDisabled(focusedKey)) {
|
|
257
|
+
p.onAction?.(focusedKey);
|
|
258
|
+
p.onClose?.();
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case 'Escape': {
|
|
263
|
+
e.preventDefault();
|
|
264
|
+
p.onClose?.();
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
case 'PageDown': {
|
|
268
|
+
e.preventDefault();
|
|
269
|
+
const currentKey = state.focusedKey();
|
|
270
|
+
const el = ref?.();
|
|
271
|
+
|
|
272
|
+
if (el) {
|
|
273
|
+
// Use DOM measurements to calculate how many items fit in a page
|
|
274
|
+
const visibleHeight = el.clientHeight;
|
|
275
|
+
let traveled = 0;
|
|
276
|
+
let targetKey = currentKey;
|
|
277
|
+
|
|
278
|
+
while (targetKey != null && traveled < visibleHeight) {
|
|
279
|
+
const nextKey = collection.getKeyAfter(targetKey);
|
|
280
|
+
if (nextKey == null) break;
|
|
281
|
+
|
|
282
|
+
// Try to measure the item height
|
|
283
|
+
const itemElement = el.querySelector(`[data-key="${targetKey}"]`);
|
|
284
|
+
traveled += itemElement?.clientHeight ?? 32;
|
|
285
|
+
|
|
286
|
+
// Skip disabled items
|
|
287
|
+
if (!isDisabled(nextKey)) {
|
|
288
|
+
targetKey = nextKey;
|
|
289
|
+
} else {
|
|
290
|
+
// Skip over disabled items without counting them
|
|
291
|
+
const afterDisabled = findNextNonDisabledKey(collection, nextKey, 'next', isDisabled, false);
|
|
292
|
+
if (afterDisabled != null) {
|
|
293
|
+
targetKey = afterDisabled;
|
|
294
|
+
} else {
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (targetKey != null && targetKey !== currentKey) {
|
|
301
|
+
state.setFocusedKey(targetKey);
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
// Fallback: move by DEFAULT_PAGE_SIZE items
|
|
305
|
+
let count = DEFAULT_PAGE_SIZE;
|
|
306
|
+
let targetKey = currentKey;
|
|
307
|
+
|
|
308
|
+
while (count > 0 && targetKey != null) {
|
|
309
|
+
const nextKey = findNextNonDisabledKey(collection, targetKey, 'next', isDisabled, false);
|
|
310
|
+
if (nextKey == null) break;
|
|
311
|
+
targetKey = nextKey;
|
|
312
|
+
count--;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (targetKey != null) {
|
|
316
|
+
state.setFocusedKey(targetKey);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case 'PageUp': {
|
|
322
|
+
e.preventDefault();
|
|
323
|
+
const currentKey = state.focusedKey();
|
|
324
|
+
const el = ref?.();
|
|
325
|
+
|
|
326
|
+
if (el) {
|
|
327
|
+
// Use DOM measurements to calculate how many items fit in a page
|
|
328
|
+
const visibleHeight = el.clientHeight;
|
|
329
|
+
let traveled = 0;
|
|
330
|
+
let targetKey = currentKey;
|
|
331
|
+
|
|
332
|
+
while (targetKey != null && traveled < visibleHeight) {
|
|
333
|
+
const prevKey = collection.getKeyBefore(targetKey);
|
|
334
|
+
if (prevKey == null) break;
|
|
335
|
+
|
|
336
|
+
// Try to measure the item height
|
|
337
|
+
const itemElement = el.querySelector(`[data-key="${targetKey}"]`);
|
|
338
|
+
traveled += itemElement?.clientHeight ?? 32;
|
|
339
|
+
|
|
340
|
+
// Skip disabled items
|
|
341
|
+
if (!isDisabled(prevKey)) {
|
|
342
|
+
targetKey = prevKey;
|
|
343
|
+
} else {
|
|
344
|
+
// Skip over disabled items without counting them
|
|
345
|
+
const beforeDisabled = findNextNonDisabledKey(collection, prevKey, 'prev', isDisabled, false);
|
|
346
|
+
if (beforeDisabled != null) {
|
|
347
|
+
targetKey = beforeDisabled;
|
|
348
|
+
} else {
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (targetKey != null && targetKey !== currentKey) {
|
|
355
|
+
state.setFocusedKey(targetKey);
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
// Fallback: move by DEFAULT_PAGE_SIZE items
|
|
359
|
+
let count = DEFAULT_PAGE_SIZE;
|
|
360
|
+
let targetKey = currentKey;
|
|
361
|
+
|
|
362
|
+
while (count > 0 && targetKey != null) {
|
|
363
|
+
const prevKey = findNextNonDisabledKey(collection, targetKey, 'prev', isDisabled, false);
|
|
364
|
+
if (prevKey == null) break;
|
|
365
|
+
targetKey = prevKey;
|
|
366
|
+
count--;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (targetKey != null) {
|
|
370
|
+
state.setFocusedKey(targetKey);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
get labelProps() {
|
|
380
|
+
return labelProps as JSX.HTMLAttributes<HTMLElement>;
|
|
381
|
+
},
|
|
382
|
+
get menuProps() {
|
|
383
|
+
const p = getProps();
|
|
384
|
+
|
|
385
|
+
const baseProps = mergeProps(
|
|
386
|
+
domProps(),
|
|
387
|
+
focusWithinProps as Record<string, unknown>,
|
|
388
|
+
fieldProps as Record<string, unknown>,
|
|
389
|
+
{
|
|
390
|
+
role: 'menu',
|
|
391
|
+
tabIndex: p.isDisabled ? undefined : 0,
|
|
392
|
+
'aria-disabled': p.isDisabled || undefined,
|
|
393
|
+
onKeyDown,
|
|
394
|
+
} as Record<string, unknown>
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// Add type-select props if enabled
|
|
398
|
+
if (!p.disallowTypeAhead) {
|
|
399
|
+
return mergeProps(baseProps, typeSelectProps as Record<string, unknown>) as JSX.HTMLAttributes<HTMLElement>;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return baseProps as JSX.HTMLAttributes<HTMLElement>;
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|