ropav 0.14.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/alert-dialog/alert-dialog-backdrop.js +3 -1
- package/dist/components/drawer/drawer-backdrop.js +3 -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/overlay/overlay-popover.js +14 -5
- 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/toast/toast-provider.js +3 -1
- package/dist/components/tooltip/tooltip-content.js +4 -1
- package/dist/composables/focus-scope-registry.d.ts +9 -1
- package/dist/composables/focus-scope-registry.js +9 -2
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/use-combo-box.js +6 -3
- package/dist/composables/use-dismissable.d.ts +6 -0
- package/dist/composables/use-dismissable.js +8 -10
- package/dist/composables/use-focus-scope.d.ts +6 -0
- package/dist/composables/use-focus-scope.js +10 -2
- package/dist/composables/use-modal-overlay.js +14 -2
- package/dist/composables/use-portal.d.ts +35 -0
- package/dist/composables/use-portal.js +47 -0
- package/dist/index.js +3 -1
- 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/region.d.ts +10 -0
- package/dist/utils/region.js +13 -0
- package/dist/version.js +1 -1
- package/package.json +6 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dataAttr } from "../../utils/assertion.js";
|
|
2
2
|
import { provideFocusResponder } from "../../composables/focus-responder.js";
|
|
3
3
|
import { providePressResponder } from "../../composables/press-responder.js";
|
|
4
|
+
import { usePortal } from "../../composables/use-portal.js";
|
|
4
5
|
import { useModalOverlay } from "../../composables/use-modal-overlay.js";
|
|
5
6
|
import { useModalTransition } from "../../composables/use-modal-transition.js";
|
|
6
7
|
import { provideAlertDialogContext, provideAlertDialogOverlayContext, useAlertDialogContext } from "./alert-dialog.context.js";
|
|
@@ -101,7 +102,8 @@ var alert_dialog_backdrop_vue_vue_type_script_setup_true_vapor_true_lang_default
|
|
|
101
102
|
}
|
|
102
103
|
});
|
|
103
104
|
const styles = computed(() => slots.value.backdrop({ class: props.class }));
|
|
104
|
-
const
|
|
105
|
+
const portal = usePortal();
|
|
106
|
+
const target = computed(() => props.portalContainer ?? portal.container.value);
|
|
105
107
|
const setElement = (next) => {
|
|
106
108
|
backdropElement.value = next ?? null;
|
|
107
109
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dataAttr } from "../../utils/assertion.js";
|
|
2
2
|
import { provideFocusResponder } from "../../composables/focus-responder.js";
|
|
3
3
|
import { providePressResponder } from "../../composables/press-responder.js";
|
|
4
|
+
import { usePortal } from "../../composables/use-portal.js";
|
|
4
5
|
import { useModalOverlay } from "../../composables/use-modal-overlay.js";
|
|
5
6
|
import { useModalTransition } from "../../composables/use-modal-transition.js";
|
|
6
7
|
import { provideDrawerContext, provideDrawerOverlayContext, useDrawerContext } from "./drawer.context.js";
|
|
@@ -98,7 +99,8 @@ var drawer_backdrop_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@
|
|
|
98
99
|
}
|
|
99
100
|
});
|
|
100
101
|
const styles = computed(() => slots.value.backdrop({ class: props.class }));
|
|
101
|
-
const
|
|
102
|
+
const portal = usePortal();
|
|
103
|
+
const target = computed(() => props.portalContainer ?? portal.container.value);
|
|
102
104
|
const setElement = (next) => {
|
|
103
105
|
backdropElement.value = next ?? null;
|
|
104
106
|
};
|
|
@@ -58,6 +58,7 @@ export * from "./modal/index.js";
|
|
|
58
58
|
export * from "./number-field/index.js";
|
|
59
59
|
export * from "./pagination/index.js";
|
|
60
60
|
export * from "./popover/index.js";
|
|
61
|
+
export * from "./portal-provider/index.js";
|
|
61
62
|
export * from "./progress-bar/index.js";
|
|
62
63
|
export * from "./progress-circle/index.js";
|
|
63
64
|
export * from "./radio/index.js";
|
|
@@ -5,7 +5,7 @@ import { useProgressValue } from "../../composables/use-progress-value.js";
|
|
|
5
5
|
import { computed, createSlot, defineVaporComponent, renderEffect, setAttr, setClass, setInsertionState, setProp, template, unref } from "vue";
|
|
6
6
|
import { meterVariants } from "@ropav/styles";
|
|
7
7
|
//#region src/components/meter/meter-root.vue?vue&type=script&setup=true&vapor=true&lang.ts
|
|
8
|
-
var t0 = template("<div data-slot=meter role
|
|
8
|
+
var t0 = template("<div data-slot=meter role=meter>", 1);
|
|
9
9
|
var meter_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PURE__*/ defineVaporComponent({
|
|
10
10
|
__name: "meter-root",
|
|
11
11
|
props: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dataAttr } from "../../utils/assertion.js";
|
|
2
2
|
import { provideFocusResponder } from "../../composables/focus-responder.js";
|
|
3
3
|
import { providePressResponder } from "../../composables/press-responder.js";
|
|
4
|
+
import { usePortal } from "../../composables/use-portal.js";
|
|
4
5
|
import { useModalOverlay } from "../../composables/use-modal-overlay.js";
|
|
5
6
|
import { useModalTransition } from "../../composables/use-modal-transition.js";
|
|
6
7
|
import { provideModalContext, provideModalOverlayContext, useModalContext } from "./modal.context.js";
|
|
@@ -95,7 +96,8 @@ var modal_backdrop_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@_
|
|
|
95
96
|
}
|
|
96
97
|
});
|
|
97
98
|
const styles = computed(() => slots.value.backdrop({ class: props.class }));
|
|
98
|
-
const
|
|
99
|
+
const portal = usePortal();
|
|
100
|
+
const target = computed(() => props.portalContainer ?? portal.container.value);
|
|
99
101
|
const setElement = (next) => {
|
|
100
102
|
backdropElement.value = next ?? null;
|
|
101
103
|
};
|
|
@@ -3,6 +3,7 @@ import { provideSurfaceContext } from "../surface/surface.context.js";
|
|
|
3
3
|
import { ariaHideOutside, keepVisible } from "../../utils/aria-hide-outside.js";
|
|
4
4
|
import { useDismissable } from "../../composables/use-dismissable.js";
|
|
5
5
|
import { useFocusScope } from "../../composables/use-focus-scope.js";
|
|
6
|
+
import { usePortal } from "../../composables/use-portal.js";
|
|
6
7
|
import { usePreventScroll } from "../../composables/use-prevent-scroll.js";
|
|
7
8
|
import { useEnterExit } from "../../composables/use-enter-exit.js";
|
|
8
9
|
import { provideOverlayGroupContext, useOverlayGroupContext, useOverlayTargetContext } from "./overlay.context.js";
|
|
@@ -54,6 +55,7 @@ var overlay_popover_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@
|
|
|
54
55
|
const props = __props;
|
|
55
56
|
const target = useOverlayTargetContext();
|
|
56
57
|
const group = useOverlayGroupContext();
|
|
58
|
+
const portal = usePortal();
|
|
57
59
|
/**
|
|
58
60
|
* Modality belongs to the overlay, not to the thing that opened it.
|
|
59
61
|
*
|
|
@@ -88,6 +90,7 @@ var overlay_popover_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@
|
|
|
88
90
|
const { arrowStyle, overlayStyle, placement } = useOverlayPosition({
|
|
89
91
|
arrowBoundaryOffset: () => props.arrowBoundaryOffset,
|
|
90
92
|
arrowRef: arrow,
|
|
93
|
+
boundaryElement: () => portal.root.value,
|
|
91
94
|
containerPadding: () => props.containerPadding,
|
|
92
95
|
crossOffset: () => props.crossOffset,
|
|
93
96
|
isOpen: () => target.state.isOpen.value,
|
|
@@ -138,6 +141,7 @@ var overlay_popover_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@
|
|
|
138
141
|
isOpen: () => target.state.isOpen.value,
|
|
139
142
|
onClose: target.state.close,
|
|
140
143
|
overlayRef: element,
|
|
144
|
+
region: () => portal.root.value,
|
|
141
145
|
shouldCloseOnBlur: true,
|
|
142
146
|
shouldCloseOnInteractOutside
|
|
143
147
|
});
|
|
@@ -157,14 +161,18 @@ var overlay_popover_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@
|
|
|
157
161
|
() => target.state.isOpen.value,
|
|
158
162
|
element,
|
|
159
163
|
ownContainer,
|
|
160
|
-
isNonModal
|
|
161
|
-
|
|
164
|
+
isNonModal,
|
|
165
|
+
() => portal.root.value
|
|
166
|
+
], ([isOpen, popover, container, nonModal, root], _previous, onCleanup) => {
|
|
162
167
|
if (!isOpen || !popover) return;
|
|
163
|
-
if (
|
|
168
|
+
if (nonModal) {
|
|
164
169
|
onCleanup(keepVisible(popover) ?? (() => {}));
|
|
165
170
|
return;
|
|
166
171
|
}
|
|
167
|
-
onCleanup(ariaHideOutside([container ?? popover], {
|
|
172
|
+
onCleanup(ariaHideOutside([container ?? popover], {
|
|
173
|
+
root: root ?? void 0,
|
|
174
|
+
shouldUseInert: true
|
|
175
|
+
}));
|
|
168
176
|
}, {
|
|
169
177
|
flush: "post",
|
|
170
178
|
immediate: true
|
|
@@ -172,6 +180,7 @@ var overlay_popover_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@
|
|
|
172
180
|
useFocusScope({
|
|
173
181
|
contain: () => isDialog.value || focusContainRequests.value > 0,
|
|
174
182
|
isActive: () => target.state.isOpen.value,
|
|
183
|
+
region: () => portal.root.value,
|
|
175
184
|
restoreFocus: true,
|
|
176
185
|
scopeRef: element
|
|
177
186
|
});
|
|
@@ -215,7 +224,7 @@ var overlay_popover_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@
|
|
|
215
224
|
ownContainer.value = next ?? null;
|
|
216
225
|
};
|
|
217
226
|
return [createIf(() => !unref(isSubOverlay) && isPresent.value, () => {
|
|
218
|
-
return createComponent(VaporTeleport, { to:
|
|
227
|
+
return createComponent(VaporTeleport, { to: () => unref(portal).container.value }, () => {
|
|
219
228
|
const n2 = t0();
|
|
220
229
|
setStyle(n2, CONTAINER_STYLE);
|
|
221
230
|
setTemplateRefBinding(n2, () => setContainer);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PortalProviderRootProps } from "./portal-provider.types.js";
|
|
2
|
+
declare const __VLS_export: (__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>['props'], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, 'attrs' | 'emit' | 'slots'>>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>['expose'], __VLS_setup?: Promise<{
|
|
3
|
+
props: import('vue').PublicProps & __VLS_PrettifyLocal<PortalProviderRootProps> & (typeof globalThis extends {
|
|
4
|
+
__VLS_PROPS_FALLBACK: infer P;
|
|
5
|
+
} ? P : {});
|
|
6
|
+
expose: (exposed: {}) => void;
|
|
7
|
+
attrs: any;
|
|
8
|
+
slots: {
|
|
9
|
+
default?: () => unknown;
|
|
10
|
+
};
|
|
11
|
+
emit: {};
|
|
12
|
+
}>) => import('vue').VNode & {
|
|
13
|
+
__ctx?: NonNullable<Awaited<typeof __VLS_setup>>;
|
|
14
|
+
};
|
|
15
|
+
declare const _default: typeof __VLS_export;
|
|
16
|
+
export default _default;
|
|
17
|
+
type __VLS_PrettifyLocal<T> = (T extends any ? {
|
|
18
|
+
[K in keyof T]: T[K];
|
|
19
|
+
} : {
|
|
20
|
+
[K in keyof T as K]: T[K];
|
|
21
|
+
}) & {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { providePortal } from "../../composables/use-portal.js";
|
|
2
|
+
import { createSlot, defineVaporComponent } from "vue";
|
|
3
|
+
//#region src/components/portal-provider/portal-provider.vue?vue&type=script&setup=true&vapor=true&lang.ts
|
|
4
|
+
var portal_provider_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PURE__*/ defineVaporComponent({
|
|
5
|
+
__name: "portal-provider",
|
|
6
|
+
props: {
|
|
7
|
+
container: {},
|
|
8
|
+
root: {}
|
|
9
|
+
},
|
|
10
|
+
setup(__props) {
|
|
11
|
+
const props = __props;
|
|
12
|
+
providePortal({
|
|
13
|
+
container: () => props.container,
|
|
14
|
+
root: () => props.root
|
|
15
|
+
});
|
|
16
|
+
return createSlot();
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
//#endregion
|
|
20
|
+
export { portal_provider_vue_vue_type_script_setup_true_vapor_true_lang_default as default };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface PortalProviderRootProps {
|
|
2
|
+
/**
|
|
3
|
+
* Where every overlay below renders: modals, alert dialogs, drawers, popovers, menus, the
|
|
4
|
+
* listboxes of pickers, tooltips and toast regions. A component's own `portalContainer` still
|
|
5
|
+
* wins.
|
|
6
|
+
*
|
|
7
|
+
* Omitted, it inherits the enclosing provider's container; `null` goes back to the document's
|
|
8
|
+
* body. An element other than the body has to be a containing block for `position: fixed` (a
|
|
9
|
+
* `transform`, or `contain: layout` or `paint`) sized to the area the overlays cover: backdrops
|
|
10
|
+
* fill it, and modals size to it rather than to the visual viewport.
|
|
11
|
+
*/
|
|
12
|
+
container?: HTMLElement | string | null;
|
|
13
|
+
/**
|
|
14
|
+
* The part of the page the overlays below belong to, usually an element holding both the content
|
|
15
|
+
* and the `container`. A modal makes only this element `inert` and holds focus and outside
|
|
16
|
+
* presses only within it, and a placed overlay flips and shifts to stay inside it — so an app
|
|
17
|
+
* rendered in a window of a larger page leaves the rest of that page alone.
|
|
18
|
+
*
|
|
19
|
+
* Omitted, it inherits the enclosing provider's root; `null` goes back to the whole document.
|
|
20
|
+
*/
|
|
21
|
+
root?: HTMLElement | null;
|
|
22
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dataAttr } from "../../utils/assertion.js";
|
|
2
2
|
import { composeSlotClassName } from "../../utils/compose.js";
|
|
3
|
+
import { usePortal } from "../../composables/use-portal.js";
|
|
3
4
|
import { useLocale } from "../../composables/use-locale.js";
|
|
4
5
|
import { provideToastRegionContext } from "./toast.context.js";
|
|
5
6
|
import { DEFAULT_SCALE_FACTOR, EXIT_FALLBACK } from "./toast.constants.js";
|
|
@@ -115,7 +116,8 @@ var toast_provider_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@_
|
|
|
115
116
|
slots,
|
|
116
117
|
visibleToasts
|
|
117
118
|
});
|
|
118
|
-
const
|
|
119
|
+
const portal = usePortal();
|
|
120
|
+
const target = computed(() => props.portalContainer ?? portal.container.value);
|
|
119
121
|
const styles = computed(() => composeSlotClassName(slots.value.region, props.class));
|
|
120
122
|
const regionStyle = computed(() => ({
|
|
121
123
|
"--gap": `${props.gap}px`,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dataAttr } from "../../utils/assertion.js";
|
|
2
|
+
import { usePortal } from "../../composables/use-portal.js";
|
|
2
3
|
import { useEnterExit } from "../../composables/use-enter-exit.js";
|
|
3
4
|
import { provideOverlayArrowContext } from "../overlay/overlay.context.js";
|
|
4
5
|
import { useOverlayPosition } from "../../composables/use-overlay-position.js";
|
|
@@ -38,6 +39,7 @@ var tooltip_content_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@
|
|
|
38
39
|
const { shouldSkipAnimation, slots, state, tooltipId, triggerElement } = useTooltipContext();
|
|
39
40
|
const element = shallowRef(null);
|
|
40
41
|
const arrow = shallowRef(null);
|
|
42
|
+
const portal = usePortal();
|
|
41
43
|
/**
|
|
42
44
|
* Built straight on the positioner rather than on the shared overlay primitive.
|
|
43
45
|
*
|
|
@@ -50,6 +52,7 @@ var tooltip_content_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@
|
|
|
50
52
|
const { arrowStyle, overlayStyle, placement } = useOverlayPosition({
|
|
51
53
|
arrowBoundaryOffset: () => props.arrowBoundaryOffset,
|
|
52
54
|
arrowRef: arrow,
|
|
55
|
+
boundaryElement: () => portal.root.value,
|
|
53
56
|
containerPadding: () => props.containerPadding,
|
|
54
57
|
crossOffset: () => props.crossOffset,
|
|
55
58
|
isOpen: () => state.isOpen.value,
|
|
@@ -76,7 +79,7 @@ var tooltip_content_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@
|
|
|
76
79
|
const isExiting = computed(() => props.isExiting ?? (!shouldSkipAnimation.value && enterExit.isExiting.value));
|
|
77
80
|
const isPresent = computed(() => state.isOpen.value || enterExit.isExiting.value || props.isExiting === true);
|
|
78
81
|
const styles = computed(() => slots.value.base({ class: props.class }));
|
|
79
|
-
const target = computed(() => props.portalContainer ??
|
|
82
|
+
const target = computed(() => props.portalContainer ?? portal.container.value);
|
|
80
83
|
const setElement = (next) => {
|
|
81
84
|
element.value = next ?? null;
|
|
82
85
|
};
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
export interface RegisteredScope {
|
|
2
2
|
root: () => HTMLElement | null;
|
|
3
3
|
contain: () => boolean;
|
|
4
|
+
/** The part of the page the scope belongs to, `null` for the whole document. */
|
|
5
|
+
region: () => Element | null;
|
|
4
6
|
}
|
|
5
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The innermost scope holding focus in a region, among the scopes whose region overlaps it.
|
|
9
|
+
*
|
|
10
|
+
* Without a region every scope overlaps every other, which is the page-wide question. With one, a
|
|
11
|
+
* modal in one app's window is not shadowed by a modal another app opened later in its own.
|
|
12
|
+
*/
|
|
13
|
+
export declare const innermostContainingScope: (region?: Element | null) => RegisteredScope | null;
|
|
6
14
|
/**
|
|
7
15
|
* Whether the element sits inside any registered scope.
|
|
8
16
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { regionsOverlap } from "../utils/region.js";
|
|
1
2
|
//#region src/composables/focus-scope-registry.ts
|
|
2
3
|
/**
|
|
3
4
|
* Registered scopes, outermost first.
|
|
@@ -7,10 +8,16 @@
|
|
|
7
8
|
* inside it has to be allowed through rather than pulled back.
|
|
8
9
|
*/
|
|
9
10
|
var scopes = [];
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The innermost scope holding focus in a region, among the scopes whose region overlaps it.
|
|
13
|
+
*
|
|
14
|
+
* Without a region every scope overlaps every other, which is the page-wide question. With one, a
|
|
15
|
+
* modal in one app's window is not shadowed by a modal another app opened later in its own.
|
|
16
|
+
*/
|
|
17
|
+
var innermostContainingScope = (region = null) => {
|
|
11
18
|
for (let index = scopes.length - 1; index >= 0; index--) {
|
|
12
19
|
const scope = scopes[index];
|
|
13
|
-
if (scope.contain()) return scope;
|
|
20
|
+
if (scope.contain() && regionsOverlap(scope.region(), region)) return scope;
|
|
14
21
|
}
|
|
15
22
|
return null;
|
|
16
23
|
};
|
|
@@ -26,6 +26,7 @@ export * from "./use-move.js";
|
|
|
26
26
|
export * from "./use-number-formatter.js";
|
|
27
27
|
export * from "./use-overlay-position.js";
|
|
28
28
|
export * from "./use-overlay-trigger-state.js";
|
|
29
|
+
export * from "./use-portal.js";
|
|
29
30
|
export * from "./use-press.js";
|
|
30
31
|
export * from "./use-prevent-scroll.js";
|
|
31
32
|
export * from "./use-selection-manager.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useId as useId$1 } from "./use-id.js";
|
|
2
2
|
import { ariaHideOutside } from "../utils/aria-hide-outside.js";
|
|
3
|
+
import { usePortal } from "./use-portal.js";
|
|
3
4
|
import { isAppleDevice } from "../utils/platform.js";
|
|
4
5
|
import { useOverlayTrigger } from "./use-overlay-trigger.js";
|
|
5
6
|
import { usePress } from "./use-press.js";
|
|
@@ -221,6 +222,7 @@ var useComboBox = (options, state) => {
|
|
|
221
222
|
flush: "post",
|
|
222
223
|
immediate: true
|
|
223
224
|
});
|
|
225
|
+
const portal = usePortal();
|
|
224
226
|
/**
|
|
225
227
|
* Everything but the field and its options is taken out of the accessibility tree while the
|
|
226
228
|
* popover is open.
|
|
@@ -232,10 +234,11 @@ var useComboBox = (options, state) => {
|
|
|
232
234
|
watch([
|
|
233
235
|
() => state.isOpen.value,
|
|
234
236
|
inputElement,
|
|
235
|
-
() => toValue(options.popoverElement)
|
|
236
|
-
|
|
237
|
+
() => toValue(options.popoverElement),
|
|
238
|
+
() => portal.root.value
|
|
239
|
+
], ([isOpen, input, popover, root], _previous, onCleanup) => {
|
|
237
240
|
if (!isOpen || !popover) return;
|
|
238
|
-
onCleanup(ariaHideOutside([input, popover].filter((element) => element != null)));
|
|
241
|
+
onCleanup(ariaHideOutside([input, popover].filter((element) => element != null), { root: root ?? void 0 }));
|
|
239
242
|
}, {
|
|
240
243
|
flush: "post",
|
|
241
244
|
immediate: true
|
|
@@ -23,6 +23,12 @@ export interface UseDismissableOptions {
|
|
|
23
23
|
* should be left alone — the trigger of an already-open submenu, for one.
|
|
24
24
|
*/
|
|
25
25
|
shouldCloseOnInteractOutside?: (element: Element) => boolean;
|
|
26
|
+
/**
|
|
27
|
+
* The part of the page the overlay belongs to. A press outside it — in another app's window
|
|
28
|
+
* beside this one — is neither an outside interaction nor swallowed, and the overlay is only
|
|
29
|
+
* stacked with overlays whose region overlaps its own. `null` or absent: the whole document.
|
|
30
|
+
*/
|
|
31
|
+
region?: MaybeRefOrGetter<Element | null | undefined>;
|
|
26
32
|
}
|
|
27
33
|
export interface UseDismissableReturn {
|
|
28
34
|
/** Bind to the overlay so Escape dismisses it. */
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
+
import { regionsOverlap } from "../utils/region.js";
|
|
1
2
|
import { isInTopLayer } from "../utils/top-layer.js";
|
|
2
3
|
import { isElementInAnyFocusScope } from "./focus-scope-registry.js";
|
|
3
4
|
import { computed, onScopeDispose, toValue, watch } from "vue";
|
|
4
5
|
//#region src/composables/use-dismissable.ts
|
|
5
|
-
/**
|
|
6
|
-
* Open overlays, innermost last.
|
|
7
|
-
*
|
|
8
|
-
* Module-level on purpose: a click outside must dismiss exactly one overlay — the innermost —
|
|
9
|
-
* and no single overlay can know whether another is nested inside it. React Aria keeps the same
|
|
10
|
-
* list for the same reason.
|
|
11
|
-
*/
|
|
12
6
|
var visibleOverlays = [];
|
|
13
7
|
/**
|
|
14
8
|
* Dismiss an overlay on Escape or on an interaction outside it, ported from React Aria's
|
|
@@ -37,11 +31,13 @@ var visibleOverlays = [];
|
|
|
37
31
|
* ```
|
|
38
32
|
*/
|
|
39
33
|
var useDismissable = (options) => {
|
|
40
|
-
const layer = {};
|
|
34
|
+
const layer = { region: () => toValue(options.region) ?? null };
|
|
41
35
|
const isOpen = computed(() => Boolean(toValue(options.isOpen)));
|
|
42
36
|
const isDismissable = computed(() => Boolean(toValue(options.isDismissable)));
|
|
43
37
|
const getOverlay = () => toValue(options.overlayRef) ?? null;
|
|
44
|
-
|
|
38
|
+
/** The innermost open overlay this one is stacked with — possibly itself. */
|
|
39
|
+
const innermost = () => visibleOverlays.findLast((other) => regionsOverlap(other.region(), layer.region()));
|
|
40
|
+
const isTopMost = () => innermost() === layer;
|
|
45
41
|
const close = () => {
|
|
46
42
|
if (isTopMost()) options.onClose?.();
|
|
47
43
|
};
|
|
@@ -61,6 +57,8 @@ var useDismissable = (options) => {
|
|
|
61
57
|
const target = event.target;
|
|
62
58
|
if (!(target instanceof Element) || !target.ownerDocument.documentElement.contains(target)) return false;
|
|
63
59
|
if (isInTopLayer(target)) return false;
|
|
60
|
+
const region = layer.region();
|
|
61
|
+
if (region && !region.contains(target)) return false;
|
|
64
62
|
return !event.composedPath().includes(overlay);
|
|
65
63
|
};
|
|
66
64
|
/** Whether the pointerdown that began this interaction was outside. */
|
|
@@ -72,7 +70,7 @@ var useDismissable = (options) => {
|
|
|
72
70
|
if (!isDismissable.value || !isOpen.value || typeof document === "undefined") return;
|
|
73
71
|
const onPointerdown = (event) => {
|
|
74
72
|
if (!isOutside(event)) return;
|
|
75
|
-
layerAtPointerDown =
|
|
73
|
+
layerAtPointerDown = innermost();
|
|
76
74
|
if (options.shouldCloseOnInteractOutside && !options.shouldCloseOnInteractOutside(event.target)) return;
|
|
77
75
|
pointerDownWasOutside = true;
|
|
78
76
|
if (isTopMost()) event.stopPropagation();
|
|
@@ -23,6 +23,12 @@ export interface UseFocusScopeOptions {
|
|
|
23
23
|
* @default false
|
|
24
24
|
*/
|
|
25
25
|
autoFocus?: MaybeRefOrGetter<boolean | "first" | "last" | undefined>;
|
|
26
|
+
/**
|
|
27
|
+
* The part of the page containment holds focus within. Focus moving to something outside it —
|
|
28
|
+
* another app's window beside this one — is left alone rather than pulled back, and Tab pressed
|
|
29
|
+
* out there is not taken over. `null` or absent: the whole document.
|
|
30
|
+
*/
|
|
31
|
+
region?: MaybeRefOrGetter<Element | null | undefined>;
|
|
26
32
|
}
|
|
27
33
|
/**
|
|
28
34
|
* Keep focus inside an overlay and give it back when the overlay closes, ported from React
|
|
@@ -31,8 +31,14 @@ var useFocusScope = (options) => {
|
|
|
31
31
|
const getRoot = () => toValue(options.scopeRef) ?? null;
|
|
32
32
|
const scope = {
|
|
33
33
|
contain: () => Boolean(toValue(options.contain)),
|
|
34
|
+
region: () => toValue(options.region) ?? null,
|
|
34
35
|
root: getRoot
|
|
35
36
|
};
|
|
37
|
+
/** Whether an element is somewhere this scope has any say over. */
|
|
38
|
+
const isInRegion = (element) => {
|
|
39
|
+
const region = scope.region();
|
|
40
|
+
return !region || !element || region.contains(element);
|
|
41
|
+
};
|
|
36
42
|
const focusElement = (element) => {
|
|
37
43
|
element?.focus({ preventScroll: true });
|
|
38
44
|
};
|
|
@@ -49,7 +55,8 @@ var useFocusScope = (options) => {
|
|
|
49
55
|
const ownerDocument = root.ownerDocument;
|
|
50
56
|
const onKeydown = (event) => {
|
|
51
57
|
if (event.key !== "Tab" || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
52
|
-
if (innermostContainingScope() !== scope) return;
|
|
58
|
+
if (innermostContainingScope(scope.region()) !== scope) return;
|
|
59
|
+
if (!isInRegion(ownerDocument.activeElement)) return;
|
|
53
60
|
const focusable = tabbableIn(root);
|
|
54
61
|
if (focusable.length === 0) {
|
|
55
62
|
event.preventDefault();
|
|
@@ -62,9 +69,10 @@ var useFocusScope = (options) => {
|
|
|
62
69
|
focusElement(next);
|
|
63
70
|
};
|
|
64
71
|
const onFocusin = (event) => {
|
|
65
|
-
if (innermostContainingScope() !== scope) return;
|
|
72
|
+
if (innermostContainingScope(scope.region()) !== scope) return;
|
|
66
73
|
const target = event.target;
|
|
67
74
|
if (!(target instanceof Element)) return;
|
|
75
|
+
if (!isInRegion(target)) return;
|
|
68
76
|
if (isElementInAnyFocusScope(target)) return;
|
|
69
77
|
focusElement(root);
|
|
70
78
|
};
|
|
@@ -2,6 +2,7 @@ import { ariaHideOutside } from "../utils/aria-hide-outside.js";
|
|
|
2
2
|
import { useDismissable } from "./use-dismissable.js";
|
|
3
3
|
import { useFocusScope } from "./use-focus-scope.js";
|
|
4
4
|
import { usePageSize } from "./use-page-size.js";
|
|
5
|
+
import { usePortal } from "./use-portal.js";
|
|
5
6
|
import { usePreventScroll } from "./use-prevent-scroll.js";
|
|
6
7
|
import { useViewportSize } from "./use-viewport-size.js";
|
|
7
8
|
import { computed, toValue, watch } from "vue";
|
|
@@ -35,17 +36,26 @@ import { computed, toValue, watch } from "vue";
|
|
|
35
36
|
*/
|
|
36
37
|
var useModalOverlay = (options) => {
|
|
37
38
|
const isOpen = () => toValue(options.isOpen);
|
|
39
|
+
const portal = usePortal();
|
|
38
40
|
const dismissable = useDismissable({
|
|
39
41
|
isDismissable: options.isDismissable,
|
|
40
42
|
isKeyboardDismissDisabled: options.isKeyboardDismissDisabled,
|
|
41
43
|
isOpen,
|
|
42
44
|
onClose: options.onClose,
|
|
43
45
|
overlayRef: options.modalRef,
|
|
46
|
+
region: () => portal.root.value,
|
|
44
47
|
shouldCloseOnInteractOutside: options.shouldCloseOnInteractOutside
|
|
45
48
|
});
|
|
46
|
-
watch([
|
|
49
|
+
watch([
|
|
50
|
+
isOpen,
|
|
51
|
+
() => toValue(options.modalRef),
|
|
52
|
+
() => portal.root.value
|
|
53
|
+
], ([open, modal, root], _previous, onCleanup) => {
|
|
47
54
|
if (!open || !modal) return;
|
|
48
|
-
onCleanup(ariaHideOutside([modal], {
|
|
55
|
+
onCleanup(ariaHideOutside([modal], {
|
|
56
|
+
root: root ?? void 0,
|
|
57
|
+
shouldUseInert: true
|
|
58
|
+
}));
|
|
49
59
|
}, {
|
|
50
60
|
flush: "post",
|
|
51
61
|
immediate: true
|
|
@@ -53,6 +63,7 @@ var useModalOverlay = (options) => {
|
|
|
53
63
|
useFocusScope({
|
|
54
64
|
contain: true,
|
|
55
65
|
isActive: isOpen,
|
|
66
|
+
region: () => portal.root.value,
|
|
56
67
|
restoreFocus: true,
|
|
57
68
|
scopeRef: options.modalRef
|
|
58
69
|
});
|
|
@@ -64,6 +75,7 @@ var useModalOverlay = (options) => {
|
|
|
64
75
|
page: computed(() => page.value),
|
|
65
76
|
viewport: computed(() => viewport.value),
|
|
66
77
|
viewportStyle: computed(() => {
|
|
78
|
+
if (portal.isContained.value) return {};
|
|
67
79
|
const style = {
|
|
68
80
|
"--visual-viewport-height": `${viewport.value.height}px`,
|
|
69
81
|
"--visual-viewport-width": `${viewport.value.width}px`
|
|
@@ -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 };
|