ropav 0.13.0 → 0.15.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.
- package/dist/components/accordion/index.d.ts +1 -0
- package/dist/components/alert-dialog/alert-dialog-backdrop.js +3 -1
- package/dist/components/autocomplete/index.d.ts +1 -0
- package/dist/components/calendar/index.d.ts +1 -0
- package/dist/components/checkbox-group/index.d.ts +1 -0
- package/dist/components/color-slider/index.d.ts +1 -0
- package/dist/components/combo-box/index.d.ts +2 -0
- package/dist/components/date-field/index.d.ts +1 -0
- package/dist/components/date-picker/index.d.ts +2 -0
- package/dist/components/date-range-picker/index.d.ts +2 -0
- package/dist/components/date-range-picker/use-date-range-picker.js +1 -1
- package/dist/components/disclosure-group/index.d.ts +1 -0
- package/dist/components/drawer/drawer-backdrop.js +3 -1
- package/dist/components/dropdown/dropdown-menu.js +7 -3
- package/dist/components/dropdown/dropdown-root.js +1 -1
- package/dist/components/dropdown/dropdown-submenu-trigger.js +1 -1
- package/dist/components/dropdown/dropdown.context.d.ts +1 -1
- package/dist/components/dropdown/index.d.ts +1 -0
- package/dist/components/field-error/field-error-root.js +1 -1
- package/dist/components/form/form-root.js +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/meter/meter-root.js +1 -1
- package/dist/components/modal/modal-backdrop.js +3 -1
- package/dist/components/number-field/index.d.ts +1 -0
- package/dist/components/overlay/overlay-popover.js +15 -6
- package/dist/components/portal-provider/index.d.ts +3 -0
- package/dist/components/portal-provider/index.js +2 -0
- package/dist/components/portal-provider/portal-provider.d.ts +21 -0
- package/dist/components/portal-provider/portal-provider.js +20 -0
- package/dist/components/portal-provider/portal-provider.types.d.ts +22 -0
- package/dist/components/radio-group/index.d.ts +1 -0
- package/dist/components/range-calendar/index.d.ts +1 -0
- package/dist/components/segmented-control/index.d.ts +1 -0
- package/dist/components/select/index.d.ts +1 -0
- package/dist/components/slider/index.d.ts +1 -0
- package/dist/components/table/table-drag-handle.js +1 -1
- package/dist/components/tabs/index.d.ts +1 -0
- package/dist/components/tabs/tabs-root.js +5 -0
- package/dist/components/tabs/tabs.types.d.ts +8 -0
- package/dist/components/time-field/index.d.ts +2 -0
- package/dist/components/toast/toast-provider.js +3 -1
- package/dist/components/toggle-button/index.d.ts +1 -0
- package/dist/components/toggle-button-group/index.d.ts +1 -0
- package/dist/components/tooltip/index.d.ts +1 -0
- package/dist/components/tooltip/tooltip-content.js +4 -1
- package/dist/composables/drag-manager.js +1 -1
- package/dist/composables/drag-modality.js +2 -1
- package/dist/composables/focus-scope-registry.d.ts +27 -0
- package/dist/composables/focus-scope-registry.js +45 -0
- package/dist/composables/form-context.d.ts +25 -0
- package/dist/composables/form-context.js +17 -0
- package/dist/composables/index.d.ts +1 -24
- package/dist/composables/interaction-modality.d.ts +42 -0
- package/dist/composables/interaction-modality.js +91 -0
- package/dist/composables/use-calendar-cell.js +1 -1
- package/dist/composables/use-checkbox-group-state.js +2 -1
- package/dist/composables/use-combo-box-state.d.ts +2 -1
- package/dist/composables/use-combo-box-state.js +3 -2
- package/dist/composables/use-combo-box.js +6 -3
- package/dist/composables/use-css-variable.js +1 -5
- package/dist/composables/use-default-locale.d.ts +9 -0
- package/dist/composables/use-default-locale.js +39 -0
- package/dist/composables/use-dismissable.d.ts +6 -0
- package/dist/composables/use-dismissable.js +9 -11
- package/dist/composables/use-display-names.js +1 -1
- package/dist/composables/use-drag.js +1 -1
- package/dist/composables/use-droppable-collection.js +1 -1
- package/dist/composables/use-focus-scope.d.ts +6 -12
- package/dist/composables/use-focus-scope.js +14 -33
- package/dist/composables/use-form-validation-state.d.ts +1 -60
- package/dist/composables/use-form-validation-state.js +3 -134
- package/dist/composables/use-form-validation.d.ts +2 -1
- package/dist/composables/use-form-validation.js +2 -2
- package/dist/composables/use-interaction-states.d.ts +0 -36
- package/dist/composables/use-interaction-states.js +8 -91
- package/dist/composables/use-locale.d.ts +0 -14
- package/dist/composables/use-locale.js +4 -37
- package/dist/composables/use-localized-string-dictionary.d.ts +10 -0
- package/dist/composables/use-localized-string-dictionary.js +26 -0
- package/dist/composables/use-localized-string-formatter.d.ts +0 -9
- package/dist/composables/use-localized-string-formatter.js +3 -24
- package/dist/composables/use-long-press.d.ts +0 -2
- package/dist/composables/use-long-press.js +2 -2
- package/dist/composables/use-menu-trigger-state.d.ts +64 -0
- package/dist/composables/use-menu-trigger-state.js +90 -0
- package/dist/composables/use-menu-trigger.d.ts +2 -1
- package/dist/composables/use-modal-overlay.d.ts +2 -1
- package/dist/composables/use-modal-overlay.js +17 -4
- package/dist/composables/use-overlay-trigger-state.d.ts +0 -62
- package/dist/composables/use-overlay-trigger-state.js +2 -87
- package/dist/composables/use-page-size.d.ts +14 -0
- package/dist/composables/use-page-size.js +42 -0
- package/dist/composables/use-portal.d.ts +35 -0
- package/dist/composables/use-portal.js +47 -0
- package/dist/composables/use-press.d.ts +0 -20
- package/dist/composables/use-press.js +3 -26
- package/dist/composables/use-range-calendar.js +1 -1
- package/dist/composables/use-select-state.d.ts +1 -1
- package/dist/composables/use-select-state.js +1 -1
- package/dist/composables/use-slider.js +1 -1
- package/dist/composables/use-toast-region.js +2 -1
- package/dist/composables/use-tooltip-trigger-state.js +1 -15
- package/dist/composables/use-tooltip-trigger.js +1 -1
- package/dist/composables/use-viewport-size.d.ts +0 -13
- package/dist/composables/use-viewport-size.js +1 -38
- package/dist/composables/validation-result.d.ts +37 -0
- package/dist/composables/validation-result.js +122 -0
- package/dist/composables/virtual-event.d.ts +20 -0
- package/dist/composables/virtual-event.js +28 -0
- package/dist/index.js +13 -35
- package/dist/ropav.min.css +1 -1
- package/dist/utils/aria-hide-outside.d.ts +4 -1
- package/dist/utils/aria-hide-outside.js +17 -5
- package/dist/utils/date-validation.js +1 -1
- package/dist/utils/region.d.ts +10 -0
- package/dist/utils/region.js +13 -0
- package/dist/version.js +1 -1
- package/package.json +6 -2
|
@@ -1,30 +1,9 @@
|
|
|
1
1
|
import { useLocale } from "./use-locale.js";
|
|
2
|
+
import { useLocalizedStringDictionary } from "./use-localized-string-dictionary.js";
|
|
2
3
|
import { computed } from "vue";
|
|
3
|
-
import {
|
|
4
|
+
import { LocalizedStringFormatter } from "@internationalized/string";
|
|
4
5
|
//#region src/composables/use-localized-string-formatter.ts
|
|
5
6
|
/**
|
|
6
|
-
* Dictionaries built so far, keyed by the table each was built from.
|
|
7
|
-
*
|
|
8
|
-
* A dictionary is derived entirely from its strings and is read-only afterwards, so every consumer
|
|
9
|
-
* of the same table can share one. Weak, so a table that goes out of scope takes its dictionary
|
|
10
|
-
* with it.
|
|
11
|
-
*/
|
|
12
|
-
var dictionaries = /* @__PURE__ */ new WeakMap();
|
|
13
|
-
/**
|
|
14
|
-
* The dictionary for a table of localized strings.
|
|
15
|
-
*
|
|
16
|
-
* Ported from React Aria's `packages/react-aria/src/i18n/useLocalizedStringFormatter.ts`
|
|
17
|
-
* (react-aria 3.51.0), without its global-dictionary lookup: that exists so a server can hand
|
|
18
|
-
* pre-loaded strings to the client, which this package does not do yet.
|
|
19
|
-
*/
|
|
20
|
-
var useLocalizedStringDictionary = (strings) => {
|
|
21
|
-
const cached = dictionaries.get(strings);
|
|
22
|
-
if (cached) return cached;
|
|
23
|
-
const dictionary = new LocalizedStringDictionary(strings);
|
|
24
|
-
dictionaries.set(strings, dictionary);
|
|
25
|
-
return dictionary;
|
|
26
|
-
};
|
|
27
|
-
/**
|
|
28
7
|
* A formatter reading a table of localized strings in the locale that applies here.
|
|
29
8
|
*
|
|
30
9
|
* Handed back as a computed rather than a formatter, so a consumer keeps up when the locale
|
|
@@ -36,4 +15,4 @@ var useLocalizedStringFormatter = (strings) => {
|
|
|
36
15
|
return computed(() => new LocalizedStringFormatter(locale.value.locale, dictionary));
|
|
37
16
|
};
|
|
38
17
|
//#endregion
|
|
39
|
-
export {
|
|
18
|
+
export { useLocalizedStringFormatter };
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { PressEvent, UsePressHandlers } from "./use-press.js";
|
|
2
2
|
import type { ComputedRef, MaybeRefOrGetter } from "vue";
|
|
3
|
-
/** How long the press has to be held before it counts as a long press. */
|
|
4
|
-
export declare const LONG_PRESS_THRESHOLD_MS = 500;
|
|
5
3
|
export interface LongPressEvent extends Omit<PressEvent, "type"> {
|
|
6
4
|
type: "longpressstart" | "longpressend" | "longpress";
|
|
7
5
|
}
|
|
@@ -69,7 +69,7 @@ var useLongPress = (options = {}) => {
|
|
|
69
69
|
if (event.pointerType === "touch") addGlobalListener(event.target, "contextmenu", (menu) => menu.preventDefault(), { once: true });
|
|
70
70
|
options.onLongPress?.(toLongPressEvent(event, "longpress"));
|
|
71
71
|
timeout = void 0;
|
|
72
|
-
}, toValue(options.threshold) ??
|
|
72
|
+
}, toValue(options.threshold) ?? LONG_PRESS_THRESHOLD_MS);
|
|
73
73
|
addGlobalListener(window, "pointerup", () => {
|
|
74
74
|
setTimeout(() => removeAllGlobalListeners(), 100);
|
|
75
75
|
}, { once: true });
|
|
@@ -82,4 +82,4 @@ var useLongPress = (options = {}) => {
|
|
|
82
82
|
};
|
|
83
83
|
};
|
|
84
84
|
//#endregion
|
|
85
|
-
export {
|
|
85
|
+
export { useLongPress };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { CollectionKey } from "./use-collection.js";
|
|
2
|
+
import type { FocusStrategy, OverlayTriggerState, UseOverlayTriggerStateOptions } from "./use-overlay-trigger-state.js";
|
|
3
|
+
import type { ComputedRef, MaybeRefOrGetter } from "vue";
|
|
4
|
+
export interface MenuTriggerState extends OverlayTriggerState {
|
|
5
|
+
/** Which item the menu focuses when it opens, or `null` to focus the menu itself. */
|
|
6
|
+
focusStrategy: ComputedRef<FocusStrategy | null>;
|
|
7
|
+
open: (focusStrategy?: FocusStrategy | null) => void;
|
|
8
|
+
toggle: (focusStrategy?: FocusStrategy | null) => void;
|
|
9
|
+
}
|
|
10
|
+
export interface RootMenuTriggerState extends MenuTriggerState {
|
|
11
|
+
/**
|
|
12
|
+
* The open submenu trigger keys, one per level: index 0 is the submenu opened from the root
|
|
13
|
+
* menu, index 1 the submenu opened from that one, and so on.
|
|
14
|
+
*/
|
|
15
|
+
expandedKeysStack: ComputedRef<CollectionKey[]>;
|
|
16
|
+
openSubmenu: (triggerKey: CollectionKey, level: number) => void;
|
|
17
|
+
closeSubmenu: (triggerKey: CollectionKey, level: number) => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Open state for a menu trigger, ported from React Stately's `useMenuTriggerState`.
|
|
21
|
+
*
|
|
22
|
+
* Adds two things to a plain overlay trigger. A focus strategy, because how a menu was opened
|
|
23
|
+
* decides where focus lands: ArrowUp opens it focused on the last item, a mouse click opens it
|
|
24
|
+
* focused on the menu itself. And the stack of open submenus, held here on the root rather
|
|
25
|
+
* than in each submenu, which is what makes "only one path through the menu tree is open at a
|
|
26
|
+
* time" true by construction — opening a submenu at level N truncates the stack to N.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const state = useMenuTriggerState({isOpen: () => props.isOpen});
|
|
31
|
+
* state.open("last"); // ArrowUp on the trigger
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare const useMenuTriggerState: (options?: UseOverlayTriggerStateOptions) => RootMenuTriggerState;
|
|
35
|
+
export interface SubmenuTriggerState extends OverlayTriggerState {
|
|
36
|
+
focusStrategy: ComputedRef<FocusStrategy | null>;
|
|
37
|
+
open: (focusStrategy?: FocusStrategy | null) => void;
|
|
38
|
+
toggle: (focusStrategy?: FocusStrategy | null) => void;
|
|
39
|
+
/** Closes every menu in the tree, which is what selecting an item does. */
|
|
40
|
+
closeAll: () => void;
|
|
41
|
+
/** How deep this submenu sits, counting the root menu as level 0. */
|
|
42
|
+
submenuLevel: number;
|
|
43
|
+
}
|
|
44
|
+
export interface UseSubmenuTriggerStateOptions {
|
|
45
|
+
/**
|
|
46
|
+
* The key of the item that opens this submenu.
|
|
47
|
+
*
|
|
48
|
+
* Read reactively, because the item is declared beside the submenu rather than around it: the
|
|
49
|
+
* submenu exists before it has been told which item it belongs to, and is closed until it has.
|
|
50
|
+
*/
|
|
51
|
+
triggerKey: MaybeRefOrGetter<CollectionKey | null>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Open state for one submenu, ported from React Stately's `useSubmenuTriggerState`.
|
|
55
|
+
*
|
|
56
|
+
* Holds no open flag of its own — it derives one by asking the root whether its own trigger
|
|
57
|
+
* key is the one expanded at its level. That is what makes opening a sibling submenu close
|
|
58
|
+
* this one with no coordination between the two.
|
|
59
|
+
*
|
|
60
|
+
* The level is captured once, when the submenu trigger is created, and deliberately not
|
|
61
|
+
* recomputed: it describes where this trigger sits in the tree, which does not change, whereas
|
|
62
|
+
* the stack it was read from changes constantly.
|
|
63
|
+
*/
|
|
64
|
+
export declare const useSubmenuTriggerState: (options: UseSubmenuTriggerStateOptions, root: RootMenuTriggerState) => SubmenuTriggerState;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useOverlayTriggerState } from "./use-overlay-trigger-state.js";
|
|
2
|
+
import { computed, shallowRef, toValue } from "vue";
|
|
3
|
+
//#region src/composables/use-menu-trigger-state.ts
|
|
4
|
+
/**
|
|
5
|
+
* Open state for a menu trigger, ported from React Stately's `useMenuTriggerState`.
|
|
6
|
+
*
|
|
7
|
+
* Adds two things to a plain overlay trigger. A focus strategy, because how a menu was opened
|
|
8
|
+
* decides where focus lands: ArrowUp opens it focused on the last item, a mouse click opens it
|
|
9
|
+
* focused on the menu itself. And the stack of open submenus, held here on the root rather
|
|
10
|
+
* than in each submenu, which is what makes "only one path through the menu tree is open at a
|
|
11
|
+
* time" true by construction — opening a submenu at level N truncates the stack to N.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const state = useMenuTriggerState({isOpen: () => props.isOpen});
|
|
16
|
+
* state.open("last"); // ArrowUp on the trigger
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
var useMenuTriggerState = (options = {}) => {
|
|
20
|
+
const overlay = useOverlayTriggerState(options);
|
|
21
|
+
const focusStrategy = shallowRef(null);
|
|
22
|
+
const expandedKeys = shallowRef([]);
|
|
23
|
+
return {
|
|
24
|
+
close: () => {
|
|
25
|
+
expandedKeys.value = [];
|
|
26
|
+
overlay.close();
|
|
27
|
+
},
|
|
28
|
+
closeSubmenu: (triggerKey, level) => {
|
|
29
|
+
if (expandedKeys.value[level] === triggerKey) expandedKeys.value = expandedKeys.value.slice(0, level);
|
|
30
|
+
},
|
|
31
|
+
expandedKeysStack: computed(() => expandedKeys.value),
|
|
32
|
+
focusStrategy: computed(() => focusStrategy.value),
|
|
33
|
+
isOpen: overlay.isOpen,
|
|
34
|
+
open: (strategy = null) => {
|
|
35
|
+
focusStrategy.value = strategy;
|
|
36
|
+
overlay.open();
|
|
37
|
+
},
|
|
38
|
+
openSubmenu: (triggerKey, level) => {
|
|
39
|
+
if (level > expandedKeys.value.length) return;
|
|
40
|
+
expandedKeys.value = [...expandedKeys.value.slice(0, level), triggerKey];
|
|
41
|
+
},
|
|
42
|
+
setOpen: overlay.setOpen,
|
|
43
|
+
toggle: (strategy = null) => {
|
|
44
|
+
focusStrategy.value = strategy;
|
|
45
|
+
overlay.toggle();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Open state for one submenu, ported from React Stately's `useSubmenuTriggerState`.
|
|
51
|
+
*
|
|
52
|
+
* Holds no open flag of its own — it derives one by asking the root whether its own trigger
|
|
53
|
+
* key is the one expanded at its level. That is what makes opening a sibling submenu close
|
|
54
|
+
* this one with no coordination between the two.
|
|
55
|
+
*
|
|
56
|
+
* The level is captured once, when the submenu trigger is created, and deliberately not
|
|
57
|
+
* recomputed: it describes where this trigger sits in the tree, which does not change, whereas
|
|
58
|
+
* the stack it was read from changes constantly.
|
|
59
|
+
*/
|
|
60
|
+
var useSubmenuTriggerState = (options, root) => {
|
|
61
|
+
const submenuLevel = root.expandedKeysStack.value.length;
|
|
62
|
+
const focusStrategy = shallowRef(null);
|
|
63
|
+
const triggerKey = computed(() => toValue(options.triggerKey));
|
|
64
|
+
const isOpen = computed(() => triggerKey.value !== null && root.expandedKeysStack.value[submenuLevel] === triggerKey.value);
|
|
65
|
+
const close = () => {
|
|
66
|
+
focusStrategy.value = null;
|
|
67
|
+
if (triggerKey.value === null) return;
|
|
68
|
+
root.closeSubmenu(triggerKey.value, submenuLevel);
|
|
69
|
+
};
|
|
70
|
+
const open = (strategy = null) => {
|
|
71
|
+
focusStrategy.value = strategy;
|
|
72
|
+
if (triggerKey.value === null) return;
|
|
73
|
+
root.openSubmenu(triggerKey.value, submenuLevel);
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
close,
|
|
77
|
+
closeAll: root.close,
|
|
78
|
+
focusStrategy: computed(() => focusStrategy.value),
|
|
79
|
+
isOpen,
|
|
80
|
+
open,
|
|
81
|
+
setOpen: (next) => next ? open() : close(),
|
|
82
|
+
submenuLevel,
|
|
83
|
+
toggle: (strategy = null) => {
|
|
84
|
+
if (isOpen.value) close();
|
|
85
|
+
else open(strategy);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
//#endregion
|
|
90
|
+
export { useMenuTriggerState, useSubmenuTriggerState };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PressResponder } from "./press-responder.js";
|
|
2
|
+
import type { MenuTriggerState } from "./use-menu-trigger-state.js";
|
|
2
3
|
import type { OverlayType } from "./use-overlay-trigger.js";
|
|
3
|
-
import type { FocusStrategy
|
|
4
|
+
import type { FocusStrategy } from "./use-overlay-trigger-state.js";
|
|
4
5
|
import type { ComputedRef, MaybeRefOrGetter } from "vue";
|
|
5
6
|
/** How the menu is opened. */
|
|
6
7
|
export type MenuTriggerType = "press" | "longPress";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { PageSize
|
|
1
|
+
import type { PageSize } from "./use-page-size.js";
|
|
2
|
+
import type { ViewportSize } from "./use-viewport-size.js";
|
|
2
3
|
import type { ComputedRef, MaybeRefOrGetter } from "vue";
|
|
3
4
|
export interface UseModalOverlayOptions {
|
|
4
5
|
/**
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ariaHideOutside } from "../utils/aria-hide-outside.js";
|
|
2
|
-
import { useFocusScope } from "./use-focus-scope.js";
|
|
3
2
|
import { useDismissable } from "./use-dismissable.js";
|
|
3
|
+
import { useFocusScope } from "./use-focus-scope.js";
|
|
4
|
+
import { usePageSize } from "./use-page-size.js";
|
|
5
|
+
import { usePortal } from "./use-portal.js";
|
|
4
6
|
import { usePreventScroll } from "./use-prevent-scroll.js";
|
|
5
|
-
import {
|
|
7
|
+
import { useViewportSize } from "./use-viewport-size.js";
|
|
6
8
|
import { computed, toValue, watch } from "vue";
|
|
7
9
|
//#region src/composables/use-modal-overlay.ts
|
|
8
10
|
/**
|
|
@@ -34,17 +36,26 @@ import { computed, toValue, watch } from "vue";
|
|
|
34
36
|
*/
|
|
35
37
|
var useModalOverlay = (options) => {
|
|
36
38
|
const isOpen = () => toValue(options.isOpen);
|
|
39
|
+
const portal = usePortal();
|
|
37
40
|
const dismissable = useDismissable({
|
|
38
41
|
isDismissable: options.isDismissable,
|
|
39
42
|
isKeyboardDismissDisabled: options.isKeyboardDismissDisabled,
|
|
40
43
|
isOpen,
|
|
41
44
|
onClose: options.onClose,
|
|
42
45
|
overlayRef: options.modalRef,
|
|
46
|
+
region: () => portal.root.value,
|
|
43
47
|
shouldCloseOnInteractOutside: options.shouldCloseOnInteractOutside
|
|
44
48
|
});
|
|
45
|
-
watch([
|
|
49
|
+
watch([
|
|
50
|
+
isOpen,
|
|
51
|
+
() => toValue(options.modalRef),
|
|
52
|
+
() => portal.root.value
|
|
53
|
+
], ([open, modal, root], _previous, onCleanup) => {
|
|
46
54
|
if (!open || !modal) return;
|
|
47
|
-
onCleanup(ariaHideOutside([modal], {
|
|
55
|
+
onCleanup(ariaHideOutside([modal], {
|
|
56
|
+
root: root ?? void 0,
|
|
57
|
+
shouldUseInert: true
|
|
58
|
+
}));
|
|
48
59
|
}, {
|
|
49
60
|
flush: "post",
|
|
50
61
|
immediate: true
|
|
@@ -52,6 +63,7 @@ var useModalOverlay = (options) => {
|
|
|
52
63
|
useFocusScope({
|
|
53
64
|
contain: true,
|
|
54
65
|
isActive: isOpen,
|
|
66
|
+
region: () => portal.root.value,
|
|
55
67
|
restoreFocus: true,
|
|
56
68
|
scopeRef: options.modalRef
|
|
57
69
|
});
|
|
@@ -63,6 +75,7 @@ var useModalOverlay = (options) => {
|
|
|
63
75
|
page: computed(() => page.value),
|
|
64
76
|
viewport: computed(() => viewport.value),
|
|
65
77
|
viewportStyle: computed(() => {
|
|
78
|
+
if (portal.isContained.value) return {};
|
|
66
79
|
const style = {
|
|
67
80
|
"--visual-viewport-height": `${viewport.value.height}px`,
|
|
68
81
|
"--visual-viewport-width": `${viewport.value.width}px`
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { CollectionKey } from "./use-collection.js";
|
|
2
1
|
import type { ComputedRef, MaybeRefOrGetter } from "vue";
|
|
3
2
|
/** Which end of a collection receives focus when an overlay opens. */
|
|
4
3
|
export type FocusStrategy = "first" | "last";
|
|
@@ -26,64 +25,3 @@ export interface OverlayTriggerState {
|
|
|
26
25
|
* ```
|
|
27
26
|
*/
|
|
28
27
|
export declare const useOverlayTriggerState: (options?: UseOverlayTriggerStateOptions) => OverlayTriggerState;
|
|
29
|
-
export interface MenuTriggerState extends OverlayTriggerState {
|
|
30
|
-
/** Which item the menu focuses when it opens, or `null` to focus the menu itself. */
|
|
31
|
-
focusStrategy: ComputedRef<FocusStrategy | null>;
|
|
32
|
-
open: (focusStrategy?: FocusStrategy | null) => void;
|
|
33
|
-
toggle: (focusStrategy?: FocusStrategy | null) => void;
|
|
34
|
-
}
|
|
35
|
-
export interface RootMenuTriggerState extends MenuTriggerState {
|
|
36
|
-
/**
|
|
37
|
-
* The open submenu trigger keys, one per level: index 0 is the submenu opened from the root
|
|
38
|
-
* menu, index 1 the submenu opened from that one, and so on.
|
|
39
|
-
*/
|
|
40
|
-
expandedKeysStack: ComputedRef<CollectionKey[]>;
|
|
41
|
-
openSubmenu: (triggerKey: CollectionKey, level: number) => void;
|
|
42
|
-
closeSubmenu: (triggerKey: CollectionKey, level: number) => void;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Open state for a menu trigger, ported from React Stately's `useMenuTriggerState`.
|
|
46
|
-
*
|
|
47
|
-
* Adds two things to a plain overlay trigger. A focus strategy, because how a menu was opened
|
|
48
|
-
* decides where focus lands: ArrowUp opens it focused on the last item, a mouse click opens it
|
|
49
|
-
* focused on the menu itself. And the stack of open submenus, held here on the root rather
|
|
50
|
-
* than in each submenu, which is what makes "only one path through the menu tree is open at a
|
|
51
|
-
* time" true by construction — opening a submenu at level N truncates the stack to N.
|
|
52
|
-
*
|
|
53
|
-
* @example
|
|
54
|
-
* ```ts
|
|
55
|
-
* const state = useMenuTriggerState({isOpen: () => props.isOpen});
|
|
56
|
-
* state.open("last"); // ArrowUp on the trigger
|
|
57
|
-
* ```
|
|
58
|
-
*/
|
|
59
|
-
export declare const useMenuTriggerState: (options?: UseOverlayTriggerStateOptions) => RootMenuTriggerState;
|
|
60
|
-
export interface SubmenuTriggerState extends OverlayTriggerState {
|
|
61
|
-
focusStrategy: ComputedRef<FocusStrategy | null>;
|
|
62
|
-
open: (focusStrategy?: FocusStrategy | null) => void;
|
|
63
|
-
toggle: (focusStrategy?: FocusStrategy | null) => void;
|
|
64
|
-
/** Closes every menu in the tree, which is what selecting an item does. */
|
|
65
|
-
closeAll: () => void;
|
|
66
|
-
/** How deep this submenu sits, counting the root menu as level 0. */
|
|
67
|
-
submenuLevel: number;
|
|
68
|
-
}
|
|
69
|
-
export interface UseSubmenuTriggerStateOptions {
|
|
70
|
-
/**
|
|
71
|
-
* The key of the item that opens this submenu.
|
|
72
|
-
*
|
|
73
|
-
* Read reactively, because the item is declared beside the submenu rather than around it: the
|
|
74
|
-
* submenu exists before it has been told which item it belongs to, and is closed until it has.
|
|
75
|
-
*/
|
|
76
|
-
triggerKey: MaybeRefOrGetter<CollectionKey | null>;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Open state for one submenu, ported from React Stately's `useSubmenuTriggerState`.
|
|
80
|
-
*
|
|
81
|
-
* Holds no open flag of its own — it derives one by asking the root whether its own trigger
|
|
82
|
-
* key is the one expanded at its level. That is what makes opening a sibling submenu close
|
|
83
|
-
* this one with no coordination between the two.
|
|
84
|
-
*
|
|
85
|
-
* The level is captured once, when the submenu trigger is created, and deliberately not
|
|
86
|
-
* recomputed: it describes where this trigger sits in the tree, which does not change, whereas
|
|
87
|
-
* the stack it was read from changes constantly.
|
|
88
|
-
*/
|
|
89
|
-
export declare const useSubmenuTriggerState: (options: UseSubmenuTriggerStateOptions, root: RootMenuTriggerState) => SubmenuTriggerState;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useControllableState } from "./use-controllable-state.js";
|
|
2
|
-
import { computed
|
|
2
|
+
import { computed } from "vue";
|
|
3
3
|
//#region src/composables/use-overlay-trigger-state.ts
|
|
4
4
|
/**
|
|
5
5
|
* Open state for an overlay trigger, ported from React Stately's `useOverlayTriggerState`.
|
|
@@ -26,90 +26,5 @@ var useOverlayTriggerState = (options = {}) => {
|
|
|
26
26
|
toggle: () => setState(!state.value)
|
|
27
27
|
};
|
|
28
28
|
};
|
|
29
|
-
/**
|
|
30
|
-
* Open state for a menu trigger, ported from React Stately's `useMenuTriggerState`.
|
|
31
|
-
*
|
|
32
|
-
* Adds two things to a plain overlay trigger. A focus strategy, because how a menu was opened
|
|
33
|
-
* decides where focus lands: ArrowUp opens it focused on the last item, a mouse click opens it
|
|
34
|
-
* focused on the menu itself. And the stack of open submenus, held here on the root rather
|
|
35
|
-
* than in each submenu, which is what makes "only one path through the menu tree is open at a
|
|
36
|
-
* time" true by construction — opening a submenu at level N truncates the stack to N.
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* ```ts
|
|
40
|
-
* const state = useMenuTriggerState({isOpen: () => props.isOpen});
|
|
41
|
-
* state.open("last"); // ArrowUp on the trigger
|
|
42
|
-
* ```
|
|
43
|
-
*/
|
|
44
|
-
var useMenuTriggerState = (options = {}) => {
|
|
45
|
-
const overlay = useOverlayTriggerState(options);
|
|
46
|
-
const focusStrategy = shallowRef(null);
|
|
47
|
-
const expandedKeys = shallowRef([]);
|
|
48
|
-
return {
|
|
49
|
-
close: () => {
|
|
50
|
-
expandedKeys.value = [];
|
|
51
|
-
overlay.close();
|
|
52
|
-
},
|
|
53
|
-
closeSubmenu: (triggerKey, level) => {
|
|
54
|
-
if (expandedKeys.value[level] === triggerKey) expandedKeys.value = expandedKeys.value.slice(0, level);
|
|
55
|
-
},
|
|
56
|
-
expandedKeysStack: computed(() => expandedKeys.value),
|
|
57
|
-
focusStrategy: computed(() => focusStrategy.value),
|
|
58
|
-
isOpen: overlay.isOpen,
|
|
59
|
-
open: (strategy = null) => {
|
|
60
|
-
focusStrategy.value = strategy;
|
|
61
|
-
overlay.open();
|
|
62
|
-
},
|
|
63
|
-
openSubmenu: (triggerKey, level) => {
|
|
64
|
-
if (level > expandedKeys.value.length) return;
|
|
65
|
-
expandedKeys.value = [...expandedKeys.value.slice(0, level), triggerKey];
|
|
66
|
-
},
|
|
67
|
-
setOpen: overlay.setOpen,
|
|
68
|
-
toggle: (strategy = null) => {
|
|
69
|
-
focusStrategy.value = strategy;
|
|
70
|
-
overlay.toggle();
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
};
|
|
74
|
-
/**
|
|
75
|
-
* Open state for one submenu, ported from React Stately's `useSubmenuTriggerState`.
|
|
76
|
-
*
|
|
77
|
-
* Holds no open flag of its own — it derives one by asking the root whether its own trigger
|
|
78
|
-
* key is the one expanded at its level. That is what makes opening a sibling submenu close
|
|
79
|
-
* this one with no coordination between the two.
|
|
80
|
-
*
|
|
81
|
-
* The level is captured once, when the submenu trigger is created, and deliberately not
|
|
82
|
-
* recomputed: it describes where this trigger sits in the tree, which does not change, whereas
|
|
83
|
-
* the stack it was read from changes constantly.
|
|
84
|
-
*/
|
|
85
|
-
var useSubmenuTriggerState = (options, root) => {
|
|
86
|
-
const submenuLevel = root.expandedKeysStack.value.length;
|
|
87
|
-
const focusStrategy = shallowRef(null);
|
|
88
|
-
const triggerKey = computed(() => toValue(options.triggerKey));
|
|
89
|
-
const isOpen = computed(() => triggerKey.value !== null && root.expandedKeysStack.value[submenuLevel] === triggerKey.value);
|
|
90
|
-
const close = () => {
|
|
91
|
-
focusStrategy.value = null;
|
|
92
|
-
if (triggerKey.value === null) return;
|
|
93
|
-
root.closeSubmenu(triggerKey.value, submenuLevel);
|
|
94
|
-
};
|
|
95
|
-
const open = (strategy = null) => {
|
|
96
|
-
focusStrategy.value = strategy;
|
|
97
|
-
if (triggerKey.value === null) return;
|
|
98
|
-
root.openSubmenu(triggerKey.value, submenuLevel);
|
|
99
|
-
};
|
|
100
|
-
return {
|
|
101
|
-
close,
|
|
102
|
-
closeAll: root.close,
|
|
103
|
-
focusStrategy: computed(() => focusStrategy.value),
|
|
104
|
-
isOpen,
|
|
105
|
-
open,
|
|
106
|
-
setOpen: (next) => next ? open() : close(),
|
|
107
|
-
submenuLevel,
|
|
108
|
-
toggle: (strategy = null) => {
|
|
109
|
-
if (isOpen.value) close();
|
|
110
|
-
else open(strategy);
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
};
|
|
114
29
|
//#endregion
|
|
115
|
-
export {
|
|
30
|
+
export { useOverlayTriggerState };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ShallowRef } from "vue";
|
|
2
|
+
export interface PageSize {
|
|
3
|
+
/** Absent on the server, and while there is no document to measure. */
|
|
4
|
+
width: number | undefined;
|
|
5
|
+
height: number | undefined;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Track the size of the whole scrollable page.
|
|
9
|
+
*
|
|
10
|
+
* Ported from the block React Aria computes inline in `ModalOverlayInner`. Published as
|
|
11
|
+
* `--page-width` / `--page-height` for a backdrop that has to cover a page taller than the
|
|
12
|
+
* viewport rather than only the part currently on screen.
|
|
13
|
+
*/
|
|
14
|
+
export declare const usePageSize: () => ShallowRef<PageSize>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isScrollable } from "../utils/focus.js";
|
|
2
|
+
import { onScopeDispose, shallowRef } from "vue";
|
|
3
|
+
//#region src/composables/use-page-size.ts
|
|
4
|
+
var getVisualViewport = () => typeof document === "undefined" ? null : window.visualViewport ?? null;
|
|
5
|
+
var measurePage = () => {
|
|
6
|
+
if (typeof document === "undefined") return {
|
|
7
|
+
height: void 0,
|
|
8
|
+
width: void 0
|
|
9
|
+
};
|
|
10
|
+
const scrolling = isScrollable(document.body) ? document.body : document.scrollingElement ?? document.documentElement;
|
|
11
|
+
const rect = scrolling.getBoundingClientRect();
|
|
12
|
+
return {
|
|
13
|
+
height: scrolling.scrollHeight - rect.height % 1,
|
|
14
|
+
width: scrolling.scrollWidth - rect.width % 1
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Track the size of the whole scrollable page.
|
|
19
|
+
*
|
|
20
|
+
* Ported from the block React Aria computes inline in `ModalOverlayInner`. Published as
|
|
21
|
+
* `--page-width` / `--page-height` for a backdrop that has to cover a page taller than the
|
|
22
|
+
* viewport rather than only the part currently on screen.
|
|
23
|
+
*/
|
|
24
|
+
var usePageSize = () => {
|
|
25
|
+
const size = shallowRef(measurePage());
|
|
26
|
+
if (typeof document === "undefined") return size;
|
|
27
|
+
const onResize = () => {
|
|
28
|
+
const next = measurePage();
|
|
29
|
+
if (next.width === size.value.width && next.height === size.value.height) return;
|
|
30
|
+
size.value = next;
|
|
31
|
+
};
|
|
32
|
+
const viewport = getVisualViewport();
|
|
33
|
+
window.addEventListener("resize", onResize);
|
|
34
|
+
viewport?.addEventListener("resize", onResize);
|
|
35
|
+
onScopeDispose(() => {
|
|
36
|
+
window.removeEventListener("resize", onResize);
|
|
37
|
+
viewport?.removeEventListener("resize", onResize);
|
|
38
|
+
}, true);
|
|
39
|
+
return size;
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
export { usePageSize };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ComputedRef, MaybeRefOrGetter } from "vue";
|
|
2
|
+
/** Where the overlays below a `PortalProvider` go, and what they treat as the page. */
|
|
3
|
+
export interface Portal {
|
|
4
|
+
/** Where every overlay is rendered: `"body"` without a provider. */
|
|
5
|
+
container: ComputedRef<HTMLElement | string>;
|
|
6
|
+
/**
|
|
7
|
+
* The part of the page an overlay belongs to: what a modal hides outside itself (`inert`), what
|
|
8
|
+
* it holds focus and outside presses within, and what placement keeps an overlay inside. `null`
|
|
9
|
+
* (the whole document) without a provider, or with one that names no root.
|
|
10
|
+
*/
|
|
11
|
+
root: ComputedRef<HTMLElement | null>;
|
|
12
|
+
/** Whether overlays render into an element of their own rather than the document's body. */
|
|
13
|
+
isContained: ComputedRef<boolean>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The portal that applies here: the nearest `PortalProvider`'s, or the document's body.
|
|
17
|
+
*
|
|
18
|
+
* Ported from React Aria's `UNSAFE_PortalProvider` (`getContainer`), with the root added: an
|
|
19
|
+
* overlay rendered into a region of the page, say an app's window, belongs to that region and has
|
|
20
|
+
* to leave the rest of the page alone — hidden, focused and pressed as it was.
|
|
21
|
+
*/
|
|
22
|
+
export declare const usePortal: () => Portal;
|
|
23
|
+
export interface PortalOptions {
|
|
24
|
+
/** `undefined` inherits the enclosing portal's container, `null` goes back to the body. */
|
|
25
|
+
container: MaybeRefOrGetter<HTMLElement | string | null | undefined>;
|
|
26
|
+
/** `undefined` inherits the enclosing portal's root, `null` goes back to the whole document. */
|
|
27
|
+
root?: MaybeRefOrGetter<HTMLElement | null | undefined>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Send every overlay below into `container`, belonging to `root`.
|
|
31
|
+
*
|
|
32
|
+
* Absent and `null` differ as they do in React Aria: a nested provider that names only a container
|
|
33
|
+
* keeps the root of the one around it, and an explicit `null` is the way back to the page.
|
|
34
|
+
*/
|
|
35
|
+
export declare const providePortal: (options: PortalOptions) => Portal;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createContext } from "../utils/create-context.js";
|
|
2
|
+
import { computed, toValue } from "vue";
|
|
3
|
+
//#region src/composables/use-portal.ts
|
|
4
|
+
var [usePortalContext, providePortalContext] = createContext({
|
|
5
|
+
defaultValue: null,
|
|
6
|
+
name: "PortalContext",
|
|
7
|
+
strict: false
|
|
8
|
+
});
|
|
9
|
+
var BODY = {
|
|
10
|
+
container: computed(() => "body"),
|
|
11
|
+
isContained: computed(() => false),
|
|
12
|
+
root: computed(() => null)
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The portal that applies here: the nearest `PortalProvider`'s, or the document's body.
|
|
16
|
+
*
|
|
17
|
+
* Ported from React Aria's `UNSAFE_PortalProvider` (`getContainer`), with the root added: an
|
|
18
|
+
* overlay rendered into a region of the page, say an app's window, belongs to that region and has
|
|
19
|
+
* to leave the rest of the page alone — hidden, focused and pressed as it was.
|
|
20
|
+
*/
|
|
21
|
+
var usePortal = () => usePortalContext() ?? BODY;
|
|
22
|
+
/**
|
|
23
|
+
* Send every overlay below into `container`, belonging to `root`.
|
|
24
|
+
*
|
|
25
|
+
* Absent and `null` differ as they do in React Aria: a nested provider that names only a container
|
|
26
|
+
* keeps the root of the one around it, and an explicit `null` is the way back to the page.
|
|
27
|
+
*/
|
|
28
|
+
var providePortal = (options) => {
|
|
29
|
+
const parent = usePortal();
|
|
30
|
+
const container = computed(() => {
|
|
31
|
+
const own = toValue(options.container);
|
|
32
|
+
return own === void 0 ? parent.container.value : own ?? "body";
|
|
33
|
+
});
|
|
34
|
+
const root = computed(() => {
|
|
35
|
+
const own = toValue(options.root);
|
|
36
|
+
return own === void 0 ? parent.root.value : own;
|
|
37
|
+
});
|
|
38
|
+
const portal = {
|
|
39
|
+
container,
|
|
40
|
+
isContained: computed(() => container.value !== "body" && (typeof document === "undefined" || container.value !== document.body)),
|
|
41
|
+
root
|
|
42
|
+
};
|
|
43
|
+
providePortalContext(portal);
|
|
44
|
+
return portal;
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { providePortal, usePortal };
|
|
@@ -64,26 +64,6 @@ export interface UsePressReturn {
|
|
|
64
64
|
*/
|
|
65
65
|
handlers: UsePressHandlers;
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Whether a click came from something other than a pointer.
|
|
69
|
-
*
|
|
70
|
-
* Ported from React Aria's `isVirtualClick`. A screen reader and `element.click()` both
|
|
71
|
-
* produce a click with no pointer data behind it, which is the only way to tell them apart
|
|
72
|
-
* from a real one.
|
|
73
|
-
*/
|
|
74
|
-
export declare const isVirtualClick: (event: MouseEvent) => boolean;
|
|
75
|
-
/**
|
|
76
|
-
* Whether a pointer event was synthesised by assistive technology.
|
|
77
|
-
*
|
|
78
|
-
* A screen reader activating an element reports a zero-sized pointer. Safari on iOS gives
|
|
79
|
-
* such events the wrong coordinates and target, so they have to be left to the click handler.
|
|
80
|
-
*
|
|
81
|
-
* Android needs a shape of its own, and the zero-size test is turned off there rather than left
|
|
82
|
-
* to miss: TalkBack's double tap reports a one-by-one pointer claiming to be a mouse. Every part
|
|
83
|
-
* of that is load-bearing — pressure alone would catch real presses in Safari, which reports zero
|
|
84
|
-
* for those too, and the mouse claim is what separates the double tap from a touch screen press.
|
|
85
|
-
*/
|
|
86
|
-
export declare const isVirtualPointerEvent: (event: PointerEvent) => boolean;
|
|
87
67
|
/**
|
|
88
68
|
* Press interactions across mouse, touch, keyboard and assistive technology, ported from
|
|
89
69
|
* React Aria's `usePress`.
|