@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/popper",
3
- "version": "0.9.2",
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.8",
26
- "@zag-js/dom-query": "0.9.2",
27
- "@zag-js/element-rect": "0.9.2",
28
- "@zag-js/utils": "0.9.2"
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,3 @@
1
+ export { getPlacement, getBasePlacement } from "./get-placement"
2
+ export { getPlacementStyles } from "./get-styles"
3
+ export type { Placement, PositioningOptions } from "./types"
@@ -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"