@zag-js/popper 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/package.json +7 -6
- package/src/auto-update.ts +67 -0
- package/src/get-placement.ts +156 -0
- package/src/get-styles.ts +45 -0
- package/src/index.ts +3 -0
- package/src/middleware.ts +78 -0
- package/src/types.ts +62 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zag-js/popper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Dynamic positioning logic for ui machines",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"js",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"repository": "https://github.com/chakra-ui/zag/tree/main/packages/utilities/popper",
|
|
14
14
|
"sideEffects": false,
|
|
15
15
|
"files": [
|
|
16
|
-
"dist
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
17
18
|
],
|
|
18
19
|
"publishConfig": {
|
|
19
20
|
"access": "public"
|
|
@@ -22,10 +23,10 @@
|
|
|
22
23
|
"url": "https://github.com/chakra-ui/zag/issues"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
25
|
-
"@floating-ui/dom": "1.2.
|
|
26
|
-
"@zag-js/dom-query": "0.
|
|
27
|
-
"@zag-js/element-rect": "0.
|
|
28
|
-
"@zag-js/utils": "0.
|
|
26
|
+
"@floating-ui/dom": "1.2.9",
|
|
27
|
+
"@zag-js/dom-query": "0.10.0",
|
|
28
|
+
"@zag-js/element-rect": "0.10.0",
|
|
29
|
+
"@zag-js/utils": "0.10.0"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"clean-package": "2.2.0"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Placement, ReferenceElement } from "@floating-ui/dom"
|
|
2
|
+
import { getOverflowAncestors } from "@floating-ui/dom"
|
|
3
|
+
import { trackElementRect } from "@zag-js/element-rect"
|
|
4
|
+
|
|
5
|
+
export type { Placement }
|
|
6
|
+
|
|
7
|
+
export type AutoUpdateOptions = {
|
|
8
|
+
ancestorScroll?: boolean
|
|
9
|
+
ancestorResize?: boolean
|
|
10
|
+
referenceResize?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Ancestors = ReturnType<typeof getOverflowAncestors>
|
|
14
|
+
|
|
15
|
+
const callAll =
|
|
16
|
+
(...fns: VoidFunction[]) =>
|
|
17
|
+
() =>
|
|
18
|
+
fns.forEach((fn) => fn())
|
|
19
|
+
|
|
20
|
+
const isHTMLElement = (el: any): el is HTMLElement => {
|
|
21
|
+
return typeof el === "object" && el !== null && el.nodeType === 1
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const addDomEvent = (el: HTMLElement, type: string, fn: VoidFunction, options?: boolean | AddEventListenerOptions) => {
|
|
25
|
+
el.addEventListener(type, fn, options)
|
|
26
|
+
return () => el.removeEventListener(type, fn, options)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveOptions(option: boolean | AutoUpdateOptions) {
|
|
30
|
+
const bool = typeof option === "boolean"
|
|
31
|
+
return {
|
|
32
|
+
ancestorResize: bool ? option : option.ancestorResize ?? true,
|
|
33
|
+
ancestorScroll: bool ? option : option.ancestorScroll ?? true,
|
|
34
|
+
referenceResize: bool ? option : option.referenceResize ?? true,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function autoUpdate(
|
|
39
|
+
reference: ReferenceElement,
|
|
40
|
+
floating: HTMLElement,
|
|
41
|
+
update: () => void,
|
|
42
|
+
options: boolean | AutoUpdateOptions = false,
|
|
43
|
+
) {
|
|
44
|
+
const { ancestorScroll, ancestorResize, referenceResize } = resolveOptions(options)
|
|
45
|
+
|
|
46
|
+
const useAncestors = ancestorScroll || ancestorResize
|
|
47
|
+
const ancestors: Ancestors = []
|
|
48
|
+
|
|
49
|
+
if (useAncestors && isHTMLElement(reference)) {
|
|
50
|
+
ancestors.push(...getOverflowAncestors(reference))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function addResizeListeners() {
|
|
54
|
+
let cleanups: VoidFunction[] = [trackElementRect(floating, { scope: "size", onChange: update })]
|
|
55
|
+
if (referenceResize && isHTMLElement(reference)) {
|
|
56
|
+
cleanups.push(trackElementRect(reference, { onChange: update }))
|
|
57
|
+
}
|
|
58
|
+
cleanups.push(callAll(...ancestors.map((el: any) => addDomEvent(el, "resize", update))))
|
|
59
|
+
return () => cleanups.forEach((fn) => fn())
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function addScrollListeners() {
|
|
63
|
+
return callAll(...ancestors.map((el: any) => addDomEvent(el, "scroll", update, { passive: true })))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return callAll(addResizeListeners(), addScrollListeners())
|
|
67
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { Middleware, Placement, VirtualElement } from "@floating-ui/dom"
|
|
2
|
+
import { ComputePositionConfig, arrow, computePosition, flip, offset, shift, size } from "@floating-ui/dom"
|
|
3
|
+
import { raf } from "@zag-js/dom-query"
|
|
4
|
+
import { callAll } from "@zag-js/utils"
|
|
5
|
+
import { autoUpdate } from "./auto-update"
|
|
6
|
+
import { shiftArrow, transformOrigin } from "./middleware"
|
|
7
|
+
import type { BasePlacement, PositioningOptions } from "./types"
|
|
8
|
+
|
|
9
|
+
const defaultOptions: PositioningOptions = {
|
|
10
|
+
strategy: "absolute",
|
|
11
|
+
placement: "bottom",
|
|
12
|
+
listeners: true,
|
|
13
|
+
gutter: 8,
|
|
14
|
+
flip: true,
|
|
15
|
+
sameWidth: false,
|
|
16
|
+
overflowPadding: 8,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type MaybeRectElement = HTMLElement | VirtualElement | null
|
|
20
|
+
type MaybeElement = HTMLElement | null
|
|
21
|
+
type MaybeFn<T> = T | (() => T)
|
|
22
|
+
|
|
23
|
+
function getPlacementImpl(reference: MaybeRectElement, floating: MaybeElement, opts: PositioningOptions = {}) {
|
|
24
|
+
if (!floating || !reference) return
|
|
25
|
+
|
|
26
|
+
const options = Object.assign({}, defaultOptions, opts)
|
|
27
|
+
|
|
28
|
+
/* -----------------------------------------------------------------------------
|
|
29
|
+
* The middleware stack
|
|
30
|
+
* -----------------------------------------------------------------------------*/
|
|
31
|
+
|
|
32
|
+
const arrowEl = floating.querySelector<HTMLElement>("[data-part=arrow]")
|
|
33
|
+
const middleware: Middleware[] = []
|
|
34
|
+
|
|
35
|
+
const boundary = typeof options.boundary === "function" ? options.boundary() : options.boundary
|
|
36
|
+
|
|
37
|
+
if (options.flip) {
|
|
38
|
+
middleware.push(
|
|
39
|
+
flip({
|
|
40
|
+
boundary,
|
|
41
|
+
padding: options.overflowPadding,
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (options.gutter || options.offset) {
|
|
47
|
+
const arrowOffset = arrowEl ? arrowEl.offsetHeight / 2 : 0
|
|
48
|
+
const data = options.gutter ? { mainAxis: options.gutter } : options.offset
|
|
49
|
+
if (data?.mainAxis != null) data.mainAxis += arrowOffset
|
|
50
|
+
middleware.push(offset(data))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
middleware.push(
|
|
54
|
+
shift({
|
|
55
|
+
boundary,
|
|
56
|
+
crossAxis: options.overlap,
|
|
57
|
+
padding: options.overflowPadding,
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if (arrowEl) {
|
|
62
|
+
// prettier-ignore
|
|
63
|
+
middleware.push(
|
|
64
|
+
arrow({ element: arrowEl, padding: 8 }),
|
|
65
|
+
shiftArrow({ element: arrowEl }),
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
middleware.push(transformOrigin)
|
|
70
|
+
|
|
71
|
+
middleware.push(
|
|
72
|
+
size({
|
|
73
|
+
padding: options.overflowPadding,
|
|
74
|
+
apply({ rects, availableHeight, availableWidth }) {
|
|
75
|
+
const referenceWidth = Math.round(rects.reference.width)
|
|
76
|
+
|
|
77
|
+
floating.style.setProperty("--reference-width", `${referenceWidth}px`)
|
|
78
|
+
floating.style.setProperty("--available-width", `${availableWidth}px`)
|
|
79
|
+
floating.style.setProperty("--available-height", `${availableHeight}px`)
|
|
80
|
+
|
|
81
|
+
if (options.sameWidth) {
|
|
82
|
+
Object.assign(floating.style, {
|
|
83
|
+
width: `${referenceWidth}px`,
|
|
84
|
+
minWidth: "unset",
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (options.fitViewport) {
|
|
89
|
+
Object.assign(floating.style, {
|
|
90
|
+
maxWidth: `${availableWidth}px`,
|
|
91
|
+
maxHeight: `${availableHeight}px`,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
/* -----------------------------------------------------------------------------
|
|
99
|
+
* The actual positioning function
|
|
100
|
+
* -----------------------------------------------------------------------------*/
|
|
101
|
+
|
|
102
|
+
function compute(config: Omit<ComputePositionConfig, "platform"> = {}) {
|
|
103
|
+
if (!reference || !floating) return
|
|
104
|
+
const { placement, strategy, onComplete } = options
|
|
105
|
+
|
|
106
|
+
computePosition(reference, floating, {
|
|
107
|
+
placement,
|
|
108
|
+
middleware,
|
|
109
|
+
strategy,
|
|
110
|
+
...config,
|
|
111
|
+
}).then((data) => {
|
|
112
|
+
const x = Math.round(data.x)
|
|
113
|
+
const y = Math.round(data.y)
|
|
114
|
+
|
|
115
|
+
Object.assign(floating.style, {
|
|
116
|
+
position: data.strategy,
|
|
117
|
+
top: "0px",
|
|
118
|
+
left: "0px",
|
|
119
|
+
transform: `translate3d(${x}px, ${y}px, 0)`,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
onComplete?.(data)
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
compute()
|
|
127
|
+
|
|
128
|
+
return callAll(
|
|
129
|
+
options.listeners ? autoUpdate(reference, floating, compute, options.listeners) : undefined,
|
|
130
|
+
options.onCleanup,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getBasePlacement(placement: Placement): BasePlacement {
|
|
135
|
+
return placement.split("-")[0] as BasePlacement
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function getPlacement(
|
|
139
|
+
referenceOrFn: MaybeFn<MaybeRectElement>,
|
|
140
|
+
floatingOrFn: MaybeFn<MaybeElement>,
|
|
141
|
+
opts: PositioningOptions & { defer?: boolean } = {},
|
|
142
|
+
) {
|
|
143
|
+
const { defer, ...restOptions } = opts
|
|
144
|
+
const func = defer ? raf : (v: any) => v()
|
|
145
|
+
const cleanups: (VoidFunction | undefined)[] = []
|
|
146
|
+
cleanups.push(
|
|
147
|
+
func(() => {
|
|
148
|
+
const reference = typeof referenceOrFn === "function" ? referenceOrFn() : referenceOrFn
|
|
149
|
+
const floating = typeof floatingOrFn === "function" ? floatingOrFn() : floatingOrFn
|
|
150
|
+
cleanups.push(getPlacementImpl(reference, floating, restOptions))
|
|
151
|
+
}),
|
|
152
|
+
)
|
|
153
|
+
return () => {
|
|
154
|
+
cleanups.forEach((fn) => fn?.())
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Placement } from "@floating-ui/dom"
|
|
2
|
+
import { cssVars } from "./middleware"
|
|
3
|
+
|
|
4
|
+
type Options = {
|
|
5
|
+
placement?: Placement
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const ARROW_FLOATING_STYLE = {
|
|
9
|
+
bottom: "rotate(45deg)",
|
|
10
|
+
left: "rotate(135deg)",
|
|
11
|
+
top: "rotate(225deg)",
|
|
12
|
+
right: "rotate(315deg)",
|
|
13
|
+
} as const
|
|
14
|
+
|
|
15
|
+
export function getPlacementStyles(options: Options) {
|
|
16
|
+
const { placement = "bottom" } = options
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
arrow: {
|
|
20
|
+
position: "absolute",
|
|
21
|
+
width: cssVars.arrowSize.reference,
|
|
22
|
+
height: cssVars.arrowSize.reference,
|
|
23
|
+
[cssVars.arrowSizeHalf.variable]: `calc(${cssVars.arrowSize.reference} / 2)`,
|
|
24
|
+
[cssVars.arrowOffset.variable]: `calc(${cssVars.arrowSizeHalf.reference} * -1)`,
|
|
25
|
+
} as const,
|
|
26
|
+
|
|
27
|
+
arrowTip: {
|
|
28
|
+
transform: ARROW_FLOATING_STYLE[placement.split("-")[0]],
|
|
29
|
+
background: cssVars.arrowBg.reference,
|
|
30
|
+
top: "0",
|
|
31
|
+
left: "0",
|
|
32
|
+
width: "100%",
|
|
33
|
+
height: "100%",
|
|
34
|
+
position: "absolute",
|
|
35
|
+
zIndex: "inherit",
|
|
36
|
+
} as const,
|
|
37
|
+
|
|
38
|
+
floating: {
|
|
39
|
+
position: "absolute",
|
|
40
|
+
minWidth: "max-content",
|
|
41
|
+
top: "0px",
|
|
42
|
+
left: "0px",
|
|
43
|
+
} as const,
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Coords, Middleware } from "@floating-ui/dom"
|
|
2
|
+
|
|
3
|
+
/* -----------------------------------------------------------------------------
|
|
4
|
+
* Shared middleware utils
|
|
5
|
+
* -----------------------------------------------------------------------------*/
|
|
6
|
+
|
|
7
|
+
const toVar = (value: string) => ({ variable: value, reference: `var(${value})` })
|
|
8
|
+
|
|
9
|
+
export const cssVars = {
|
|
10
|
+
arrowSize: toVar("--arrow-size"),
|
|
11
|
+
arrowSizeHalf: toVar("--arrow-size-half"),
|
|
12
|
+
arrowBg: toVar("--arrow-background"),
|
|
13
|
+
transformOrigin: toVar("--transform-origin"),
|
|
14
|
+
arrowOffset: toVar("--arrow-offset"),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* -----------------------------------------------------------------------------
|
|
18
|
+
* Transform Origin Middleware
|
|
19
|
+
* -----------------------------------------------------------------------------*/
|
|
20
|
+
|
|
21
|
+
const getTransformOrigin = (arrow?: Partial<Coords>) => ({
|
|
22
|
+
top: "bottom center",
|
|
23
|
+
"top-start": arrow ? `${arrow.x}px bottom` : "left bottom",
|
|
24
|
+
"top-end": arrow ? `${arrow.x}px bottom` : "right bottom",
|
|
25
|
+
bottom: "top center",
|
|
26
|
+
"bottom-start": arrow ? `${arrow.x}px top` : "top left",
|
|
27
|
+
"bottom-end": arrow ? `${arrow.x}px top` : "top right",
|
|
28
|
+
left: "right center",
|
|
29
|
+
"left-start": arrow ? `right ${arrow.y}px` : "right top",
|
|
30
|
+
"left-end": arrow ? `right ${arrow.y}px` : "right bottom",
|
|
31
|
+
right: "left center",
|
|
32
|
+
"right-start": arrow ? `left ${arrow.y}px` : "left top",
|
|
33
|
+
"right-end": arrow ? `left ${arrow.y}px` : "left bottom",
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export const transformOrigin: Middleware = {
|
|
37
|
+
name: "transformOrigin",
|
|
38
|
+
fn({ placement, elements, middlewareData }) {
|
|
39
|
+
const { arrow } = middlewareData
|
|
40
|
+
const transformOrigin = getTransformOrigin(arrow)[placement]
|
|
41
|
+
|
|
42
|
+
const { floating } = elements
|
|
43
|
+
floating.style.setProperty(cssVars.transformOrigin.variable, transformOrigin)
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
data: { transformOrigin },
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* -----------------------------------------------------------------------------
|
|
52
|
+
* Arrow Middleware
|
|
53
|
+
* -----------------------------------------------------------------------------*/
|
|
54
|
+
|
|
55
|
+
type ArrowOptions = { element: HTMLElement }
|
|
56
|
+
|
|
57
|
+
type BasePlacement = "top" | "bottom" | "left" | "right"
|
|
58
|
+
|
|
59
|
+
export const shiftArrow = (opts: ArrowOptions): Middleware => ({
|
|
60
|
+
name: "shiftArrow",
|
|
61
|
+
fn({ placement, middlewareData }) {
|
|
62
|
+
const { element: arrow } = opts
|
|
63
|
+
|
|
64
|
+
if (middlewareData.arrow) {
|
|
65
|
+
const { x, y } = middlewareData.arrow
|
|
66
|
+
|
|
67
|
+
const dir = placement.split("-")[0] as BasePlacement
|
|
68
|
+
|
|
69
|
+
Object.assign(arrow.style, {
|
|
70
|
+
left: x != null ? `${x}px` : "",
|
|
71
|
+
top: y != null ? `${y}px` : "",
|
|
72
|
+
[dir]: `calc(100% + ${cssVars.arrowOffset.reference})`,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {}
|
|
77
|
+
},
|
|
78
|
+
})
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Boundary, ComputePositionReturn, Placement } from "@floating-ui/dom"
|
|
2
|
+
import type { AutoUpdateOptions } from "./auto-update"
|
|
3
|
+
|
|
4
|
+
export type { Placement }
|
|
5
|
+
|
|
6
|
+
export type PositioningOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* The strategy to use for positioning
|
|
9
|
+
*/
|
|
10
|
+
strategy?: "absolute" | "fixed"
|
|
11
|
+
/**
|
|
12
|
+
* The initial placement of the floating element
|
|
13
|
+
*/
|
|
14
|
+
placement?: Placement
|
|
15
|
+
/**
|
|
16
|
+
* The offset of the floating element
|
|
17
|
+
*/
|
|
18
|
+
offset?: { mainAxis?: number; crossAxis?: number }
|
|
19
|
+
/**
|
|
20
|
+
* The main axis offset or gap between the reference and floating elements
|
|
21
|
+
*/
|
|
22
|
+
gutter?: number
|
|
23
|
+
/**
|
|
24
|
+
* The virtual padding around the viewport edges to check for overflow
|
|
25
|
+
*/
|
|
26
|
+
overflowPadding?: number
|
|
27
|
+
/**
|
|
28
|
+
* Whether to flip the placement
|
|
29
|
+
*/
|
|
30
|
+
flip?: boolean
|
|
31
|
+
/**
|
|
32
|
+
* Whether the floating element can overlap the reference element
|
|
33
|
+
* @default false
|
|
34
|
+
*/
|
|
35
|
+
overlap?: boolean
|
|
36
|
+
/**
|
|
37
|
+
* Whether to make the floating element same width as the reference element
|
|
38
|
+
*/
|
|
39
|
+
sameWidth?: boolean
|
|
40
|
+
/**
|
|
41
|
+
* Whether the popover should fit the viewport.
|
|
42
|
+
*/
|
|
43
|
+
fitViewport?: boolean
|
|
44
|
+
/**
|
|
45
|
+
* The overflow boundary of the reference element
|
|
46
|
+
*/
|
|
47
|
+
boundary?: Boundary | (() => Boundary)
|
|
48
|
+
/**
|
|
49
|
+
* Options to activate auto-update listeners
|
|
50
|
+
*/
|
|
51
|
+
listeners?: boolean | AutoUpdateOptions
|
|
52
|
+
/**
|
|
53
|
+
* Function called when the placement is computed
|
|
54
|
+
*/
|
|
55
|
+
onComplete?(data: ComputePositionReturn): void
|
|
56
|
+
/**
|
|
57
|
+
* Function called on cleanup of all listeners
|
|
58
|
+
*/
|
|
59
|
+
onCleanup?: VoidFunction
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type BasePlacement = "top" | "right" | "bottom" | "left"
|