ropav 0.11.1 → 0.12.1
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/AGENTS.md +150 -0
- package/README.md +5 -1
- package/dist/components/autocomplete/autocomplete-clear-button.js +1 -1
- package/dist/components/autocomplete/autocomplete-root.js +3 -0
- package/dist/components/checkbox/checkbox-root.js +6 -1
- package/dist/components/combo-box/combo-box-root.js +1 -0
- package/dist/components/date-picker/date-picker-root.js +1 -0
- package/dist/components/date-range-picker/date-range-picker-root.js +1 -0
- package/dist/components/form/form-root.js +7 -1
- package/dist/components/radio/radio-root.js +4 -1
- package/dist/components/scroll-shadow/scroll-shadow-root.js +2 -10
- package/dist/components/scroll-shadow/use-scroll-shadow.d.ts +10 -0
- package/dist/components/scroll-shadow/use-scroll-shadow.js +34 -2
- package/dist/components/search-field/search-field-group.js +21 -1
- package/dist/components/select/index.d.ts +3 -2
- package/dist/components/select/index.js +2 -1
- package/dist/components/select/select-clear-button.d.ts +21 -0
- package/dist/components/select/select-clear-button.js +66 -0
- package/dist/components/select/select-root.d.ts +2 -1
- package/dist/components/select/select-root.js +16 -1
- package/dist/components/select/select.context.d.ts +11 -0
- package/dist/components/select/select.types.d.ts +3 -0
- package/dist/components/switch/switch-root.js +6 -1
- package/dist/components/table/table-row.js +3 -2
- package/dist/components/table/use-grid-keyboard.js +2 -3
- package/dist/components/table/use-table-collection.d.ts +0 -11
- package/dist/components/table/use-table-collection.js +1 -17
- package/dist/components/tabs/tabs-root.js +5 -1
- package/dist/components/tabs/tabs.types.d.ts +6 -0
- package/dist/components/toast/index.d.ts +1 -1
- package/dist/components/toast/index.js +3 -3
- package/dist/components/toast/toast-default-content.js +26 -4
- package/dist/components/toast/toast-indicator.js +8 -2
- package/dist/components/toast/toast-provider.js +53 -5
- package/dist/components/toast/toast-queue.d.ts +60 -5
- package/dist/components/toast/toast-queue.js +85 -27
- package/dist/components/toast/toast-root.js +67 -14
- package/dist/components/toast/toast.constants.d.ts +17 -1
- package/dist/components/toast/toast.constants.js +18 -2
- package/dist/components/toast/toast.context.d.ts +6 -0
- package/dist/components/toast/toast.types.d.ts +19 -0
- package/dist/composables/use-checkbox-group-state.js +2 -0
- package/dist/composables/use-color-field-state.js +1 -0
- package/dist/composables/use-combo-box-state.d.ts +2 -0
- package/dist/composables/use-combo-box-state.js +3 -1
- package/dist/composables/use-date-field-state.js +1 -0
- package/dist/composables/use-date-picker-state.d.ts +2 -0
- package/dist/composables/use-date-picker-state.js +1 -0
- package/dist/composables/use-date-range-picker-state.d.ts +2 -0
- package/dist/composables/use-date-range-picker-state.js +2 -0
- package/dist/composables/use-description.js +1 -0
- package/dist/composables/use-form-validation-state.d.ts +48 -5
- package/dist/composables/use-form-validation-state.js +76 -7
- package/dist/composables/use-list-keyboard.d.ts +1 -1
- package/dist/composables/use-list-keyboard.js +4 -5
- package/dist/composables/use-measured-height.d.ts +18 -10
- package/dist/composables/use-measured-height.js +53 -15
- package/dist/composables/use-number-field-state.js +1 -0
- package/dist/composables/use-overlay-position.js +7 -4
- package/dist/composables/use-radio-group-state.js +2 -0
- package/dist/composables/use-select-state.d.ts +10 -0
- package/dist/composables/use-select-state.js +7 -0
- package/dist/composables/use-select.d.ts +7 -0
- package/dist/composables/use-select.js +6 -0
- package/dist/composables/use-text-field.js +1 -0
- package/dist/composables/use-toast-region.d.ts +18 -4
- package/dist/composables/use-toast-region.js +128 -6
- package/dist/composables/use-toast.d.ts +1 -1
- package/dist/composables/use-toast.js +4 -3
- package/dist/composables/use-toolbar.js +3 -1
- package/dist/composables/use-typeahead.js +3 -0
- package/dist/index.js +6 -5
- package/dist/ropav.min.css +1 -1
- package/dist/utils/focus.d.ts +13 -0
- package/dist/utils/focus.js +18 -1
- package/dist/version.js +1 -1
- package/package.json +5 -4
|
@@ -20,6 +20,12 @@ var CUSTOM_VALIDITY_STATE = Object.freeze({
|
|
|
20
20
|
customError: true,
|
|
21
21
|
valid: false
|
|
22
22
|
});
|
|
23
|
+
/** A required field with nothing in it. What the browser reports under `"native"`. */
|
|
24
|
+
var MISSING_VALIDITY_STATE = Object.freeze({
|
|
25
|
+
...VALID_VALIDITY_STATE,
|
|
26
|
+
valid: false,
|
|
27
|
+
valueMissing: true
|
|
28
|
+
});
|
|
23
29
|
var DEFAULT_VALIDATION_RESULT = Object.freeze({
|
|
24
30
|
isInvalid: false,
|
|
25
31
|
validationDetails: VALID_VALIDITY_STATE,
|
|
@@ -68,6 +74,41 @@ var getNativeValidation = (element) => {
|
|
|
68
74
|
validationErrors: element.validationMessage ? [element.validationMessage] : []
|
|
69
75
|
};
|
|
70
76
|
};
|
|
77
|
+
var missingValueMessages = /* @__PURE__ */ new Map();
|
|
78
|
+
/**
|
|
79
|
+
* What the browser would say about an empty control of this kind.
|
|
80
|
+
*
|
|
81
|
+
* Read off a detached probe rather than translated here. The platform already holds the sentence,
|
|
82
|
+
* in the *browser's* locale — which is the locale a validation message belongs in, since it sits
|
|
83
|
+
* beside the browser's own — and it is the one the same field would report under `"native"`.
|
|
84
|
+
*
|
|
85
|
+
* Empty on a server, where the field cannot have been revealed yet and so has nothing to say.
|
|
86
|
+
*/
|
|
87
|
+
var missingValueMessage = (kind) => {
|
|
88
|
+
const cached = missingValueMessages.get(kind);
|
|
89
|
+
if (cached !== void 0) return cached;
|
|
90
|
+
if (typeof document === "undefined") return "";
|
|
91
|
+
let probe;
|
|
92
|
+
if (kind === "select") probe = document.createElement("select");
|
|
93
|
+
else {
|
|
94
|
+
const input = document.createElement("input");
|
|
95
|
+
if (kind !== "text") {
|
|
96
|
+
input.type = kind;
|
|
97
|
+
input.name = "probe";
|
|
98
|
+
}
|
|
99
|
+
probe = input;
|
|
100
|
+
}
|
|
101
|
+
probe.required = true;
|
|
102
|
+
const message = probe.validationMessage;
|
|
103
|
+
missingValueMessages.set(kind, message);
|
|
104
|
+
return message;
|
|
105
|
+
};
|
|
106
|
+
/** Whether a value counts as nothing. Covers every shape a field holds when it is empty. */
|
|
107
|
+
var isValueMissing = (value) => {
|
|
108
|
+
if (value == null || value === "" || value === false) return true;
|
|
109
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
110
|
+
return typeof value === "number" && Number.isNaN(value);
|
|
111
|
+
};
|
|
71
112
|
/**
|
|
72
113
|
* React splits this in two — `FormValidationContext` in react-stately for the errors,
|
|
73
114
|
* `FormContext` in react-aria-components for the behaviour — only because the two live in
|
|
@@ -109,15 +150,19 @@ var isEqualValidation = (a, b) => {
|
|
|
109
150
|
* Decide what a field's validation currently says, ported from React Aria's
|
|
110
151
|
* `packages/react-stately/src/form/useFormValidationState.ts` (react-stately 3.49.0).
|
|
111
152
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
153
|
+
* Holds no element of its own — `useFormValidation` is what connects this to a real input.
|
|
154
|
+
* Five sources feed it, in a fixed order of precedence:
|
|
114
155
|
*
|
|
115
156
|
* ```
|
|
116
|
-
* realtime = controlled ?? server ?? client ?? builtin ?? valid
|
|
157
|
+
* realtime = controlled ?? server ?? client ?? required ?? builtin ?? valid
|
|
117
158
|
* display(native) = controlled ?? server ?? committed
|
|
118
|
-
* display(aria) = controlled ?? server ?? client ?? builtin ?? committed
|
|
159
|
+
* display(aria) = controlled ?? server ?? client ?? required* ?? builtin ?? committed
|
|
119
160
|
* ```
|
|
120
161
|
*
|
|
162
|
+
* `required` is the missing-value rule, and it is there for `"aria"` alone: under `"native"` the
|
|
163
|
+
* control carries the attribute and the browser reaches the verdict itself. The `*` is the reveal
|
|
164
|
+
* — it waits for a commit or a submit, so a form does not arrive with every required field red.
|
|
165
|
+
*
|
|
121
166
|
* `realtimeValidation` is what the field pushes onto the input through `setCustomValidity`,
|
|
122
167
|
* so the browser blocks submission the moment the value stops being acceptable.
|
|
123
168
|
* `displayValidation` is what the user reads, and under `"native"` it moves only on commit —
|
|
@@ -157,6 +202,17 @@ var useFormValidationState = (options) => {
|
|
|
157
202
|
const result = toValue(options.builtinValidation);
|
|
158
203
|
return result && !result.validationDetails.valid ? result : null;
|
|
159
204
|
});
|
|
205
|
+
const requiredError = computed(() => {
|
|
206
|
+
if (validationBehavior.value !== "aria") return null;
|
|
207
|
+
if (!toValue(options.isRequired)) return null;
|
|
208
|
+
if (!(options.isEmpty ?? isValueMissing)(toValue(options.value))) return null;
|
|
209
|
+
const message = missingValueMessage(options.requiredControl ?? "text");
|
|
210
|
+
return {
|
|
211
|
+
isInvalid: true,
|
|
212
|
+
validationDetails: MISSING_VALIDITY_STATE,
|
|
213
|
+
validationErrors: message ? [message] : []
|
|
214
|
+
};
|
|
215
|
+
});
|
|
160
216
|
const serverErrors = computed(() => form?.validationErrors.value ?? {});
|
|
161
217
|
const serverErrorMessages = computed(() => {
|
|
162
218
|
const name = toValue(options.name);
|
|
@@ -172,11 +228,22 @@ var useFormValidationState = (options) => {
|
|
|
172
228
|
const nextNative = shallowRef(DEFAULT_VALIDATION_RESULT);
|
|
173
229
|
/** What a commit has already revealed. */
|
|
174
230
|
const committed = shallowRef(DEFAULT_VALIDATION_RESULT);
|
|
231
|
+
/**
|
|
232
|
+
* Whether the field has been asked to prove itself yet.
|
|
233
|
+
*
|
|
234
|
+
* A required field is empty from its first render, and `"aria"` shows a client error the moment
|
|
235
|
+
* it appears — so without this a form would arrive with every required field already red. It
|
|
236
|
+
* gates the missing-value verdict alone; a `validate` the caller wrote still reports as it types.
|
|
237
|
+
*/
|
|
238
|
+
const isRevealed = shallowRef(false);
|
|
239
|
+
watch(() => form?.submitCount?.value ?? 0, (count) => {
|
|
240
|
+
if (count > 0) isRevealed.value = true;
|
|
241
|
+
});
|
|
175
242
|
let isCommitQueued = false;
|
|
176
|
-
const realtimeValidation = computed(() => controlledError.value ?? serverError.value ?? clientError.value ?? builtinValidation.value ?? DEFAULT_VALIDATION_RESULT);
|
|
243
|
+
const realtimeValidation = computed(() => controlledError.value ?? serverError.value ?? clientError.value ?? requiredError.value ?? builtinValidation.value ?? DEFAULT_VALIDATION_RESULT);
|
|
177
244
|
const displayValidation = computed(() => {
|
|
178
245
|
if (validationBehavior.value === "native") return controlledError.value ?? serverError.value ?? committed.value;
|
|
179
|
-
return controlledError.value ?? serverError.value ?? clientError.value ?? builtinValidation.value ?? committed.value;
|
|
246
|
+
return controlledError.value ?? serverError.value ?? clientError.value ?? (isRevealed.value ? requiredError.value : null) ?? builtinValidation.value ?? committed.value;
|
|
180
247
|
});
|
|
181
248
|
const setCommitted = (result) => {
|
|
182
249
|
if (!isEqualValidation(result, committed.value)) committed.value = result;
|
|
@@ -184,6 +251,7 @@ var useFormValidationState = (options) => {
|
|
|
184
251
|
return {
|
|
185
252
|
commitValidation: () => {
|
|
186
253
|
isServerErrorCleared.value = true;
|
|
254
|
+
isRevealed.value = true;
|
|
187
255
|
if (validationBehavior.value !== "native" || isCommitQueued) return;
|
|
188
256
|
isCommitQueued = true;
|
|
189
257
|
nextTick(() => {
|
|
@@ -196,6 +264,7 @@ var useFormValidationState = (options) => {
|
|
|
196
264
|
realtimeValidation,
|
|
197
265
|
resetValidation: () => {
|
|
198
266
|
setCommitted(DEFAULT_VALIDATION_RESULT);
|
|
267
|
+
isRevealed.value = false;
|
|
199
268
|
isCommitQueued = false;
|
|
200
269
|
isServerErrorCleared.value = true;
|
|
201
270
|
},
|
|
@@ -210,4 +279,4 @@ var useFormValidationState = (options) => {
|
|
|
210
279
|
};
|
|
211
280
|
};
|
|
212
281
|
//#endregion
|
|
213
|
-
export { CUSTOM_VALIDITY_STATE, DEFAULT_VALIDATION_RESULT, VALID_VALIDITY_STATE, getNativeValidation, isEqualValidation, mergeValidation, provideFormContext, useFormContext, useFormValidationState };
|
|
282
|
+
export { CUSTOM_VALIDITY_STATE, DEFAULT_VALIDATION_RESULT, MISSING_VALIDITY_STATE, VALID_VALIDITY_STATE, getNativeValidation, isEqualValidation, isValueMissing, mergeValidation, missingValueMessage, provideFormContext, useFormContext, useFormValidationState };
|
|
@@ -50,7 +50,7 @@ export interface UseListKeyboardOptions {
|
|
|
50
50
|
disallowSelectAll?: MaybeRefOrGetter<boolean | undefined>;
|
|
51
51
|
/** @default "clearSelection" */
|
|
52
52
|
escapeKeyBehavior?: MaybeRefOrGetter<"clearSelection" | "none" | undefined>;
|
|
53
|
-
/** Called when an item is activated
|
|
53
|
+
/** Called when an item is activated: Enter, or a press where there is no selection to make. */
|
|
54
54
|
onAction?: (key: CollectionKey) => void;
|
|
55
55
|
/**
|
|
56
56
|
* Whether focus over the collection is nominal rather than real.
|
|
@@ -278,14 +278,13 @@ var useListKeyboard = (options) => {
|
|
|
278
278
|
return;
|
|
279
279
|
}
|
|
280
280
|
case "Enter":
|
|
281
|
-
if (focused == null || toValue(options.disallowActivation)) return;
|
|
282
|
-
options.onAction?.(focused);
|
|
283
|
-
event.preventDefault();
|
|
284
|
-
return;
|
|
285
281
|
case " ":
|
|
286
282
|
if (focused == null || toValue(options.disallowActivation)) return;
|
|
287
283
|
if (selection.selectionMode.value === "none") options.onAction?.(focused);
|
|
288
|
-
else
|
|
284
|
+
else {
|
|
285
|
+
selection.select(focused, { isShiftPressed: event.shiftKey });
|
|
286
|
+
if (event.key === "Enter") options.onAction?.(focused);
|
|
287
|
+
}
|
|
289
288
|
event.preventDefault();
|
|
290
289
|
return;
|
|
291
290
|
default: return;
|
|
@@ -10,16 +10,24 @@ export interface UseMeasuredHeightReturn {
|
|
|
10
10
|
* the content *wants*: a toast that is not frontmost is clipped to the front one's height, so its
|
|
11
11
|
* own border box has already been overwritten by the very number this feeds.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* That overwriting is also why the height is unset before every reading. The value this produces
|
|
14
|
+
* is what decides the element's own `height`, so reading it back off a sized box would return the
|
|
15
|
+
* previous answer for ever — a toast whose content is replaced in place would keep the height of
|
|
16
|
+
* the content it no longer holds.
|
|
14
17
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* Three narrowings against the usual shape, each measured rather than assumed:
|
|
19
|
+
*
|
|
20
|
+
* 1. It measures directly on attach and then lets the observers take over, rather than relying on
|
|
21
|
+
* a first notification alone. jsdom *has* a `ResizeObserver` constructor but it never notifies
|
|
22
|
+
* anything, so an observer-only reading is permanently absent there — and this value drives an
|
|
23
|
+
* inline custom property that decides layout, which is exactly the kind of thing a fast suite
|
|
24
|
+
* should be able to assert. In a real browser the two readings agree, and the repeat is
|
|
25
|
+
* dropped, so the only difference is that the first one lands a frame earlier.
|
|
26
|
+
* 2. Only the inline axis of a resize counts. The block axis is transitioned by the caller, and a
|
|
27
|
+
* reading forces layout twice to unset and restore the height — so reacting to it would
|
|
28
|
+
* re-measure on every frame of that animation and retarget the very transition it is watching.
|
|
29
|
+
* 3. Content is watched separately, because a box whose height is already forced does not resize
|
|
30
|
+
* when what is inside it changes. Attributes are left out of that watch: the caller writes its
|
|
31
|
+
* state onto this element, and none of it changes what the content wants.
|
|
24
32
|
*/
|
|
25
33
|
export declare const useMeasuredHeight: (elementRef: MaybeRefOrGetter<HTMLElement | null | undefined>) => UseMeasuredHeightReturn;
|
|
@@ -7,36 +7,74 @@ import { computed, onScopeDispose, shallowRef, toValue, watch } from "vue";
|
|
|
7
7
|
* the content *wants*: a toast that is not frontmost is clipped to the front one's height, so its
|
|
8
8
|
* own border box has already been overwritten by the very number this feeds.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* That overwriting is also why the height is unset before every reading. The value this produces
|
|
11
|
+
* is what decides the element's own `height`, so reading it back off a sized box would return the
|
|
12
|
+
* previous answer for ever — a toast whose content is replaced in place would keep the height of
|
|
13
|
+
* the content it no longer holds.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
15
|
+
* Three narrowings against the usual shape, each measured rather than assumed:
|
|
16
|
+
*
|
|
17
|
+
* 1. It measures directly on attach and then lets the observers take over, rather than relying on
|
|
18
|
+
* a first notification alone. jsdom *has* a `ResizeObserver` constructor but it never notifies
|
|
19
|
+
* anything, so an observer-only reading is permanently absent there — and this value drives an
|
|
20
|
+
* inline custom property that decides layout, which is exactly the kind of thing a fast suite
|
|
21
|
+
* should be able to assert. In a real browser the two readings agree, and the repeat is
|
|
22
|
+
* dropped, so the only difference is that the first one lands a frame earlier.
|
|
23
|
+
* 2. Only the inline axis of a resize counts. The block axis is transitioned by the caller, and a
|
|
24
|
+
* reading forces layout twice to unset and restore the height — so reacting to it would
|
|
25
|
+
* re-measure on every frame of that animation and retarget the very transition it is watching.
|
|
26
|
+
* 3. Content is watched separately, because a box whose height is already forced does not resize
|
|
27
|
+
* when what is inside it changes. Attributes are left out of that watch: the caller writes its
|
|
28
|
+
* state onto this element, and none of it changes what the content wants.
|
|
21
29
|
*/
|
|
22
30
|
var useMeasuredHeight = (elementRef) => {
|
|
23
31
|
const height = shallowRef(void 0);
|
|
24
|
-
let
|
|
32
|
+
let resizeObserver;
|
|
33
|
+
let contentObserver;
|
|
34
|
+
let frame;
|
|
25
35
|
const detach = () => {
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
resizeObserver?.disconnect();
|
|
37
|
+
resizeObserver = void 0;
|
|
38
|
+
contentObserver?.disconnect();
|
|
39
|
+
contentObserver = void 0;
|
|
40
|
+
if (frame !== void 0 && typeof cancelAnimationFrame === "function") cancelAnimationFrame(frame);
|
|
41
|
+
frame = void 0;
|
|
28
42
|
};
|
|
29
43
|
watch(() => toValue(elementRef) ?? null, (element) => {
|
|
30
44
|
detach();
|
|
31
45
|
if (!element) return;
|
|
32
46
|
const measure = () => {
|
|
47
|
+
const forced = element.style.height;
|
|
48
|
+
element.style.height = "auto";
|
|
33
49
|
const next = element.scrollHeight;
|
|
50
|
+
element.style.height = forced;
|
|
34
51
|
if (height.value !== next) height.value = next;
|
|
35
52
|
};
|
|
53
|
+
const scheduleMeasure = () => {
|
|
54
|
+
if (frame !== void 0 || typeof requestAnimationFrame !== "function") return;
|
|
55
|
+
frame = requestAnimationFrame(() => {
|
|
56
|
+
frame = void 0;
|
|
57
|
+
measure();
|
|
58
|
+
});
|
|
59
|
+
};
|
|
36
60
|
measure();
|
|
61
|
+
if (typeof MutationObserver === "function") {
|
|
62
|
+
contentObserver = new MutationObserver(scheduleMeasure);
|
|
63
|
+
contentObserver.observe(element, {
|
|
64
|
+
characterData: true,
|
|
65
|
+
childList: true,
|
|
66
|
+
subtree: true
|
|
67
|
+
});
|
|
68
|
+
}
|
|
37
69
|
if (typeof ResizeObserver === "undefined") return;
|
|
38
|
-
|
|
39
|
-
|
|
70
|
+
let lastWidth = element.getBoundingClientRect().width;
|
|
71
|
+
resizeObserver = new ResizeObserver((entries) => {
|
|
72
|
+
const width = entries[0]?.contentRect.width;
|
|
73
|
+
if (width === void 0 || Math.abs(width - lastWidth) < .5) return;
|
|
74
|
+
lastWidth = width;
|
|
75
|
+
scheduleMeasure();
|
|
76
|
+
});
|
|
77
|
+
resizeObserver.observe(element);
|
|
40
78
|
}, {
|
|
41
79
|
flush: "post",
|
|
42
80
|
immediate: true
|
|
@@ -87,6 +87,7 @@ var useNumberFieldState = (options = {}) => {
|
|
|
87
87
|
inputValue.value = format(numberValueState.value);
|
|
88
88
|
const validation = useFormValidationState({
|
|
89
89
|
isInvalid: () => toValue(options.isInvalid),
|
|
90
|
+
isRequired: () => toValue(options.isRequired),
|
|
90
91
|
name: () => toValue(options.name),
|
|
91
92
|
validate: () => toValue(options.validate),
|
|
92
93
|
validationBehavior: () => toValue(options.validationBehavior),
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { calculatePosition, getRect, translateRTL } from "../utils/position.js";
|
|
2
2
|
import { computed, onScopeDispose, shallowRef, toValue, watch } from "vue";
|
|
3
3
|
//#region src/composables/use-overlay-position.ts
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Overlays sit above the page. The value lives in the stylesheet so the modal-level surfaces,
|
|
6
|
+
* which can only declare theirs in CSS, resolve to the same level as this inline one.
|
|
7
|
+
*/
|
|
8
|
+
var OVERLAY_Z_INDEX = "var(--rp-z-index-overlay)";
|
|
6
9
|
/** Whether an element or one of its descendants holds focus. */
|
|
7
10
|
var isFocusWithin = (element) => {
|
|
8
11
|
const active = document.activeElement;
|
|
@@ -212,12 +215,12 @@ var useOverlayPosition = (options) => {
|
|
|
212
215
|
"max-height": "100vh",
|
|
213
216
|
position: "fixed",
|
|
214
217
|
top: "0px",
|
|
215
|
-
"z-index":
|
|
218
|
+
"z-index": OVERLAY_Z_INDEX
|
|
216
219
|
};
|
|
217
220
|
const style = {
|
|
218
221
|
"max-height": `${resolved.maxHeight}px`,
|
|
219
222
|
position: "absolute",
|
|
220
|
-
"z-index":
|
|
223
|
+
"z-index": OVERLAY_Z_INDEX
|
|
221
224
|
};
|
|
222
225
|
for (const [key, value] of Object.entries(resolved.position)) style[key] = `${value}px`;
|
|
223
226
|
style["--trigger-anchor-point"] = `${resolved.triggerAnchorPoint.x}px ${resolved.triggerAnchorPoint.y}px`;
|
|
@@ -38,7 +38,9 @@ var useRadioGroupState = (options = {}) => {
|
|
|
38
38
|
const lastFocused = shallowRef(null);
|
|
39
39
|
const validation = useFormValidationState({
|
|
40
40
|
isInvalid: () => toValue(options.isInvalid),
|
|
41
|
+
isRequired,
|
|
41
42
|
name: () => toValue(options.name),
|
|
43
|
+
requiredControl: "radio",
|
|
42
44
|
validate: () => toValue(options.validate),
|
|
43
45
|
validationBehavior: () => toValue(options.validationBehavior),
|
|
44
46
|
value: () => selectedValue.value
|
|
@@ -40,6 +40,8 @@ export interface UseSelectStateOptions<T> {
|
|
|
40
40
|
/** @default "all" */
|
|
41
41
|
disabledBehavior?: MaybeRefOrGetter<DisabledBehavior | undefined>;
|
|
42
42
|
isInvalid?: MaybeRefOrGetter<boolean | undefined>;
|
|
43
|
+
/** Whether the field has to hold a value. Enforced by the validation state under `"aria"`. */
|
|
44
|
+
isRequired?: MaybeRefOrGetter<boolean | undefined>;
|
|
43
45
|
validate?: MaybeRefOrGetter<ValidationFunction<SelectedValue> | undefined>;
|
|
44
46
|
validationBehavior?: MaybeRefOrGetter<ValidationBehavior | undefined>;
|
|
45
47
|
name?: MaybeRefOrGetter<string | undefined>;
|
|
@@ -61,6 +63,14 @@ export interface UseSelectStateReturn<T> extends MenuTriggerState, FormValidatio
|
|
|
61
63
|
/** The value a form reset goes back to. */
|
|
62
64
|
defaultValue: ComputedRef<SelectedValue>;
|
|
63
65
|
setValue: (value: SelectedValue) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Empties the selection.
|
|
68
|
+
*
|
|
69
|
+
* Not `selection.clearSelection()`: a single select sets `disallowEmptySelection`, which is
|
|
70
|
+
* what stops a press on the chosen option from un-choosing it — and which makes that call a
|
|
71
|
+
* no-op in the one mode most selects are in.
|
|
72
|
+
*/
|
|
73
|
+
clearValue: () => void;
|
|
64
74
|
/** The first chosen key, which is the whole value when single. */
|
|
65
75
|
selectedKey: ComputedRef<CollectionKey | null>;
|
|
66
76
|
selectedItems: ComputedRef<SelectedItem<T>[]>;
|
|
@@ -91,7 +91,9 @@ var useSelectState = (options) => {
|
|
|
91
91
|
};
|
|
92
92
|
const validation = useFormValidationState({
|
|
93
93
|
isInvalid: options.isInvalid,
|
|
94
|
+
isRequired: options.isRequired,
|
|
94
95
|
name: options.name,
|
|
96
|
+
requiredControl: "select",
|
|
95
97
|
validate: options.validate,
|
|
96
98
|
validationBehavior: options.validationBehavior,
|
|
97
99
|
value: () => {
|
|
@@ -99,6 +101,10 @@ var useSelectState = (options) => {
|
|
|
99
101
|
return Array.isArray(current) && current.length === 0 ? null : current;
|
|
100
102
|
}
|
|
101
103
|
});
|
|
104
|
+
const clearValue = () => {
|
|
105
|
+
setValue(null);
|
|
106
|
+
validation.commitValidation();
|
|
107
|
+
};
|
|
102
108
|
const selection = useSelectionManager({
|
|
103
109
|
collection,
|
|
104
110
|
disabledBehavior: options.disabledBehavior,
|
|
@@ -132,6 +138,7 @@ var useSelectState = (options) => {
|
|
|
132
138
|
const canOpen = () => collection.size.value !== 0 || Boolean(toValue(options.allowsEmptyCollection));
|
|
133
139
|
return {
|
|
134
140
|
...validation,
|
|
141
|
+
clearValue,
|
|
135
142
|
close: trigger.close,
|
|
136
143
|
collection,
|
|
137
144
|
defaultValue,
|
|
@@ -10,6 +10,13 @@ export interface UseSelectOptions {
|
|
|
10
10
|
ariaLabelledby?: MaybeRefOrGetter<string | undefined>;
|
|
11
11
|
ariaDescribedby?: MaybeRefOrGetter<string | undefined>;
|
|
12
12
|
onFocusChange?: (isFocused: boolean) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Whether a clear button is composed into the select, which is what opens the trigger's
|
|
15
|
+
* Backspace/Delete shortcut. Read at event time, so registering one never costs a render.
|
|
16
|
+
*/
|
|
17
|
+
hasClearButton?: () => boolean;
|
|
18
|
+
/** Empties the selection. Called by the shortcut above. */
|
|
19
|
+
onClear?: () => void;
|
|
13
20
|
}
|
|
14
21
|
/** Attributes the trigger element renders, beside its own class and `data-slot`. */
|
|
15
22
|
export type SelectTriggerAttributes = Record<string, string | number | boolean | undefined>;
|
|
@@ -83,6 +83,12 @@ var useSelect = (options, state) => {
|
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
if ((event.key === "Backspace" || event.key === "Delete") && !state.isOpen.value && options.hasClearButton?.() && state.selectedItems.value.length > 0) {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
state.clearValue();
|
|
89
|
+
options.onClear?.();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
86
92
|
typeahead.onKeydown(event);
|
|
87
93
|
if (event.defaultPrevented) return;
|
|
88
94
|
trigger.responder.handlers.value.onKeydown?.(event);
|
|
@@ -47,6 +47,7 @@ var useTextField = (options = {}) => {
|
|
|
47
47
|
const isRequired = computed(() => Boolean(toValue(options.isRequired)));
|
|
48
48
|
const validation = options.validationState ?? useFormValidationState({
|
|
49
49
|
isInvalid: () => toValue(options.isInvalid),
|
|
50
|
+
isRequired: () => toValue(options.isRequired),
|
|
50
51
|
name: () => toValue(options.name),
|
|
51
52
|
validate: () => toValue(options.validate),
|
|
52
53
|
validationBehavior: () => toValue(options.validationBehavior),
|
|
@@ -9,6 +9,10 @@ export interface UseToastRegionOptions {
|
|
|
9
9
|
ariaLabel?: MaybeRefOrGetter<string | undefined>;
|
|
10
10
|
/** The region element, which is also what the toasts are looked up inside. */
|
|
11
11
|
elementRef: MaybeRefOrGetter<HTMLElement | null | undefined>;
|
|
12
|
+
/** Key combination that moves focus to the region. An empty list turns it off. */
|
|
13
|
+
hotkey?: MaybeRefOrGetter<readonly string[] | undefined>;
|
|
14
|
+
/** Forces the stack open regardless of pointer or focus. */
|
|
15
|
+
isExpanded?: MaybeRefOrGetter<boolean | undefined>;
|
|
12
16
|
/** Stops every visible toast's clock. */
|
|
13
17
|
onPauseAll: () => void;
|
|
14
18
|
/** Restarts every visible toast's clock. */
|
|
@@ -22,17 +26,21 @@ export interface ToastRegionAttrs {
|
|
|
22
26
|
tabindex: -1;
|
|
23
27
|
}
|
|
24
28
|
export interface UseToastRegionReturn {
|
|
29
|
+
/** Whether the stack should be opened out, so every toast shows at its own height. */
|
|
30
|
+
isExpanded: ComputedRef<boolean>;
|
|
25
31
|
onFocusin: (event: FocusEvent) => void;
|
|
26
32
|
onFocusout: (event: FocusEvent) => void;
|
|
27
33
|
onPointerenter: (event: PointerEvent) => void;
|
|
28
34
|
onPointerleave: () => void;
|
|
35
|
+
/** Reasserts hover when the stack moves under a cursor that has not itself moved. */
|
|
36
|
+
onPointermove: (event: PointerEvent) => void;
|
|
29
37
|
regionAttrs: ComputedRef<ToastRegionAttrs>;
|
|
30
38
|
}
|
|
31
39
|
/**
|
|
32
40
|
* The behaviour and accessibility wiring of the toast region, ported from react-aria's
|
|
33
41
|
* `useToastRegion`.
|
|
34
42
|
*
|
|
35
|
-
*
|
|
43
|
+
* Four jobs, and they are only in one composable because they share the same state:
|
|
36
44
|
*
|
|
37
45
|
* 1. **Naming.** A landmark region labelled with how many notifications it holds.
|
|
38
46
|
* 2. **Pausing.** Hover *or* focus anywhere inside stops every visible toast's clock, so a toast
|
|
@@ -41,13 +49,19 @@ export interface UseToastRegionReturn {
|
|
|
41
49
|
* rather than dropping it on `<body>` — except under a pointer, where focus is pushed back out
|
|
42
50
|
* of the region, because a pointer user who is no longer hovering would otherwise hold every
|
|
43
51
|
* remaining clock paused by the focus they did not ask for.
|
|
52
|
+
* 4. **Reaching it.** A key combination that moves focus to the region from anywhere, because a
|
|
53
|
+
* toast is announced where the user is not and tabbing to it means tabbing past everything
|
|
54
|
+
* between. The listener is on the document rather than the region: the region is only in the
|
|
55
|
+
* DOM while it holds a toast, so a listener of its own could never be the thing that reaches
|
|
56
|
+
* it first.
|
|
44
57
|
*
|
|
45
|
-
* One narrowing, recorded rather than hidden:
|
|
58
|
+
* One narrowing, recorded rather than hidden: react-aria also registers the region with a
|
|
46
59
|
* document-level landmark manager, which is what makes F6 cycle between landmarks. Nothing else
|
|
47
60
|
* in react-aria registers one, so with a single registrant F6 has nowhere to go — the rendered
|
|
48
|
-
* DOM is identical either way, and toasts are still reachable by Tab.
|
|
61
|
+
* DOM is identical either way, and toasts are still reachable by Tab. That is an argument about
|
|
62
|
+
* cycling between landmarks and not about reaching this one, which is why job 4 stands beside it.
|
|
49
63
|
*
|
|
50
|
-
* One simplification:
|
|
64
|
+
* One simplification: react-aria runs focus-within and raw focus as two channels, because its
|
|
51
65
|
* focus-within fires once on entry and it needs every change. `focusin` and `focusout` bubble and
|
|
52
66
|
* fire on every change already, so one pair answers both questions.
|
|
53
67
|
*/
|