|
1 | | -import { box } from '$lib/box.svelte.js'; |
2 | | -import type { UseFloatingOptions, UseFloatingReturn } from '$lib/types.js'; |
| 1 | +import type { FloatingElements, OpenChangeReason, UseFloatingOptions } from '$lib/types.js'; |
3 | 2 | import { getDPR, noop, roundByDPR, styleObjectToString } from '$lib/utils.js'; |
4 | | -import type { MiddlewareData, ReferenceElement } from '@floating-ui/dom'; |
5 | | -import { computePosition } from '@floating-ui/dom'; |
| 3 | +import { |
| 4 | + computePosition, |
| 5 | + type Strategy, |
| 6 | + type Placement, |
| 7 | + type MiddlewareData |
| 8 | +} from '@floating-ui/dom'; |
6 | 9 |
|
7 | | -/** |
8 | | - * Hook for managing floating elements. |
9 | | - * Aims to keep as much parity with `@floating-ui/react` as possible. |
10 | | - * For now see: https://floating-ui.com/docs/useFloating for API documentation. |
11 | | - */ |
12 | | -export function useFloating<T extends ReferenceElement = ReferenceElement>( |
13 | | - options: UseFloatingOptions<T> = {} |
14 | | -): UseFloatingReturn { |
15 | | - const openOption = box.derived(() => options.open ?? true); |
16 | | - const onOpenChangeOption = options.onOpenChange ?? noop; |
17 | | - const placementOption = box.derived(() => options.placement ?? 'bottom'); |
18 | | - const strategyOption = box.derived(() => options.strategy ?? 'absolute'); |
19 | | - const middlewareOption = box.derived(() => options.middleware); |
20 | | - const transformOption = box.derived(() => options.transform ?? true); |
21 | | - const referenceElement = box.derived(() => options.elements?.reference); |
22 | | - const floatingElement = box.derived(() => options.elements?.floating); |
23 | | - const whileElementsMountedOption = options.whileElementsMounted; |
24 | | - |
25 | | - const x = box(0); |
26 | | - const y = box(0); |
27 | | - const strategy = box(strategyOption.value); |
28 | | - const placement = box(placementOption.value); |
29 | | - const middlewareData = box<MiddlewareData>({}); |
30 | | - const isPositioned = box(false); |
31 | | - const floatingStyles = box.derived(() => { |
| 10 | +class FloatingState { |
| 11 | + readonly #options: UseFloatingOptions; |
| 12 | + |
| 13 | + constructor(options: UseFloatingOptions) { |
| 14 | + this.#options = options; |
| 15 | + this.placement = this.placementOption; |
| 16 | + this.strategy = this.strategyOption; |
| 17 | + } |
| 18 | + |
| 19 | + open = $derived.by(() => this.#options.open ?? true); |
| 20 | + onOpenChange = $derived.by(() => this.#options.onOpenChange ?? noop); |
| 21 | + placementOption = $derived.by(() => this.#options.placement ?? 'bottom'); |
| 22 | + strategyOption = $derived.by(() => this.#options.strategy ?? 'absolute'); |
| 23 | + middleware = $derived.by(() => this.#options.middleware); |
| 24 | + transform = $derived.by(() => this.#options.transform ?? true); |
| 25 | + elements = $derived.by(() => this.#options.elements ?? {}); |
| 26 | + whileElementsMounted = $derived.by(() => this.#options.whileElementsMounted); |
| 27 | + |
| 28 | + x = $state(0); |
| 29 | + y = $state(0); |
| 30 | + placement: Placement = $state('bottom'); |
| 31 | + strategy: Strategy = $state('absolute'); |
| 32 | + middlewareData: MiddlewareData = $state.frozen({}); |
| 33 | + isPositioned = $state(false); |
| 34 | + floatingStyles = $derived.by(() => { |
32 | 35 | const initialStyles = { |
33 | | - position: strategy.value, |
| 36 | + position: this.strategy, |
34 | 37 | left: '0', |
35 | 38 | top: '0' |
36 | 39 | }; |
37 | 40 |
|
38 | | - if (!floatingElement.value) { |
| 41 | + const { floating } = this.elements; |
| 42 | + if (floating == null) { |
39 | 43 | return styleObjectToString(initialStyles); |
40 | 44 | } |
41 | 45 |
|
42 | | - const xVal = roundByDPR(floatingElement.value, x.value); |
43 | | - const yVal = roundByDPR(floatingElement.value, y.value); |
| 46 | + const xVal = roundByDPR(floating, this.x); |
| 47 | + const yVal = roundByDPR(floating, this.y); |
44 | 48 |
|
45 | | - if (transformOption.value) { |
| 49 | + if (this.transform) { |
46 | 50 | return styleObjectToString({ |
47 | 51 | ...initialStyles, |
48 | 52 | transform: `translate(${xVal}px, ${yVal}px)`, |
49 | | - ...(getDPR(floatingElement.value) >= 1.5 && { willChange: 'transform' }) |
| 53 | + ...(getDPR(floating) >= 1.5 && { willChange: 'transform' }) |
50 | 54 | }); |
51 | 55 | } |
52 | 56 |
|
53 | 57 | return styleObjectToString({ |
54 | | - position: strategy.value, |
| 58 | + position: this.strategyOption, |
55 | 59 | left: `${xVal}px`, |
56 | 60 | top: `${yVal}px` |
57 | 61 | }); |
58 | 62 | }); |
| 63 | +} |
| 64 | + |
| 65 | +export class FloatingContext { |
| 66 | + readonly #state: FloatingState; |
| 67 | + |
| 68 | + constructor(state: FloatingState) { |
| 69 | + this.#state = state; |
| 70 | + } |
| 71 | + |
| 72 | + /** |
| 73 | + * Represents the open/close state of the floating element. |
| 74 | + * @default true |
| 75 | + */ |
| 76 | + get open(): boolean { |
| 77 | + return this.#state.open; |
| 78 | + } |
| 79 | + |
| 80 | + /** |
| 81 | + * Event handler that can be invoked whenever the open state changes. |
| 82 | + */ |
| 83 | + get onOpenChange(): (open: boolean, event?: Event, reason?: OpenChangeReason) => void { |
| 84 | + return this.#state.onOpenChange; |
| 85 | + } |
| 86 | + |
| 87 | + /** |
| 88 | + * The reference and floating elements. |
| 89 | + */ |
| 90 | + get elements(): FloatingElements { |
| 91 | + return this.#state.elements; |
| 92 | + } |
| 93 | +} |
| 94 | + |
| 95 | +export class UseFloatingReturn { |
| 96 | + readonly #state: FloatingState; |
| 97 | + readonly #context: FloatingContext; |
| 98 | + readonly #update: () => void; |
| 99 | + |
| 100 | + constructor(state: FloatingState, update: () => void) { |
| 101 | + this.#state = state; |
| 102 | + this.#context = new FloatingContext(state); |
| 103 | + this.#update = update; |
| 104 | + } |
| 105 | + |
| 106 | + /** |
| 107 | + * The x-coord of the floating element. |
| 108 | + */ |
| 109 | + get x(): number { |
| 110 | + return this.#state.x; |
| 111 | + } |
| 112 | + |
| 113 | + /** |
| 114 | + * The y-coord of the floating element. |
| 115 | + */ |
| 116 | + get y(): number { |
| 117 | + return this.#state.y; |
| 118 | + } |
| 119 | + |
| 120 | + /** |
| 121 | + * The stateful placement, which can be different from the initial `placement` passed as options. |
| 122 | + */ |
| 123 | + get placement(): Placement { |
| 124 | + return this.#state.placement; |
| 125 | + } |
| 126 | + |
| 127 | + /** |
| 128 | + * The type of CSS position property to use. |
| 129 | + */ |
| 130 | + get strategy(): Strategy { |
| 131 | + return this.#state.strategy; |
| 132 | + } |
| 133 | + |
| 134 | + /** |
| 135 | + * Additional data from middleware. |
| 136 | + */ |
| 137 | + get middlewareData(): MiddlewareData { |
| 138 | + return this.#state.middlewareData; |
| 139 | + } |
| 140 | + |
| 141 | + /** |
| 142 | + * The boolean that let you know if the floating element has been positioned. |
| 143 | + */ |
| 144 | + get isPositioned(): boolean { |
| 145 | + return this.#state.isPositioned; |
| 146 | + } |
| 147 | + |
| 148 | + /** |
| 149 | + * CSS styles to apply to the floating element to position it. |
| 150 | + */ |
| 151 | + get floatingStyles(): string { |
| 152 | + return this.#state.floatingStyles; |
| 153 | + } |
| 154 | + |
| 155 | + /** |
| 156 | + * The function to update floating position manually. |
| 157 | + */ |
| 158 | + get update(): () => void { |
| 159 | + return this.#update; |
| 160 | + } |
| 161 | + |
| 162 | + /** |
| 163 | + * Context object containing internal logic to alter the behavior of the floating element. |
| 164 | + * Commonly used to inject into others hooks. |
| 165 | + */ |
| 166 | + get context(): FloatingContext { |
| 167 | + return this.#context; |
| 168 | + } |
| 169 | +} |
| 170 | + |
| 171 | +/** |
| 172 | + * Hook for managing floating elements. |
| 173 | + */ |
| 174 | +export function useFloating(options: UseFloatingOptions = {}): UseFloatingReturn { |
| 175 | + const state = new FloatingState(options); |
59 | 176 |
|
60 | 177 | function update() { |
61 | | - if (referenceElement.value == null || floatingElement.value == null) { |
| 178 | + const { reference, floating } = state.elements; |
| 179 | + if (reference == null || floating == null) { |
62 | 180 | return; |
63 | 181 | } |
64 | 182 |
|
65 | | - computePosition(referenceElement.value, floatingElement.value, { |
66 | | - middleware: middlewareOption.value, |
67 | | - placement: placementOption.value, |
68 | | - strategy: strategyOption.value |
| 183 | + computePosition(reference, floating, { |
| 184 | + middleware: state.middleware, |
| 185 | + placement: state.placementOption, |
| 186 | + strategy: state.strategyOption |
69 | 187 | }).then((position) => { |
70 | | - x.value = position.x; |
71 | | - y.value = position.y; |
72 | | - strategy.value = position.strategy; |
73 | | - placement.value = position.placement; |
74 | | - middlewareData.value = position.middlewareData; |
75 | | - isPositioned.value = true; |
| 188 | + state.x = position.x; |
| 189 | + state.y = position.y; |
| 190 | + state.strategy = position.strategy; |
| 191 | + state.placement = position.placement; |
| 192 | + state.middlewareData = position.middlewareData; |
| 193 | + state.isPositioned = true; |
76 | 194 | }); |
77 | 195 | } |
78 | 196 |
|
79 | 197 | function attach() { |
80 | | - if (whileElementsMountedOption === undefined) { |
| 198 | + if (state.whileElementsMounted === undefined) { |
81 | 199 | update(); |
82 | 200 | return; |
83 | 201 | } |
84 | 202 |
|
85 | | - if (referenceElement.value != null && floatingElement.value != null) { |
86 | | - return whileElementsMountedOption(referenceElement.value, floatingElement.value, update); |
| 203 | + const { floating, reference } = state.elements; |
| 204 | + if (reference != null && floating != null) { |
| 205 | + return state.whileElementsMounted(reference, floating, update); |
87 | 206 | } |
88 | 207 | } |
89 | 208 |
|
90 | 209 | function reset() { |
91 | | - if (!openOption.value) { |
92 | | - isPositioned.value = false; |
| 210 | + if (!state.open) { |
| 211 | + state.isPositioned = false; |
93 | 212 | } |
94 | 213 | } |
95 | 214 |
|
96 | 215 | $effect.pre(update); |
97 | 216 | $effect.pre(attach); |
98 | 217 | $effect.pre(reset); |
99 | 218 |
|
100 | | - return { |
101 | | - x: box.readonly(x), |
102 | | - y: box.readonly(y), |
103 | | - strategy: box.readonly(strategy), |
104 | | - placement: box.readonly(placement), |
105 | | - middlewareData: box.readonly(middlewareData), |
106 | | - isPositioned: box.readonly(isPositioned), |
107 | | - floatingStyles, |
108 | | - update, |
109 | | - context: { |
110 | | - open: openOption, |
111 | | - onOpenChange: onOpenChangeOption, |
112 | | - elements: { |
113 | | - reference: box.readonly(referenceElement), |
114 | | - floating: box.readonly(floatingElement) |
115 | | - } |
116 | | - } |
117 | | - }; |
| 219 | + return new UseFloatingReturn(state, update); |
118 | 220 | } |
0 commit comments