@seed-design/react-floating 0.0.0-alpha-20260622094810
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/lib/index.cjs +153 -0
- package/lib/index.d.ts +81 -0
- package/lib/index.js +153 -0
- package/package.json +45 -0
- package/src/index.ts +258 -0
package/lib/index.cjs
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
var react$1 = require('@floating-ui/react');
|
|
2
|
+
var reactUseControllableState = require('@radix-ui/react-use-controllable-state');
|
|
3
|
+
var react = require('react');
|
|
4
|
+
|
|
5
|
+
const defaultPositioningOptions = {
|
|
6
|
+
strategy: "absolute",
|
|
7
|
+
placement: "bottom",
|
|
8
|
+
flip: true,
|
|
9
|
+
slide: true,
|
|
10
|
+
overflowPadding: 8,
|
|
11
|
+
arrowPadding: 4
|
|
12
|
+
};
|
|
13
|
+
function getArrowMiddleware(arrowElement, opts) {
|
|
14
|
+
if (!arrowElement) return;
|
|
15
|
+
return react$1.arrow({
|
|
16
|
+
element: arrowElement,
|
|
17
|
+
padding: opts.arrowPadding
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function getOffsetMiddleware(arrowOffset, opts) {
|
|
21
|
+
const offsetMainAxis = (opts.gutter ?? 0) + arrowOffset;
|
|
22
|
+
return react$1.offset(offsetMainAxis);
|
|
23
|
+
}
|
|
24
|
+
function getFlipMiddleware(opts) {
|
|
25
|
+
if (!opts.flip) return;
|
|
26
|
+
return react$1.flip({
|
|
27
|
+
padding: opts.overflowPadding,
|
|
28
|
+
fallbackPlacements: opts.flip === true ? undefined : opts.flip
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function getShiftMiddleware(opts) {
|
|
32
|
+
if (!opts.slide) return;
|
|
33
|
+
return react$1.shift({
|
|
34
|
+
mainAxis: opts.slide,
|
|
35
|
+
padding: opts.overflowPadding,
|
|
36
|
+
limiter: react$1.limitShift()
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function getSizeMiddleware(opts) {
|
|
40
|
+
return react$1.size({
|
|
41
|
+
padding: opts.overflowPadding,
|
|
42
|
+
apply ({ availableWidth, elements }) {
|
|
43
|
+
elements.floating.style.setProperty("--seed-popover-available-width", `${Math.max(0, availableWidth)}px`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const rectMiddleware = {
|
|
48
|
+
name: "rects",
|
|
49
|
+
fn ({ rects }) {
|
|
50
|
+
return {
|
|
51
|
+
data: rects
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const ARROW_FLOATING_STYLE = {
|
|
56
|
+
top: "",
|
|
57
|
+
right: "rotate(90deg)",
|
|
58
|
+
bottom: "rotate(180deg)",
|
|
59
|
+
left: "rotate(270deg)"
|
|
60
|
+
};
|
|
61
|
+
function usePositionedFloating(props) {
|
|
62
|
+
const options = {
|
|
63
|
+
...defaultPositioningOptions,
|
|
64
|
+
...props
|
|
65
|
+
};
|
|
66
|
+
const [open, onOpenChange] = reactUseControllableState.useControllableState({
|
|
67
|
+
prop: props.open,
|
|
68
|
+
defaultProp: props.defaultOpen ?? false,
|
|
69
|
+
onChange: props.onOpenChange
|
|
70
|
+
});
|
|
71
|
+
const [arrowEl, setArrowEl] = react.useState(null);
|
|
72
|
+
const [arrowTipEl, setArrowTipEl] = react.useState(null);
|
|
73
|
+
const arrowTipWidth = arrowTipEl?.clientWidth ?? 0;
|
|
74
|
+
const arrowTipHeight = arrowTipEl?.clientHeight ?? 0;
|
|
75
|
+
const arrowTipOffset = arrowTipHeight;
|
|
76
|
+
const { refs, context, floatingStyles, middlewareData, isPositioned } = react$1.useFloating({
|
|
77
|
+
strategy: options.strategy,
|
|
78
|
+
open,
|
|
79
|
+
placement: options.placement,
|
|
80
|
+
onOpenChange: onOpenChange,
|
|
81
|
+
middleware: [
|
|
82
|
+
getOffsetMiddleware(arrowTipOffset, options),
|
|
83
|
+
getFlipMiddleware(options),
|
|
84
|
+
getShiftMiddleware(options),
|
|
85
|
+
getSizeMiddleware(options),
|
|
86
|
+
getArrowMiddleware(arrowEl, options),
|
|
87
|
+
rectMiddleware
|
|
88
|
+
]
|
|
89
|
+
});
|
|
90
|
+
// https://floating-ui.com/docs/react#anchoring
|
|
91
|
+
react.useEffect(()=>{
|
|
92
|
+
if (!open) return;
|
|
93
|
+
if (!refs.reference.current || !refs.floating.current) return;
|
|
94
|
+
return react$1.autoUpdate(refs.reference.current, refs.floating.current, context.update);
|
|
95
|
+
}, [
|
|
96
|
+
open,
|
|
97
|
+
refs.reference,
|
|
98
|
+
refs.floating,
|
|
99
|
+
context
|
|
100
|
+
]);
|
|
101
|
+
const [side, alignment] = context.placement.split("-");
|
|
102
|
+
const arrowStyles = react.useMemo(()=>({
|
|
103
|
+
position: "absolute",
|
|
104
|
+
left: middlewareData.arrow?.x,
|
|
105
|
+
top: middlewareData.arrow?.y,
|
|
106
|
+
[side]: "100%",
|
|
107
|
+
transform: ARROW_FLOATING_STYLE[side]
|
|
108
|
+
}), [
|
|
109
|
+
middlewareData.arrow,
|
|
110
|
+
side
|
|
111
|
+
]);
|
|
112
|
+
return react.useMemo(()=>({
|
|
113
|
+
open,
|
|
114
|
+
onOpenChange,
|
|
115
|
+
refs: {
|
|
116
|
+
...refs,
|
|
117
|
+
arrow: arrowEl,
|
|
118
|
+
setArrow: setArrowEl,
|
|
119
|
+
arrowTip: arrowTipEl,
|
|
120
|
+
setArrowTip: setArrowTipEl
|
|
121
|
+
},
|
|
122
|
+
rects: {
|
|
123
|
+
...middlewareData["rects"],
|
|
124
|
+
arrowTip: {
|
|
125
|
+
width: arrowTipWidth,
|
|
126
|
+
height: arrowTipHeight
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
isPositioned,
|
|
130
|
+
side,
|
|
131
|
+
alignment,
|
|
132
|
+
context,
|
|
133
|
+
floatingStyles,
|
|
134
|
+
arrowStyles
|
|
135
|
+
}), [
|
|
136
|
+
open,
|
|
137
|
+
onOpenChange,
|
|
138
|
+
refs,
|
|
139
|
+
arrowEl,
|
|
140
|
+
arrowTipEl,
|
|
141
|
+
middlewareData["rects"],
|
|
142
|
+
context,
|
|
143
|
+
side,
|
|
144
|
+
alignment,
|
|
145
|
+
floatingStyles,
|
|
146
|
+
arrowStyles,
|
|
147
|
+
isPositioned,
|
|
148
|
+
arrowTipWidth,
|
|
149
|
+
arrowTipHeight
|
|
150
|
+
]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
exports.usePositionedFloating = usePositionedFloating;
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Placement, ReferenceType, ExtendedRefs, Rect, Side, Alignment, FloatingContext } from '@floating-ui/react';
|
|
2
|
+
import { CSSProperties } from 'react';
|
|
3
|
+
|
|
4
|
+
interface PositioningOptions {
|
|
5
|
+
/**
|
|
6
|
+
* The strategy to use for positioning
|
|
7
|
+
* @default "absolute"
|
|
8
|
+
*/
|
|
9
|
+
strategy?: "absolute" | "fixed";
|
|
10
|
+
/**
|
|
11
|
+
* The initial placement of the floating element
|
|
12
|
+
* @default "bottom"
|
|
13
|
+
*/
|
|
14
|
+
placement?: Placement;
|
|
15
|
+
/**
|
|
16
|
+
* The gutter between the floating element and the reference element
|
|
17
|
+
*/
|
|
18
|
+
gutter?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Whether to flip the placement
|
|
21
|
+
* @default true
|
|
22
|
+
*/
|
|
23
|
+
flip?: boolean | Placement[];
|
|
24
|
+
/**
|
|
25
|
+
* Whether the popover should slide when it overflows.
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
slide?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The virtual padding around the viewport edges to check for overflow
|
|
31
|
+
* @default 8
|
|
32
|
+
*/
|
|
33
|
+
overflowPadding?: number;
|
|
34
|
+
/**
|
|
35
|
+
* The minimum padding between the arrow and the floating element's corner.
|
|
36
|
+
* @default 4
|
|
37
|
+
*/
|
|
38
|
+
arrowPadding?: number;
|
|
39
|
+
}
|
|
40
|
+
interface UsePositionedFloatingProps extends PositioningOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Whether the floating element is initially open
|
|
43
|
+
*/
|
|
44
|
+
defaultOpen?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Whether the floating element is open
|
|
47
|
+
*/
|
|
48
|
+
open?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Callback when the floating element is opened or closed
|
|
51
|
+
*/
|
|
52
|
+
onOpenChange?: (open: boolean) => void;
|
|
53
|
+
}
|
|
54
|
+
interface UsePositionedFloatingReturn<RT extends ReferenceType = ReferenceType> {
|
|
55
|
+
open: boolean;
|
|
56
|
+
onOpenChange: ((open: boolean) => void) | undefined;
|
|
57
|
+
refs: ExtendedRefs<RT> & {
|
|
58
|
+
arrow: HTMLElement | null;
|
|
59
|
+
setArrow: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
|
|
60
|
+
arrowTip: HTMLElement | null;
|
|
61
|
+
setArrowTip: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
|
|
62
|
+
};
|
|
63
|
+
rects: {
|
|
64
|
+
reference: Rect;
|
|
65
|
+
floating: Rect;
|
|
66
|
+
arrowTip: {
|
|
67
|
+
width: number;
|
|
68
|
+
height: number;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
isPositioned: boolean;
|
|
72
|
+
side: Side;
|
|
73
|
+
alignment: Alignment | undefined;
|
|
74
|
+
context: FloatingContext<RT>;
|
|
75
|
+
floatingStyles: CSSProperties;
|
|
76
|
+
arrowStyles: CSSProperties;
|
|
77
|
+
}
|
|
78
|
+
declare function usePositionedFloating<RT extends ReferenceType = ReferenceType>(props: UsePositionedFloatingProps): UsePositionedFloatingReturn<RT>;
|
|
79
|
+
|
|
80
|
+
export { usePositionedFloating };
|
|
81
|
+
export type { PositioningOptions, UsePositionedFloatingProps, UsePositionedFloatingReturn };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { useFloating, autoUpdate, offset, flip, shift, limitShift, size, arrow } from '@floating-ui/react';
|
|
2
|
+
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
|
3
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
const defaultPositioningOptions = {
|
|
6
|
+
strategy: "absolute",
|
|
7
|
+
placement: "bottom",
|
|
8
|
+
flip: true,
|
|
9
|
+
slide: true,
|
|
10
|
+
overflowPadding: 8,
|
|
11
|
+
arrowPadding: 4
|
|
12
|
+
};
|
|
13
|
+
function getArrowMiddleware(arrowElement, opts) {
|
|
14
|
+
if (!arrowElement) return;
|
|
15
|
+
return arrow({
|
|
16
|
+
element: arrowElement,
|
|
17
|
+
padding: opts.arrowPadding
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function getOffsetMiddleware(arrowOffset, opts) {
|
|
21
|
+
const offsetMainAxis = (opts.gutter ?? 0) + arrowOffset;
|
|
22
|
+
return offset(offsetMainAxis);
|
|
23
|
+
}
|
|
24
|
+
function getFlipMiddleware(opts) {
|
|
25
|
+
if (!opts.flip) return;
|
|
26
|
+
return flip({
|
|
27
|
+
padding: opts.overflowPadding,
|
|
28
|
+
fallbackPlacements: opts.flip === true ? undefined : opts.flip
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function getShiftMiddleware(opts) {
|
|
32
|
+
if (!opts.slide) return;
|
|
33
|
+
return shift({
|
|
34
|
+
mainAxis: opts.slide,
|
|
35
|
+
padding: opts.overflowPadding,
|
|
36
|
+
limiter: limitShift()
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function getSizeMiddleware(opts) {
|
|
40
|
+
return size({
|
|
41
|
+
padding: opts.overflowPadding,
|
|
42
|
+
apply ({ availableWidth, elements }) {
|
|
43
|
+
elements.floating.style.setProperty("--seed-popover-available-width", `${Math.max(0, availableWidth)}px`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const rectMiddleware = {
|
|
48
|
+
name: "rects",
|
|
49
|
+
fn ({ rects }) {
|
|
50
|
+
return {
|
|
51
|
+
data: rects
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const ARROW_FLOATING_STYLE = {
|
|
56
|
+
top: "",
|
|
57
|
+
right: "rotate(90deg)",
|
|
58
|
+
bottom: "rotate(180deg)",
|
|
59
|
+
left: "rotate(270deg)"
|
|
60
|
+
};
|
|
61
|
+
function usePositionedFloating(props) {
|
|
62
|
+
const options = {
|
|
63
|
+
...defaultPositioningOptions,
|
|
64
|
+
...props
|
|
65
|
+
};
|
|
66
|
+
const [open, onOpenChange] = useControllableState({
|
|
67
|
+
prop: props.open,
|
|
68
|
+
defaultProp: props.defaultOpen ?? false,
|
|
69
|
+
onChange: props.onOpenChange
|
|
70
|
+
});
|
|
71
|
+
const [arrowEl, setArrowEl] = useState(null);
|
|
72
|
+
const [arrowTipEl, setArrowTipEl] = useState(null);
|
|
73
|
+
const arrowTipWidth = arrowTipEl?.clientWidth ?? 0;
|
|
74
|
+
const arrowTipHeight = arrowTipEl?.clientHeight ?? 0;
|
|
75
|
+
const arrowTipOffset = arrowTipHeight;
|
|
76
|
+
const { refs, context, floatingStyles, middlewareData, isPositioned } = useFloating({
|
|
77
|
+
strategy: options.strategy,
|
|
78
|
+
open,
|
|
79
|
+
placement: options.placement,
|
|
80
|
+
onOpenChange: onOpenChange,
|
|
81
|
+
middleware: [
|
|
82
|
+
getOffsetMiddleware(arrowTipOffset, options),
|
|
83
|
+
getFlipMiddleware(options),
|
|
84
|
+
getShiftMiddleware(options),
|
|
85
|
+
getSizeMiddleware(options),
|
|
86
|
+
getArrowMiddleware(arrowEl, options),
|
|
87
|
+
rectMiddleware
|
|
88
|
+
]
|
|
89
|
+
});
|
|
90
|
+
// https://floating-ui.com/docs/react#anchoring
|
|
91
|
+
useEffect(()=>{
|
|
92
|
+
if (!open) return;
|
|
93
|
+
if (!refs.reference.current || !refs.floating.current) return;
|
|
94
|
+
return autoUpdate(refs.reference.current, refs.floating.current, context.update);
|
|
95
|
+
}, [
|
|
96
|
+
open,
|
|
97
|
+
refs.reference,
|
|
98
|
+
refs.floating,
|
|
99
|
+
context
|
|
100
|
+
]);
|
|
101
|
+
const [side, alignment] = context.placement.split("-");
|
|
102
|
+
const arrowStyles = useMemo(()=>({
|
|
103
|
+
position: "absolute",
|
|
104
|
+
left: middlewareData.arrow?.x,
|
|
105
|
+
top: middlewareData.arrow?.y,
|
|
106
|
+
[side]: "100%",
|
|
107
|
+
transform: ARROW_FLOATING_STYLE[side]
|
|
108
|
+
}), [
|
|
109
|
+
middlewareData.arrow,
|
|
110
|
+
side
|
|
111
|
+
]);
|
|
112
|
+
return useMemo(()=>({
|
|
113
|
+
open,
|
|
114
|
+
onOpenChange,
|
|
115
|
+
refs: {
|
|
116
|
+
...refs,
|
|
117
|
+
arrow: arrowEl,
|
|
118
|
+
setArrow: setArrowEl,
|
|
119
|
+
arrowTip: arrowTipEl,
|
|
120
|
+
setArrowTip: setArrowTipEl
|
|
121
|
+
},
|
|
122
|
+
rects: {
|
|
123
|
+
...middlewareData["rects"],
|
|
124
|
+
arrowTip: {
|
|
125
|
+
width: arrowTipWidth,
|
|
126
|
+
height: arrowTipHeight
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
isPositioned,
|
|
130
|
+
side,
|
|
131
|
+
alignment,
|
|
132
|
+
context,
|
|
133
|
+
floatingStyles,
|
|
134
|
+
arrowStyles
|
|
135
|
+
}), [
|
|
136
|
+
open,
|
|
137
|
+
onOpenChange,
|
|
138
|
+
refs,
|
|
139
|
+
arrowEl,
|
|
140
|
+
arrowTipEl,
|
|
141
|
+
middlewareData["rects"],
|
|
142
|
+
context,
|
|
143
|
+
side,
|
|
144
|
+
alignment,
|
|
145
|
+
floatingStyles,
|
|
146
|
+
arrowStyles,
|
|
147
|
+
isPositioned,
|
|
148
|
+
arrowTipWidth,
|
|
149
|
+
arrowTipHeight
|
|
150
|
+
]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export { usePositionedFloating };
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@seed-design/react-floating",
|
|
3
|
+
"version": "0.0.0-alpha-20260622094810",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/daangn/seed-design.git",
|
|
7
|
+
"directory": "packages/react-headless/floating"
|
|
8
|
+
},
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./lib/index.d.ts",
|
|
14
|
+
"import": "./lib/index.js",
|
|
15
|
+
"require": "./lib/index.cjs"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"main": "./lib/index.cjs",
|
|
20
|
+
"files": [
|
|
21
|
+
"lib",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clean": "rm -rf lib",
|
|
26
|
+
"build": "bunchee",
|
|
27
|
+
"lint:publish": "bun publint"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@floating-ui/react": "^0.27.0",
|
|
31
|
+
"@radix-ui/react-use-controllable-state": "1.2.2"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/react": "^19.1.6",
|
|
35
|
+
"react": "^19.1.0",
|
|
36
|
+
"react-dom": "^19.1.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"react": ">=18.0.0",
|
|
40
|
+
"react-dom": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import {
|
|
2
|
+
arrow,
|
|
3
|
+
autoUpdate,
|
|
4
|
+
flip,
|
|
5
|
+
limitShift,
|
|
6
|
+
offset,
|
|
7
|
+
shift,
|
|
8
|
+
size,
|
|
9
|
+
useFloating,
|
|
10
|
+
type Alignment,
|
|
11
|
+
type ExtendedRefs,
|
|
12
|
+
type FloatingContext,
|
|
13
|
+
type Middleware,
|
|
14
|
+
type Placement,
|
|
15
|
+
type Rect,
|
|
16
|
+
type ReferenceType,
|
|
17
|
+
type Side,
|
|
18
|
+
} from "@floating-ui/react";
|
|
19
|
+
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
|
20
|
+
import { useEffect, useMemo, useState, type CSSProperties } from "react";
|
|
21
|
+
|
|
22
|
+
export interface PositioningOptions {
|
|
23
|
+
/**
|
|
24
|
+
* The strategy to use for positioning
|
|
25
|
+
* @default "absolute"
|
|
26
|
+
*/
|
|
27
|
+
strategy?: "absolute" | "fixed";
|
|
28
|
+
/**
|
|
29
|
+
* The initial placement of the floating element
|
|
30
|
+
* @default "bottom"
|
|
31
|
+
*/
|
|
32
|
+
placement?: Placement;
|
|
33
|
+
/**
|
|
34
|
+
* The gutter between the floating element and the reference element
|
|
35
|
+
*/
|
|
36
|
+
gutter?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Whether to flip the placement
|
|
39
|
+
* @default true
|
|
40
|
+
*/
|
|
41
|
+
flip?: boolean | Placement[];
|
|
42
|
+
/**
|
|
43
|
+
* Whether the popover should slide when it overflows.
|
|
44
|
+
* @default true
|
|
45
|
+
*/
|
|
46
|
+
slide?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The virtual padding around the viewport edges to check for overflow
|
|
49
|
+
* @default 8
|
|
50
|
+
*/
|
|
51
|
+
overflowPadding?: number;
|
|
52
|
+
/**
|
|
53
|
+
* The minimum padding between the arrow and the floating element's corner.
|
|
54
|
+
* @default 4
|
|
55
|
+
*/
|
|
56
|
+
arrowPadding?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const defaultPositioningOptions: PositioningOptions = {
|
|
60
|
+
strategy: "absolute",
|
|
61
|
+
placement: "bottom",
|
|
62
|
+
flip: true,
|
|
63
|
+
slide: true,
|
|
64
|
+
overflowPadding: 8,
|
|
65
|
+
arrowPadding: 4,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function getArrowMiddleware(arrowElement: HTMLElement | null, opts: PositioningOptions) {
|
|
69
|
+
if (!arrowElement) return;
|
|
70
|
+
return arrow({ element: arrowElement, padding: opts.arrowPadding });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getOffsetMiddleware(arrowOffset: number, opts: PositioningOptions) {
|
|
74
|
+
const offsetMainAxis = (opts.gutter ?? 0) + arrowOffset;
|
|
75
|
+
return offset(offsetMainAxis);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getFlipMiddleware(opts: PositioningOptions) {
|
|
79
|
+
if (!opts.flip) return;
|
|
80
|
+
return flip({
|
|
81
|
+
padding: opts.overflowPadding,
|
|
82
|
+
fallbackPlacements: opts.flip === true ? undefined : opts.flip,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getShiftMiddleware(opts: PositioningOptions) {
|
|
87
|
+
if (!opts.slide) return;
|
|
88
|
+
return shift({
|
|
89
|
+
mainAxis: opts.slide,
|
|
90
|
+
padding: opts.overflowPadding,
|
|
91
|
+
limiter: limitShift(),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getSizeMiddleware(opts: PositioningOptions) {
|
|
96
|
+
return size({
|
|
97
|
+
padding: opts.overflowPadding,
|
|
98
|
+
apply({ availableWidth, elements }) {
|
|
99
|
+
elements.floating.style.setProperty(
|
|
100
|
+
"--seed-popover-available-width",
|
|
101
|
+
`${Math.max(0, availableWidth)}px`,
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const rectMiddleware: Middleware = {
|
|
108
|
+
name: "rects",
|
|
109
|
+
fn({ rects }) {
|
|
110
|
+
return {
|
|
111
|
+
data: rects,
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export interface UsePositionedFloatingProps extends PositioningOptions {
|
|
117
|
+
/**
|
|
118
|
+
* Whether the floating element is initially open
|
|
119
|
+
*/
|
|
120
|
+
defaultOpen?: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Whether the floating element is open
|
|
123
|
+
*/
|
|
124
|
+
open?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Callback when the floating element is opened or closed
|
|
127
|
+
*/
|
|
128
|
+
onOpenChange?: (open: boolean) => void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const ARROW_FLOATING_STYLE = {
|
|
132
|
+
top: "",
|
|
133
|
+
right: "rotate(90deg)",
|
|
134
|
+
bottom: "rotate(180deg)",
|
|
135
|
+
left: "rotate(270deg)",
|
|
136
|
+
} as const;
|
|
137
|
+
|
|
138
|
+
// Explicit return type interface - leveraging @floating-ui/react types
|
|
139
|
+
export interface UsePositionedFloatingReturn<RT extends ReferenceType = ReferenceType> {
|
|
140
|
+
open: boolean;
|
|
141
|
+
onOpenChange: ((open: boolean) => void) | undefined;
|
|
142
|
+
refs: ExtendedRefs<RT> & {
|
|
143
|
+
arrow: HTMLElement | null;
|
|
144
|
+
setArrow: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
|
|
145
|
+
arrowTip: HTMLElement | null;
|
|
146
|
+
setArrowTip: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
|
|
147
|
+
};
|
|
148
|
+
rects: {
|
|
149
|
+
reference: Rect;
|
|
150
|
+
floating: Rect;
|
|
151
|
+
arrowTip: { width: number; height: number };
|
|
152
|
+
};
|
|
153
|
+
isPositioned: boolean;
|
|
154
|
+
side: Side;
|
|
155
|
+
alignment: Alignment | undefined;
|
|
156
|
+
context: FloatingContext<RT>;
|
|
157
|
+
floatingStyles: CSSProperties;
|
|
158
|
+
arrowStyles: CSSProperties;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function usePositionedFloating<RT extends ReferenceType = ReferenceType>(
|
|
162
|
+
props: UsePositionedFloatingProps,
|
|
163
|
+
): UsePositionedFloatingReturn<RT> {
|
|
164
|
+
const options = { ...defaultPositioningOptions, ...props };
|
|
165
|
+
|
|
166
|
+
const [open, onOpenChange] = useControllableState({
|
|
167
|
+
prop: props.open,
|
|
168
|
+
defaultProp: props.defaultOpen ?? false,
|
|
169
|
+
onChange: props.onOpenChange,
|
|
170
|
+
});
|
|
171
|
+
const [arrowEl, setArrowEl] = useState<HTMLElement | null>(null);
|
|
172
|
+
const [arrowTipEl, setArrowTipEl] = useState<HTMLElement | null>(null);
|
|
173
|
+
|
|
174
|
+
const arrowTipWidth = arrowTipEl?.clientWidth ?? 0;
|
|
175
|
+
const arrowTipHeight = arrowTipEl?.clientHeight ?? 0;
|
|
176
|
+
const arrowTipOffset = arrowTipHeight;
|
|
177
|
+
|
|
178
|
+
const { refs, context, floatingStyles, middlewareData, isPositioned } = useFloating<RT>({
|
|
179
|
+
strategy: options.strategy,
|
|
180
|
+
open,
|
|
181
|
+
placement: options.placement,
|
|
182
|
+
onOpenChange: onOpenChange,
|
|
183
|
+
middleware: [
|
|
184
|
+
getOffsetMiddleware(arrowTipOffset, options),
|
|
185
|
+
getFlipMiddleware(options),
|
|
186
|
+
getShiftMiddleware(options),
|
|
187
|
+
getSizeMiddleware(options),
|
|
188
|
+
getArrowMiddleware(arrowEl, options),
|
|
189
|
+
rectMiddleware,
|
|
190
|
+
],
|
|
191
|
+
// instead of defining `whileElementsMounted` here, we use an effect below
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// https://floating-ui.com/docs/react#anchoring
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (!open) return;
|
|
197
|
+
if (!refs.reference.current || !refs.floating.current) return;
|
|
198
|
+
|
|
199
|
+
return autoUpdate(refs.reference.current, refs.floating.current, context.update);
|
|
200
|
+
}, [open, refs.reference, refs.floating, context]);
|
|
201
|
+
|
|
202
|
+
const [side, alignment] = context.placement.split("-") as [Side, Alignment | undefined];
|
|
203
|
+
|
|
204
|
+
const arrowStyles = useMemo(
|
|
205
|
+
() =>
|
|
206
|
+
({
|
|
207
|
+
position: "absolute",
|
|
208
|
+
left: middlewareData.arrow?.x,
|
|
209
|
+
top: middlewareData.arrow?.y,
|
|
210
|
+
[side]: "100%",
|
|
211
|
+
transform: ARROW_FLOATING_STYLE[side],
|
|
212
|
+
}) as const,
|
|
213
|
+
[middlewareData.arrow, side],
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
return useMemo(
|
|
217
|
+
() => ({
|
|
218
|
+
open,
|
|
219
|
+
onOpenChange,
|
|
220
|
+
refs: {
|
|
221
|
+
...refs,
|
|
222
|
+
arrow: arrowEl,
|
|
223
|
+
setArrow: setArrowEl,
|
|
224
|
+
arrowTip: arrowTipEl,
|
|
225
|
+
setArrowTip: setArrowTipEl,
|
|
226
|
+
},
|
|
227
|
+
rects: {
|
|
228
|
+
...middlewareData["rects"],
|
|
229
|
+
arrowTip: {
|
|
230
|
+
width: arrowTipWidth,
|
|
231
|
+
height: arrowTipHeight,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
isPositioned,
|
|
235
|
+
side,
|
|
236
|
+
alignment,
|
|
237
|
+
context,
|
|
238
|
+
floatingStyles,
|
|
239
|
+
arrowStyles,
|
|
240
|
+
}),
|
|
241
|
+
[
|
|
242
|
+
open,
|
|
243
|
+
onOpenChange,
|
|
244
|
+
refs,
|
|
245
|
+
arrowEl,
|
|
246
|
+
arrowTipEl,
|
|
247
|
+
middlewareData["rects"],
|
|
248
|
+
context,
|
|
249
|
+
side,
|
|
250
|
+
alignment,
|
|
251
|
+
floatingStyles,
|
|
252
|
+
arrowStyles,
|
|
253
|
+
isPositioned,
|
|
254
|
+
arrowTipWidth,
|
|
255
|
+
arrowTipHeight,
|
|
256
|
+
],
|
|
257
|
+
);
|
|
258
|
+
}
|