@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,6 +1,6 @@
1
- export {
2
- createComboBox,
3
- getComboBoxData,
4
- type AriaComboBoxProps,
5
- type ComboBoxAria,
6
- } from './createComboBox';
1
+ export {
2
+ createComboBox,
3
+ getComboBoxData,
4
+ type AriaComboBoxProps,
5
+ type ComboBoxAria,
6
+ } from './createComboBox';
@@ -10,6 +10,7 @@ import { createId } from '../ssr';
10
10
  import { createLabel } from '../label/createLabel';
11
11
  import { access, type MaybeAccessor } from '../utils/reactivity';
12
12
  import { mergeProps } from '../utils/mergeProps';
13
+ import { useLocale } from '../i18n';
13
14
  import type { DateFieldState, CalendarState } from '@proyecto-viviana/solid-stately';
14
15
 
15
16
  // ============================================
@@ -39,6 +40,12 @@ export interface AriaDatePickerProps {
39
40
  description?: string;
40
41
  /** Error message. */
41
42
  errorMessage?: string;
43
+ /** Accessible label for the calendar trigger button. */
44
+ buttonAriaLabel?: string;
45
+ /** Accessible label for the calendar dialog. */
46
+ dialogAriaLabel?: string;
47
+ /** Accessible label for the calendar grid region. */
48
+ calendarAriaLabel?: string;
42
49
  }
43
50
 
44
51
  export interface DatePickerState {
@@ -84,6 +91,7 @@ export function createDatePicker<T extends DateFieldState, C extends CalendarSta
84
91
  overlayState: DatePickerState,
85
92
  _calendarState?: C
86
93
  ): DatePickerAria {
94
+ const locale = useLocale();
87
95
  const getProps = () => access(props);
88
96
  const id = createId(getProps().id);
89
97
  const descriptionId = createId();
@@ -117,29 +125,31 @@ export function createDatePicker<T extends DateFieldState, C extends CalendarSta
117
125
  // Group props
118
126
  const groupProps = createMemo(() => {
119
127
  const p = getProps();
128
+ const isInvalid = p.isInvalid || state.isInvalid();
120
129
 
121
130
  return mergeProps(labelFieldProps as Record<string, unknown>, {
122
131
  id,
123
132
  role: 'group',
124
133
  'aria-disabled': p.isDisabled || state.isDisabled() || undefined,
134
+ 'aria-readonly': p.isReadOnly || state.isReadOnly() || undefined,
135
+ 'aria-required': p.isRequired || state.isRequired() || undefined,
136
+ 'aria-invalid': isInvalid || undefined,
125
137
  'aria-describedby': getAriaDescribedBy(),
126
138
  });
127
139
  });
128
140
 
129
- // Field props
130
- const fieldProps = createMemo(() => ({
131
- 'aria-haspopup': 'dialog' as const,
132
- 'aria-expanded': overlayState.isOpen,
133
- 'aria-controls': overlayState.isOpen ? dialogId : undefined,
134
- }));
141
+ // Field props are applied to DateInput (a non-popup container), so avoid
142
+ // popup trigger attributes like aria-expanded/aria-haspopup here.
143
+ const fieldProps = createMemo(() => ({}));
135
144
 
136
145
  // Button props
137
146
  const buttonProps = createMemo(() => {
138
147
  const p = getProps();
139
148
  const isDisabled = p.isDisabled || state.isDisabled();
149
+ const defaults = getDatePickerLabelDefaults(locale().locale);
140
150
 
141
151
  return {
142
- 'aria-label': 'Open calendar',
152
+ 'aria-label': p.buttonAriaLabel ?? defaults.button,
143
153
  'aria-haspopup': 'dialog' as const,
144
154
  'aria-expanded': overlayState.isOpen,
145
155
  'aria-controls': overlayState.isOpen ? dialogId : undefined,
@@ -154,17 +164,23 @@ export function createDatePicker<T extends DateFieldState, C extends CalendarSta
154
164
  });
155
165
 
156
166
  // Dialog props
157
- const dialogProps = createMemo(() => ({
158
- id: dialogId,
159
- role: 'dialog',
160
- 'aria-modal': true,
161
- 'aria-label': 'Calendar',
162
- }));
167
+ const dialogProps = createMemo(() => {
168
+ const defaults = getDatePickerLabelDefaults(locale().locale);
169
+ return {
170
+ id: dialogId,
171
+ role: 'dialog',
172
+ 'aria-modal': true,
173
+ 'aria-label': getProps().dialogAriaLabel ?? defaults.dialog,
174
+ };
175
+ });
163
176
 
164
177
  // Calendar props
165
- const calendarProps = createMemo(() => ({
166
- 'aria-label': 'Calendar',
167
- }));
178
+ const calendarProps = createMemo(() => {
179
+ const defaults = getDatePickerLabelDefaults(locale().locale);
180
+ return {
181
+ 'aria-label': getProps().calendarAriaLabel ?? getProps().dialogAriaLabel ?? defaults.calendar,
182
+ };
183
+ });
168
184
 
169
185
  // Description props
170
186
  const descriptionProps = createMemo(() => ({
@@ -204,3 +220,25 @@ export function createDatePicker<T extends DateFieldState, C extends CalendarSta
204
220
  },
205
221
  };
206
222
  }
223
+
224
+ function getDatePickerLabelDefaults(locale: string): {
225
+ button: string;
226
+ dialog: string;
227
+ calendar: string;
228
+ } {
229
+ const language = locale.toLowerCase().split('-')[0] ?? 'en';
230
+
231
+ if (language === 'es') {
232
+ return {
233
+ button: 'Abrir calendario',
234
+ dialog: 'Calendario',
235
+ calendar: 'Calendario',
236
+ };
237
+ }
238
+
239
+ return {
240
+ button: 'Open calendar',
241
+ dialog: 'Calendar',
242
+ calendar: 'Calendar',
243
+ };
244
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * createDateRangePicker hook for Solidaria
3
+ *
4
+ * Provides behavior and accessibility wiring for range date pickers.
5
+ * Compatibility target: @react-aria/datepicker useDateRangePicker.
6
+ */
7
+
8
+ import { createMemo } from 'solid-js';
9
+ import { createId } from '../ssr';
10
+ import { createLabel } from '../label/createLabel';
11
+ import { access, type MaybeAccessor } from '../utils/reactivity';
12
+ import { mergeProps } from '../utils/mergeProps';
13
+ import { useLocale } from '../i18n';
14
+ import type { RangeCalendarState } from '@proyecto-viviana/solid-stately';
15
+ import type { DatePickerState } from './createDatePicker';
16
+
17
+ export interface AriaDateRangePickerProps {
18
+ id?: string;
19
+ label?: string;
20
+ 'aria-label'?: string;
21
+ 'aria-labelledby'?: string;
22
+ 'aria-describedby'?: string;
23
+ isDisabled?: boolean;
24
+ isReadOnly?: boolean;
25
+ isRequired?: boolean;
26
+ isInvalid?: boolean;
27
+ description?: string;
28
+ errorMessage?: string;
29
+ buttonAriaLabel?: string;
30
+ dialogAriaLabel?: string;
31
+ calendarAriaLabel?: string;
32
+ startFieldAriaLabel?: string;
33
+ endFieldAriaLabel?: string;
34
+ }
35
+
36
+ export interface DateRangePickerAria {
37
+ groupProps: Record<string, unknown>;
38
+ labelProps: Record<string, unknown>;
39
+ startFieldProps: Record<string, unknown>;
40
+ endFieldProps: Record<string, unknown>;
41
+ buttonProps: Record<string, unknown>;
42
+ dialogProps: Record<string, unknown>;
43
+ calendarProps: Record<string, unknown>;
44
+ descriptionProps: Record<string, unknown>;
45
+ errorMessageProps: Record<string, unknown>;
46
+ }
47
+
48
+ export function createDateRangePicker<T extends RangeCalendarState>(
49
+ props: MaybeAccessor<AriaDateRangePickerProps>,
50
+ state: T,
51
+ overlayState: DatePickerState
52
+ ): DateRangePickerAria {
53
+ const locale = useLocale();
54
+ const getProps = () => access(props);
55
+ const id = createId(getProps().id);
56
+ const startFieldId = createId();
57
+ const endFieldId = createId();
58
+ const descriptionId = createId();
59
+ const errorMessageId = createId();
60
+ const dialogId = createId();
61
+
62
+ const { labelProps, fieldProps: labelFieldProps } = createLabel({
63
+ get label() { return getProps().label; },
64
+ get 'aria-label'() { return getProps()['aria-label']; },
65
+ get 'aria-labelledby'() { return getProps()['aria-labelledby']; },
66
+ labelElementType: 'span',
67
+ });
68
+
69
+ const getAriaDescribedBy = () => {
70
+ const p = getProps();
71
+ const ids: string[] = [];
72
+ if (p['aria-describedby']) ids.push(p['aria-describedby']);
73
+ if (p.description) ids.push(descriptionId);
74
+ if (p.isInvalid && p.errorMessage) ids.push(errorMessageId);
75
+ return ids.length > 0 ? ids.join(' ') : undefined;
76
+ };
77
+
78
+ const groupProps = createMemo(() => {
79
+ const p = getProps();
80
+ const isInvalid = p.isInvalid;
81
+ return mergeProps(labelFieldProps as Record<string, unknown>, {
82
+ id,
83
+ role: 'group',
84
+ 'aria-disabled': p.isDisabled || state.isDisabled() || undefined,
85
+ 'aria-readonly': p.isReadOnly || state.isReadOnly() || undefined,
86
+ 'aria-required': p.isRequired || undefined,
87
+ 'aria-invalid': isInvalid || undefined,
88
+ 'aria-describedby': getAriaDescribedBy(),
89
+ });
90
+ });
91
+
92
+ const startFieldProps = createMemo(() => {
93
+ const p = getProps();
94
+ const defaults = getDateRangePickerLabelDefaults(locale().locale);
95
+ const isDisabled = p.isDisabled || state.isDisabled();
96
+ const isReadOnly = p.isReadOnly || state.isReadOnly();
97
+ const isInvalid = p.isInvalid;
98
+
99
+ return {
100
+ id: startFieldId,
101
+ 'aria-label': p.startFieldAriaLabel ?? defaults.startField,
102
+ 'aria-describedby': getAriaDescribedBy(),
103
+ 'aria-disabled': isDisabled || undefined,
104
+ 'aria-readonly': isReadOnly || undefined,
105
+ 'aria-invalid': isInvalid || undefined,
106
+ tabIndex: isDisabled ? -1 : 0,
107
+ onClick: () => {
108
+ if (!isDisabled) {
109
+ overlayState.open();
110
+ }
111
+ },
112
+ onKeyDown: (event: KeyboardEvent) => {
113
+ if (isDisabled) return;
114
+ if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
115
+ event.preventDefault();
116
+ overlayState.open();
117
+ }
118
+ },
119
+ };
120
+ });
121
+
122
+ const endFieldProps = createMemo(() => {
123
+ const p = getProps();
124
+ const defaults = getDateRangePickerLabelDefaults(locale().locale);
125
+ const isDisabled = p.isDisabled || state.isDisabled();
126
+ const isReadOnly = p.isReadOnly || state.isReadOnly();
127
+ const isInvalid = p.isInvalid;
128
+
129
+ return {
130
+ id: endFieldId,
131
+ 'aria-label': p.endFieldAriaLabel ?? defaults.endField,
132
+ 'aria-describedby': getAriaDescribedBy(),
133
+ 'aria-disabled': isDisabled || undefined,
134
+ 'aria-readonly': isReadOnly || undefined,
135
+ 'aria-invalid': isInvalid || undefined,
136
+ tabIndex: isDisabled ? -1 : 0,
137
+ onClick: () => {
138
+ if (!isDisabled) {
139
+ overlayState.open();
140
+ }
141
+ },
142
+ onKeyDown: (event: KeyboardEvent) => {
143
+ if (isDisabled) return;
144
+ if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
145
+ event.preventDefault();
146
+ overlayState.open();
147
+ }
148
+ },
149
+ };
150
+ });
151
+
152
+ const buttonProps = createMemo(() => {
153
+ const p = getProps();
154
+ const defaults = getDateRangePickerLabelDefaults(locale().locale);
155
+ const isDisabled = p.isDisabled || state.isDisabled();
156
+ return {
157
+ 'aria-label': p.buttonAriaLabel ?? defaults.button,
158
+ 'aria-haspopup': 'dialog' as const,
159
+ 'aria-expanded': overlayState.isOpen,
160
+ 'aria-controls': overlayState.isOpen ? dialogId : undefined,
161
+ disabled: isDisabled,
162
+ tabIndex: 0,
163
+ onClick: () => {
164
+ if (!isDisabled) overlayState.toggle();
165
+ },
166
+ };
167
+ });
168
+
169
+ const dialogProps = createMemo(() => {
170
+ const defaults = getDateRangePickerLabelDefaults(locale().locale);
171
+ return {
172
+ id: dialogId,
173
+ role: 'dialog',
174
+ 'aria-modal': true,
175
+ 'aria-label': getProps().dialogAriaLabel ?? defaults.dialog,
176
+ };
177
+ });
178
+
179
+ const calendarProps = createMemo(() => {
180
+ const defaults = getDateRangePickerLabelDefaults(locale().locale);
181
+ return {
182
+ 'aria-label': getProps().calendarAriaLabel ?? getProps().dialogAriaLabel ?? defaults.calendar,
183
+ };
184
+ });
185
+
186
+ const descriptionProps = createMemo(() => ({ id: descriptionId }));
187
+ const errorMessageProps = createMemo(() => ({ id: errorMessageId, role: 'alert' }));
188
+
189
+ return {
190
+ get groupProps() {
191
+ return groupProps();
192
+ },
193
+ get labelProps() {
194
+ return labelProps as Record<string, unknown>;
195
+ },
196
+ get startFieldProps() {
197
+ return startFieldProps();
198
+ },
199
+ get endFieldProps() {
200
+ return endFieldProps();
201
+ },
202
+ get buttonProps() {
203
+ return buttonProps();
204
+ },
205
+ get dialogProps() {
206
+ return dialogProps();
207
+ },
208
+ get calendarProps() {
209
+ return calendarProps();
210
+ },
211
+ get descriptionProps() {
212
+ return descriptionProps();
213
+ },
214
+ get errorMessageProps() {
215
+ return errorMessageProps();
216
+ },
217
+ };
218
+ }
219
+
220
+ function getDateRangePickerLabelDefaults(locale: string): {
221
+ button: string;
222
+ dialog: string;
223
+ calendar: string;
224
+ startField: string;
225
+ endField: string;
226
+ } {
227
+ const language = locale.toLowerCase().split('-')[0] ?? 'en';
228
+
229
+ if (language === 'es') {
230
+ return {
231
+ button: 'Abrir calendario de rango',
232
+ dialog: 'Calendario de rango',
233
+ calendar: 'Calendario de rango',
234
+ startField: 'Fecha de inicio',
235
+ endField: 'Fecha de fin',
236
+ };
237
+ }
238
+
239
+ return {
240
+ button: 'Open range calendar',
241
+ dialog: 'Range calendar',
242
+ calendar: 'Range calendar',
243
+ startField: 'Start date',
244
+ endField: 'End date',
245
+ };
246
+ }
@@ -8,6 +8,7 @@
8
8
  import { createSignal, createMemo } from 'solid-js';
9
9
  import { access, type MaybeAccessor } from '../utils/reactivity';
10
10
  import type { DateFieldState, DateSegment, DateSegmentType } from '@proyecto-viviana/solid-stately';
11
+ import { useLocale } from '../i18n';
11
12
 
12
13
  // ============================================
13
14
  // TYPES
@@ -41,11 +42,13 @@ export interface DateSegmentAria {
41
42
  export function createDateSegment<T extends DateFieldState>(
42
43
  props: MaybeAccessor<AriaDateSegmentProps>,
43
44
  state: T,
44
- _ref?: () => HTMLElement | null
45
+ ref?: () => HTMLElement | null
45
46
  ): DateSegmentAria {
46
47
  const getProps = () => access(props);
47
48
  const [isFocused, setIsFocused] = createSignal(false);
48
49
  const [enteredKeys, setEnteredKeys] = createSignal('');
50
+ const [isComposing, setIsComposing] = createSignal(false);
51
+ const locale = useLocale();
49
52
 
50
53
  // Get the segment from props
51
54
  const segment = createMemo(() => getProps().segment);
@@ -56,9 +59,47 @@ export function createDateSegment<T extends DateFieldState>(
56
59
  return seg.isEditable && !state.isDisabled() && !state.isReadOnly();
57
60
  });
58
61
 
62
+ const focusSegment = (target: 'first' | 'last' | 'prev' | 'next') => {
63
+ const el = ref?.();
64
+ if (!el) return;
65
+
66
+ const container = el.parentElement;
67
+ if (!container) return;
68
+
69
+ const segments = Array.from(
70
+ container.querySelectorAll<HTMLElement>('[role="spinbutton"]')
71
+ );
72
+
73
+ if (segments.length === 0) return;
74
+
75
+ if (target === 'first') {
76
+ segments[0]?.focus();
77
+ return;
78
+ }
79
+
80
+ if (target === 'last') {
81
+ segments[segments.length - 1]?.focus();
82
+ return;
83
+ }
84
+
85
+ const currentIndex = segments.indexOf(el);
86
+ if (currentIndex < 0) return;
87
+
88
+ const nextIndex = target === 'next' ? currentIndex + 1 : currentIndex - 1;
89
+ if (nextIndex >= 0 && nextIndex < segments.length) {
90
+ segments[nextIndex]?.focus();
91
+ }
92
+ };
93
+
94
+ const clearCurrentSegment = (type: DateSegmentType) => {
95
+ state.clearSegment(type);
96
+ setEnteredKeys('');
97
+ };
98
+
59
99
  // Handle keyboard input
60
100
  const handleKeyDown = (e: KeyboardEvent) => {
61
101
  if (!isEditable()) return;
102
+ if (isComposing()) return;
62
103
 
63
104
  const seg = segment();
64
105
  const type = seg.type;
@@ -66,6 +107,14 @@ export function createDateSegment<T extends DateFieldState>(
66
107
  if (type === 'literal') return;
67
108
 
68
109
  switch (e.key) {
110
+ case 'ArrowRight':
111
+ e.preventDefault();
112
+ focusSegment(locale().direction === 'rtl' ? 'prev' : 'next');
113
+ break;
114
+ case 'ArrowLeft':
115
+ e.preventDefault();
116
+ focusSegment(locale().direction === 'rtl' ? 'next' : 'prev');
117
+ break;
69
118
  case 'ArrowUp':
70
119
  e.preventDefault();
71
120
  state.incrementSegment(type);
@@ -74,17 +123,33 @@ export function createDateSegment<T extends DateFieldState>(
74
123
  e.preventDefault();
75
124
  state.decrementSegment(type);
76
125
  break;
126
+ case 'Home':
127
+ e.preventDefault();
128
+ focusSegment('first');
129
+ break;
130
+ case 'End':
131
+ e.preventDefault();
132
+ focusSegment('last');
133
+ break;
77
134
  case 'Backspace':
135
+ e.preventDefault();
136
+ // Match common date-field UX: backspace on an empty placeholder moves to previous segment.
137
+ if (seg.isPlaceholder) {
138
+ focusSegment('prev');
139
+ } else {
140
+ clearCurrentSegment(type);
141
+ }
142
+ break;
78
143
  case 'Delete':
79
144
  e.preventDefault();
80
- state.clearSegment(type);
81
- setEnteredKeys('');
145
+ clearCurrentSegment(type);
82
146
  break;
83
147
  default:
84
148
  // Handle numeric input
85
- if (/^\d$/.test(e.key)) {
149
+ const normalizedDigit = normalizeDigit(e.key);
150
+ if (normalizedDigit) {
86
151
  e.preventDefault();
87
- handleNumericInput(e.key, type, seg);
152
+ handleNumericInput(normalizedDigit, type, seg);
88
153
  }
89
154
  break;
90
155
  }
@@ -100,6 +165,7 @@ export function createDateSegment<T extends DateFieldState>(
100
165
  const numValue = parseInt(newKeys, 10);
101
166
  const maxValue = seg.maxValue ?? 99;
102
167
  const minValue = seg.minValue ?? 0;
168
+ const maxDigits = String(maxValue).length;
103
169
 
104
170
  // Check if we should accept more digits
105
171
  if (numValue <= maxValue) {
@@ -107,9 +173,9 @@ export function createDateSegment<T extends DateFieldState>(
107
173
 
108
174
  // If entering more digits wouldn't make sense, clear entered keys
109
175
  const potentialNextValue = parseInt(newKeys + '0', 10);
110
- if (potentialNextValue > maxValue || newKeys.length >= String(maxValue).length) {
176
+ if (potentialNextValue > maxValue || newKeys.length >= maxDigits) {
111
177
  setEnteredKeys('');
112
- // Move to next segment (would need focus management)
178
+ focusSegment('next');
113
179
  } else {
114
180
  setEnteredKeys(newKeys);
115
181
  }
@@ -118,8 +184,58 @@ export function createDateSegment<T extends DateFieldState>(
118
184
  const singleValue = parseInt(key, 10);
119
185
  if (singleValue >= minValue && singleValue <= maxValue) {
120
186
  state.setSegment(type, singleValue);
187
+ const potentialNextValue = parseInt(key + '0', 10);
188
+ if (potentialNextValue > maxValue || key.length >= maxDigits) {
189
+ setEnteredKeys('');
190
+ focusSegment('next');
191
+ } else {
192
+ setEnteredKeys(key);
193
+ }
194
+ } else {
195
+ setEnteredKeys('');
121
196
  }
122
- setEnteredKeys(key);
197
+ }
198
+ };
199
+
200
+ const handleBeforeInput = (e: InputEvent) => {
201
+ if (!isEditable()) return;
202
+ if (isComposing()) return;
203
+
204
+ const seg = segment();
205
+ if (seg.type === 'literal') return;
206
+
207
+ if (e.inputType === 'deleteContentBackward' || e.inputType === 'deleteContentForward') {
208
+ e.preventDefault();
209
+ clearCurrentSegment(seg.type);
210
+ return;
211
+ }
212
+
213
+ if (e.inputType === 'insertText' && e.data) {
214
+ const normalizedDigit = normalizeDigit(e.data);
215
+ if (!normalizedDigit) return;
216
+ e.preventDefault();
217
+ handleNumericInput(normalizedDigit, seg.type, seg);
218
+ }
219
+ };
220
+
221
+ const handleCompositionStart = () => {
222
+ if (!isEditable()) return;
223
+ setIsComposing(true);
224
+ setEnteredKeys('');
225
+ };
226
+
227
+ const handleCompositionEnd = (e: CompositionEvent) => {
228
+ if (!isEditable()) return;
229
+ setIsComposing(false);
230
+
231
+ const seg = segment();
232
+ if (seg.type === 'literal') return;
233
+
234
+ const digits = extractNormalizedDigits(e.data ?? '');
235
+ if (digits.length === 0) return;
236
+
237
+ for (const digit of digits) {
238
+ handleNumericInput(digit, seg.type, seg);
123
239
  }
124
240
  };
125
241
 
@@ -150,7 +266,7 @@ export function createDateSegment<T extends DateFieldState>(
150
266
  return {
151
267
  role: 'spinbutton',
152
268
  tabIndex: isEditable() ? 0 : -1,
153
- 'aria-label': getSegmentLabel(type),
269
+ 'aria-label': getSegmentLabel(type, locale().locale),
154
270
  'aria-valuenow': seg.value,
155
271
  'aria-valuemin': seg.minValue,
156
272
  'aria-valuemax': seg.maxValue,
@@ -167,6 +283,9 @@ export function createDateSegment<T extends DateFieldState>(
167
283
  onKeyDown: handleKeyDown,
168
284
  onFocus: handleFocus,
169
285
  onBlur: handleBlur,
286
+ onBeforeInput: handleBeforeInput,
287
+ onCompositionStart: handleCompositionStart,
288
+ onCompositionEnd: handleCompositionEnd,
170
289
  onMouseDown: (e: MouseEvent) => {
171
290
  // Prevent cursor positioning in the middle of the segment
172
291
  e.preventDefault();
@@ -203,27 +322,62 @@ export function createDateSegment<T extends DateFieldState>(
203
322
  // HELPER FUNCTIONS
204
323
  // ============================================
205
324
 
206
- function getSegmentLabel(type: DateSegmentType): string {
207
- switch (type) {
208
- case 'year':
209
- return 'Year';
210
- case 'month':
211
- return 'Month';
212
- case 'day':
213
- return 'Day';
214
- case 'hour':
215
- return 'Hour';
216
- case 'minute':
217
- return 'Minute';
218
- case 'second':
219
- return 'Second';
220
- case 'dayPeriod':
221
- return 'AM/PM';
222
- case 'era':
223
- return 'Era';
224
- case 'timeZoneName':
225
- return 'Time zone';
226
- default:
227
- return '';
325
+ const SEGMENT_LABELS: Record<string, Record<Exclude<DateSegmentType, 'literal'>, string>> = {
326
+ en: {
327
+ year: 'Year',
328
+ month: 'Month',
329
+ day: 'Day',
330
+ hour: 'Hour',
331
+ minute: 'Minute',
332
+ second: 'Second',
333
+ dayPeriod: 'AM/PM',
334
+ era: 'Era',
335
+ timeZoneName: 'Time zone',
336
+ },
337
+ es: {
338
+ year: 'Año',
339
+ month: 'Mes',
340
+ day: 'Día',
341
+ hour: 'Hora',
342
+ minute: 'Minuto',
343
+ second: 'Segundo',
344
+ dayPeriod: 'AM/PM',
345
+ era: 'Era',
346
+ timeZoneName: 'Zona horaria',
347
+ },
348
+ };
349
+
350
+ function getSegmentLabel(type: DateSegmentType, locale: string): string {
351
+ if (type === 'literal') return '';
352
+
353
+ const language = locale.toLowerCase().split('-')[0] ?? 'en';
354
+ const labels = SEGMENT_LABELS[language] ?? SEGMENT_LABELS.en;
355
+ return labels[type];
356
+ }
357
+
358
+ function normalizeDigit(input: string): string | null {
359
+ if (/^[0-9]$/.test(input)) {
360
+ return input;
361
+ }
362
+
363
+ const codePoint = input.codePointAt(0);
364
+ if (codePoint == null) return null;
365
+
366
+ // Full-width digits 0-9
367
+ if (codePoint >= 0xff10 && codePoint <= 0xff19) {
368
+ return String(codePoint - 0xff10);
369
+ }
370
+
371
+ return null;
372
+ }
373
+
374
+ function extractNormalizedDigits(value: string): string[] {
375
+ const digits: string[] = [];
376
+ for (const char of value) {
377
+ const normalized = normalizeDigit(char);
378
+ if (normalized) {
379
+ digits.push(normalized);
380
+ }
228
381
  }
382
+ return digits;
229
383
  }