ropav 0.13.0 → 0.14.0

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 (98) hide show
  1. package/dist/components/accordion/index.d.ts +1 -0
  2. package/dist/components/autocomplete/index.d.ts +1 -0
  3. package/dist/components/calendar/index.d.ts +1 -0
  4. package/dist/components/checkbox-group/index.d.ts +1 -0
  5. package/dist/components/color-slider/index.d.ts +1 -0
  6. package/dist/components/combo-box/index.d.ts +2 -0
  7. package/dist/components/date-field/index.d.ts +1 -0
  8. package/dist/components/date-picker/index.d.ts +2 -0
  9. package/dist/components/date-range-picker/index.d.ts +2 -0
  10. package/dist/components/date-range-picker/use-date-range-picker.js +1 -1
  11. package/dist/components/disclosure-group/index.d.ts +1 -0
  12. package/dist/components/dropdown/dropdown-menu.js +7 -3
  13. package/dist/components/dropdown/dropdown-root.js +1 -1
  14. package/dist/components/dropdown/dropdown-submenu-trigger.js +1 -1
  15. package/dist/components/dropdown/dropdown.context.d.ts +1 -1
  16. package/dist/components/dropdown/index.d.ts +1 -0
  17. package/dist/components/field-error/field-error-root.js +1 -1
  18. package/dist/components/form/form-root.js +1 -1
  19. package/dist/components/number-field/index.d.ts +1 -0
  20. package/dist/components/overlay/overlay-popover.js +1 -1
  21. package/dist/components/radio-group/index.d.ts +1 -0
  22. package/dist/components/range-calendar/index.d.ts +1 -0
  23. package/dist/components/segmented-control/index.d.ts +1 -0
  24. package/dist/components/select/index.d.ts +1 -0
  25. package/dist/components/slider/index.d.ts +1 -0
  26. package/dist/components/table/table-drag-handle.js +1 -1
  27. package/dist/components/tabs/index.d.ts +1 -0
  28. package/dist/components/tabs/tabs-root.js +5 -0
  29. package/dist/components/tabs/tabs.types.d.ts +8 -0
  30. package/dist/components/time-field/index.d.ts +2 -0
  31. package/dist/components/toggle-button/index.d.ts +1 -0
  32. package/dist/components/toggle-button-group/index.d.ts +1 -0
  33. package/dist/components/tooltip/index.d.ts +1 -0
  34. package/dist/composables/drag-manager.js +1 -1
  35. package/dist/composables/drag-modality.js +2 -1
  36. package/dist/composables/focus-scope-registry.d.ts +19 -0
  37. package/dist/composables/focus-scope-registry.js +38 -0
  38. package/dist/composables/form-context.d.ts +25 -0
  39. package/dist/composables/form-context.js +17 -0
  40. package/dist/composables/index.d.ts +0 -24
  41. package/dist/composables/interaction-modality.d.ts +42 -0
  42. package/dist/composables/interaction-modality.js +91 -0
  43. package/dist/composables/use-calendar-cell.js +1 -1
  44. package/dist/composables/use-checkbox-group-state.js +2 -1
  45. package/dist/composables/use-combo-box-state.d.ts +2 -1
  46. package/dist/composables/use-combo-box-state.js +3 -2
  47. package/dist/composables/use-css-variable.js +1 -5
  48. package/dist/composables/use-default-locale.d.ts +9 -0
  49. package/dist/composables/use-default-locale.js +39 -0
  50. package/dist/composables/use-dismissable.js +1 -1
  51. package/dist/composables/use-display-names.js +1 -1
  52. package/dist/composables/use-drag.js +1 -1
  53. package/dist/composables/use-droppable-collection.js +1 -1
  54. package/dist/composables/use-focus-scope.d.ts +0 -12
  55. package/dist/composables/use-focus-scope.js +4 -31
  56. package/dist/composables/use-form-validation-state.d.ts +1 -60
  57. package/dist/composables/use-form-validation-state.js +3 -134
  58. package/dist/composables/use-form-validation.d.ts +2 -1
  59. package/dist/composables/use-form-validation.js +2 -2
  60. package/dist/composables/use-interaction-states.d.ts +0 -36
  61. package/dist/composables/use-interaction-states.js +8 -91
  62. package/dist/composables/use-locale.d.ts +0 -14
  63. package/dist/composables/use-locale.js +4 -37
  64. package/dist/composables/use-localized-string-dictionary.d.ts +10 -0
  65. package/dist/composables/use-localized-string-dictionary.js +26 -0
  66. package/dist/composables/use-localized-string-formatter.d.ts +0 -9
  67. package/dist/composables/use-localized-string-formatter.js +3 -24
  68. package/dist/composables/use-long-press.d.ts +0 -2
  69. package/dist/composables/use-long-press.js +2 -2
  70. package/dist/composables/use-menu-trigger-state.d.ts +64 -0
  71. package/dist/composables/use-menu-trigger-state.js +90 -0
  72. package/dist/composables/use-menu-trigger.d.ts +2 -1
  73. package/dist/composables/use-modal-overlay.d.ts +2 -1
  74. package/dist/composables/use-modal-overlay.js +3 -2
  75. package/dist/composables/use-overlay-trigger-state.d.ts +0 -62
  76. package/dist/composables/use-overlay-trigger-state.js +2 -87
  77. package/dist/composables/use-page-size.d.ts +14 -0
  78. package/dist/composables/use-page-size.js +42 -0
  79. package/dist/composables/use-press.d.ts +0 -20
  80. package/dist/composables/use-press.js +3 -26
  81. package/dist/composables/use-range-calendar.js +1 -1
  82. package/dist/composables/use-select-state.d.ts +1 -1
  83. package/dist/composables/use-select-state.js +1 -1
  84. package/dist/composables/use-slider.js +1 -1
  85. package/dist/composables/use-toast-region.js +2 -1
  86. package/dist/composables/use-tooltip-trigger-state.js +1 -15
  87. package/dist/composables/use-tooltip-trigger.js +1 -1
  88. package/dist/composables/use-viewport-size.d.ts +0 -13
  89. package/dist/composables/use-viewport-size.js +1 -38
  90. package/dist/composables/validation-result.d.ts +37 -0
  91. package/dist/composables/validation-result.js +122 -0
  92. package/dist/composables/virtual-event.d.ts +20 -0
  93. package/dist/composables/virtual-event.js +28 -0
  94. package/dist/index.js +11 -35
  95. package/dist/ropav.min.css +1 -1
  96. package/dist/utils/date-validation.js +1 -1
  97. package/dist/version.js +1 -1
  98. package/package.json +2 -2
@@ -0,0 +1,91 @@
1
+ import { shallowRef } from "vue";
2
+ //#region src/composables/interaction-modality.ts
3
+ var modality = shallowRef("keyboard");
4
+ /**
5
+ * The same answer, kept out of the reactive graph.
6
+ *
7
+ * These are two different questions and they need two different answers. "Should a focus ring be
8
+ * painted" may only change when something is pressed or a key is struck — a bare mouse move must
9
+ * not erase a ring that is already there. "How is the user driving the page right now" has to
10
+ * follow the pointer as it moves, because a tooltip decides whether to open on hover by asking it.
11
+ * Answering the second reactively would make every mouse move re-render every focusable thing on
12
+ * the page and drop the ring while doing it, which is why React Aria keeps the same split.
13
+ */
14
+ var latestModality = "keyboard";
15
+ /** Number of live consumers, so the document listeners are attached exactly once. */
16
+ var consumerCount = 0;
17
+ var onGlobalKeydown = (event) => {
18
+ if (event.metaKey || event.altKey || event.ctrlKey) return;
19
+ latestModality = "keyboard";
20
+ modality.value = "keyboard";
21
+ };
22
+ var onGlobalPointerdown = () => {
23
+ latestModality = "pointer";
24
+ modality.value = "pointer";
25
+ };
26
+ /** Tracked without touching the reactive ref, for the reason above. */
27
+ var onGlobalPointerMoved = () => {
28
+ latestModality = "pointer";
29
+ };
30
+ /**
31
+ * Keep the shared modality listeners attached, returning the release.
32
+ *
33
+ * For a component that has to ask how the user is driving the page without otherwise taking part
34
+ * in the interaction lifecycle — a tooltip trigger, which opens on hover only for a real pointer.
35
+ * Without this the answer would be whatever it was when the last interactive component unmounted.
36
+ */
37
+ var retainInteractionModality = () => {
38
+ if (typeof document === "undefined") return () => {};
39
+ if (++consumerCount === 1) {
40
+ document.addEventListener("keydown", onGlobalKeydown, true);
41
+ document.addEventListener("pointerdown", onGlobalPointerdown, true);
42
+ document.addEventListener("pointermove", onGlobalPointerMoved, true);
43
+ document.addEventListener("pointerup", onGlobalPointerMoved, true);
44
+ }
45
+ return () => {
46
+ if (--consumerCount === 0) {
47
+ document.removeEventListener("keydown", onGlobalKeydown, true);
48
+ document.removeEventListener("pointerdown", onGlobalPointerdown, true);
49
+ document.removeEventListener("pointermove", onGlobalPointerMoved, true);
50
+ document.removeEventListener("pointerup", onGlobalPointerMoved, true);
51
+ }
52
+ };
53
+ };
54
+ /**
55
+ * How the user is driving the page right now.
56
+ *
57
+ * Ported from React Aria's `getInteractionModality`. Not reactive on purpose — read it inside an
58
+ * event handler, where the answer is the one that matters.
59
+ */
60
+ var getInteractionModality = () => latestModality;
61
+ /**
62
+ * Whether focus arriving right now is the kind that came from a keyboard.
63
+ *
64
+ * Ported from React Aria's `isFocusVisible`, and reads the same answer React Aria reads — the one
65
+ * that follows the pointer as it moves. Read it inside a handler: a tooltip asks it on focus to
66
+ * tell tabbing to a button apart from clicking it, and only the pointer-following answer knows the
67
+ * user had already reached for the mouse.
68
+ *
69
+ * Not the answer that decides whether a ring is painted. That one lives on `useInteractionStates`
70
+ * and deliberately ignores a bare mouse move, so moving the pointer cannot erase a ring that is
71
+ * already there. The two can disagree, and that is the point.
72
+ */
73
+ var isFocusVisible = () => latestModality === "keyboard";
74
+ /**
75
+ * Declare how the last interaction reached the page.
76
+ *
77
+ * Ported from React Aria's `setInteractionModality`. A component that moves focus itself —
78
+ * a slider label handing focus to its first thumb — has to say that the move came from the
79
+ * keyboard, or the ring it just earned would not be painted.
80
+ */
81
+ var setInteractionModality = (next) => {
82
+ latestModality = next;
83
+ modality.value = next;
84
+ };
85
+ /**
86
+ * The answer that decides whether a ring is painted: moved by a keystroke or a press, never by a
87
+ * bare mouse move. Reactive when read inside a computed.
88
+ */
89
+ var getFocusRingModality = () => modality.value;
90
+ //#endregion
91
+ export { getFocusRingModality, getInteractionModality, isFocusVisible, retainInteractionModality, setInteractionModality };
@@ -1,4 +1,4 @@
1
- import { getInteractionModality } from "./use-interaction-states.js";
1
+ import { getInteractionModality } from "./interaction-modality.js";
2
2
  import { getScrollParent } from "../utils/focus.js";
3
3
  import { usePress } from "./use-press.js";
4
4
  import { useLocalizedStringFormatter } from "./use-localized-string-formatter.js";
@@ -1,5 +1,6 @@
1
1
  import { useControllableState } from "./use-controllable-state.js";
2
- import { DEFAULT_VALIDATION_RESULT, getNativeValidation, useFormValidationState } from "./use-form-validation-state.js";
2
+ import { DEFAULT_VALIDATION_RESULT, getNativeValidation } from "./validation-result.js";
3
+ import { useFormValidationState } from "./use-form-validation-state.js";
3
4
  import { computed, nextTick, toValue } from "vue";
4
5
  //#region src/composables/use-checkbox-group-state.ts
5
6
  /**
@@ -1,6 +1,7 @@
1
1
  import type { CollectionKey, UseCollectionReturn } from "./use-collection.js";
2
2
  import type { FormValidationState, ValidationBehavior, ValidationFunction } from "./use-form-validation-state.js";
3
- import type { FocusStrategy, MenuTriggerState } from "./use-overlay-trigger-state.js";
3
+ import type { MenuTriggerState } from "./use-menu-trigger-state.js";
4
+ import type { FocusStrategy } from "./use-overlay-trigger-state.js";
4
5
  import type { SelectSelectionMode, SelectedItem, SelectedValue } from "./use-select-state.js";
5
6
  import type { DisabledBehavior, UseSelectionManagerReturn } from "./use-selection-manager.js";
6
7
  import type { ComputedRef, MaybeRefOrGetter } from "vue";
@@ -1,8 +1,9 @@
1
1
  import { useControllableState } from "./use-controllable-state.js";
2
- import { useMenuTriggerState } from "./use-overlay-trigger-state.js";
3
2
  import { useCollection } from "./use-collection.js";
4
3
  import { createListCollection } from "../utils/virtualizer-collection.js";
5
- import { isValueMissing, useFormValidationState } from "./use-form-validation-state.js";
4
+ import { isValueMissing } from "./validation-result.js";
5
+ import { useFormValidationState } from "./use-form-validation-state.js";
6
+ import { useMenuTriggerState } from "./use-menu-trigger-state.js";
6
7
  import { useSelectionManager } from "./use-selection-manager.js";
7
8
  import { defaultItemTextValue } from "./use-select-state.js";
8
9
  import { useFilter } from "./use-filter.js";
@@ -32,9 +32,5 @@ var useCssVariable = (name, options = {}) => {
32
32
  return value;
33
33
  });
34
34
  };
35
- /** Forget everything read so far. For a test that changes a property between cases. */
36
- var clearCssVariableCache = () => {
37
- cache.clear();
38
- };
39
35
  //#endregion
40
- export { clearCssVariableCache, useCssVariable };
36
+ export { useCssVariable };
@@ -0,0 +1,9 @@
1
+ import type { Locale } from "../utils/locale.js";
2
+ import type { ComputedRef } from "vue";
3
+ /**
4
+ * The locale the browser is set to, following the user changing it.
5
+ *
6
+ * Ported from React Aria's `packages/react-aria/src/i18n/useDefaultLocale.ts` (react-aria 3.51.0),
7
+ * minus its server branch — this package does not render on the server yet.
8
+ */
9
+ export declare const useDefaultLocale: () => ComputedRef<Locale>;
@@ -0,0 +1,39 @@
1
+ import { getDefaultLocale } from "../utils/locale.js";
2
+ import { computed, onScopeDispose, shallowRef } from "vue";
3
+ //#region src/composables/use-default-locale.ts
4
+ /**
5
+ * The browser's locale, shared by every consumer and updated when the user changes it.
6
+ *
7
+ * Module-level rather than per-component: `languagechange` is a window event and the answer is the
8
+ * same for the whole page, so one listener serves all of them. Held behind a reference count so a
9
+ * page with no locale-aware component on it carries no listener.
10
+ */
11
+ var browserLocale = shallowRef(getDefaultLocale());
12
+ var consumerCount = 0;
13
+ var onLanguageChange = () => {
14
+ browserLocale.value = getDefaultLocale();
15
+ };
16
+ /** Attach the shared `languagechange` listener, returning the release for this consumer. */
17
+ var retainBrowserLocale = () => {
18
+ /* c8 ignore next */
19
+ if (typeof window === "undefined") return () => {};
20
+ if (++consumerCount === 1) {
21
+ onLanguageChange();
22
+ window.addEventListener("languagechange", onLanguageChange);
23
+ }
24
+ return () => {
25
+ if (--consumerCount === 0) window.removeEventListener("languagechange", onLanguageChange);
26
+ };
27
+ };
28
+ /**
29
+ * The locale the browser is set to, following the user changing it.
30
+ *
31
+ * Ported from React Aria's `packages/react-aria/src/i18n/useDefaultLocale.ts` (react-aria 3.51.0),
32
+ * minus its server branch — this package does not render on the server yet.
33
+ */
34
+ var useDefaultLocale = () => {
35
+ onScopeDispose(retainBrowserLocale());
36
+ return computed(() => browserLocale.value);
37
+ };
38
+ //#endregion
39
+ export { useDefaultLocale };
@@ -1,5 +1,5 @@
1
1
  import { isInTopLayer } from "../utils/top-layer.js";
2
- import { isElementInAnyFocusScope } from "./use-focus-scope.js";
2
+ import { isElementInAnyFocusScope } from "./focus-scope-registry.js";
3
3
  import { computed, onScopeDispose, toValue, watch } from "vue";
4
4
  //#region src/composables/use-dismissable.ts
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { useLocale } from "./use-locale.js";
2
- import { useLocalizedStringDictionary } from "./use-localized-string-formatter.js";
2
+ import { useLocalizedStringDictionary } from "./use-localized-string-dictionary.js";
3
3
  import { datepickerStrings } from "../i18n/datepicker.js";
4
4
  import { computed } from "vue";
5
5
  //#region src/composables/use-display-names.ts
@@ -1,5 +1,5 @@
1
1
  import { isIOS, isWebKit } from "../utils/platform.js";
2
- import { isVirtualClick, isVirtualPointerEvent } from "./use-press.js";
2
+ import { isVirtualClick, isVirtualPointerEvent } from "./virtual-event.js";
3
3
  import { useLocalizedStringFormatter } from "./use-localized-string-formatter.js";
4
4
  import { useDescription } from "./use-description.js";
5
5
  import { dndStrings } from "../i18n/dnd.js";
@@ -1,4 +1,4 @@
1
- import { setInteractionModality } from "./use-interaction-states.js";
1
+ import { setInteractionModality } from "./interaction-modality.js";
2
2
  import { useLocale } from "./use-locale.js";
3
3
  import { DIRECTORY_DRAG_TYPE } from "../utils/dnd-constants.js";
4
4
  import { dragTypesFromSet, getTypes } from "../utils/dnd-data-transfer.js";
@@ -1,16 +1,4 @@
1
1
  import type { MaybeRefOrGetter } from "vue";
2
- /**
3
- * Whether the element sits inside any registered scope.
4
- *
5
- * Any scope rather than only the descendants of one, because an overlay opened from inside
6
- * another is a **sibling** in the DOM rather than a descendant — a submenu renders into its root
7
- * popover's container, and a dropdown opened from a popover makes a container of its own. There
8
- * is no tree to walk here, so a scope boundary is the thing that answers "focus is still in an
9
- * overlay". React Aria walks its scope tree and asks the narrower question; the difference shows
10
- * only when two unrelated overlays are open at once and focus moves between them, where this
11
- * errs towards leaving them open.
12
- */
13
- export declare const isElementInAnyFocusScope: (element: Element) => boolean;
14
2
  export interface UseFocusScopeOptions {
15
3
  /** The element the scope covers. */
16
4
  scopeRef: MaybeRefOrGetter<HTMLElement | null | undefined>;
@@ -1,34 +1,8 @@
1
+ import { innermostContainingScope, isElementInAnyFocusScope, registerFocusScope } from "./focus-scope-registry.js";
1
2
  import { tabbableIn } from "../utils/focus.js";
2
3
  import { onScopeDispose, toValue, watch } from "vue";
3
4
  //#region src/composables/use-focus-scope.ts
4
5
  /**
5
- * Registered scopes, outermost first.
6
- *
7
- * Module-level because containment is a question about the whole page, not about one overlay:
8
- * only the innermost containing scope may hold focus, and focus moving into a scope nested
9
- * inside it has to be allowed through rather than pulled back.
10
- */
11
- var scopes = [];
12
- var innermostContainingScope = () => {
13
- for (let index = scopes.length - 1; index >= 0; index--) {
14
- const scope = scopes[index];
15
- if (scope.contain()) return scope;
16
- }
17
- return null;
18
- };
19
- /**
20
- * Whether the element sits inside any registered scope.
21
- *
22
- * Any scope rather than only the descendants of one, because an overlay opened from inside
23
- * another is a **sibling** in the DOM rather than a descendant — a submenu renders into its root
24
- * popover's container, and a dropdown opened from a popover makes a container of its own. There
25
- * is no tree to walk here, so a scope boundary is the thing that answers "focus is still in an
26
- * overlay". React Aria walks its scope tree and asks the narrower question; the difference shows
27
- * only when two unrelated overlays are open at once and focus moves between them, where this
28
- * errs towards leaving them open.
29
- */
30
- var isElementInAnyFocusScope = (element) => scopes.some((scope) => scope.root()?.contains(element));
31
- /**
32
6
  * Keep focus inside an overlay and give it back when the overlay closes, ported from React
33
7
  * Aria's `FocusScope`.
34
8
  *
@@ -105,15 +79,14 @@ var useFocusScope = (options) => {
105
79
  detach();
106
80
  if (!root || !isActive) return;
107
81
  const previouslyFocused = root.ownerDocument.activeElement;
108
- scopes.push(scope);
82
+ const unregister = registerFocusScope(scope);
109
83
  attach(root);
110
84
  const autoFocus = toValue(options.autoFocus);
111
85
  if (autoFocus === "first" || autoFocus === "last") focusEnd(root, autoFocus);
112
86
  else if (autoFocus) focusElement(root);
113
87
  onCleanup(() => {
114
88
  detach();
115
- const index = scopes.indexOf(scope);
116
- if (index >= 0) scopes.splice(index, 1);
89
+ unregister();
117
90
  if (!toValue(options.restoreFocus)) return;
118
91
  const active = root.ownerDocument.activeElement;
119
92
  if (!(!active || active === root.ownerDocument.body || root.contains(active))) return;
@@ -127,4 +100,4 @@ var useFocusScope = (options) => {
127
100
  onScopeDispose(() => detach(), true);
128
101
  };
129
102
  //#endregion
130
- export { isElementInAnyFocusScope, useFocusScope };
103
+ export { useFocusScope };
@@ -1,4 +1,4 @@
1
- import type { ComputedRef, MaybeRefOrGetter, Ref } from "vue";
1
+ import type { ComputedRef, MaybeRefOrGetter } from "vue";
2
2
  /**
3
3
  * Snapshot of an element's `ValidityState`.
4
4
  *
@@ -36,65 +36,13 @@ export interface ValidationResult {
36
36
  export type ValidationBehavior = "aria" | "native";
37
37
  /** Returns a message, several, or nothing at all when the value is acceptable. */
38
38
  export type ValidationFunction<T> = (value: T) => string | string[] | true | null | undefined;
39
- export declare const VALID_VALIDITY_STATE: ValidationDetails;
40
- /** Failure that came from a prop, a `validate` function or the server rather than the browser. */
41
- export declare const CUSTOM_VALIDITY_STATE: ValidationDetails;
42
- /** A required field with nothing in it. What the browser reports under `"native"`. */
43
- export declare const MISSING_VALIDITY_STATE: ValidationDetails;
44
- export declare const DEFAULT_VALIDATION_RESULT: ValidationResult;
45
- /**
46
- * One verdict out of several, for a control whose value has more than one part.
47
- *
48
- * Invalid if any part is, with the messages collected in order and deduplicated — two ends of a
49
- * range that are both out of bounds say the same thing, and saying it twice is not more helpful.
50
- */
51
- export declare const mergeValidation: (...results: ValidationResult[]) => ValidationResult;
52
- /** Form controls that take part in constraint validation. */
53
- export type ValidatableElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
54
- /** Freeze an element's live validity into a result the state layer can hold. */
55
- export declare const getNativeValidation: (element: ValidatableElement) => ValidationResult;
56
39
  /**
57
40
  * The kinds of control a required field can be. Each is a different sentence: a list asks to be
58
41
  * chosen from, a checkbox to be ticked.
59
42
  */
60
43
  export type RequiredControl = "checkbox" | "radio" | "select" | "text";
61
- /**
62
- * What the browser would say about an empty control of this kind.
63
- *
64
- * Read off a detached probe rather than translated here. The platform already holds the sentence,
65
- * in the *browser's* locale — which is the locale a validation message belongs in, since it sits
66
- * beside the browser's own — and it is the one the same field would report under `"native"`.
67
- *
68
- * Empty on a server, where the field cannot have been revealed yet and so has nothing to say.
69
- */
70
- export declare const missingValueMessage: (kind: RequiredControl) => string;
71
- /** Whether a value counts as nothing. Covers every shape a field holds when it is empty. */
72
- export declare const isValueMissing: (value: unknown) => boolean;
73
44
  /** Errors a server returned, keyed by the `name` each field submits under. */
74
45
  export type FormValidationErrors = Record<string, string | string[]>;
75
- export interface FormContext {
76
- /** Errors keyed by field `name`, shown until the user edits the value. */
77
- validationErrors: ComputedRef<FormValidationErrors>;
78
- /** Default for every field inside, unless the field names its own. */
79
- validationBehavior: ComputedRef<ValidationBehavior>;
80
- /**
81
- * Bumped by every submit attempt.
82
- *
83
- * Under `"native"` a field learns of a failed submit from the browser, which fires `invalid` at
84
- * it. Under `"aria"` the browser is not involved, so this is the only thing that tells a field
85
- * holding an unrevealed error that it is now being asked for.
86
- */
87
- submitCount?: Readonly<Ref<number>>;
88
- }
89
- /**
90
- * React splits this in two — `FormValidationContext` in react-stately for the errors,
91
- * `FormContext` in react-aria-components for the behaviour — only because the two live in
92
- * different packages. Here one provider hands out both, so a field injects once and the two
93
- * halves cannot drift apart.
94
- *
95
- * Loose: a field outside a form is the normal case, not an error.
96
- */
97
- export declare const useFormContext: () => FormContext | null, provideFormContext: (value: FormContext | null) => FormContext | null;
98
46
  export interface UseFormValidationStateOptions<T> {
99
47
  /** Value handed to `validate`. `null` or `undefined` skips custom validation entirely. */
100
48
  value: MaybeRefOrGetter<T | null | undefined>;
@@ -146,13 +94,6 @@ export interface FormValidationState {
146
94
  /** Reveal what validation currently says — on change, or on a failed submit. */
147
95
  commitValidation: () => void;
148
96
  }
149
- /**
150
- * Whether two results say the same thing.
151
- *
152
- * Not an optimisation: the results live in `shallowRef`s, and assigning an equal-but-new
153
- * object still retriggers every computed reading them.
154
- */
155
- export declare const isEqualValidation: (a: ValidationResult | null, b: ValidationResult | null) => boolean;
156
97
  /**
157
98
  * Decide what a field's validation currently says, ported from React Aria's
158
99
  * `packages/react-stately/src/form/useFormValidationState.ts` (react-stately 3.49.0).
@@ -1,127 +1,7 @@
1
- import { createContext } from "../utils/create-context.js";
1
+ import { useFormContext } from "./form-context.js";
2
+ import { CUSTOM_VALIDITY_STATE, DEFAULT_VALIDATION_RESULT, MISSING_VALIDITY_STATE, VALID_VALIDITY_STATE, isEqualValidation, isValueMissing, missingValueMessage } from "./validation-result.js";
2
3
  import { computed, nextTick, shallowRef, toValue, watch } from "vue";
3
4
  //#region src/composables/use-form-validation-state.ts
4
- var VALID_VALIDITY_STATE = Object.freeze({
5
- badInput: false,
6
- customError: false,
7
- patternMismatch: false,
8
- rangeOverflow: false,
9
- rangeUnderflow: false,
10
- stepMismatch: false,
11
- tooLong: false,
12
- tooShort: false,
13
- typeMismatch: false,
14
- valid: true,
15
- valueMissing: false
16
- });
17
- /** Failure that came from a prop, a `validate` function or the server rather than the browser. */
18
- var CUSTOM_VALIDITY_STATE = Object.freeze({
19
- ...VALID_VALIDITY_STATE,
20
- customError: true,
21
- valid: false
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
- });
29
- var DEFAULT_VALIDATION_RESULT = Object.freeze({
30
- isInvalid: false,
31
- validationDetails: VALID_VALIDITY_STATE,
32
- validationErrors: []
33
- });
34
- /**
35
- * One verdict out of several, for a control whose value has more than one part.
36
- *
37
- * Invalid if any part is, with the messages collected in order and deduplicated — two ends of a
38
- * range that are both out of bounds say the same thing, and saying it twice is not more helpful.
39
- */
40
- var mergeValidation = (...results) => {
41
- const errors = /* @__PURE__ */ new Set();
42
- const details = { ...VALID_VALIDITY_STATE };
43
- let isInvalid = false;
44
- for (const result of results) {
45
- for (const error of result.validationErrors) errors.add(error);
46
- isInvalid ||= result.isInvalid;
47
- for (const key of Object.keys(details)) details[key] ||= result.validationDetails[key];
48
- }
49
- details.valid = !isInvalid;
50
- return {
51
- isInvalid,
52
- validationDetails: details,
53
- validationErrors: [...errors]
54
- };
55
- };
56
- /** Freeze an element's live validity into a result the state layer can hold. */
57
- var getNativeValidation = (element) => {
58
- const validity = element.validity;
59
- return {
60
- isInvalid: !validity.valid,
61
- validationDetails: {
62
- badInput: validity.badInput,
63
- customError: validity.customError,
64
- patternMismatch: validity.patternMismatch,
65
- rangeOverflow: validity.rangeOverflow,
66
- rangeUnderflow: validity.rangeUnderflow,
67
- stepMismatch: validity.stepMismatch,
68
- tooLong: validity.tooLong,
69
- tooShort: validity.tooShort,
70
- typeMismatch: validity.typeMismatch,
71
- valid: validity.valid,
72
- valueMissing: validity.valueMissing
73
- },
74
- validationErrors: element.validationMessage ? [element.validationMessage] : []
75
- };
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
- };
112
- /**
113
- * React splits this in two — `FormValidationContext` in react-stately for the errors,
114
- * `FormContext` in react-aria-components for the behaviour — only because the two live in
115
- * different packages. Here one provider hands out both, so a field injects once and the two
116
- * halves cannot drift apart.
117
- *
118
- * Loose: a field outside a form is the normal case, not an error.
119
- */
120
- var [useFormContext, provideFormContext] = createContext({
121
- defaultValue: null,
122
- name: "FormContext",
123
- strict: false
124
- });
125
5
  var asArray = (value) => {
126
6
  if (!value) return [];
127
7
  return Array.isArray(value) ? value : [value];
@@ -136,17 +16,6 @@ var toValidationResult = (errors) => errors.length > 0 ? {
136
16
  validationErrors: errors
137
17
  } : null;
138
18
  /**
139
- * Whether two results say the same thing.
140
- *
141
- * Not an optimisation: the results live in `shallowRef`s, and assigning an equal-but-new
142
- * object still retriggers every computed reading them.
143
- */
144
- var isEqualValidation = (a, b) => {
145
- if (a === b) return true;
146
- if (!a || !b) return false;
147
- return a.isInvalid === b.isInvalid && a.validationErrors.length === b.validationErrors.length && a.validationErrors.every((error, index) => error === b.validationErrors[index]) && Object.keys(a.validationDetails).every((key) => a.validationDetails[key] === b.validationDetails[key]);
148
- };
149
- /**
150
19
  * Decide what a field's validation currently says, ported from React Aria's
151
20
  * `packages/react-stately/src/form/useFormValidationState.ts` (react-stately 3.49.0).
152
21
  *
@@ -279,4 +148,4 @@ var useFormValidationState = (options) => {
279
148
  };
280
149
  };
281
150
  //#endregion
282
- export { CUSTOM_VALIDITY_STATE, DEFAULT_VALIDATION_RESULT, MISSING_VALIDITY_STATE, VALID_VALIDITY_STATE, getNativeValidation, isEqualValidation, isValueMissing, mergeValidation, missingValueMessage, provideFormContext, useFormContext, useFormValidationState };
151
+ export { useFormValidationState };
@@ -1,4 +1,5 @@
1
- import type { FormValidationState, ValidatableElement } from "./use-form-validation-state.js";
1
+ import type { FormValidationState } from "./use-form-validation-state.js";
2
+ import type { ValidatableElement } from "./validation-result.js";
2
3
  import type { MaybeRefOrGetter, Ref } from "vue";
3
4
  export interface UseFormValidationOptions {
4
5
  /** Called instead of focusing the element when a failed submit lands on this field. */
@@ -1,5 +1,5 @@
1
- import { setInteractionModality } from "./use-interaction-states.js";
2
- import { getNativeValidation } from "./use-form-validation-state.js";
1
+ import { setInteractionModality } from "./interaction-modality.js";
2
+ import { getNativeValidation } from "./validation-result.js";
3
3
  import { onScopeDispose, toValue, watch, watchEffect } from "vue";
4
4
  //#region src/composables/use-form-validation.ts
5
5
  /** First control in the form the browser considers invalid, in document order. */
@@ -1,21 +1,6 @@
1
1
  import type { ComputedRef, MaybeRefOrGetter } from "vue";
2
2
  /** How the last interaction reached the page, which decides whether focus is visible. */
3
3
  export type InteractionModality = "keyboard" | "pointer";
4
- /**
5
- * Keep the shared modality listeners attached, returning the release.
6
- *
7
- * For a component that has to ask how the user is driving the page without otherwise taking part
8
- * in the interaction lifecycle — a tooltip trigger, which opens on hover only for a real pointer.
9
- * Without this the answer would be whatever it was when the last interactive component unmounted.
10
- */
11
- export declare const retainInteractionModality: () => (() => void);
12
- /**
13
- * How the user is driving the page right now.
14
- *
15
- * Ported from React Aria's `getInteractionModality`. Not reactive on purpose — read it inside an
16
- * event handler, where the answer is the one that matters.
17
- */
18
- export declare const getInteractionModality: () => InteractionModality;
19
4
  /**
20
5
  * The same answer as a reactive ref, for text that is rendered rather than read in a handler.
21
6
  *
@@ -29,27 +14,6 @@ export declare const getInteractionModality: () => InteractionModality;
29
14
  * interaction lifecycle.
30
15
  */
31
16
  export declare const useInteractionModality: () => ComputedRef<InteractionModality>;
32
- /**
33
- * Whether focus arriving right now is the kind that came from a keyboard.
34
- *
35
- * Ported from React Aria's `isFocusVisible`, and reads the same answer React Aria reads — the one
36
- * that follows the pointer as it moves. Read it inside a handler: a tooltip asks it on focus to
37
- * tell tabbing to a button apart from clicking it, and only the pointer-following answer knows the
38
- * user had already reached for the mouse.
39
- *
40
- * Not the answer that decides whether a ring is painted. That one lives on `useInteractionStates`
41
- * and deliberately ignores a bare mouse move, so moving the pointer cannot erase a ring that is
42
- * already there. The two can disagree, and that is the point.
43
- */
44
- export declare const isFocusVisible: () => boolean;
45
- /**
46
- * Declare how the last interaction reached the page.
47
- *
48
- * Ported from React Aria's `setInteractionModality`. A component that moves focus itself —
49
- * a slider label handing focus to its first thumb — has to say that the move came from the
50
- * keyboard, or the ring it just earned would not be painted.
51
- */
52
- export declare const setInteractionModality: (next: InteractionModality) => void;
53
17
  export interface UseInteractionStatesOptions {
54
18
  /** Suppresses every state and keeps the element out of the interaction lifecycle. */
55
19
  isDisabled?: MaybeRefOrGetter<boolean | undefined>;