@passui/react 0.1.46 → 0.1.47
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/dist/types/src/components/popover/popover-content.d.ts +7 -1
- package/package.json +1 -1
- package/src/components/locale-switcher/locale-switcher.tsx +1 -2
- package/src/components/popover/popover-content.tsx +128 -88
- package/dist/types/src/components/popover/popover-fit.d.ts +0 -21
- package/src/components/popover/popover-fit.ts +0 -74
|
@@ -2,6 +2,7 @@ import { type ReactNode } from "react";
|
|
|
2
2
|
export type PopoverOrigin = "top-left" | "top-right";
|
|
3
3
|
export interface PopoverContentProps {
|
|
4
4
|
children: ReactNode;
|
|
5
|
+
/** Extra classes for the panel (sizing, padding, max-height). */
|
|
5
6
|
className?: string;
|
|
6
7
|
/** Accessible name (used when there is no labelledBy). */
|
|
7
8
|
label?: string;
|
|
@@ -22,6 +23,11 @@ export interface PopoverContentProps {
|
|
|
22
23
|
* @default "top-left"
|
|
23
24
|
*/
|
|
24
25
|
origin?: PopoverOrigin;
|
|
26
|
+
/**
|
|
27
|
+
* Gap between the trigger and the panel, in pixels.
|
|
28
|
+
* @default 8
|
|
29
|
+
*/
|
|
30
|
+
gap?: number;
|
|
25
31
|
/**
|
|
26
32
|
* ARIA role for the panel. Use "menu" for action menus, "dialog"
|
|
27
33
|
* (the default) for free-form content, "tooltip" for tips.
|
|
@@ -29,4 +35,4 @@ export interface PopoverContentProps {
|
|
|
29
35
|
*/
|
|
30
36
|
role?: "dialog" | "menu" | "tooltip";
|
|
31
37
|
}
|
|
32
|
-
export declare function PopoverContent({ children, className, label, labelledBy, dismissOnOutsidePress, placement, origin, role, }: PopoverContentProps): import("react").JSX.Element;
|
|
38
|
+
export declare function PopoverContent({ children, className, label, labelledBy, dismissOnOutsidePress, placement, origin, gap, role, }: PopoverContentProps): import("react").JSX.Element;
|
package/package.json
CHANGED
|
@@ -252,9 +252,8 @@ export function LocaleSwitcher<L extends string>({
|
|
|
252
252
|
role="menu"
|
|
253
253
|
label={label}
|
|
254
254
|
className={cn(
|
|
255
|
-
"
|
|
255
|
+
"flex max-w-[calc(100vw-2.5rem)] gap-2 p-2",
|
|
256
256
|
popoverWidths[size],
|
|
257
|
-
anchor.side === "right" ? "right-0" : "left-0",
|
|
258
257
|
growsUp ? "flex-col-reverse" : "flex-col",
|
|
259
258
|
)}
|
|
260
259
|
>
|
|
@@ -11,13 +11,13 @@ import { SheetPortal } from "../sheet/sheet-portal";
|
|
|
11
11
|
import { cn } from "../../lib/class-names";
|
|
12
12
|
import { screenEdges } from "../../lib/screen-edges";
|
|
13
13
|
import { fluidCollapse, fluidOvershoot } from "../../motion/transitions";
|
|
14
|
-
import { fitPopover, type PopoverFit } from "./popover-fit";
|
|
15
14
|
import { usePopover } from "./popover-root";
|
|
16
15
|
|
|
17
16
|
export type PopoverOrigin = "top-left" | "top-right";
|
|
18
17
|
|
|
19
18
|
export interface PopoverContentProps {
|
|
20
19
|
children: ReactNode;
|
|
20
|
+
/** Extra classes for the panel (sizing, padding, max-height). */
|
|
21
21
|
className?: string;
|
|
22
22
|
/** Accessible name (used when there is no labelledBy). */
|
|
23
23
|
label?: string;
|
|
@@ -38,6 +38,11 @@ export interface PopoverContentProps {
|
|
|
38
38
|
* @default "top-left"
|
|
39
39
|
*/
|
|
40
40
|
origin?: PopoverOrigin;
|
|
41
|
+
/**
|
|
42
|
+
* Gap between the trigger and the panel, in pixels.
|
|
43
|
+
* @default 8
|
|
44
|
+
*/
|
|
45
|
+
gap?: number;
|
|
41
46
|
/**
|
|
42
47
|
* ARIA role for the panel. Use "menu" for action menus, "dialog"
|
|
43
48
|
* (the default) for free-form content, "tooltip" for tips.
|
|
@@ -46,6 +51,67 @@ export interface PopoverContentProps {
|
|
|
46
51
|
role?: "dialog" | "menu" | "tooltip";
|
|
47
52
|
}
|
|
48
53
|
|
|
54
|
+
interface Placement {
|
|
55
|
+
/** Fixed left in viewport px. */
|
|
56
|
+
left: number;
|
|
57
|
+
/** Fixed top in viewport px. */
|
|
58
|
+
top: number;
|
|
59
|
+
/** Width cap so the panel never crosses the right safe edge. */
|
|
60
|
+
maxWidth: number;
|
|
61
|
+
/** Height cap; when set, the panel scrolls inside it. */
|
|
62
|
+
maxHeight?: number;
|
|
63
|
+
/** True when the panel opens above the trigger. */
|
|
64
|
+
above: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const NO_PLACEMENT: Placement = {
|
|
68
|
+
left: 0,
|
|
69
|
+
top: 0,
|
|
70
|
+
maxWidth: 0,
|
|
71
|
+
above: false,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Place the panel below (or above) the trigger, aligned to the chosen
|
|
76
|
+
* corner, kept inside the screen's safe edges. Pure: no DOM, just math.
|
|
77
|
+
*/
|
|
78
|
+
function placePanel(
|
|
79
|
+
trigger: DOMRect,
|
|
80
|
+
panel: { width: number; height: number },
|
|
81
|
+
origin: PopoverOrigin,
|
|
82
|
+
gap: number,
|
|
83
|
+
): Placement {
|
|
84
|
+
const edges = screenEdges();
|
|
85
|
+
const vv = window.visualViewport;
|
|
86
|
+
const vw = (vv?.width ?? window.innerWidth) + (vv?.offsetLeft ?? 0);
|
|
87
|
+
const vh = (vv?.height ?? window.innerHeight) + (vv?.offsetTop ?? 0);
|
|
88
|
+
const safeLeft = edges.left + (vv?.offsetLeft ?? 0);
|
|
89
|
+
const safeRight = vw - edges.right;
|
|
90
|
+
const safeTop = edges.top + (vv?.offsetTop ?? 0);
|
|
91
|
+
const safeBottom = vh - edges.bottom;
|
|
92
|
+
|
|
93
|
+
// Horizontal: align the chosen edge to the trigger, then clamp into view.
|
|
94
|
+
const maxWidth = Math.max(0, safeRight - safeLeft);
|
|
95
|
+
const width = Math.min(panel.width, maxWidth);
|
|
96
|
+
const left =
|
|
97
|
+
origin === "top-right"
|
|
98
|
+
? Math.max(safeLeft, Math.min(trigger.right - width, safeRight - width))
|
|
99
|
+
: Math.max(safeLeft, Math.min(trigger.left, safeRight - width));
|
|
100
|
+
|
|
101
|
+
// Vertical: prefer below; flip above when there isn't room below but
|
|
102
|
+
// there is above (matching the legacy popover's behaviour).
|
|
103
|
+
const roomBelow = safeBottom - trigger.bottom - gap;
|
|
104
|
+
const roomAbove = trigger.top - gap - safeTop;
|
|
105
|
+
const above = roomBelow < panel.height && roomAbove > roomBelow;
|
|
106
|
+
const room = above ? roomAbove : roomBelow;
|
|
107
|
+
const top = above
|
|
108
|
+
? trigger.top - gap - Math.min(panel.height, room)
|
|
109
|
+
: trigger.bottom + gap;
|
|
110
|
+
const maxHeight = panel.height > room ? room : undefined;
|
|
111
|
+
|
|
112
|
+
return { left, top, maxWidth, maxHeight, above };
|
|
113
|
+
}
|
|
114
|
+
|
|
49
115
|
export function PopoverContent({
|
|
50
116
|
children,
|
|
51
117
|
className,
|
|
@@ -54,6 +120,7 @@ export function PopoverContent({
|
|
|
54
120
|
dismissOnOutsidePress = true,
|
|
55
121
|
placement = "anchor",
|
|
56
122
|
origin = "top-left",
|
|
123
|
+
gap = 8,
|
|
57
124
|
role = "dialog",
|
|
58
125
|
}: PopoverContentProps) {
|
|
59
126
|
const { state, send, triggerRef, contentId } = usePopover();
|
|
@@ -61,96 +128,69 @@ export function PopoverContent({
|
|
|
61
128
|
const reducedMotion = useReducedMotion();
|
|
62
129
|
const [settled, setSettled] = useState(false);
|
|
63
130
|
const [material, setMaterial] = useState({ width: 0, height: 0, radius: 0 });
|
|
64
|
-
const [
|
|
65
|
-
shiftX: 0,
|
|
66
|
-
shiftY: 0,
|
|
67
|
-
maxWidth: 0,
|
|
68
|
-
above: false,
|
|
69
|
-
});
|
|
131
|
+
const [placementState, setPlacement] = useState<Placement>(NO_PLACEMENT);
|
|
70
132
|
const [reveal, setReveal] = useState<ReturnType<
|
|
71
133
|
typeof menuSurfaceReveal
|
|
72
134
|
> | null>(null);
|
|
73
135
|
|
|
74
136
|
useSheetFocus(panel, () => send("CLOSE"));
|
|
75
137
|
|
|
76
|
-
//
|
|
77
|
-
//
|
|
138
|
+
// Re-measure on every layout pass: trigger position, panel size, scroll,
|
|
139
|
+
// viewport resize. Recompute the fixed placement + the morph source rect.
|
|
78
140
|
useLayoutEffect(() => {
|
|
79
141
|
const surface = panel.current;
|
|
80
142
|
const source = triggerRef.current;
|
|
81
143
|
if (!surface) return;
|
|
82
144
|
|
|
83
145
|
const refit = () => {
|
|
84
|
-
const
|
|
146
|
+
const trigger = source?.getBoundingClientRect();
|
|
147
|
+
// Measure the authored size (padding/width from className) so the
|
|
148
|
+
// morph target is the natural panel the author styled.
|
|
149
|
+
const natural = surface.getBoundingClientRect();
|
|
85
150
|
const radius =
|
|
86
151
|
Number.parseFloat(getComputedStyle(surface).borderTopLeftRadius) || 0;
|
|
87
152
|
setMaterial((prev) =>
|
|
88
|
-
prev.width ===
|
|
89
|
-
prev.height ===
|
|
153
|
+
prev.width === natural.width &&
|
|
154
|
+
prev.height === natural.height &&
|
|
90
155
|
prev.radius === radius
|
|
91
156
|
? prev
|
|
92
|
-
: { width:
|
|
157
|
+
: { width: natural.width, height: natural.height, radius },
|
|
93
158
|
);
|
|
94
159
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
height: panelRect.height,
|
|
106
|
-
},
|
|
107
|
-
trigger ? { top: trigger.top, bottom: trigger.bottom } : undefined,
|
|
108
|
-
{
|
|
109
|
-
viewport: {
|
|
110
|
-
width: offsetLeft + (visualViewport?.width ?? window.innerWidth),
|
|
111
|
-
height: offsetTop + (visualViewport?.height ?? window.innerHeight),
|
|
112
|
-
},
|
|
113
|
-
edges: {
|
|
114
|
-
...edges,
|
|
115
|
-
left: offsetLeft + edges.left,
|
|
116
|
-
top: offsetTop + edges.top,
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
trigger ? -trigger.height : 6,
|
|
120
|
-
origin,
|
|
121
|
-
);
|
|
122
|
-
setFit((current) => {
|
|
123
|
-
if (
|
|
124
|
-
current.shiftX === next.shiftX &&
|
|
125
|
-
current.shiftY === next.shiftY &&
|
|
160
|
+
if (trigger && trigger.width > 0 && trigger.height > 0) {
|
|
161
|
+
const next = placePanel(
|
|
162
|
+
trigger,
|
|
163
|
+
{ width: natural.width, height: natural.height },
|
|
164
|
+
origin,
|
|
165
|
+
gap,
|
|
166
|
+
);
|
|
167
|
+
setPlacement((current) =>
|
|
168
|
+
current.left === next.left &&
|
|
169
|
+
current.top === next.top &&
|
|
126
170
|
current.maxWidth === next.maxWidth &&
|
|
127
171
|
current.maxHeight === next.maxHeight &&
|
|
128
172
|
current.above === next.above
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
: panelRect.height,
|
|
151
|
-
radius,
|
|
152
|
-
}),
|
|
153
|
-
);
|
|
173
|
+
? current
|
|
174
|
+
: next,
|
|
175
|
+
);
|
|
176
|
+
const sourceArg: MenuRect = {
|
|
177
|
+
left: trigger.left,
|
|
178
|
+
top: trigger.top,
|
|
179
|
+
width: trigger.width,
|
|
180
|
+
height: trigger.height,
|
|
181
|
+
};
|
|
182
|
+
setReveal(
|
|
183
|
+
menuSurfaceReveal(sourceArg, {
|
|
184
|
+
left: next.left,
|
|
185
|
+
top: next.top,
|
|
186
|
+
width: Math.min(natural.width, next.maxWidth),
|
|
187
|
+
height: next.maxHeight
|
|
188
|
+
? Math.min(natural.height, next.maxHeight)
|
|
189
|
+
: natural.height,
|
|
190
|
+
radius,
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
154
194
|
};
|
|
155
195
|
|
|
156
196
|
refit();
|
|
@@ -175,9 +215,9 @@ export function PopoverContent({
|
|
|
175
215
|
window.removeEventListener("resize", schedule);
|
|
176
216
|
window.removeEventListener("scroll", schedule, true);
|
|
177
217
|
};
|
|
178
|
-
}, [triggerRef, origin]);
|
|
218
|
+
}, [triggerRef, origin, gap]);
|
|
179
219
|
|
|
180
|
-
// Conceal the trigger while the surface is morphing.
|
|
220
|
+
// Conceal the trigger while the surface is morphing out of it.
|
|
181
221
|
useLayoutEffect(() => {
|
|
182
222
|
const source = triggerRef.current;
|
|
183
223
|
if (!source) return;
|
|
@@ -188,6 +228,7 @@ export function PopoverContent({
|
|
|
188
228
|
}, [triggerRef]);
|
|
189
229
|
|
|
190
230
|
const anchored = placement === "anchor";
|
|
231
|
+
const scrollable = settled && placementState.maxHeight !== undefined;
|
|
191
232
|
|
|
192
233
|
return (
|
|
193
234
|
<AnimatePresence>
|
|
@@ -213,31 +254,30 @@ export function PopoverContent({
|
|
|
213
254
|
aria-labelledby={labelledBy}
|
|
214
255
|
tabIndex={-1}
|
|
215
256
|
data-state={state}
|
|
257
|
+
data-placement={
|
|
258
|
+
anchored ? (placementState.above ? "top" : "bottom") : "center"
|
|
259
|
+
}
|
|
216
260
|
className={cn(
|
|
217
|
-
"
|
|
261
|
+
"isolate flex flex-col outline-none",
|
|
218
262
|
anchored
|
|
219
|
-
?
|
|
220
|
-
"rounded-5xl absolute z-40",
|
|
221
|
-
"[&>:not([data-popover-surface])]:opacity-[var(--popover-content-opacity)]",
|
|
222
|
-
)
|
|
263
|
+
? "rounded-5xl absolute z-40 [&>:not([data-popover-surface])]:opacity-[var(--popover-content-opacity)]"
|
|
223
264
|
: "rounded-5xl fixed z-[76] max-h-[92%] w-full max-w-[560px] p-6",
|
|
224
|
-
|
|
265
|
+
scrollable ? "overflow-hidden" : undefined,
|
|
225
266
|
className,
|
|
226
267
|
)}
|
|
227
268
|
style={
|
|
228
269
|
anchored
|
|
229
270
|
? {
|
|
271
|
+
position: "fixed",
|
|
272
|
+
left: placementState.left,
|
|
273
|
+
top: placementState.top,
|
|
274
|
+
maxWidth: placementState.maxWidth || undefined,
|
|
275
|
+
maxHeight: placementState.maxHeight,
|
|
276
|
+
overflowY: scrollable ? "auto" : undefined,
|
|
230
277
|
transformOrigin:
|
|
231
278
|
origin === "top-left"
|
|
232
|
-
? `0% ${
|
|
233
|
-
: `100% ${
|
|
234
|
-
translate: `${fit.shiftX}px ${fit.shiftY}px`,
|
|
235
|
-
maxWidth: fit.maxWidth || undefined,
|
|
236
|
-
maxHeight: fit.maxHeight,
|
|
237
|
-
overflowY:
|
|
238
|
-
fit.maxHeight !== undefined && settled
|
|
239
|
-
? "auto"
|
|
240
|
-
: undefined,
|
|
279
|
+
? `0% ${placementState.above ? "100%" : "0%"}`
|
|
280
|
+
: `100% ${placementState.above ? "100%" : "0%"}`,
|
|
241
281
|
}
|
|
242
282
|
: undefined
|
|
243
283
|
}
|
|
@@ -287,8 +327,8 @@ export function PopoverContent({
|
|
|
287
327
|
x: 0,
|
|
288
328
|
y: 0,
|
|
289
329
|
width: material.width,
|
|
290
|
-
height:
|
|
291
|
-
? Math.min(material.height,
|
|
330
|
+
height: placementState.maxHeight
|
|
331
|
+
? Math.min(material.height, placementState.maxHeight)
|
|
292
332
|
: material.height,
|
|
293
333
|
opacity: 1,
|
|
294
334
|
borderRadius: material.radius,
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { type PlacementFrame } from "../../lib/anchored-placement";
|
|
2
|
-
interface PanelRect {
|
|
3
|
-
left: number;
|
|
4
|
-
top: number;
|
|
5
|
-
width: number;
|
|
6
|
-
height: number;
|
|
7
|
-
}
|
|
8
|
-
interface AnchorRect {
|
|
9
|
-
top: number;
|
|
10
|
-
bottom: number;
|
|
11
|
-
}
|
|
12
|
-
export interface PopoverFit {
|
|
13
|
-
shiftX: number;
|
|
14
|
-
shiftY: number;
|
|
15
|
-
maxWidth: number;
|
|
16
|
-
maxHeight?: number;
|
|
17
|
-
above: boolean;
|
|
18
|
-
}
|
|
19
|
-
/** Keep an absolutely positioned popover visible without scaling its contents. */
|
|
20
|
-
export declare function fitPopover(panel: PanelRect, anchor: AnchorRect | undefined, frame: Pick<PlacementFrame, "viewport" | "edges">, gap?: number, origin?: "top-left" | "top-right"): PopoverFit;
|
|
21
|
-
export {};
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type PlacementFrame,
|
|
3
|
-
placeInViewport,
|
|
4
|
-
} from "../../lib/anchored-placement";
|
|
5
|
-
|
|
6
|
-
interface PanelRect {
|
|
7
|
-
left: number;
|
|
8
|
-
top: number;
|
|
9
|
-
width: number;
|
|
10
|
-
height: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface AnchorRect {
|
|
14
|
-
top: number;
|
|
15
|
-
bottom: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface PopoverFit {
|
|
19
|
-
shiftX: number;
|
|
20
|
-
shiftY: number;
|
|
21
|
-
maxWidth: number;
|
|
22
|
-
maxHeight?: number;
|
|
23
|
-
above: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Keep an absolutely positioned popover visible without scaling its contents. */
|
|
27
|
-
export function fitPopover(
|
|
28
|
-
panel: PanelRect,
|
|
29
|
-
anchor: AnchorRect | undefined,
|
|
30
|
-
frame: Pick<PlacementFrame, "viewport" | "edges">,
|
|
31
|
-
gap = 6,
|
|
32
|
-
origin: "top-left" | "top-right" = "top-left",
|
|
33
|
-
): PopoverFit {
|
|
34
|
-
const { viewport, edges } = frame;
|
|
35
|
-
const topEdge = edges.top;
|
|
36
|
-
const bottomEdge = viewport.height - edges.bottom;
|
|
37
|
-
const availableHeight = Math.max(0, bottomEdge - topEdge);
|
|
38
|
-
const placement = placeInViewport(
|
|
39
|
-
{
|
|
40
|
-
...frame,
|
|
41
|
-
anchor: {
|
|
42
|
-
left: panel.left,
|
|
43
|
-
top: anchor?.top ?? panel.top,
|
|
44
|
-
bottom: anchor?.bottom ?? panel.top,
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
panel.width,
|
|
48
|
-
panel.height,
|
|
49
|
-
gap,
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const above = Boolean(
|
|
53
|
-
anchor && panel.top + panel.height > bottomEdge && placement.above,
|
|
54
|
-
);
|
|
55
|
-
const aboveHeight = anchor ? Math.max(0, anchor.top - topEdge - gap) : 0;
|
|
56
|
-
const visibleHeight = above
|
|
57
|
-
? Math.min(panel.height, aboveHeight)
|
|
58
|
-
: Math.min(panel.height, availableHeight);
|
|
59
|
-
const desiredTop = above
|
|
60
|
-
? (anchor?.top ?? panel.top) - gap - visibleHeight
|
|
61
|
-
: Math.max(topEdge, Math.min(panel.top, bottomEdge - visibleHeight));
|
|
62
|
-
const heightRoom = above ? aboveHeight : Math.max(0, bottomEdge - desiredTop);
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
shiftX:
|
|
66
|
-
placement.left -
|
|
67
|
-
panel.left -
|
|
68
|
-
(origin === "top-right" ? panel.width - placement.width : 0),
|
|
69
|
-
shiftY: desiredTop - panel.top,
|
|
70
|
-
maxWidth: Math.max(0, viewport.width - edges.left - edges.right),
|
|
71
|
-
maxHeight: panel.height > heightRoom ? heightRoom : undefined,
|
|
72
|
-
above,
|
|
73
|
-
};
|
|
74
|
-
}
|