@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.
- 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
|
@@ -1,224 +1,224 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* createFormValidation hook for solidaria
|
|
3
|
-
*
|
|
4
|
-
* Connects form validation state to native HTML form validation.
|
|
5
|
-
* Handles the invalid event, form reset, and focus management.
|
|
6
|
-
*
|
|
7
|
-
* Port of react-aria's useFormValidation.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { type Accessor, createEffect, onCleanup } from 'solid-js';
|
|
11
|
-
import {
|
|
12
|
-
type FormValidationState,
|
|
13
|
-
type ValidationResult,
|
|
14
|
-
} from '@proyecto-viviana/solid-stately';
|
|
15
|
-
import { setInteractionModality } from '../interactions/createInteractionModality';
|
|
16
|
-
|
|
17
|
-
// ============================================
|
|
18
|
-
// TYPES
|
|
19
|
-
// ============================================
|
|
20
|
-
|
|
21
|
-
export type ValidatableElement =
|
|
22
|
-
| HTMLInputElement
|
|
23
|
-
| HTMLTextAreaElement
|
|
24
|
-
| HTMLSelectElement;
|
|
25
|
-
|
|
26
|
-
export type ValidationBehavior = 'aria' | 'native';
|
|
27
|
-
|
|
28
|
-
export interface FormValidationProps {
|
|
29
|
-
/** Validation behavior: 'aria' for realtime, 'native' for on submit. */
|
|
30
|
-
validationBehavior?: ValidationBehavior;
|
|
31
|
-
/** Custom focus function to call on validation error. */
|
|
32
|
-
focus?: () => void;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// ============================================
|
|
36
|
-
// HELPERS
|
|
37
|
-
// ============================================
|
|
38
|
-
|
|
39
|
-
function getValidity(input: ValidatableElement): ValidityState {
|
|
40
|
-
// Create a snapshot of the validity state (the native object is live)
|
|
41
|
-
const validity = input.validity;
|
|
42
|
-
return {
|
|
43
|
-
badInput: validity.badInput,
|
|
44
|
-
customError: validity.customError,
|
|
45
|
-
patternMismatch: validity.patternMismatch,
|
|
46
|
-
rangeOverflow: validity.rangeOverflow,
|
|
47
|
-
rangeUnderflow: validity.rangeUnderflow,
|
|
48
|
-
stepMismatch: validity.stepMismatch,
|
|
49
|
-
tooLong: validity.tooLong,
|
|
50
|
-
tooShort: validity.tooShort,
|
|
51
|
-
typeMismatch: validity.typeMismatch,
|
|
52
|
-
valueMissing: validity.valueMissing,
|
|
53
|
-
valid: validity.valid,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function getNativeValidity(input: ValidatableElement): ValidationResult {
|
|
58
|
-
return {
|
|
59
|
-
isInvalid: !input.validity.valid,
|
|
60
|
-
validationDetails: getValidity(input),
|
|
61
|
-
validationErrors: input.validationMessage ? [input.validationMessage] : [],
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function getFirstInvalidInput(form: HTMLFormElement): ValidatableElement | null {
|
|
66
|
-
for (let i = 0; i < form.elements.length; i++) {
|
|
67
|
-
const element = form.elements[i] as ValidatableElement;
|
|
68
|
-
if (!element.validity.valid) {
|
|
69
|
-
return element;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ============================================
|
|
76
|
-
// HOOK
|
|
77
|
-
// ============================================
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Connects form validation state to a native HTML form input.
|
|
81
|
-
*
|
|
82
|
-
* This hook:
|
|
83
|
-
* - Sets custom validity on the native input based on validation state
|
|
84
|
-
* - Handles the 'invalid' event to commit validation and focus the first invalid input
|
|
85
|
-
* - Handles form reset to clear validation state
|
|
86
|
-
* - Handles input change to commit validation
|
|
87
|
-
*
|
|
88
|
-
* @example
|
|
89
|
-
* ```tsx
|
|
90
|
-
* function MyTextField(props) {
|
|
91
|
-
* let inputRef: HTMLInputElement | undefined;
|
|
92
|
-
*
|
|
93
|
-
* const validationState = createFormValidationState({
|
|
94
|
-
* value: props.value,
|
|
95
|
-
* validate: props.validate,
|
|
96
|
-
* validationBehavior: 'native',
|
|
97
|
-
* });
|
|
98
|
-
*
|
|
99
|
-
* createFormValidation(
|
|
100
|
-
* { validationBehavior: 'native' },
|
|
101
|
-
* validationState,
|
|
102
|
-
* () => inputRef
|
|
103
|
-
* );
|
|
104
|
-
*
|
|
105
|
-
* return (
|
|
106
|
-
* <input
|
|
107
|
-
* ref={inputRef}
|
|
108
|
-
* value={props.value}
|
|
109
|
-
* aria-invalid={validationState.displayValidation().isInvalid || undefined}
|
|
110
|
-
* />
|
|
111
|
-
* );
|
|
112
|
-
* }
|
|
113
|
-
* ```
|
|
114
|
-
*/
|
|
115
|
-
export function createFormValidation(
|
|
116
|
-
props: FormValidationProps,
|
|
117
|
-
state: FormValidationState,
|
|
118
|
-
ref: Accessor<ValidatableElement | undefined>
|
|
119
|
-
): void {
|
|
120
|
-
const validationBehavior = () => props.validationBehavior ?? 'aria';
|
|
121
|
-
const focus = () => props.focus;
|
|
122
|
-
|
|
123
|
-
// Track whether we should ignore form reset (for React-like programmatic resets)
|
|
124
|
-
let isIgnoredReset = false;
|
|
125
|
-
|
|
126
|
-
// Set custom validity on the native input
|
|
127
|
-
createEffect(() => {
|
|
128
|
-
const input = ref();
|
|
129
|
-
if (
|
|
130
|
-
validationBehavior() === 'native' &&
|
|
131
|
-
input &&
|
|
132
|
-
!input.disabled
|
|
133
|
-
) {
|
|
134
|
-
const realtimeValidation = state.realtimeValidation();
|
|
135
|
-
const errorMessage = realtimeValidation.isInvalid
|
|
136
|
-
? realtimeValidation.validationErrors.join(' ') || 'Invalid value.'
|
|
137
|
-
: '';
|
|
138
|
-
input.setCustomValidity(errorMessage);
|
|
139
|
-
|
|
140
|
-
// Prevent default tooltip for validation message
|
|
141
|
-
if (!input.hasAttribute('title')) {
|
|
142
|
-
input.title = '';
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Update validation with native validity if not already invalid
|
|
146
|
-
if (!realtimeValidation.isInvalid) {
|
|
147
|
-
state.updateValidation(getNativeValidity(input));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// Set up event listeners
|
|
153
|
-
createEffect(() => {
|
|
154
|
-
const input = ref();
|
|
155
|
-
if (!input) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const form = input.form;
|
|
160
|
-
|
|
161
|
-
// Handle invalid event
|
|
162
|
-
const onInvalid = (e: Event) => {
|
|
163
|
-
// Only commit validation if we are not already displaying one
|
|
164
|
-
if (!state.displayValidation().isInvalid) {
|
|
165
|
-
state.commitValidation();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Auto focus the first invalid input in a form
|
|
169
|
-
if (!e.defaultPrevented && form && getFirstInvalidInput(form) === input) {
|
|
170
|
-
const focusFn = focus();
|
|
171
|
-
if (focusFn) {
|
|
172
|
-
focusFn();
|
|
173
|
-
} else {
|
|
174
|
-
input.focus();
|
|
175
|
-
}
|
|
176
|
-
// Always show focus ring
|
|
177
|
-
setInteractionModality('keyboard');
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Prevent default browser error UI
|
|
181
|
-
e.preventDefault();
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
// Handle change event
|
|
185
|
-
const onChange = () => {
|
|
186
|
-
state.commitValidation();
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
// Handle form reset
|
|
190
|
-
const onReset = () => {
|
|
191
|
-
if (!isIgnoredReset) {
|
|
192
|
-
state.resetValidation();
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
// Patch form.reset to detect programmatic resets
|
|
197
|
-
let originalReset: (() => void) | undefined;
|
|
198
|
-
if (form) {
|
|
199
|
-
originalReset = form.reset.bind(form);
|
|
200
|
-
form.reset = () => {
|
|
201
|
-
// Ignore programmatic resets outside user events
|
|
202
|
-
isIgnoredReset =
|
|
203
|
-
!window.event ||
|
|
204
|
-
(window.event.type === 'message' &&
|
|
205
|
-
window.event.target instanceof MessagePort);
|
|
206
|
-
originalReset?.();
|
|
207
|
-
isIgnoredReset = false;
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
input.addEventListener('invalid', onInvalid);
|
|
212
|
-
input.addEventListener('change', onChange);
|
|
213
|
-
form?.addEventListener('reset', onReset);
|
|
214
|
-
|
|
215
|
-
onCleanup(() => {
|
|
216
|
-
input.removeEventListener('invalid', onInvalid);
|
|
217
|
-
input.removeEventListener('change', onChange);
|
|
218
|
-
form?.removeEventListener('reset', onReset);
|
|
219
|
-
if (form && originalReset) {
|
|
220
|
-
form.reset = originalReset;
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* createFormValidation hook for solidaria
|
|
3
|
+
*
|
|
4
|
+
* Connects form validation state to native HTML form validation.
|
|
5
|
+
* Handles the invalid event, form reset, and focus management.
|
|
6
|
+
*
|
|
7
|
+
* Port of react-aria's useFormValidation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { type Accessor, createEffect, onCleanup } from 'solid-js';
|
|
11
|
+
import {
|
|
12
|
+
type FormValidationState,
|
|
13
|
+
type ValidationResult,
|
|
14
|
+
} from '@proyecto-viviana/solid-stately';
|
|
15
|
+
import { setInteractionModality } from '../interactions/createInteractionModality';
|
|
16
|
+
|
|
17
|
+
// ============================================
|
|
18
|
+
// TYPES
|
|
19
|
+
// ============================================
|
|
20
|
+
|
|
21
|
+
export type ValidatableElement =
|
|
22
|
+
| HTMLInputElement
|
|
23
|
+
| HTMLTextAreaElement
|
|
24
|
+
| HTMLSelectElement;
|
|
25
|
+
|
|
26
|
+
export type ValidationBehavior = 'aria' | 'native';
|
|
27
|
+
|
|
28
|
+
export interface FormValidationProps {
|
|
29
|
+
/** Validation behavior: 'aria' for realtime, 'native' for on submit. */
|
|
30
|
+
validationBehavior?: ValidationBehavior;
|
|
31
|
+
/** Custom focus function to call on validation error. */
|
|
32
|
+
focus?: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================
|
|
36
|
+
// HELPERS
|
|
37
|
+
// ============================================
|
|
38
|
+
|
|
39
|
+
function getValidity(input: ValidatableElement): ValidityState {
|
|
40
|
+
// Create a snapshot of the validity state (the native object is live)
|
|
41
|
+
const validity = input.validity;
|
|
42
|
+
return {
|
|
43
|
+
badInput: validity.badInput,
|
|
44
|
+
customError: validity.customError,
|
|
45
|
+
patternMismatch: validity.patternMismatch,
|
|
46
|
+
rangeOverflow: validity.rangeOverflow,
|
|
47
|
+
rangeUnderflow: validity.rangeUnderflow,
|
|
48
|
+
stepMismatch: validity.stepMismatch,
|
|
49
|
+
tooLong: validity.tooLong,
|
|
50
|
+
tooShort: validity.tooShort,
|
|
51
|
+
typeMismatch: validity.typeMismatch,
|
|
52
|
+
valueMissing: validity.valueMissing,
|
|
53
|
+
valid: validity.valid,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getNativeValidity(input: ValidatableElement): ValidationResult {
|
|
58
|
+
return {
|
|
59
|
+
isInvalid: !input.validity.valid,
|
|
60
|
+
validationDetails: getValidity(input),
|
|
61
|
+
validationErrors: input.validationMessage ? [input.validationMessage] : [],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getFirstInvalidInput(form: HTMLFormElement): ValidatableElement | null {
|
|
66
|
+
for (let i = 0; i < form.elements.length; i++) {
|
|
67
|
+
const element = form.elements[i] as ValidatableElement;
|
|
68
|
+
if (!element.validity.valid) {
|
|
69
|
+
return element;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================
|
|
76
|
+
// HOOK
|
|
77
|
+
// ============================================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Connects form validation state to a native HTML form input.
|
|
81
|
+
*
|
|
82
|
+
* This hook:
|
|
83
|
+
* - Sets custom validity on the native input based on validation state
|
|
84
|
+
* - Handles the 'invalid' event to commit validation and focus the first invalid input
|
|
85
|
+
* - Handles form reset to clear validation state
|
|
86
|
+
* - Handles input change to commit validation
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```tsx
|
|
90
|
+
* function MyTextField(props) {
|
|
91
|
+
* let inputRef: HTMLInputElement | undefined;
|
|
92
|
+
*
|
|
93
|
+
* const validationState = createFormValidationState({
|
|
94
|
+
* value: props.value,
|
|
95
|
+
* validate: props.validate,
|
|
96
|
+
* validationBehavior: 'native',
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* createFormValidation(
|
|
100
|
+
* { validationBehavior: 'native' },
|
|
101
|
+
* validationState,
|
|
102
|
+
* () => inputRef
|
|
103
|
+
* );
|
|
104
|
+
*
|
|
105
|
+
* return (
|
|
106
|
+
* <input
|
|
107
|
+
* ref={inputRef}
|
|
108
|
+
* value={props.value}
|
|
109
|
+
* aria-invalid={validationState.displayValidation().isInvalid || undefined}
|
|
110
|
+
* />
|
|
111
|
+
* );
|
|
112
|
+
* }
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export function createFormValidation(
|
|
116
|
+
props: FormValidationProps,
|
|
117
|
+
state: FormValidationState,
|
|
118
|
+
ref: Accessor<ValidatableElement | undefined>
|
|
119
|
+
): void {
|
|
120
|
+
const validationBehavior = () => props.validationBehavior ?? 'aria';
|
|
121
|
+
const focus = () => props.focus;
|
|
122
|
+
|
|
123
|
+
// Track whether we should ignore form reset (for React-like programmatic resets)
|
|
124
|
+
let isIgnoredReset = false;
|
|
125
|
+
|
|
126
|
+
// Set custom validity on the native input
|
|
127
|
+
createEffect(() => {
|
|
128
|
+
const input = ref();
|
|
129
|
+
if (
|
|
130
|
+
validationBehavior() === 'native' &&
|
|
131
|
+
input &&
|
|
132
|
+
!input.disabled
|
|
133
|
+
) {
|
|
134
|
+
const realtimeValidation = state.realtimeValidation();
|
|
135
|
+
const errorMessage = realtimeValidation.isInvalid
|
|
136
|
+
? realtimeValidation.validationErrors.join(' ') || 'Invalid value.'
|
|
137
|
+
: '';
|
|
138
|
+
input.setCustomValidity(errorMessage);
|
|
139
|
+
|
|
140
|
+
// Prevent default tooltip for validation message
|
|
141
|
+
if (!input.hasAttribute('title')) {
|
|
142
|
+
input.title = '';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Update validation with native validity if not already invalid
|
|
146
|
+
if (!realtimeValidation.isInvalid) {
|
|
147
|
+
state.updateValidation(getNativeValidity(input));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Set up event listeners
|
|
153
|
+
createEffect(() => {
|
|
154
|
+
const input = ref();
|
|
155
|
+
if (!input) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const form = input.form;
|
|
160
|
+
|
|
161
|
+
// Handle invalid event
|
|
162
|
+
const onInvalid = (e: Event) => {
|
|
163
|
+
// Only commit validation if we are not already displaying one
|
|
164
|
+
if (!state.displayValidation().isInvalid) {
|
|
165
|
+
state.commitValidation();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Auto focus the first invalid input in a form
|
|
169
|
+
if (!e.defaultPrevented && form && getFirstInvalidInput(form) === input) {
|
|
170
|
+
const focusFn = focus();
|
|
171
|
+
if (focusFn) {
|
|
172
|
+
focusFn();
|
|
173
|
+
} else {
|
|
174
|
+
input.focus();
|
|
175
|
+
}
|
|
176
|
+
// Always show focus ring
|
|
177
|
+
setInteractionModality('keyboard');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Prevent default browser error UI
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Handle change event
|
|
185
|
+
const onChange = () => {
|
|
186
|
+
state.commitValidation();
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Handle form reset
|
|
190
|
+
const onReset = () => {
|
|
191
|
+
if (!isIgnoredReset) {
|
|
192
|
+
state.resetValidation();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Patch form.reset to detect programmatic resets
|
|
197
|
+
let originalReset: (() => void) | undefined;
|
|
198
|
+
if (form) {
|
|
199
|
+
originalReset = form.reset.bind(form);
|
|
200
|
+
form.reset = () => {
|
|
201
|
+
// Ignore programmatic resets outside user events
|
|
202
|
+
isIgnoredReset =
|
|
203
|
+
!window.event ||
|
|
204
|
+
(window.event.type === 'message' &&
|
|
205
|
+
window.event.target instanceof MessagePort);
|
|
206
|
+
originalReset?.();
|
|
207
|
+
isIgnoredReset = false;
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
input.addEventListener('invalid', onInvalid);
|
|
212
|
+
input.addEventListener('change', onChange);
|
|
213
|
+
form?.addEventListener('reset', onReset);
|
|
214
|
+
|
|
215
|
+
onCleanup(() => {
|
|
216
|
+
input.removeEventListener('invalid', onInvalid);
|
|
217
|
+
input.removeEventListener('change', onChange);
|
|
218
|
+
form?.removeEventListener('reset', onReset);
|
|
219
|
+
if (form && originalReset) {
|
|
220
|
+
form.reset = originalReset;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
}
|
package/src/form/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export {
|
|
2
|
-
createFormValidation,
|
|
3
|
-
type FormValidationProps,
|
|
4
|
-
type ValidatableElement,
|
|
5
|
-
type ValidationBehavior,
|
|
6
|
-
} from './createFormValidation';
|
|
7
|
-
|
|
8
|
-
export {
|
|
9
|
-
createFormReset,
|
|
10
|
-
type FormResetOptions,
|
|
11
|
-
} from './createFormReset';
|
|
1
|
+
export {
|
|
2
|
+
createFormValidation,
|
|
3
|
+
type FormValidationProps,
|
|
4
|
+
type ValidatableElement,
|
|
5
|
+
type ValidationBehavior,
|
|
6
|
+
} from './createFormValidation';
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
createFormReset,
|
|
10
|
+
type FormResetOptions,
|
|
11
|
+
} from './createFormReset';
|
package/src/grid/createGrid.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { createId } from '@proyecto-viviana/solid-stately';
|
|
|
9
9
|
import type { GridState, GridCollection, Key } from '@proyecto-viviana/solid-stately';
|
|
10
10
|
import type { GridProps, GridAria, KeyboardDelegate } from './types';
|
|
11
11
|
import { GridKeyboardDelegate } from './GridKeyboardDelegate';
|
|
12
|
+
import { useLocale } from '../i18n';
|
|
12
13
|
|
|
13
14
|
// Global map to store grid metadata for child components
|
|
14
15
|
const gridMap = new WeakMap<
|
|
@@ -37,6 +38,7 @@ export function createGrid<T extends object>(
|
|
|
37
38
|
ref: Accessor<HTMLElement | null>
|
|
38
39
|
): GridAria {
|
|
39
40
|
const id = createId(props().id);
|
|
41
|
+
const locale = useLocale();
|
|
40
42
|
|
|
41
43
|
// Track focused state
|
|
42
44
|
const [_isFocused, setIsFocused] = createSignal(false);
|
|
@@ -55,7 +57,7 @@ export function createGrid<T extends object>(
|
|
|
55
57
|
disabledKeys: s.disabledKeys,
|
|
56
58
|
ref,
|
|
57
59
|
focusMode: p.focusMode ?? 'row',
|
|
58
|
-
direction:
|
|
60
|
+
direction: locale().direction,
|
|
59
61
|
});
|
|
60
62
|
});
|
|
61
63
|
|
|
@@ -123,6 +123,22 @@ export function createGridList<T extends object, C extends GridCollection<T> = G
|
|
|
123
123
|
}
|
|
124
124
|
break;
|
|
125
125
|
}
|
|
126
|
+
case ' ':
|
|
127
|
+
case 'Space':
|
|
128
|
+
case 'Spacebar': {
|
|
129
|
+
if (focusedKey != null && s.selectionMode !== 'none' && !s.isDisabled(focusedKey)) {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
s.toggleSelection(focusedKey);
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case 'Enter': {
|
|
136
|
+
if (focusedKey != null && !s.isDisabled(focusedKey)) {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
p.onAction?.(focusedKey);
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
126
142
|
case 'Escape': {
|
|
127
143
|
if (s.selectionMode !== 'none') {
|
|
128
144
|
e.preventDefault();
|
|
@@ -106,7 +106,7 @@ export function createGridListItem<T extends object, C extends GridCollection<T>
|
|
|
106
106
|
p.onAction();
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
-
} else if (e.key === ' ') {
|
|
109
|
+
} else if (e.key === ' ' || e.key === 'Space' || e.key === 'Spacebar') {
|
|
110
110
|
// Space toggles selection
|
|
111
111
|
if (s.selectionMode !== 'none') {
|
|
112
112
|
e.preventDefault();
|