@zag-js/popper 0.30.0 → 0.31.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.
@@ -0,0 +1,42 @@
1
+ import { isHTMLElement } from "@zag-js/dom-query"
2
+ import type { AnchorRect, MaybeRectElement } from "./types"
3
+
4
+ export function createDOMRect(x = 0, y = 0, width = 0, height = 0) {
5
+ if (typeof DOMRect === "function") {
6
+ return new DOMRect(x, y, width, height)
7
+ }
8
+ const rect = {
9
+ x,
10
+ y,
11
+ width,
12
+ height,
13
+ top: y,
14
+ right: x + width,
15
+ bottom: y + height,
16
+ left: x,
17
+ }
18
+ return { ...rect, toJSON: () => rect }
19
+ }
20
+
21
+ function getDOMRect(anchorRect?: AnchorRect | null) {
22
+ if (!anchorRect) return createDOMRect()
23
+ const { x, y, width, height } = anchorRect
24
+ return createDOMRect(x, y, width, height)
25
+ }
26
+
27
+ export function getAnchorElement(
28
+ anchorElement: MaybeRectElement,
29
+ getAnchorRect?: (anchor: MaybeRectElement) => AnchorRect | null,
30
+ ) {
31
+ return {
32
+ contextElement: isHTMLElement(anchorElement) ? anchorElement : undefined,
33
+ getBoundingClientRect: () => {
34
+ const anchor = anchorElement
35
+ const anchorRect = getAnchorRect?.(anchor)
36
+ if (anchorRect || !anchor) {
37
+ return getDOMRect(anchorRect)
38
+ }
39
+ return anchor.getBoundingClientRect()
40
+ },
41
+ }
42
+ }
@@ -1,10 +1,12 @@
1
- import type { Middleware, Placement, VirtualElement } from "@floating-ui/dom"
2
- import { arrow, computePosition, flip, offset, shift, size, type ComputePositionConfig } from "@floating-ui/dom"
1
+ import type { Middleware } from "@floating-ui/dom"
2
+ import { arrow, computePosition, flip, offset, shift, size } from "@floating-ui/dom"
3
3
  import { getWindow, raf } from "@zag-js/dom-query"
4
- import { callAll, compact } from "@zag-js/utils"
4
+ import { compact, isNull, runIfFn } from "@zag-js/utils"
5
5
  import { autoUpdate } from "./auto-update"
6
- import { shiftArrow, transformOrigin } from "./middleware"
7
- import type { BasePlacement, PositioningOptions } from "./types"
6
+ import { getAnchorElement } from "./get-anchor"
7
+ import { __shiftArrow, __transformOrigin } from "./middleware"
8
+ import { getPlacementDetails } from "./placement"
9
+ import type { MaybeElement, MaybeFn, MaybeRectElement, PositioningOptions } from "./types"
8
10
 
9
11
  const defaultOptions: PositioningOptions = {
10
12
  strategy: "absolute",
@@ -12,122 +14,159 @@ const defaultOptions: PositioningOptions = {
12
14
  listeners: true,
13
15
  gutter: 8,
14
16
  flip: true,
17
+ slide: true,
18
+ overlap: false,
15
19
  sameWidth: false,
20
+ fitViewport: false,
16
21
  overflowPadding: 8,
22
+ arrowPadding: 4,
17
23
  }
18
24
 
19
- type MaybeRectElement = HTMLElement | VirtualElement | null
20
- type MaybeElement = HTMLElement | null
21
- type MaybeFn<T> = T | (() => T)
25
+ function __dpr(win: Window, value: number) {
26
+ const dpr = win.devicePixelRatio || 1
27
+ return Math.round(value * dpr) / dpr
28
+ }
22
29
 
23
- function getPlacementImpl(reference: MaybeRectElement, floating: MaybeElement, opts: PositioningOptions = {}) {
24
- if (!floating || !reference) return
30
+ function __boundary(opts: PositioningOptions) {
31
+ return runIfFn(opts.boundary)
32
+ }
25
33
 
26
- const options = Object.assign({}, defaultOptions, opts)
34
+ function __arrow(arrowElement: HTMLElement | null, opts: PositioningOptions) {
35
+ if (!arrowElement) return
36
+ return arrow({
37
+ element: arrowElement,
38
+ padding: opts.arrowPadding,
39
+ })
40
+ }
27
41
 
28
- /* -----------------------------------------------------------------------------
29
- * The middleware stack
30
- * -----------------------------------------------------------------------------*/
42
+ function __offset(arrowElement: HTMLElement | null, opts: PositioningOptions) {
43
+ if (isNull(opts.offset ?? opts.gutter)) return
44
+ return offset(({ placement }) => {
45
+ const arrowOffset = (arrowElement?.clientHeight || 0) / 2
31
46
 
32
- const arrowEl = floating.querySelector<HTMLElement>("[data-part=arrow]")
33
- const middleware: Middleware[] = []
47
+ const gutter = opts.offset?.mainAxis ?? opts.gutter
48
+ const mainAxis = typeof gutter === "number" ? gutter + arrowOffset : gutter ?? arrowOffset
34
49
 
35
- const boundary = typeof options.boundary === "function" ? options.boundary() : options.boundary
50
+ const { hasAlign } = getPlacementDetails(placement)
36
51
 
37
- if (options.flip) {
38
- middleware.push(
39
- flip({
40
- boundary,
41
- padding: options.overflowPadding,
42
- }),
43
- )
44
- }
52
+ return compact({
53
+ crossAxis: hasAlign ? opts.shift : undefined,
54
+ mainAxis: mainAxis,
55
+ alignmentAxis: opts.shift,
56
+ })
57
+ })
58
+ }
45
59
 
46
- if (options.gutter || options.offset) {
47
- const arrowOffset = arrowEl ? arrowEl.offsetHeight / 2 : 0
60
+ function __flip(opts: PositioningOptions) {
61
+ if (!opts.flip) return
62
+ return flip({
63
+ boundary: __boundary(opts),
64
+ padding: opts.overflowPadding,
65
+ fallbackPlacements: opts.flip === true ? undefined : opts.flip,
66
+ })
67
+ }
48
68
 
49
- let mainAxis = options.offset?.mainAxis ?? options.gutter
50
- let crossAxis = options.offset?.crossAxis
69
+ function __shift(opts: PositioningOptions) {
70
+ if (!opts.slide && !opts.overlap) return
71
+ return shift({
72
+ boundary: __boundary(opts),
73
+ mainAxis: opts.slide,
74
+ crossAxis: opts.overlap,
75
+ padding: opts.overflowPadding,
76
+ })
77
+ }
51
78
 
52
- if (mainAxis != null) mainAxis += arrowOffset
79
+ function __size(opts: PositioningOptions) {
80
+ return size({
81
+ padding: opts.overflowPadding,
82
+ apply({ elements, rects, availableHeight, availableWidth }) {
83
+ const floating = elements.floating
84
+
85
+ const referenceWidth = Math.round(rects.reference.width)
86
+ availableWidth = Math.floor(availableWidth)
87
+ availableHeight = Math.floor(availableHeight)
88
+
89
+ floating.style.setProperty("--reference-width", `${referenceWidth}px`)
90
+ floating.style.setProperty("--available-width", `${availableWidth}px`)
91
+ floating.style.setProperty("--available-height", `${availableHeight}px`)
92
+ },
93
+ })
94
+ }
53
95
 
54
- const offsetOptions = compact({ mainAxis, crossAxis })
55
- middleware.push(offset(offsetOptions))
56
- }
96
+ function getPlacementImpl(referenceOrVirtual: MaybeRectElement, floating: MaybeElement, opts: PositioningOptions = {}) {
97
+ const reference = getAnchorElement(referenceOrVirtual, opts.getAnchorRect)
57
98
 
58
- middleware.push(
59
- shift({
60
- boundary,
61
- crossAxis: options.overlap,
62
- padding: options.overflowPadding,
63
- }),
64
- )
99
+ if (!floating || !reference) return
65
100
 
66
- if (arrowEl) {
67
- // prettier-ignore
68
- middleware.push(
69
- arrow({ element: arrowEl, padding: 8 }),
70
- shiftArrow({ element: arrowEl }),
71
- )
72
- }
101
+ const options = Object.assign({}, defaultOptions, opts)
73
102
 
74
- middleware.push(transformOrigin)
75
-
76
- middleware.push(
77
- size({
78
- padding: options.overflowPadding,
79
- apply({ rects, availableHeight, availableWidth }) {
80
- const referenceWidth = Math.round(rects.reference.width)
81
- floating.style.setProperty("--reference-width", `${referenceWidth}px`)
82
- floating.style.setProperty("--available-width", `${availableWidth}px`)
83
- floating.style.setProperty("--available-height", `${availableHeight}px`)
84
- },
85
- }),
86
- )
103
+ /* -----------------------------------------------------------------------------
104
+ * The middleware stack
105
+ * -----------------------------------------------------------------------------*/
106
+
107
+ const arrowEl = floating.querySelector<HTMLElement>("[data-part=arrow]")
108
+
109
+ const middleware: (Middleware | undefined)[] = [
110
+ __offset(arrowEl, options),
111
+ __flip(options),
112
+ __shift(options),
113
+ __arrow(arrowEl, options),
114
+ __shiftArrow(arrowEl),
115
+ __transformOrigin,
116
+ __size(options),
117
+ ]
87
118
 
88
119
  /* -----------------------------------------------------------------------------
89
120
  * The actual positioning function
90
121
  * -----------------------------------------------------------------------------*/
91
122
 
92
- function compute(config: Omit<ComputePositionConfig, "platform"> = {}) {
93
- const { placement, strategy, onComplete } = options
123
+ const { placement, strategy, onComplete, onPositioned } = options
94
124
 
125
+ const updatePosition = async () => {
95
126
  if (!reference || !floating) return
96
127
 
97
- computePosition(reference, floating, {
128
+ const pos = await computePosition(reference, floating, {
98
129
  placement,
99
130
  middleware,
100
131
  strategy,
101
- ...config,
102
- }).then((data) => {
103
- const x = Math.round(data.x)
104
- const y = Math.round(data.y)
132
+ })
105
133
 
106
- floating.style.setProperty("--x", `${x}px`)
107
- floating.style.setProperty("--y", `${y}px`)
134
+ onComplete?.(pos)
135
+ onPositioned?.({ placed: true })
108
136
 
109
- const win = getWindow(floating)
110
- const contentEl = floating.firstElementChild
137
+ const win = getWindow(floating)
138
+ const x = __dpr(win, pos.x)
139
+ const y = __dpr(win, pos.y)
111
140
 
112
- if (contentEl) {
113
- const zIndex = win.getComputedStyle(contentEl).zIndex
114
- floating.style.setProperty("--z-index", zIndex)
115
- }
141
+ floating.style.setProperty("--x", `${x}px`)
142
+ floating.style.setProperty("--y", `${y}px`)
116
143
 
117
- onComplete?.(data)
118
- })
144
+ const contentEl = floating.firstElementChild
145
+
146
+ if (contentEl) {
147
+ const zIndex = win.getComputedStyle(contentEl).zIndex
148
+ floating.style.setProperty("--z-index", zIndex)
149
+ }
119
150
  }
120
151
 
121
- compute()
152
+ const update = async () => {
153
+ if (opts.updatePosition) {
154
+ await opts.updatePosition({ updatePosition })
155
+ onPositioned?.({ placed: true })
156
+ } else {
157
+ await updatePosition()
158
+ }
159
+ }
122
160
 
123
- return callAll(
124
- options.listeners ? autoUpdate(reference, floating, compute, options.listeners) : undefined,
125
- options.onCleanup,
126
- )
127
- }
161
+ const cancelAutoUpdate = options.listeners ? autoUpdate(reference, floating, update, options.listeners) : undefined
162
+
163
+ update()
128
164
 
129
- export function getBasePlacement(placement: Placement): BasePlacement {
130
- return placement.split("-")[0] as BasePlacement
165
+ return () => {
166
+ cancelAutoUpdate?.()
167
+ onPositioned?.({ placed: false })
168
+ options.onCleanup?.()
169
+ }
131
170
  }
132
171
 
133
172
  export function getPlacement(
@@ -135,14 +174,14 @@ export function getPlacement(
135
174
  floatingOrFn: MaybeFn<MaybeElement>,
136
175
  opts: PositioningOptions & { defer?: boolean } = {},
137
176
  ) {
138
- const { defer, ...restOptions } = opts
177
+ const { defer, ...options } = opts
139
178
  const func = defer ? raf : (v: any) => v()
140
179
  const cleanups: (VoidFunction | undefined)[] = []
141
180
  cleanups.push(
142
181
  func(() => {
143
182
  const reference = typeof referenceOrFn === "function" ? referenceOrFn() : referenceOrFn
144
183
  const floating = typeof floatingOrFn === "function" ? floatingOrFn() : floatingOrFn
145
- cleanups.push(getPlacementImpl(reference, floating, restOptions))
184
+ cleanups.push(getPlacementImpl(reference, floating, options))
146
185
  }),
147
186
  )
148
187
  return () => {
package/src/get-styles.ts CHANGED
@@ -14,7 +14,7 @@ const ARROW_FLOATING_STYLE = {
14
14
  } as const
15
15
 
16
16
  export function getPlacementStyles(options: PositioningOptions = {}) {
17
- const { placement = "bottom", sameWidth, fitViewport, strategy = "absolute" } = options
17
+ const { placement, sameWidth, fitViewport, strategy = "absolute" } = options
18
18
 
19
19
  return {
20
20
  arrow: {
@@ -26,7 +26,7 @@ export function getPlacementStyles(options: PositioningOptions = {}) {
26
26
  } as const,
27
27
 
28
28
  arrowTip: {
29
- transform: ARROW_FLOATING_STYLE[placement.split("-")[0]],
29
+ transform: placement ? ARROW_FLOATING_STYLE[placement.split("-")[0]] : undefined,
30
30
  background: cssVars.arrowBg.reference,
31
31
  top: "0",
32
32
  left: "0",
@@ -45,7 +45,8 @@ export function getPlacementStyles(options: PositioningOptions = {}) {
45
45
  maxHeight: fitViewport ? "var(--available-height)" : undefined,
46
46
  top: "0px",
47
47
  left: "0px",
48
- transform: `translate3d(var(--x), var(--y), 0)`,
48
+ // move off-screen if placement is not defined
49
+ transform: placement ? "translate3d(var(--x), var(--y), 0)" : "translate3d(0, -100vh, 0)",
49
50
  zIndex: "var(--z-index)",
50
51
  } as const,
51
52
  }
package/src/index.ts CHANGED
@@ -1,10 +1,13 @@
1
- export { getBasePlacement, getPlacement } from "./get-placement"
1
+ export { getPlacement } from "./get-placement"
2
2
  export { getPlacementStyles, type GetPlacementStylesOptions } from "./get-styles"
3
+ export { getPlacementSide, isValidPlacement } from "./placement"
3
4
  export type {
5
+ AnchorRect,
4
6
  AutoUpdateOptions,
5
- BasePlacement,
6
7
  Boundary,
7
8
  ComputePositionReturn,
8
9
  Placement,
10
+ PlacementAlign,
11
+ PlacementSide,
9
12
  PositioningOptions,
10
13
  } from "./types"
package/src/middleware.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Coords, Middleware } from "@floating-ui/dom"
2
+ import type { PlacementSide } from "./types"
2
3
 
3
4
  /* -----------------------------------------------------------------------------
4
5
  * Shared middleware utils
@@ -33,7 +34,7 @@ const getTransformOrigin = (arrow?: Partial<Coords>) => ({
33
34
  "right-end": arrow ? `left ${arrow.y}px` : "left bottom",
34
35
  })
35
36
 
36
- export const transformOrigin: Middleware = {
37
+ export const __transformOrigin: Middleware = {
37
38
  name: "transformOrigin",
38
39
  fn({ placement, elements, middlewareData }) {
39
40
  const { arrow } = middlewareData
@@ -52,27 +53,22 @@ export const transformOrigin: Middleware = {
52
53
  * Arrow Middleware
53
54
  * -----------------------------------------------------------------------------*/
54
55
 
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) {
56
+ export const __shiftArrow = (arrowEl: HTMLElement | null): Middleware | undefined => {
57
+ if (!arrowEl) return
58
+ return {
59
+ name: "shiftArrow",
60
+ fn({ placement, middlewareData }) {
61
+ if (!middlewareData.arrow) return {}
65
62
  const { x, y } = middlewareData.arrow
63
+ const dir = placement.split("-")[0] as PlacementSide
66
64
 
67
- const dir = placement.split("-")[0] as BasePlacement
68
-
69
- Object.assign(arrow.style, {
65
+ Object.assign(arrowEl.style, {
70
66
  left: x != null ? `${x}px` : "",
71
67
  top: y != null ? `${y}px` : "",
72
68
  [dir]: `calc(100% + ${cssVars.arrowOffset.reference})`,
73
69
  })
74
- }
75
70
 
76
- return {}
77
- },
78
- })
71
+ return {}
72
+ },
73
+ }
74
+ }
@@ -0,0 +1,15 @@
1
+ import type { Placement } from "@floating-ui/dom"
2
+ import type { PlacementAlign, PlacementSide } from "./types"
3
+
4
+ export function isValidPlacement(v: string): v is Placement {
5
+ return /^(?:top|bottom|left|right)(?:-(?:start|end))?$/.test(v)
6
+ }
7
+
8
+ export function getPlacementDetails(placement: Placement) {
9
+ const [side, align] = placement.split("-") as [PlacementSide, PlacementAlign | undefined]
10
+ return { side, align, hasAlign: align != null }
11
+ }
12
+
13
+ export function getPlacementSide(placement: Placement): PlacementSide {
14
+ return placement.split("-")[0] as PlacementSide
15
+ }
package/src/types.ts CHANGED
@@ -1,7 +1,21 @@
1
- import type { Boundary, ComputePositionReturn, Placement } from "@floating-ui/dom"
1
+ import type { Boundary, ComputePositionReturn, Placement, VirtualElement } from "@floating-ui/dom"
2
2
  import type { AutoUpdateOptions } from "./auto-update"
3
3
 
4
- export type { Placement, Boundary, ComputePositionReturn, AutoUpdateOptions }
4
+ export type MaybeRectElement = HTMLElement | VirtualElement | null
5
+
6
+ export type MaybeElement = HTMLElement | null
7
+
8
+ export type MaybeFn<T> = T | (() => T)
9
+
10
+ export type PlacementSide = "top" | "right" | "bottom" | "left"
11
+ export type PlacementAlign = "start" | "center" | "end"
12
+
13
+ export interface AnchorRect {
14
+ x?: number
15
+ y?: number
16
+ width?: number
17
+ height?: number
18
+ }
5
19
 
6
20
  export interface PositioningOptions {
7
21
  /**
@@ -20,14 +34,27 @@ export interface PositioningOptions {
20
34
  * The main axis offset or gap between the reference and floating elements
21
35
  */
22
36
  gutter?: number
37
+ /**
38
+ * The secondary axis offset or gap between the reference and floating elements
39
+ */
40
+ shift?: number
23
41
  /**
24
42
  * The virtual padding around the viewport edges to check for overflow
25
43
  */
26
44
  overflowPadding?: number
45
+ /**
46
+ * The minimum padding between the arrow and the floating element's corner.
47
+ * @default 4
48
+ */
49
+ arrowPadding?: number
27
50
  /**
28
51
  * Whether to flip the placement
29
52
  */
30
- flip?: boolean
53
+ flip?: boolean | Placement[]
54
+ /**
55
+ * Whether the popover should slide when it overflows.
56
+ */
57
+ slide?: boolean
31
58
  /**
32
59
  * Whether the floating element can overlap the reference element
33
60
  * @default false
@@ -53,10 +80,23 @@ export interface PositioningOptions {
53
80
  * Function called when the placement is computed
54
81
  */
55
82
  onComplete?(data: ComputePositionReturn): void
83
+ /**
84
+ * Function called when the floating element is positioned or not
85
+ */
86
+ onPositioned?(data: { placed: boolean }): void
56
87
  /**
57
88
  * Function called on cleanup of all listeners
58
89
  */
59
90
  onCleanup?: VoidFunction
91
+ /**
92
+ * Function that returns the anchor rect of the combobox
93
+ */
94
+ getAnchorRect?: (element: HTMLElement | VirtualElement | null) => AnchorRect | null
95
+ /**
96
+ * A callback that will be called when the popover needs to calculate its
97
+ * position.
98
+ */
99
+ updatePosition?: (data: { updatePosition: () => Promise<void> }) => void | Promise<void>
60
100
  }
61
101
 
62
- export type BasePlacement = "top" | "right" | "bottom" | "left"
102
+ export type { AutoUpdateOptions, Boundary, ComputePositionReturn, Placement }