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.
Files changed (77) hide show
  1. package/AGENTS.md +150 -0
  2. package/README.md +5 -1
  3. package/dist/components/autocomplete/autocomplete-clear-button.js +1 -1
  4. package/dist/components/autocomplete/autocomplete-root.js +3 -0
  5. package/dist/components/checkbox/checkbox-root.js +6 -1
  6. package/dist/components/combo-box/combo-box-root.js +1 -0
  7. package/dist/components/date-picker/date-picker-root.js +1 -0
  8. package/dist/components/date-range-picker/date-range-picker-root.js +1 -0
  9. package/dist/components/form/form-root.js +7 -1
  10. package/dist/components/radio/radio-root.js +4 -1
  11. package/dist/components/scroll-shadow/scroll-shadow-root.js +2 -10
  12. package/dist/components/scroll-shadow/use-scroll-shadow.d.ts +10 -0
  13. package/dist/components/scroll-shadow/use-scroll-shadow.js +34 -2
  14. package/dist/components/search-field/search-field-group.js +21 -1
  15. package/dist/components/select/index.d.ts +3 -2
  16. package/dist/components/select/index.js +2 -1
  17. package/dist/components/select/select-clear-button.d.ts +21 -0
  18. package/dist/components/select/select-clear-button.js +66 -0
  19. package/dist/components/select/select-root.d.ts +2 -1
  20. package/dist/components/select/select-root.js +16 -1
  21. package/dist/components/select/select.context.d.ts +11 -0
  22. package/dist/components/select/select.types.d.ts +3 -0
  23. package/dist/components/switch/switch-root.js +6 -1
  24. package/dist/components/table/table-row.js +3 -2
  25. package/dist/components/table/use-grid-keyboard.js +2 -3
  26. package/dist/components/table/use-table-collection.d.ts +0 -11
  27. package/dist/components/table/use-table-collection.js +1 -17
  28. package/dist/components/tabs/tabs-root.js +5 -1
  29. package/dist/components/tabs/tabs.types.d.ts +6 -0
  30. package/dist/components/toast/index.d.ts +1 -1
  31. package/dist/components/toast/index.js +3 -3
  32. package/dist/components/toast/toast-default-content.js +26 -4
  33. package/dist/components/toast/toast-indicator.js +8 -2
  34. package/dist/components/toast/toast-provider.js +53 -5
  35. package/dist/components/toast/toast-queue.d.ts +60 -5
  36. package/dist/components/toast/toast-queue.js +85 -27
  37. package/dist/components/toast/toast-root.js +67 -14
  38. package/dist/components/toast/toast.constants.d.ts +17 -1
  39. package/dist/components/toast/toast.constants.js +18 -2
  40. package/dist/components/toast/toast.context.d.ts +6 -0
  41. package/dist/components/toast/toast.types.d.ts +19 -0
  42. package/dist/composables/use-checkbox-group-state.js +2 -0
  43. package/dist/composables/use-color-field-state.js +1 -0
  44. package/dist/composables/use-combo-box-state.d.ts +2 -0
  45. package/dist/composables/use-combo-box-state.js +3 -1
  46. package/dist/composables/use-date-field-state.js +1 -0
  47. package/dist/composables/use-date-picker-state.d.ts +2 -0
  48. package/dist/composables/use-date-picker-state.js +1 -0
  49. package/dist/composables/use-date-range-picker-state.d.ts +2 -0
  50. package/dist/composables/use-date-range-picker-state.js +2 -0
  51. package/dist/composables/use-description.js +1 -0
  52. package/dist/composables/use-form-validation-state.d.ts +48 -5
  53. package/dist/composables/use-form-validation-state.js +76 -7
  54. package/dist/composables/use-list-keyboard.d.ts +1 -1
  55. package/dist/composables/use-list-keyboard.js +4 -5
  56. package/dist/composables/use-measured-height.d.ts +18 -10
  57. package/dist/composables/use-measured-height.js +53 -15
  58. package/dist/composables/use-number-field-state.js +1 -0
  59. package/dist/composables/use-overlay-position.js +7 -4
  60. package/dist/composables/use-radio-group-state.js +2 -0
  61. package/dist/composables/use-select-state.d.ts +10 -0
  62. package/dist/composables/use-select-state.js +7 -0
  63. package/dist/composables/use-select.d.ts +7 -0
  64. package/dist/composables/use-select.js +6 -0
  65. package/dist/composables/use-text-field.js +1 -0
  66. package/dist/composables/use-toast-region.d.ts +18 -4
  67. package/dist/composables/use-toast-region.js +128 -6
  68. package/dist/composables/use-toast.d.ts +1 -1
  69. package/dist/composables/use-toast.js +4 -3
  70. package/dist/composables/use-toolbar.js +3 -1
  71. package/dist/composables/use-typeahead.js +3 -0
  72. package/dist/index.js +6 -5
  73. package/dist/ropav.min.css +1 -1
  74. package/dist/utils/focus.d.ts +13 -0
  75. package/dist/utils/focus.js +18 -1
  76. package/dist/version.js +1 -1
  77. 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
- * Knows nothing about the DOM — `useFormValidation` is what connects this to a real input.
113
- * Four sources feed it, in a fixed order of precedence:
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 rather than selected. */
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 selection.select(focused, { isShiftPressed: event.shiftKey });
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
- * Two narrowings against the usual shape, both measured rather than assumed:
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
- * 1. It measures directly on attach and then lets the observer take over, rather than relying on
16
- * the observer's first notification alone. jsdom *has* a `ResizeObserver` constructor but it
17
- * never notifies anything, so an observer-only reading is permanently absent there — and this
18
- * value drives an inline custom property that decides layout, which is exactly the kind of
19
- * thing a fast suite should be able to assert. In a real browser the two readings agree, and
20
- * the repeat is dropped, so the only difference is that the first one lands a frame earlier.
21
- * 2. React also watches `aria-hidden` with a `MutationObserver`. That attribute never reaches the
22
- * DOM there — `react-aria-components` filters it off the toast — so the observer is dead code,
23
- * and this package renders no `aria-hidden` for it to watch either.
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
- * Two narrowings against the usual shape, both measured rather than assumed:
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
- * 1. It measures directly on attach and then lets the observer take over, rather than relying on
13
- * the observer's first notification alone. jsdom *has* a `ResizeObserver` constructor but it
14
- * never notifies anything, so an observer-only reading is permanently absent there — and this
15
- * value drives an inline custom property that decides layout, which is exactly the kind of
16
- * thing a fast suite should be able to assert. In a real browser the two readings agree, and
17
- * the repeat is dropped, so the only difference is that the first one lands a frame earlier.
18
- * 2. React also watches `aria-hidden` with a `MutationObserver`. That attribute never reaches the
19
- * DOM there — `react-aria-components` filters it off the toast — so the observer is dead code,
20
- * and this package renders no `aria-hidden` for it to watch either.
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 observer;
32
+ let resizeObserver;
33
+ let contentObserver;
34
+ let frame;
25
35
  const detach = () => {
26
- observer?.disconnect();
27
- observer = void 0;
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
- observer = new ResizeObserver(measure);
39
- observer.observe(element);
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
- /** Overlays sit above the page; matched to what the React build renders. */
5
- var OVERLAY_Z_INDEX = 1e5;
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": String(OVERLAY_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": String(OVERLAY_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
- * Three jobs, and they are only in one composable because they share the same state:
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: upstream also registers the region with a
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: upstream runs focus-within and raw focus as two channels, because its
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
  */