@proyecto-viviana/solidaria 0.2.4 → 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.
Files changed (219) hide show
  1. package/LICENSE +21 -0
  2. package/dist/actiongroup/createActionGroup.d.ts +29 -0
  3. package/dist/actiongroup/createActionGroup.d.ts.map +1 -0
  4. package/dist/actiongroup/index.d.ts +2 -0
  5. package/dist/actiongroup/index.d.ts.map +1 -0
  6. package/dist/autocomplete/createAutocomplete.d.ts +6 -2
  7. package/dist/autocomplete/createAutocomplete.d.ts.map +1 -1
  8. package/dist/breadcrumbs/createBreadcrumbs.d.ts +2 -0
  9. package/dist/breadcrumbs/createBreadcrumbs.d.ts.map +1 -1
  10. package/dist/button/createToggleButtonGroup.d.ts +32 -0
  11. package/dist/button/createToggleButtonGroup.d.ts.map +1 -0
  12. package/dist/button/index.d.ts +2 -0
  13. package/dist/button/index.d.ts.map +1 -1
  14. package/dist/calendar/createCalendarCell.d.ts +2 -0
  15. package/dist/calendar/createCalendarCell.d.ts.map +1 -1
  16. package/dist/calendar/createCalendarGrid.d.ts.map +1 -1
  17. package/dist/calendar/createRangeCalendarCell.d.ts +3 -1
  18. package/dist/calendar/createRangeCalendarCell.d.ts.map +1 -1
  19. package/dist/checkbox/createCheckboxGroup.d.ts +5 -1
  20. package/dist/checkbox/createCheckboxGroup.d.ts.map +1 -1
  21. package/dist/collections/index.d.ts +56 -0
  22. package/dist/collections/index.d.ts.map +1 -0
  23. package/dist/color/createColorArea.d.ts.map +1 -1
  24. package/dist/color/createColorSlider.d.ts.map +1 -1
  25. package/dist/color/createColorWheel.d.ts.map +1 -1
  26. package/dist/combobox/createComboBox.d.ts +6 -0
  27. package/dist/combobox/createComboBox.d.ts.map +1 -1
  28. package/dist/datepicker/createDatePicker.d.ts +6 -0
  29. package/dist/datepicker/createDatePicker.d.ts.map +1 -1
  30. package/dist/datepicker/createDateRangePicker.d.ts +40 -0
  31. package/dist/datepicker/createDateRangePicker.d.ts.map +1 -0
  32. package/dist/datepicker/createDateSegment.d.ts +1 -1
  33. package/dist/datepicker/createDateSegment.d.ts.map +1 -1
  34. package/dist/datepicker/createTimeSegment.d.ts +29 -0
  35. package/dist/datepicker/createTimeSegment.d.ts.map +1 -0
  36. package/dist/datepicker/index.d.ts +2 -0
  37. package/dist/datepicker/index.d.ts.map +1 -1
  38. package/dist/disclosure/createDisclosureGroup.d.ts +2 -1
  39. package/dist/disclosure/createDisclosureGroup.d.ts.map +1 -1
  40. package/dist/dnd/createDrag.d.ts.map +1 -1
  41. package/dist/dnd/createDraggableCollection.d.ts +4 -0
  42. package/dist/dnd/createDraggableCollection.d.ts.map +1 -1
  43. package/dist/dnd/createDraggableItem.d.ts.map +1 -1
  44. package/dist/dnd/createDrop.d.ts.map +1 -1
  45. package/dist/dnd/createDroppableCollection.d.ts +32 -1
  46. package/dist/dnd/createDroppableCollection.d.ts.map +1 -1
  47. package/dist/dnd/createDroppableItem.d.ts.map +1 -1
  48. package/dist/dnd/index.d.ts +1 -1
  49. package/dist/dnd/index.d.ts.map +1 -1
  50. package/dist/grid/createGrid.d.ts.map +1 -1
  51. package/dist/gridlist/createGridList.d.ts.map +1 -1
  52. package/dist/index.d.ts +6 -4
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +4659 -3452
  55. package/dist/index.js.map +1 -7
  56. package/dist/index.ssr.js +4659 -3452
  57. package/dist/index.ssr.js.map +1 -7
  58. package/dist/interactions/createFocus.d.ts.map +1 -1
  59. package/dist/interactions/createFocusWithin.d.ts.map +1 -1
  60. package/dist/link/createLink.d.ts +10 -0
  61. package/dist/link/createLink.d.ts.map +1 -1
  62. package/dist/listbox/createListBox.d.ts +1 -0
  63. package/dist/listbox/createListBox.d.ts.map +1 -1
  64. package/dist/listbox/createOption.d.ts.map +1 -1
  65. package/dist/menu/createMenu.d.ts +1 -0
  66. package/dist/menu/createMenu.d.ts.map +1 -1
  67. package/dist/meter/createMeter.d.ts.map +1 -1
  68. package/dist/numberfield/createNumberField.d.ts +18 -0
  69. package/dist/numberfield/createNumberField.d.ts.map +1 -1
  70. package/dist/overlays/createModal.d.ts +16 -0
  71. package/dist/overlays/createModal.d.ts.map +1 -1
  72. package/dist/overlays/createOverlay.d.ts.map +1 -1
  73. package/dist/overlays/index.d.ts +1 -1
  74. package/dist/overlays/index.d.ts.map +1 -1
  75. package/dist/popover/createOverlayPosition.d.ts.map +1 -1
  76. package/dist/popover/createPopover.d.ts.map +1 -1
  77. package/dist/progress/createProgressBar.d.ts.map +1 -1
  78. package/dist/radio/createRadioGroup.d.ts +2 -2
  79. package/dist/radio/createRadioGroup.d.ts.map +1 -1
  80. package/dist/searchfield/createSearchField.d.ts.map +1 -1
  81. package/dist/select/createHiddenSelect.d.ts.map +1 -1
  82. package/dist/select/createSelect.d.ts.map +1 -1
  83. package/dist/slider/createSlider.d.ts.map +1 -1
  84. package/dist/table/createTable.d.ts.map +1 -1
  85. package/dist/tabs/createTabs.d.ts +1 -1
  86. package/dist/tabs/createTabs.d.ts.map +1 -1
  87. package/dist/tag/createTag.d.ts.map +1 -1
  88. package/dist/tag/createTagGroup.d.ts.map +1 -1
  89. package/dist/toast/createToast.d.ts +4 -0
  90. package/dist/toast/createToast.d.ts.map +1 -1
  91. package/dist/toast/createToastRegion.d.ts.map +1 -1
  92. package/dist/toolbar/createToolbar.d.ts.map +1 -1
  93. package/dist/tooltip/createTooltipTrigger.d.ts.map +1 -1
  94. package/dist/tree/createTree.d.ts.map +1 -1
  95. package/dist/tree/createTreeItem.d.ts.map +1 -1
  96. package/dist/tree/types.d.ts +4 -0
  97. package/dist/tree/types.d.ts.map +1 -1
  98. package/dist/utils/env.d.ts +1 -1
  99. package/dist/utils/env.d.ts.map +1 -1
  100. package/dist/utils/platform.d.ts.map +1 -1
  101. package/dist/visually-hidden/createVisuallyHidden.d.ts.map +1 -1
  102. package/package.json +8 -6
  103. package/src/actiongroup/createActionGroup.ts +324 -0
  104. package/src/actiongroup/index.ts +8 -0
  105. package/src/autocomplete/createAutocomplete.ts +32 -9
  106. package/src/breadcrumbs/createBreadcrumbs.ts +10 -15
  107. package/src/button/createButton.ts +1 -1
  108. package/src/button/createToggleButtonGroup.ts +128 -0
  109. package/src/button/index.ts +9 -0
  110. package/src/calendar/createCalendarCell.ts +6 -4
  111. package/src/calendar/createCalendarGrid.ts +27 -18
  112. package/src/calendar/createRangeCalendarCell.ts +26 -9
  113. package/src/checkbox/createCheckboxGroup.ts +21 -4
  114. package/src/collections/index.ts +242 -0
  115. package/src/color/createColorArea.ts +380 -314
  116. package/src/color/createColorField.ts +137 -137
  117. package/src/color/createColorSlider.ts +286 -197
  118. package/src/color/createColorSwatch.ts +40 -40
  119. package/src/color/createColorWheel.ts +218 -208
  120. package/src/color/index.ts +24 -24
  121. package/src/color/types.ts +116 -116
  122. package/src/combobox/createComboBox.ts +670 -647
  123. package/src/combobox/index.ts +6 -6
  124. package/src/datepicker/createDatePicker.ts +54 -16
  125. package/src/datepicker/createDateRangePicker.ts +246 -0
  126. package/src/datepicker/createDateSegment.ts +185 -31
  127. package/src/datepicker/createTimeSegment.ts +370 -0
  128. package/src/datepicker/index.ts +14 -0
  129. package/src/dialog/createDialog.ts +120 -120
  130. package/src/dialog/index.ts +2 -2
  131. package/src/dialog/types.ts +19 -19
  132. package/src/disclosure/createDisclosureGroup.ts +5 -2
  133. package/src/dnd/createDrag.ts +224 -209
  134. package/src/dnd/createDraggableCollection.ts +96 -63
  135. package/src/dnd/createDraggableItem.ts +259 -243
  136. package/src/dnd/createDrop.ts +322 -321
  137. package/src/dnd/createDroppableCollection.ts +682 -293
  138. package/src/dnd/createDroppableItem.ts +215 -213
  139. package/src/dnd/index.ts +55 -47
  140. package/src/dnd/types.ts +89 -89
  141. package/src/dnd/utils.ts +294 -294
  142. package/src/focus/createAutoFocus.ts +321 -321
  143. package/src/focus/createFocusRestore.ts +313 -313
  144. package/src/focus/createVirtualFocus.ts +396 -396
  145. package/src/form/createFormValidation.ts +224 -224
  146. package/src/form/index.ts +11 -11
  147. package/src/grid/createGrid.ts +3 -1
  148. package/src/gridlist/createGridList.ts +16 -0
  149. package/src/gridlist/createGridListItem.ts +1 -1
  150. package/src/i18n/NumberFormatter.ts +266 -266
  151. package/src/i18n/createCollator.ts +79 -79
  152. package/src/i18n/createDateFormatter.ts +83 -83
  153. package/src/i18n/createFilter.ts +131 -131
  154. package/src/i18n/createNumberFormatter.ts +52 -52
  155. package/src/i18n/index.ts +40 -40
  156. package/src/i18n/locale.tsx +188 -188
  157. package/src/i18n/utils.ts +99 -99
  158. package/src/index.ts +51 -0
  159. package/src/interactions/createFocus.ts +6 -5
  160. package/src/interactions/createFocusWithin.ts +6 -5
  161. package/src/interactions/createLongPress.ts +174 -174
  162. package/src/interactions/createMove.ts +289 -289
  163. package/src/interactions/createPress.ts +5 -5
  164. package/src/landmark/createLandmark.ts +377 -377
  165. package/src/landmark/index.ts +8 -8
  166. package/src/link/createLink.ts +23 -8
  167. package/src/listbox/createListBox.ts +308 -269
  168. package/src/listbox/createOption.ts +162 -151
  169. package/src/listbox/index.ts +12 -12
  170. package/src/live-announcer/announce.ts +322 -322
  171. package/src/live-announcer/index.ts +9 -9
  172. package/src/menu/createMenu.ts +405 -396
  173. package/src/menu/createMenuItem.ts +149 -149
  174. package/src/menu/createMenuTrigger.ts +88 -88
  175. package/src/menu/index.ts +18 -18
  176. package/src/meter/createMeter.ts +1 -6
  177. package/src/numberfield/createNumberField.ts +311 -268
  178. package/src/numberfield/index.ts +5 -5
  179. package/src/overlays/ariaHideOutside.ts +219 -219
  180. package/src/overlays/createInteractOutside.ts +149 -149
  181. package/src/overlays/createModal.tsx +238 -202
  182. package/src/overlays/createOverlay.ts +165 -155
  183. package/src/overlays/createOverlayTrigger.ts +85 -85
  184. package/src/overlays/createPreventScroll.ts +266 -266
  185. package/src/overlays/index.ts +48 -44
  186. package/src/popover/calculatePosition.ts +6 -6
  187. package/src/popover/createOverlayPosition.ts +7 -4
  188. package/src/popover/createPopover.ts +21 -7
  189. package/src/progress/createProgressBar.ts +6 -1
  190. package/src/radio/createRadioGroup.ts +88 -14
  191. package/src/searchfield/createSearchField.ts +241 -186
  192. package/src/searchfield/index.ts +2 -2
  193. package/src/select/createHiddenSelect.tsx +263 -236
  194. package/src/select/createSelect.ts +373 -395
  195. package/src/select/index.ts +14 -14
  196. package/src/slider/createSlider.ts +364 -349
  197. package/src/slider/index.ts +2 -2
  198. package/src/ssr/index.tsx +370 -370
  199. package/src/table/createTable.ts +3 -1
  200. package/src/table/createTableColumnHeader.ts +1 -1
  201. package/src/table/createTableRow.ts +1 -1
  202. package/src/tabs/createTabs.ts +80 -51
  203. package/src/tag/createTag.ts +135 -6
  204. package/src/tag/createTagGroup.ts +7 -2
  205. package/src/toast/createToast.ts +8 -2
  206. package/src/toast/createToastRegion.ts +0 -1
  207. package/src/toolbar/createToolbar.ts +75 -1
  208. package/src/tooltip/createTooltip.ts +79 -79
  209. package/src/tooltip/createTooltipTrigger.ts +226 -222
  210. package/src/tooltip/index.ts +6 -6
  211. package/src/tree/createTree.ts +261 -246
  212. package/src/tree/createTreeItem.ts +282 -233
  213. package/src/tree/createTreeSelectionCheckbox.ts +68 -68
  214. package/src/tree/index.ts +16 -16
  215. package/src/tree/types.ts +91 -87
  216. package/src/utils/env.ts +55 -54
  217. package/src/utils/platform.ts +16 -6
  218. package/src/visually-hidden/createVisuallyHidden.ts +139 -124
  219. package/src/visually-hidden/index.ts +6 -6
@@ -1,8 +1,8 @@
1
- export {
2
- createLandmark,
3
- getLandmarkController,
4
- type AriaLandmarkRole,
5
- type AriaLandmarkProps,
6
- type LandmarkAria,
7
- type LandmarkController,
8
- } from './createLandmark';
1
+ export {
2
+ createLandmark,
3
+ getLandmarkController,
4
+ type AriaLandmarkRole,
5
+ type AriaLandmarkProps,
6
+ type LandmarkAria,
7
+ type LandmarkController,
8
+ } from './createLandmark';
@@ -27,10 +27,29 @@ export interface AriaLinkProps {
27
27
  elementType?: string;
28
28
  /** The URL to link to. */
29
29
  href?: string;
30
+ /** Additional options forwarded to client-side router navigation handlers. */
31
+ routerOptions?: Record<string, unknown>;
30
32
  /** The target window for the link. */
31
33
  target?: string;
32
34
  /** The relationship between the linked resource and the current page. */
33
35
  rel?: string;
36
+ /** Hints the language of the linked resource. */
37
+ hrefLang?: string;
38
+ /** Instructs the browser to download the URL instead of navigating to it. */
39
+ download?: string | boolean;
40
+ /** Space-separated list of URLs to ping when following the link. */
41
+ ping?: string;
42
+ /** Referrer policy for fetches initiated by this link. */
43
+ referrerPolicy?:
44
+ | ''
45
+ | 'no-referrer'
46
+ | 'no-referrer-when-downgrade'
47
+ | 'origin'
48
+ | 'origin-when-cross-origin'
49
+ | 'same-origin'
50
+ | 'strict-origin'
51
+ | 'strict-origin-when-cross-origin'
52
+ | 'unsafe-url';
34
53
  /** Handler that is called when the press is released over the target. */
35
54
  onPress?: (e: PressEvent) => void;
36
55
  /** Handler that is called when a press interaction starts. */
@@ -120,13 +139,6 @@ export function createLink(
120
139
  };
121
140
  }
122
141
 
123
- // Add link-specific props
124
- if (elType === 'a') {
125
- if (p.href) baseProps.href = p.href;
126
- if (p.target) baseProps.target = p.target;
127
- if (p.rel) baseProps.rel = p.rel;
128
- }
129
-
130
142
  // ARIA attributes
131
143
  const ariaProps: Record<string, unknown> = {
132
144
  'aria-disabled': disabled || undefined,
@@ -164,7 +176,10 @@ export function createLink(
164
176
  };
165
177
 
166
178
  return mergeProps(
167
- filterDOMProps(p as Record<string, unknown>, { labelable: true }),
179
+ filterDOMProps(p as Record<string, unknown>, {
180
+ labelable: true,
181
+ isLink: elType === 'a',
182
+ }),
168
183
  baseProps,
169
184
  ariaProps,
170
185
  focusableProps as Record<string, unknown>,
@@ -1,269 +1,308 @@
1
- /**
2
- * Provides the behavior and accessibility implementation for a listbox component.
3
- * A listbox displays a list of options and allows a user to select one or more of them.
4
- * Based on @react-aria/listbox useListBox.
5
- */
6
-
7
- import { createEffect, onCleanup, type JSX } 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 { ListState, Key } from '@proyecto-viviana/solid-stately';
17
-
18
- export interface AriaListBoxProps {
19
- /** An ID for the listbox. */
20
- id?: string;
21
- /** Whether the listbox is disabled. */
22
- isDisabled?: boolean;
23
- /** The label for the listbox. */
24
- label?: JSX.Element;
25
- /** An accessible label for the listbox when no visible label is provided. */
26
- 'aria-label'?: string;
27
- /** The ID of an element that labels the listbox. */
28
- 'aria-labelledby'?: string;
29
- /** The ID of an element that describes the listbox. */
30
- 'aria-describedby'?: string;
31
- /** Handler called when focus moves into the listbox. */
32
- onFocus?: (e: FocusEvent) => void;
33
- /** Handler called when focus moves out of the listbox. */
34
- onBlur?: (e: FocusEvent) => void;
35
- /** Handler called when the focus state changes. */
36
- onFocusChange?: (isFocused: boolean) => void;
37
- /** Handler called when an item is activated (pressed). */
38
- onAction?: (key: Key) => void;
39
- /** Whether focus should automatically wrap around. */
40
- shouldFocusWrap?: boolean;
41
- /** Whether selection should occur on press up. */
42
- shouldSelectOnPressUp?: boolean;
43
- /** Whether to focus items on hover. */
44
- shouldFocusOnHover?: boolean;
45
- /** Whether type-to-select is disabled. @default false */
46
- disallowTypeAhead?: boolean;
47
- }
48
-
49
- export interface ListBoxAria {
50
- /** Props for the listbox element. */
51
- listBoxProps: JSX.HTMLAttributes<HTMLElement>;
52
- /** Props for the listbox's label element (if any). */
53
- labelProps: JSX.HTMLAttributes<HTMLElement>;
54
- }
55
-
56
- // Shared data between listbox and options
57
- const listBoxData = new WeakMap<object, ListBoxData>();
58
-
59
- interface ListBoxData {
60
- id: string;
61
- onAction?: (key: Key) => void;
62
- shouldSelectOnPressUp?: boolean;
63
- shouldFocusOnHover?: boolean;
64
- }
65
-
66
- export function getListBoxData(state: ListState): ListBoxData | undefined {
67
- return listBoxData.get(state);
68
- }
69
-
70
- /**
71
- * Provides the behavior and accessibility implementation for a listbox component.
72
- * A listbox displays a list of options and allows a user to select one or more of them.
73
- */
74
- export function createListBox<T>(
75
- props: MaybeAccessor<AriaListBoxProps>,
76
- state: ListState<T>,
77
- _ref?: () => HTMLElement | null
78
- ): ListBoxAria {
79
- const getProps = () => access(props);
80
- const id = createId(getProps().id);
81
-
82
- // Development-time warning for missing accessibility labels
83
- if (isDevEnv()) {
84
- const p = getProps();
85
- if (!p.label && !p['aria-label'] && !p['aria-labelledby']) {
86
- console.warn(
87
- '[solidaria] A ListBox requires an aria-label or aria-labelledby attribute for accessibility.'
88
- );
89
- }
90
- }
91
-
92
- // Filter DOM props
93
- const domProps = () => filterDOMProps(getProps() as unknown as Record<string, unknown>, { labelable: true });
94
-
95
- // Share data with child options
96
- createEffect(() => {
97
- const p = getProps();
98
- listBoxData.set(state, {
99
- id,
100
- onAction: p.onAction,
101
- shouldSelectOnPressUp: p.shouldSelectOnPressUp,
102
- shouldFocusOnHover: p.shouldFocusOnHover,
103
- });
104
-
105
- onCleanup(() => {
106
- listBoxData.delete(state);
107
- });
108
- });
109
-
110
- // Handle focus within
111
- const { focusWithinProps } = createFocusWithin({
112
- onFocusWithin: (e) => getProps().onFocus?.(e),
113
- onBlurWithin: (e) => getProps().onBlur?.(e),
114
- onFocusWithinChange: (isFocused) => {
115
- getProps().onFocusChange?.(isFocused);
116
- state.setFocused(isFocused);
117
- },
118
- });
119
-
120
- // Label handling
121
- const { labelProps, fieldProps } = createLabel({
122
- get id() {
123
- return id;
124
- },
125
- get label() {
126
- return getProps().label;
127
- },
128
- get 'aria-label'() {
129
- return getProps()['aria-label'];
130
- },
131
- get 'aria-labelledby'() {
132
- return getProps()['aria-labelledby'];
133
- },
134
- labelElementType: 'span',
135
- });
136
-
137
- // Type-to-select
138
- const { typeSelectProps } = createTypeSelect({
139
- collection: () => state.collection(),
140
- focusedKey: () => state.focusedKey(),
141
- onFocusedKeyChange: (key) => state.setFocusedKey(key),
142
- isKeyDisabled: (key) => state.isDisabled(key),
143
- get isDisabled() {
144
- return getProps().disallowTypeAhead ?? false;
145
- },
146
- });
147
-
148
- // Keyboard navigation
149
- const onKeyDown: JSX.EventHandler<HTMLElement, KeyboardEvent> = (e) => {
150
- if (getProps().isDisabled) return;
151
-
152
- const collection = state.collection();
153
-
154
- switch (e.key) {
155
- case 'ArrowDown': {
156
- e.preventDefault();
157
- const currentKey = state.focusedKey();
158
- const nextKey = currentKey ? collection.getKeyAfter(currentKey) : collection.getFirstKey();
159
- if (nextKey) {
160
- state.setFocusedKey(nextKey);
161
- if (!e.shiftKey && state.selectionMode() === 'single') {
162
- state.replaceSelection(nextKey);
163
- } else if (e.shiftKey && state.selectionMode() === 'multiple') {
164
- state.extendSelection(nextKey, collection);
165
- }
166
- }
167
- break;
168
- }
169
- case 'ArrowUp': {
170
- e.preventDefault();
171
- const currentKey = state.focusedKey();
172
- const prevKey = currentKey ? collection.getKeyBefore(currentKey) : collection.getLastKey();
173
- if (prevKey) {
174
- state.setFocusedKey(prevKey);
175
- if (!e.shiftKey && state.selectionMode() === 'single') {
176
- state.replaceSelection(prevKey);
177
- } else if (e.shiftKey && state.selectionMode() === 'multiple') {
178
- state.extendSelection(prevKey, collection);
179
- }
180
- }
181
- break;
182
- }
183
- case 'Home': {
184
- e.preventDefault();
185
- const firstKey = collection.getFirstKey();
186
- if (firstKey) {
187
- state.setFocusedKey(firstKey);
188
- if (e.ctrlKey && e.shiftKey && state.selectionMode() === 'multiple') {
189
- // Select from current to first
190
- state.extendSelection(firstKey, collection);
191
- } else if (!e.shiftKey && state.selectionMode() === 'single') {
192
- state.replaceSelection(firstKey);
193
- }
194
- }
195
- break;
196
- }
197
- case 'End': {
198
- e.preventDefault();
199
- const lastKey = collection.getLastKey();
200
- if (lastKey) {
201
- state.setFocusedKey(lastKey);
202
- if (e.ctrlKey && e.shiftKey && state.selectionMode() === 'multiple') {
203
- // Select from current to last
204
- state.extendSelection(lastKey, collection);
205
- } else if (!e.shiftKey && state.selectionMode() === 'single') {
206
- state.replaceSelection(lastKey);
207
- }
208
- }
209
- break;
210
- }
211
- case ' ':
212
- case 'Enter': {
213
- e.preventDefault();
214
- const focusedKey = state.focusedKey();
215
- if (focusedKey != null) {
216
- if (state.selectionMode() !== 'none') {
217
- state.toggleSelection(focusedKey);
218
- }
219
- getProps().onAction?.(focusedKey);
220
- }
221
- break;
222
- }
223
- case 'a': {
224
- if (e.ctrlKey && state.selectionMode() === 'multiple') {
225
- e.preventDefault();
226
- state.selectAll();
227
- }
228
- break;
229
- }
230
- case 'Escape': {
231
- e.preventDefault();
232
- if (!state.disallowEmptySelection()) {
233
- state.clearSelection();
234
- }
235
- break;
236
- }
237
- }
238
- };
239
-
240
- return {
241
- get labelProps() {
242
- return labelProps as JSX.HTMLAttributes<HTMLElement>;
243
- },
244
- get listBoxProps() {
245
- const p = getProps();
246
- const selectionMode = state.selectionMode();
247
-
248
- const baseProps = mergeProps(
249
- domProps(),
250
- focusWithinProps as Record<string, unknown>,
251
- fieldProps as Record<string, unknown>,
252
- {
253
- role: 'listbox',
254
- tabIndex: p.isDisabled ? undefined : 0,
255
- 'aria-disabled': p.isDisabled || undefined,
256
- 'aria-multiselectable': selectionMode === 'multiple' ? true : undefined,
257
- onKeyDown,
258
- }
259
- );
260
-
261
- // Add type-select props if enabled
262
- if (!p.disallowTypeAhead) {
263
- return mergeProps(baseProps, typeSelectProps as Record<string, unknown>) as JSX.HTMLAttributes<HTMLElement>;
264
- }
265
-
266
- return baseProps as JSX.HTMLAttributes<HTMLElement>;
267
- },
268
- };
269
- }
1
+ /**
2
+ * Provides the behavior and accessibility implementation for a listbox component.
3
+ * A listbox displays a list of options and allows a user to select one or more of them.
4
+ * Based on @react-aria/listbox useListBox.
5
+ */
6
+
7
+ import { createEffect, onCleanup, type JSX } 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 { ListState, Key } from '@proyecto-viviana/solid-stately';
17
+
18
+ export interface AriaListBoxProps {
19
+ /** An ID for the listbox. */
20
+ id?: string;
21
+ /** Whether the listbox is disabled. */
22
+ isDisabled?: boolean;
23
+ /** The label for the listbox. */
24
+ label?: JSX.Element;
25
+ /** An accessible label for the listbox when no visible label is provided. */
26
+ 'aria-label'?: string;
27
+ /** The ID of an element that labels the listbox. */
28
+ 'aria-labelledby'?: string;
29
+ /** The ID of an element that describes the listbox. */
30
+ 'aria-describedby'?: string;
31
+ /** Handler called when focus moves into the listbox. */
32
+ onFocus?: (e: FocusEvent) => void;
33
+ /** Handler called when focus moves out of the listbox. */
34
+ onBlur?: (e: FocusEvent) => void;
35
+ /** Handler called when the focus state changes. */
36
+ onFocusChange?: (isFocused: boolean) => void;
37
+ /** Handler called when an item is activated (pressed). */
38
+ onAction?: (key: Key) => void;
39
+ /** Whether focus should automatically wrap around. */
40
+ shouldFocusWrap?: boolean;
41
+ /** Whether selection should occur on press up. */
42
+ shouldSelectOnPressUp?: boolean;
43
+ /** Whether to focus items on hover. */
44
+ shouldFocusOnHover?: boolean;
45
+ /** Whether type-to-select is disabled. @default false */
46
+ disallowTypeAhead?: boolean;
47
+ }
48
+
49
+ export interface ListBoxAria {
50
+ /** Props for the listbox element. */
51
+ listBoxProps: JSX.HTMLAttributes<HTMLElement>;
52
+ /** Props for the listbox's label element (if any). */
53
+ labelProps: JSX.HTMLAttributes<HTMLElement>;
54
+ }
55
+
56
+ // Shared data between listbox and options
57
+ const listBoxData = new WeakMap<object, ListBoxData>();
58
+
59
+ interface ListBoxData {
60
+ id: string;
61
+ onAction?: (key: Key) => void;
62
+ shouldSelectOnPressUp?: boolean;
63
+ shouldFocusOnHover?: boolean;
64
+ isDisabled?: boolean;
65
+ }
66
+
67
+ export function getListBoxData(state: ListState): ListBoxData | undefined {
68
+ return listBoxData.get(state);
69
+ }
70
+
71
+ function findNextEnabledKey<T>(
72
+ state: ListState<T>,
73
+ currentKey: Key | null,
74
+ direction: 'next' | 'prev',
75
+ wrap: boolean
76
+ ): Key | null {
77
+ const collection = state.collection();
78
+ const getAdjacentKey = direction === 'next'
79
+ ? (key: Key) => collection.getKeyAfter(key)
80
+ : (key: Key) => collection.getKeyBefore(key);
81
+ const getBoundaryKey = direction === 'next'
82
+ ? () => collection.getFirstKey()
83
+ : () => collection.getLastKey();
84
+
85
+ let key = currentKey != null ? getAdjacentKey(currentKey) : getBoundaryKey();
86
+ while (key != null && state.isDisabled(key)) {
87
+ key = getAdjacentKey(key);
88
+ }
89
+
90
+ if (key == null && wrap) {
91
+ key = getBoundaryKey();
92
+ while (key != null && state.isDisabled(key)) {
93
+ key = getAdjacentKey(key);
94
+ }
95
+ }
96
+
97
+ return key;
98
+ }
99
+
100
+ /**
101
+ * Provides the behavior and accessibility implementation for a listbox component.
102
+ * A listbox displays a list of options and allows a user to select one or more of them.
103
+ */
104
+ export function createListBox<T>(
105
+ props: MaybeAccessor<AriaListBoxProps>,
106
+ state: ListState<T>,
107
+ _ref?: () => HTMLElement | null
108
+ ): ListBoxAria {
109
+ const getProps = () => access(props);
110
+ const id = createId(getProps().id);
111
+
112
+ // Development-time warning for missing accessibility labels
113
+ if (isDevEnv()) {
114
+ const p = getProps();
115
+ if (!p.label && !p['aria-label'] && !p['aria-labelledby']) {
116
+ console.warn(
117
+ '[solidaria] A ListBox requires an aria-label or aria-labelledby attribute for accessibility.'
118
+ );
119
+ }
120
+ }
121
+
122
+ // Filter DOM props
123
+ const domProps = () => filterDOMProps(getProps() as unknown as Record<string, unknown>, { labelable: true });
124
+
125
+ const updateSharedData = () => {
126
+ const p = getProps();
127
+ listBoxData.set(state, {
128
+ id,
129
+ onAction: p.onAction,
130
+ shouldSelectOnPressUp: p.shouldSelectOnPressUp,
131
+ shouldFocusOnHover: p.shouldFocusOnHover,
132
+ isDisabled: p.isDisabled,
133
+ });
134
+ };
135
+
136
+ // Ensure options created in the same render pass can read parent metadata.
137
+ updateSharedData();
138
+
139
+ // Share data with child options
140
+ createEffect(() => {
141
+ updateSharedData();
142
+
143
+ onCleanup(() => {
144
+ listBoxData.delete(state);
145
+ });
146
+ });
147
+
148
+ // Handle focus within
149
+ const { focusWithinProps } = createFocusWithin({
150
+ onFocusWithin: (e) => getProps().onFocus?.(e),
151
+ onBlurWithin: (e) => getProps().onBlur?.(e),
152
+ onFocusWithinChange: (isFocused) => {
153
+ getProps().onFocusChange?.(isFocused);
154
+ state.setFocused(isFocused);
155
+ },
156
+ });
157
+
158
+ // Label handling
159
+ const { labelProps, fieldProps } = createLabel({
160
+ get id() {
161
+ return id;
162
+ },
163
+ get label() {
164
+ return getProps().label;
165
+ },
166
+ get 'aria-label'() {
167
+ return getProps()['aria-label'];
168
+ },
169
+ get 'aria-labelledby'() {
170
+ return getProps()['aria-labelledby'];
171
+ },
172
+ labelElementType: 'span',
173
+ });
174
+
175
+ // Type-to-select
176
+ const { typeSelectProps } = createTypeSelect({
177
+ collection: () => state.collection(),
178
+ focusedKey: () => state.focusedKey(),
179
+ onFocusedKeyChange: (key) => state.setFocusedKey(key),
180
+ isKeyDisabled: (key) => state.isDisabled(key),
181
+ get isDisabled() {
182
+ return getProps().disallowTypeAhead ?? false;
183
+ },
184
+ });
185
+
186
+ // Keyboard navigation
187
+ const onKeyDown: JSX.EventHandler<HTMLElement, KeyboardEvent> = (e) => {
188
+ const p = getProps();
189
+ if (p.isDisabled) return;
190
+
191
+ const collection = state.collection();
192
+ const shouldWrap = p.shouldFocusWrap ?? false;
193
+
194
+ switch (e.key) {
195
+ case 'ArrowDown': {
196
+ e.preventDefault();
197
+ const nextKey = findNextEnabledKey(state, state.focusedKey(), 'next', shouldWrap);
198
+ if (nextKey != null) {
199
+ state.setFocusedKey(nextKey);
200
+ if (!e.shiftKey && state.selectionMode() === 'single') {
201
+ state.replaceSelection(nextKey);
202
+ } else if (e.shiftKey && state.selectionMode() === 'multiple') {
203
+ state.extendSelection(nextKey, collection);
204
+ }
205
+ }
206
+ break;
207
+ }
208
+ case 'ArrowUp': {
209
+ e.preventDefault();
210
+ const prevKey = findNextEnabledKey(state, state.focusedKey(), 'prev', shouldWrap);
211
+ if (prevKey != null) {
212
+ state.setFocusedKey(prevKey);
213
+ if (!e.shiftKey && state.selectionMode() === 'single') {
214
+ state.replaceSelection(prevKey);
215
+ } else if (e.shiftKey && state.selectionMode() === 'multiple') {
216
+ state.extendSelection(prevKey, collection);
217
+ }
218
+ }
219
+ break;
220
+ }
221
+ case 'Home': {
222
+ e.preventDefault();
223
+ const firstKey = findNextEnabledKey(state, null, 'next', false);
224
+ if (firstKey != null) {
225
+ state.setFocusedKey(firstKey);
226
+ if (e.ctrlKey && e.shiftKey && state.selectionMode() === 'multiple') {
227
+ // Select from current to first
228
+ state.extendSelection(firstKey, collection);
229
+ } else if (!e.shiftKey && state.selectionMode() === 'single') {
230
+ state.replaceSelection(firstKey);
231
+ }
232
+ }
233
+ break;
234
+ }
235
+ case 'End': {
236
+ e.preventDefault();
237
+ const lastKey = findNextEnabledKey(state, null, 'prev', false);
238
+ if (lastKey != null) {
239
+ state.setFocusedKey(lastKey);
240
+ if (e.ctrlKey && e.shiftKey && state.selectionMode() === 'multiple') {
241
+ // Select from current to last
242
+ state.extendSelection(lastKey, collection);
243
+ } else if (!e.shiftKey && state.selectionMode() === 'single') {
244
+ state.replaceSelection(lastKey);
245
+ }
246
+ }
247
+ break;
248
+ }
249
+ case ' ':
250
+ case 'Enter': {
251
+ e.preventDefault();
252
+ const focusedKey = state.focusedKey();
253
+ if (focusedKey != null && !state.isDisabled(focusedKey)) {
254
+ if (state.selectionMode() !== 'none') {
255
+ state.toggleSelection(focusedKey);
256
+ }
257
+ p.onAction?.(focusedKey);
258
+ }
259
+ break;
260
+ }
261
+ case 'a': {
262
+ if ((e.ctrlKey || e.metaKey) && state.selectionMode() === 'multiple') {
263
+ e.preventDefault();
264
+ state.selectAll();
265
+ }
266
+ break;
267
+ }
268
+ case 'Escape': {
269
+ e.preventDefault();
270
+ if (!state.disallowEmptySelection()) {
271
+ state.clearSelection();
272
+ }
273
+ break;
274
+ }
275
+ }
276
+ };
277
+
278
+ return {
279
+ get labelProps() {
280
+ return labelProps as JSX.HTMLAttributes<HTMLElement>;
281
+ },
282
+ get listBoxProps() {
283
+ const p = getProps();
284
+ const selectionMode = state.selectionMode();
285
+
286
+ const baseProps = mergeProps(
287
+ domProps(),
288
+ focusWithinProps as Record<string, unknown>,
289
+ fieldProps as Record<string, unknown>,
290
+ {
291
+ role: 'listbox',
292
+ tabIndex: p.isDisabled ? undefined : 0,
293
+ 'aria-disabled': p.isDisabled || undefined,
294
+ 'aria-multiselectable': selectionMode === 'multiple' ? true : undefined,
295
+ 'aria-activedescendant': state.focusedKey() != null ? String(state.focusedKey()) : undefined,
296
+ onKeyDown,
297
+ }
298
+ );
299
+
300
+ // Add type-select props if enabled
301
+ if (!p.disallowTypeAhead) {
302
+ return mergeProps(baseProps, typeSelectProps as Record<string, unknown>) as JSX.HTMLAttributes<HTMLElement>;
303
+ }
304
+
305
+ return baseProps as JSX.HTMLAttributes<HTMLElement>;
306
+ },
307
+ };
308
+ }