Skip to content
This repository was archived by the owner on Oct 14, 2025. It is now read-only.

Commit 5c456da

Browse files
AbdelrahmanHugos68
andauthored
Use Classes (#27)
* drop boxing * use classes * Updated tests, merged dev * Outdated doc change * Removed generic * Removed generics for now * Removed redundant import * Unified imports --------- Co-authored-by: hugos68 <[email protected]>
1 parent 2a2874a commit 5c456da

File tree

6 files changed

+249
-292
lines changed

6 files changed

+249
-292
lines changed

src/lib/box.svelte.ts

Lines changed: 0 additions & 68 deletions
This file was deleted.
Lines changed: 172 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,220 @@
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';
32
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';
69

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(() => {
3235
const initialStyles = {
33-
position: strategy.value,
36+
position: this.strategy,
3437
left: '0',
3538
top: '0'
3639
};
3740

38-
if (!floatingElement.value) {
41+
const { floating } = this.elements;
42+
if (floating == null) {
3943
return styleObjectToString(initialStyles);
4044
}
4145

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);
4448

45-
if (transformOption.value) {
49+
if (this.transform) {
4650
return styleObjectToString({
4751
...initialStyles,
4852
transform: `translate(${xVal}px, ${yVal}px)`,
49-
...(getDPR(floatingElement.value) >= 1.5 && { willChange: 'transform' })
53+
...(getDPR(floating) >= 1.5 && { willChange: 'transform' })
5054
});
5155
}
5256

5357
return styleObjectToString({
54-
position: strategy.value,
58+
position: this.strategyOption,
5559
left: `${xVal}px`,
5660
top: `${yVal}px`
5761
});
5862
});
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);
59176

60177
function update() {
61-
if (referenceElement.value == null || floatingElement.value == null) {
178+
const { reference, floating } = state.elements;
179+
if (reference == null || floating == null) {
62180
return;
63181
}
64182

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
69187
}).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;
76194
});
77195
}
78196

79197
function attach() {
80-
if (whileElementsMountedOption === undefined) {
198+
if (state.whileElementsMounted === undefined) {
81199
update();
82200
return;
83201
}
84202

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);
87206
}
88207
}
89208

90209
function reset() {
91-
if (!openOption.value) {
92-
isPositioned.value = false;
210+
if (!state.open) {
211+
state.isPositioned = false;
93212
}
94213
}
95214

96215
$effect.pre(update);
97216
$effect.pre(attach);
98217
$effect.pre(reset);
99218

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);
118220
}

0 commit comments

Comments
 (0)