@zag-js/popover 0.9.2 → 0.10.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/{chunk-KWS6ZCAX.mjs → chunk-EDQUDEDS.mjs} +3 -2
- package/dist/index.js +3 -2
- package/dist/index.mjs +1 -1
- package/dist/popover.machine.js +3 -2
- package/dist/popover.machine.mjs +1 -1
- package/package.json +14 -13
- package/src/index.ts +4 -0
- package/src/popover.anatomy.ts +14 -0
- package/src/popover.connect.ts +119 -0
- package/src/popover.dom.ts +39 -0
- package/src/popover.machine.ts +222 -0
- package/src/popover.types.ts +106 -0
|
@@ -168,6 +168,7 @@ function machine(userContext) {
|
|
|
168
168
|
trap = createFocusTrap(el, {
|
|
169
169
|
escapeDeactivates: false,
|
|
170
170
|
allowOutsideClick: true,
|
|
171
|
+
preventScroll: true,
|
|
171
172
|
returnFocusOnDeactivate: true,
|
|
172
173
|
document: dom.getDoc(ctx2),
|
|
173
174
|
fallbackFocus: el,
|
|
@@ -202,14 +203,14 @@ function machine(userContext) {
|
|
|
202
203
|
},
|
|
203
204
|
setInitialFocus(ctx2) {
|
|
204
205
|
raf(() => {
|
|
205
|
-
dom.getInitialFocusEl(ctx2)?.focus();
|
|
206
|
+
dom.getInitialFocusEl(ctx2)?.focus({ preventScroll: true });
|
|
206
207
|
});
|
|
207
208
|
},
|
|
208
209
|
restoreFocusIfNeeded(ctx2, evt) {
|
|
209
210
|
if (!evt.restoreFocus)
|
|
210
211
|
return;
|
|
211
212
|
raf(() => {
|
|
212
|
-
dom.getTriggerEl(ctx2)?.focus();
|
|
213
|
+
dom.getTriggerEl(ctx2)?.focus({ preventScroll: true });
|
|
213
214
|
});
|
|
214
215
|
},
|
|
215
216
|
invokeOnOpen(ctx2) {
|
package/dist/index.js
CHANGED
|
@@ -348,6 +348,7 @@ function machine(userContext) {
|
|
|
348
348
|
trap = (0, import_focus_trap.createFocusTrap)(el, {
|
|
349
349
|
escapeDeactivates: false,
|
|
350
350
|
allowOutsideClick: true,
|
|
351
|
+
preventScroll: true,
|
|
351
352
|
returnFocusOnDeactivate: true,
|
|
352
353
|
document: dom.getDoc(ctx2),
|
|
353
354
|
fallbackFocus: el,
|
|
@@ -382,14 +383,14 @@ function machine(userContext) {
|
|
|
382
383
|
},
|
|
383
384
|
setInitialFocus(ctx2) {
|
|
384
385
|
(0, import_dom_query3.raf)(() => {
|
|
385
|
-
dom.getInitialFocusEl(ctx2)?.focus();
|
|
386
|
+
dom.getInitialFocusEl(ctx2)?.focus({ preventScroll: true });
|
|
386
387
|
});
|
|
387
388
|
},
|
|
388
389
|
restoreFocusIfNeeded(ctx2, evt) {
|
|
389
390
|
if (!evt.restoreFocus)
|
|
390
391
|
return;
|
|
391
392
|
(0, import_dom_query3.raf)(() => {
|
|
392
|
-
dom.getTriggerEl(ctx2)?.focus();
|
|
393
|
+
dom.getTriggerEl(ctx2)?.focus({ preventScroll: true });
|
|
393
394
|
});
|
|
394
395
|
},
|
|
395
396
|
invokeOnOpen(ctx2) {
|
package/dist/index.mjs
CHANGED
package/dist/popover.machine.js
CHANGED
|
@@ -226,6 +226,7 @@ function machine(userContext) {
|
|
|
226
226
|
trap = (0, import_focus_trap.createFocusTrap)(el, {
|
|
227
227
|
escapeDeactivates: false,
|
|
228
228
|
allowOutsideClick: true,
|
|
229
|
+
preventScroll: true,
|
|
229
230
|
returnFocusOnDeactivate: true,
|
|
230
231
|
document: dom.getDoc(ctx2),
|
|
231
232
|
fallbackFocus: el,
|
|
@@ -260,14 +261,14 @@ function machine(userContext) {
|
|
|
260
261
|
},
|
|
261
262
|
setInitialFocus(ctx2) {
|
|
262
263
|
(0, import_dom_query2.raf)(() => {
|
|
263
|
-
dom.getInitialFocusEl(ctx2)?.focus();
|
|
264
|
+
dom.getInitialFocusEl(ctx2)?.focus({ preventScroll: true });
|
|
264
265
|
});
|
|
265
266
|
},
|
|
266
267
|
restoreFocusIfNeeded(ctx2, evt) {
|
|
267
268
|
if (!evt.restoreFocus)
|
|
268
269
|
return;
|
|
269
270
|
(0, import_dom_query2.raf)(() => {
|
|
270
|
-
dom.getTriggerEl(ctx2)?.focus();
|
|
271
|
+
dom.getTriggerEl(ctx2)?.focus({ preventScroll: true });
|
|
271
272
|
});
|
|
272
273
|
},
|
|
273
274
|
invokeOnOpen(ctx2) {
|
package/dist/popover.machine.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zag-js/popover",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Core logic for the popover widget implemented as a state machine",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"js",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"repository": "https://github.com/chakra-ui/zag/tree/main/packages/popover",
|
|
18
18
|
"sideEffects": false,
|
|
19
19
|
"files": [
|
|
20
|
-
"dist
|
|
20
|
+
"dist",
|
|
21
|
+
"src"
|
|
21
22
|
],
|
|
22
23
|
"publishConfig": {
|
|
23
24
|
"access": "public"
|
|
@@ -26,17 +27,17 @@
|
|
|
26
27
|
"url": "https://github.com/chakra-ui/zag/issues"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"focus-trap": "7.4.
|
|
30
|
-
"@zag-js/anatomy": "0.
|
|
31
|
-
"@zag-js/aria-hidden": "0.
|
|
32
|
-
"@zag-js/core": "0.
|
|
33
|
-
"@zag-js/dom-query": "0.
|
|
34
|
-
"@zag-js/utils": "0.
|
|
35
|
-
"@zag-js/dismissable": "0.
|
|
36
|
-
"@zag-js/tabbable": "0.
|
|
37
|
-
"@zag-js/popper": "0.
|
|
38
|
-
"@zag-js/remove-scroll": "0.
|
|
39
|
-
"@zag-js/types": "0.
|
|
30
|
+
"focus-trap": "7.4.3",
|
|
31
|
+
"@zag-js/anatomy": "0.10.0",
|
|
32
|
+
"@zag-js/aria-hidden": "0.10.0",
|
|
33
|
+
"@zag-js/core": "0.10.0",
|
|
34
|
+
"@zag-js/dom-query": "0.10.0",
|
|
35
|
+
"@zag-js/utils": "0.10.0",
|
|
36
|
+
"@zag-js/dismissable": "0.10.0",
|
|
37
|
+
"@zag-js/tabbable": "0.10.0",
|
|
38
|
+
"@zag-js/popper": "0.10.0",
|
|
39
|
+
"@zag-js/remove-scroll": "0.10.0",
|
|
40
|
+
"@zag-js/types": "0.10.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"clean-package": "2.2.0"
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createAnatomy } from "@zag-js/anatomy"
|
|
2
|
+
|
|
3
|
+
export const anatomy = createAnatomy("popover").parts(
|
|
4
|
+
"arrow",
|
|
5
|
+
"arrowTip",
|
|
6
|
+
"anchor",
|
|
7
|
+
"trigger",
|
|
8
|
+
"positioner",
|
|
9
|
+
"content",
|
|
10
|
+
"title",
|
|
11
|
+
"description",
|
|
12
|
+
"closeTrigger",
|
|
13
|
+
)
|
|
14
|
+
export const parts = anatomy.build()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { dataAttr } from "@zag-js/dom-query"
|
|
2
|
+
import type { PositioningOptions } from "@zag-js/popper"
|
|
3
|
+
import { getPlacementStyles } from "@zag-js/popper"
|
|
4
|
+
import type { NormalizeProps, PropTypes } from "@zag-js/types"
|
|
5
|
+
import { parts } from "./popover.anatomy"
|
|
6
|
+
import { dom } from "./popover.dom"
|
|
7
|
+
import type { Send, State } from "./popover.types"
|
|
8
|
+
|
|
9
|
+
export function connect<T extends PropTypes>(state: State, send: Send, normalize: NormalizeProps<T>) {
|
|
10
|
+
const isOpen = state.matches("open")
|
|
11
|
+
|
|
12
|
+
const currentPlacement = state.context.currentPlacement
|
|
13
|
+
const portalled = state.context.currentPortalled
|
|
14
|
+
const rendered = state.context.renderedElements
|
|
15
|
+
|
|
16
|
+
const popperStyles = getPlacementStyles({
|
|
17
|
+
placement: currentPlacement,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
/**
|
|
22
|
+
* Whether the popover is portalled
|
|
23
|
+
*/
|
|
24
|
+
portalled,
|
|
25
|
+
/**
|
|
26
|
+
* Whether the popover is open
|
|
27
|
+
*/
|
|
28
|
+
isOpen,
|
|
29
|
+
/**
|
|
30
|
+
* Function to open the popover
|
|
31
|
+
*/
|
|
32
|
+
open() {
|
|
33
|
+
send("OPEN")
|
|
34
|
+
},
|
|
35
|
+
/**
|
|
36
|
+
* Function to close the popover
|
|
37
|
+
*/
|
|
38
|
+
close() {
|
|
39
|
+
send("CLOSE")
|
|
40
|
+
},
|
|
41
|
+
/**
|
|
42
|
+
* Function to reposition the popover
|
|
43
|
+
*/
|
|
44
|
+
setPositioning(options: Partial<PositioningOptions> = {}) {
|
|
45
|
+
send({ type: "SET_POSITIONING", options })
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
arrowProps: normalize.element({
|
|
49
|
+
id: dom.getArrowId(state.context),
|
|
50
|
+
...parts.arrow.attrs,
|
|
51
|
+
style: popperStyles.arrow,
|
|
52
|
+
}),
|
|
53
|
+
|
|
54
|
+
arrowTipProps: normalize.element({
|
|
55
|
+
...parts.arrowTip.attrs,
|
|
56
|
+
style: popperStyles.arrowTip,
|
|
57
|
+
}),
|
|
58
|
+
|
|
59
|
+
anchorProps: normalize.element({
|
|
60
|
+
...parts.anchor.attrs,
|
|
61
|
+
id: dom.getAnchorId(state.context),
|
|
62
|
+
}),
|
|
63
|
+
|
|
64
|
+
triggerProps: normalize.button({
|
|
65
|
+
...parts.trigger.attrs,
|
|
66
|
+
type: "button",
|
|
67
|
+
"data-placement": currentPlacement,
|
|
68
|
+
id: dom.getTriggerId(state.context),
|
|
69
|
+
"aria-haspopup": "dialog",
|
|
70
|
+
"aria-expanded": isOpen,
|
|
71
|
+
"data-expanded": dataAttr(isOpen),
|
|
72
|
+
"aria-controls": dom.getContentId(state.context),
|
|
73
|
+
onClick() {
|
|
74
|
+
send("TOGGLE")
|
|
75
|
+
},
|
|
76
|
+
onBlur(event) {
|
|
77
|
+
send({ type: "TRIGGER_BLUR", target: event.relatedTarget })
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
|
|
81
|
+
positionerProps: normalize.element({
|
|
82
|
+
id: dom.getPositionerId(state.context),
|
|
83
|
+
...parts.positioner.attrs,
|
|
84
|
+
style: popperStyles.floating,
|
|
85
|
+
}),
|
|
86
|
+
|
|
87
|
+
contentProps: normalize.element({
|
|
88
|
+
...parts.content.attrs,
|
|
89
|
+
id: dom.getContentId(state.context),
|
|
90
|
+
tabIndex: -1,
|
|
91
|
+
role: "dialog",
|
|
92
|
+
hidden: !isOpen,
|
|
93
|
+
"data-expanded": dataAttr(isOpen),
|
|
94
|
+
"aria-labelledby": rendered.title ? dom.getTitleId(state.context) : undefined,
|
|
95
|
+
"aria-describedby": rendered.description ? dom.getDescriptionId(state.context) : undefined,
|
|
96
|
+
"data-placement": currentPlacement,
|
|
97
|
+
}),
|
|
98
|
+
|
|
99
|
+
titleProps: normalize.element({
|
|
100
|
+
...parts.title.attrs,
|
|
101
|
+
id: dom.getTitleId(state.context),
|
|
102
|
+
}),
|
|
103
|
+
|
|
104
|
+
descriptionProps: normalize.element({
|
|
105
|
+
...parts.description.attrs,
|
|
106
|
+
id: dom.getDescriptionId(state.context),
|
|
107
|
+
}),
|
|
108
|
+
|
|
109
|
+
closeTriggerProps: normalize.button({
|
|
110
|
+
...parts.closeTrigger.attrs,
|
|
111
|
+
id: dom.getCloseTriggerId(state.context),
|
|
112
|
+
type: "button",
|
|
113
|
+
"aria-label": "close",
|
|
114
|
+
onClick() {
|
|
115
|
+
send("REQUEST_CLOSE")
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createScope } from "@zag-js/dom-query"
|
|
2
|
+
import { getFirstTabbable, getFocusables, getLastTabbable, getTabbables } from "@zag-js/tabbable"
|
|
3
|
+
import { runIfFn } from "@zag-js/utils"
|
|
4
|
+
import type { MachineContext as Ctx } from "./popover.types"
|
|
5
|
+
|
|
6
|
+
export const dom = createScope({
|
|
7
|
+
getActiveEl: (ctx: Ctx) => dom.getDoc(ctx).activeElement,
|
|
8
|
+
|
|
9
|
+
getAnchorId: (ctx: Ctx) => ctx.ids?.anchor ?? `popover:${ctx.id}:anchor`,
|
|
10
|
+
getTriggerId: (ctx: Ctx) => ctx.ids?.trigger ?? `popover:${ctx.id}:trigger`,
|
|
11
|
+
getContentId: (ctx: Ctx) => ctx.ids?.content ?? `popover:${ctx.id}:content`,
|
|
12
|
+
getPositionerId: (ctx: Ctx) => ctx.ids?.positioner ?? `popover:${ctx.id}:popper`,
|
|
13
|
+
getArrowId: (ctx: Ctx) => ctx.ids?.arrow ?? `popover:${ctx.id}:arrow`,
|
|
14
|
+
getTitleId: (ctx: Ctx) => ctx.ids?.title ?? `popover:${ctx.id}:title`,
|
|
15
|
+
getDescriptionId: (ctx: Ctx) => ctx.ids?.description ?? `popover:${ctx.id}:desc`,
|
|
16
|
+
getCloseTriggerId: (ctx: Ctx) => ctx.ids?.closeTrigger ?? `popover:${ctx.id}:close`,
|
|
17
|
+
|
|
18
|
+
getAnchorEl: (ctx: Ctx) => dom.getById(ctx, dom.getAnchorId(ctx)),
|
|
19
|
+
getTriggerEl: (ctx: Ctx) => dom.getById(ctx, dom.getTriggerId(ctx)),
|
|
20
|
+
getContentEl: (ctx: Ctx) => dom.getById(ctx, dom.getContentId(ctx)),
|
|
21
|
+
getPositionerEl: (ctx: Ctx) => dom.getById(ctx, dom.getPositionerId(ctx)),
|
|
22
|
+
getTitleEl: (ctx: Ctx) => dom.getById(ctx, dom.getTitleId(ctx)),
|
|
23
|
+
getDescriptionEl: (ctx: Ctx) => dom.getById(ctx, dom.getDescriptionId(ctx)),
|
|
24
|
+
|
|
25
|
+
getFocusableEls: (ctx: Ctx) => getFocusables(dom.getContentEl(ctx)),
|
|
26
|
+
getFirstFocusableEl: (ctx: Ctx) => dom.getFocusableEls(ctx)[0],
|
|
27
|
+
|
|
28
|
+
getDocTabbableEls: (ctx: Ctx) => getTabbables(dom.getDoc(ctx).body),
|
|
29
|
+
getTabbableEls: (ctx: Ctx) => getTabbables(dom.getContentEl(ctx), "if-empty"),
|
|
30
|
+
getFirstTabbableEl: (ctx: Ctx) => getFirstTabbable(dom.getContentEl(ctx), "if-empty"),
|
|
31
|
+
getLastTabbableEl: (ctx: Ctx) => getLastTabbable(dom.getContentEl(ctx), "if-empty"),
|
|
32
|
+
|
|
33
|
+
getInitialFocusEl: (ctx: Ctx) => {
|
|
34
|
+
let el: HTMLElement | null = runIfFn(ctx.initialFocusEl)
|
|
35
|
+
if (!el && ctx.autoFocus) el = dom.getFirstFocusableEl(ctx)
|
|
36
|
+
if (!el) el = dom.getContentEl(ctx)
|
|
37
|
+
return el
|
|
38
|
+
},
|
|
39
|
+
})
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { ariaHidden } from "@zag-js/aria-hidden"
|
|
2
|
+
import { createMachine } from "@zag-js/core"
|
|
3
|
+
import { trackDismissableElement } from "@zag-js/dismissable"
|
|
4
|
+
import { nextTick, raf } from "@zag-js/dom-query"
|
|
5
|
+
import { getPlacement } from "@zag-js/popper"
|
|
6
|
+
import { preventBodyScroll } from "@zag-js/remove-scroll"
|
|
7
|
+
import { proxyTabFocus } from "@zag-js/tabbable"
|
|
8
|
+
import { compact, runIfFn } from "@zag-js/utils"
|
|
9
|
+
import { FocusTrap, createFocusTrap } from "focus-trap"
|
|
10
|
+
import { dom } from "./popover.dom"
|
|
11
|
+
import type { MachineContext, MachineState, UserDefinedContext } from "./popover.types"
|
|
12
|
+
|
|
13
|
+
export function machine(userContext: UserDefinedContext) {
|
|
14
|
+
const ctx = compact(userContext)
|
|
15
|
+
return createMachine<MachineContext, MachineState>(
|
|
16
|
+
{
|
|
17
|
+
id: "popover",
|
|
18
|
+
initial: ctx.open ? "open" : "closed",
|
|
19
|
+
context: {
|
|
20
|
+
closeOnInteractOutside: true,
|
|
21
|
+
closeOnEsc: true,
|
|
22
|
+
autoFocus: true,
|
|
23
|
+
modal: false,
|
|
24
|
+
positioning: {
|
|
25
|
+
placement: "bottom",
|
|
26
|
+
...ctx.positioning,
|
|
27
|
+
},
|
|
28
|
+
currentPlacement: undefined,
|
|
29
|
+
...ctx,
|
|
30
|
+
renderedElements: {
|
|
31
|
+
title: true,
|
|
32
|
+
description: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
computed: {
|
|
37
|
+
currentPortalled: (ctx) => !!ctx.modal || !!ctx.portalled,
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
watch: {
|
|
41
|
+
open: ["toggleVisibility"],
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
entry: ["checkRenderedElements"],
|
|
45
|
+
|
|
46
|
+
states: {
|
|
47
|
+
closed: {
|
|
48
|
+
on: {
|
|
49
|
+
TOGGLE: {
|
|
50
|
+
target: "open",
|
|
51
|
+
actions: ["invokeOnOpen"],
|
|
52
|
+
},
|
|
53
|
+
OPEN: {
|
|
54
|
+
target: "open",
|
|
55
|
+
actions: ["invokeOnOpen"],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
open: {
|
|
61
|
+
activities: [
|
|
62
|
+
"trapFocus",
|
|
63
|
+
"preventScroll",
|
|
64
|
+
"hideContentBelow",
|
|
65
|
+
"trackPositioning",
|
|
66
|
+
"trackDismissableElement",
|
|
67
|
+
"proxyTabFocus",
|
|
68
|
+
],
|
|
69
|
+
entry: ["setInitialFocus"],
|
|
70
|
+
on: {
|
|
71
|
+
CLOSE: {
|
|
72
|
+
target: "closed",
|
|
73
|
+
actions: ["invokeOnClose"],
|
|
74
|
+
},
|
|
75
|
+
REQUEST_CLOSE: {
|
|
76
|
+
target: "closed",
|
|
77
|
+
actions: ["restoreFocusIfNeeded", "invokeOnClose"],
|
|
78
|
+
},
|
|
79
|
+
TOGGLE: {
|
|
80
|
+
target: "closed",
|
|
81
|
+
actions: ["invokeOnClose"],
|
|
82
|
+
},
|
|
83
|
+
SET_POSITIONING: {
|
|
84
|
+
actions: "setPositioning",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
activities: {
|
|
92
|
+
trackPositioning(ctx) {
|
|
93
|
+
ctx.currentPlacement = ctx.positioning.placement
|
|
94
|
+
const anchorEl = dom.getAnchorEl(ctx) ?? dom.getTriggerEl(ctx)
|
|
95
|
+
const getPositionerEl = () => dom.getPositionerEl(ctx)
|
|
96
|
+
return getPlacement(anchorEl, getPositionerEl, {
|
|
97
|
+
...ctx.positioning,
|
|
98
|
+
defer: true,
|
|
99
|
+
onComplete(data) {
|
|
100
|
+
ctx.currentPlacement = data.placement
|
|
101
|
+
},
|
|
102
|
+
onCleanup() {
|
|
103
|
+
ctx.currentPlacement = undefined
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
},
|
|
107
|
+
trackDismissableElement(ctx, _evt, { send }) {
|
|
108
|
+
const getContentEl = () => dom.getContentEl(ctx)
|
|
109
|
+
let restoreFocus = true
|
|
110
|
+
return trackDismissableElement(getContentEl, {
|
|
111
|
+
pointerBlocking: ctx.modal,
|
|
112
|
+
exclude: dom.getTriggerEl(ctx),
|
|
113
|
+
defer: true,
|
|
114
|
+
onEscapeKeyDown(event) {
|
|
115
|
+
ctx.onEscapeKeyDown?.(event)
|
|
116
|
+
if (ctx.closeOnEsc) return
|
|
117
|
+
event.preventDefault()
|
|
118
|
+
},
|
|
119
|
+
onInteractOutside(event) {
|
|
120
|
+
ctx.onInteractOutside?.(event)
|
|
121
|
+
if (event.defaultPrevented) return
|
|
122
|
+
restoreFocus = !(event.detail.focusable || event.detail.contextmenu)
|
|
123
|
+
if (!ctx.closeOnInteractOutside) {
|
|
124
|
+
event.preventDefault()
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
onPointerDownOutside(event) {
|
|
128
|
+
ctx.onPointerDownOutside?.(event)
|
|
129
|
+
},
|
|
130
|
+
onFocusOutside(event) {
|
|
131
|
+
ctx.onFocusOutside?.(event)
|
|
132
|
+
},
|
|
133
|
+
onDismiss() {
|
|
134
|
+
send({ type: "REQUEST_CLOSE", src: "interact-outside", restoreFocus })
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
},
|
|
138
|
+
proxyTabFocus(ctx) {
|
|
139
|
+
if (ctx.modal || !ctx.portalled) return
|
|
140
|
+
const getContentEl = () => dom.getContentEl(ctx)
|
|
141
|
+
return proxyTabFocus(getContentEl, {
|
|
142
|
+
triggerElement: dom.getTriggerEl(ctx),
|
|
143
|
+
defer: true,
|
|
144
|
+
onFocus(el) {
|
|
145
|
+
el.focus({ preventScroll: true })
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
},
|
|
149
|
+
hideContentBelow(ctx) {
|
|
150
|
+
if (!ctx.modal) return
|
|
151
|
+
const getElements = () => [dom.getContentEl(ctx), dom.getTriggerEl(ctx)]
|
|
152
|
+
return ariaHidden(getElements, { defer: true })
|
|
153
|
+
},
|
|
154
|
+
preventScroll(ctx) {
|
|
155
|
+
if (!ctx.modal) return
|
|
156
|
+
return preventBodyScroll(dom.getDoc(ctx))
|
|
157
|
+
},
|
|
158
|
+
trapFocus(ctx) {
|
|
159
|
+
if (!ctx.modal) return
|
|
160
|
+
let trap: FocusTrap | undefined
|
|
161
|
+
nextTick(() => {
|
|
162
|
+
const el = dom.getContentEl(ctx)
|
|
163
|
+
if (!el) return
|
|
164
|
+
trap = createFocusTrap(el, {
|
|
165
|
+
escapeDeactivates: false,
|
|
166
|
+
allowOutsideClick: true,
|
|
167
|
+
preventScroll: true,
|
|
168
|
+
returnFocusOnDeactivate: true,
|
|
169
|
+
document: dom.getDoc(ctx),
|
|
170
|
+
fallbackFocus: el,
|
|
171
|
+
initialFocus: runIfFn(ctx.initialFocusEl),
|
|
172
|
+
})
|
|
173
|
+
try {
|
|
174
|
+
trap.activate()
|
|
175
|
+
} catch {}
|
|
176
|
+
})
|
|
177
|
+
return () => trap?.deactivate()
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
actions: {
|
|
181
|
+
setPositioning(ctx, evt) {
|
|
182
|
+
const anchorEl = dom.getAnchorEl(ctx) ?? dom.getTriggerEl(ctx)
|
|
183
|
+
const getPositionerEl = () => dom.getPositionerEl(ctx)
|
|
184
|
+
getPlacement(anchorEl, getPositionerEl, {
|
|
185
|
+
...ctx.positioning,
|
|
186
|
+
...evt.options,
|
|
187
|
+
defer: true,
|
|
188
|
+
listeners: false,
|
|
189
|
+
})
|
|
190
|
+
},
|
|
191
|
+
checkRenderedElements(ctx) {
|
|
192
|
+
raf(() => {
|
|
193
|
+
Object.assign(ctx.renderedElements, {
|
|
194
|
+
title: !!dom.getTitleEl(ctx),
|
|
195
|
+
description: !!dom.getDescriptionEl(ctx),
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
},
|
|
199
|
+
setInitialFocus(ctx) {
|
|
200
|
+
raf(() => {
|
|
201
|
+
dom.getInitialFocusEl(ctx)?.focus({ preventScroll: true })
|
|
202
|
+
})
|
|
203
|
+
},
|
|
204
|
+
restoreFocusIfNeeded(ctx, evt) {
|
|
205
|
+
if (!evt.restoreFocus) return
|
|
206
|
+
raf(() => {
|
|
207
|
+
dom.getTriggerEl(ctx)?.focus({ preventScroll: true })
|
|
208
|
+
})
|
|
209
|
+
},
|
|
210
|
+
invokeOnOpen(ctx) {
|
|
211
|
+
ctx.onOpen?.()
|
|
212
|
+
},
|
|
213
|
+
invokeOnClose(ctx) {
|
|
214
|
+
ctx.onClose?.()
|
|
215
|
+
},
|
|
216
|
+
toggleVisibility(ctx, _evt, { send }) {
|
|
217
|
+
send({ type: ctx.open ? "OPEN" : "CLOSE", src: "controlled" })
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { StateMachine as S } from "@zag-js/core"
|
|
2
|
+
import type { DismissableElementHandlers } from "@zag-js/dismissable"
|
|
3
|
+
import type { PositioningOptions, Placement } from "@zag-js/popper"
|
|
4
|
+
import type { CommonProperties, Context, MaybeElement, RequiredBy } from "@zag-js/types"
|
|
5
|
+
|
|
6
|
+
type ElementIds = Partial<{
|
|
7
|
+
anchor: string
|
|
8
|
+
trigger: string
|
|
9
|
+
content: string
|
|
10
|
+
title: string
|
|
11
|
+
description: string
|
|
12
|
+
closeTrigger: string
|
|
13
|
+
positioner: string
|
|
14
|
+
arrow: string
|
|
15
|
+
}>
|
|
16
|
+
|
|
17
|
+
type PublicContext = DismissableElementHandlers &
|
|
18
|
+
CommonProperties & {
|
|
19
|
+
/**
|
|
20
|
+
* The ids of the elements in the popover. Useful for composition.
|
|
21
|
+
*/
|
|
22
|
+
ids?: ElementIds
|
|
23
|
+
/**
|
|
24
|
+
* Whether the popover should be modal. When set to `true`:
|
|
25
|
+
* - interaction with outside elements will be disabled
|
|
26
|
+
* - only popover content will be visible to screen readers
|
|
27
|
+
* - scrolling is blocked
|
|
28
|
+
* - focus is trapped within the popover
|
|
29
|
+
*
|
|
30
|
+
* @default false
|
|
31
|
+
*/
|
|
32
|
+
modal?: boolean
|
|
33
|
+
/**
|
|
34
|
+
* Whether the popover is rendered in a portal
|
|
35
|
+
*/
|
|
36
|
+
portalled?: boolean
|
|
37
|
+
/**
|
|
38
|
+
* Whether to automatically set focus on the first focusable
|
|
39
|
+
* content within the popover when opened.
|
|
40
|
+
*/
|
|
41
|
+
autoFocus?: boolean
|
|
42
|
+
/**
|
|
43
|
+
* The element to focus on when the popover is opened.
|
|
44
|
+
*/
|
|
45
|
+
initialFocusEl?: MaybeElement | (() => MaybeElement)
|
|
46
|
+
/**
|
|
47
|
+
* Whether to close the popover when the user clicks outside of the popover.
|
|
48
|
+
*/
|
|
49
|
+
closeOnInteractOutside?: boolean
|
|
50
|
+
/**
|
|
51
|
+
* Whether to close the popover when the escape key is pressed.
|
|
52
|
+
*/
|
|
53
|
+
closeOnEsc?: boolean
|
|
54
|
+
/**
|
|
55
|
+
* Function invoked when the popover is closed
|
|
56
|
+
*/
|
|
57
|
+
onClose?: VoidFunction
|
|
58
|
+
/**
|
|
59
|
+
* Function invoked when the popover is opened
|
|
60
|
+
*/
|
|
61
|
+
onOpen?: VoidFunction
|
|
62
|
+
/**
|
|
63
|
+
* The user provided options used to position the popover content
|
|
64
|
+
*/
|
|
65
|
+
positioning: PositioningOptions
|
|
66
|
+
/**
|
|
67
|
+
* Whether the popover is open
|
|
68
|
+
*/
|
|
69
|
+
open?: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type UserDefinedContext = RequiredBy<PublicContext, "id">
|
|
73
|
+
|
|
74
|
+
type ComputedContext = Readonly<{
|
|
75
|
+
/**
|
|
76
|
+
* @computed
|
|
77
|
+
* The computed value of `portalled`
|
|
78
|
+
*/
|
|
79
|
+
currentPortalled: boolean
|
|
80
|
+
}>
|
|
81
|
+
|
|
82
|
+
type PrivateContext = Context<{
|
|
83
|
+
/**
|
|
84
|
+
* @internal
|
|
85
|
+
* The elements that are rendered on mount
|
|
86
|
+
*/
|
|
87
|
+
renderedElements: {
|
|
88
|
+
title: boolean
|
|
89
|
+
description: boolean
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* @internal
|
|
93
|
+
* The computed placement (maybe different from initial placement)
|
|
94
|
+
*/
|
|
95
|
+
currentPlacement?: Placement
|
|
96
|
+
}>
|
|
97
|
+
|
|
98
|
+
export type MachineContext = PublicContext & ComputedContext & PrivateContext
|
|
99
|
+
|
|
100
|
+
export type MachineState = {
|
|
101
|
+
value: "open" | "closed"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type State = S.State<MachineContext, MachineState>
|
|
105
|
+
|
|
106
|
+
export type Send = S.Send<S.AnyEventObject>
|