Skip to content

Commit 86b66d9

Browse files
authored
feat: open popout with anchor coordinates (#40)
* fix: types and api of tooltip provide and popout component * fix: auto fallback position and align * style: move inline style to file * feat: make popout openable with anchor cords instead of anchor element BREAKING CHANGE: popout children forward ref and open prop is now removed with anchor prop
1 parent 637f9be commit 86b66d9

File tree

7 files changed

+536
-165
lines changed

7 files changed

+536
-165
lines changed

src/components/pop-out/PopOut.css.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { style } from "@vanilla-extract/css";
2+
import { config } from "../../theme";
3+
4+
export const PopOut = style({
5+
position: "fixed",
6+
top: 0,
7+
right: 0,
8+
bottom: 0,
9+
left: 0,
10+
zIndex: config.zIndex.Max,
11+
...style,
12+
});
13+
14+
export const PopOutContainer = style({
15+
display: "inline-block",
16+
position: "fixed",
17+
maxWidth: "100vw",
18+
maxHeight: "100vh",
19+
});
Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,54 @@
11
import FocusTrap from "focus-trap-react";
2-
import React, { useState } from "react";
3-
import { ComponentMeta } from "@storybook/react";
2+
import React, { MouseEventHandler, useState } from "react";
3+
import { ComponentMeta, ComponentStory } from "@storybook/react";
44
import { Text } from "../text";
55
import { PopOut } from "./PopOut";
66
import { Menu, MenuItem } from "../menu";
77
import { Icon, Icons } from "../icon";
88
import { IconButton } from "../icon-button";
99
import { config } from "../../theme/config.css";
10+
import { Box } from "../box";
11+
import { RectCords } from "../util";
1012

1113
export default {
1214
title: "PopOut",
1315
component: PopOut,
1416
} as ComponentMeta<typeof PopOut>;
1517

16-
export const Interactive = () => {
17-
const [open, setOpen] = useState(false);
18+
const Template: ComponentStory<typeof PopOut> = (args) => {
19+
const [anchor, setAnchor] = useState<RectCords>();
20+
21+
const handleOpen: MouseEventHandler<HTMLElement> = (evt) => {
22+
const rect = evt.currentTarget?.getBoundingClientRect();
23+
setAnchor(anchor ? undefined : rect);
24+
};
25+
const handleContextOpen: MouseEventHandler<HTMLElement> = (evt) => {
26+
evt.preventDefault();
27+
const rect = {
28+
x: evt.clientX,
29+
y: evt.clientY,
30+
width: 0,
31+
height: 0,
32+
};
33+
setAnchor(anchor ? undefined : rect);
34+
};
1835

1936
return (
20-
<div style={{ height: "100vh" }}>
37+
<Box
38+
onContextMenu={handleContextOpen}
39+
justifyContent="Center"
40+
alignItems="Center"
41+
style={{ height: "100vh" }}
42+
>
2143
<PopOut
22-
open={open}
23-
align="Start"
44+
{...args}
45+
anchor={anchor}
46+
offset={anchor?.width === 0 ? 0 : undefined}
2447
content={
2548
<FocusTrap
2649
focusTrapOptions={{
2750
initialFocus: false,
28-
onDeactivate: () => setOpen(false),
51+
onDeactivate: () => setAnchor(undefined),
2952
clickOutsideDeactivates: true,
3053
isKeyForward: (evt: KeyboardEvent) => evt.key === "ArrowDown",
3154
isKeyBackward: (evt: KeyboardEvent) => evt.key === "ArrowUp",
@@ -44,13 +67,12 @@ export const Interactive = () => {
4467
</Menu>
4568
</FocusTrap>
4669
}
47-
>
48-
{(ref) => (
49-
<IconButton variant="SurfaceVariant" onClick={() => setOpen((state) => !state)} ref={ref}>
50-
<Icon src={Icons.VerticalDots} />
51-
</IconButton>
52-
)}
53-
</PopOut>
54-
</div>
70+
/>
71+
<IconButton variant="SurfaceVariant" onClick={handleOpen}>
72+
<Icon src={Icons.VerticalDots} />
73+
</IconButton>
74+
</Box>
5575
);
5676
};
77+
78+
export const Interactive = Template.bind({});

src/components/pop-out/PopOut.tsx

Lines changed: 26 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,53 @@
1-
import React, {
2-
ReactNode,
3-
RefCallback,
4-
useCallback,
5-
useEffect,
6-
useLayoutEffect,
7-
useRef,
8-
} from "react";
9-
import { config } from "../../theme";
1+
import classNames from "classnames";
2+
import React, { ReactNode, useCallback, useEffect, useLayoutEffect, useRef } from "react";
103
import { as } from "../as";
114
import { Portal } from "../portal";
12-
import { Align, getRelativeFixedPosition, Position } from "../util";
5+
import { Align, getRelativeFixedPosition, Position, RectCords } from "../util";
6+
import * as css from "./PopOut.css";
137

148
export interface PopOutProps {
15-
open: boolean;
9+
anchor?: RectCords;
1610
position?: Position;
1711
align?: Align;
1812
offset?: number;
1913
alignOffset?: number;
2014
content: ReactNode;
21-
children: (anchorRef: RefCallback<HTMLElement | SVGElement>) => ReactNode;
2215
}
2316
export const PopOut = as<"div", PopOutProps>(
2417
(
2518
{
2619
as: AsPopOut = "div",
27-
open,
20+
className,
21+
anchor,
2822
position = "Bottom",
2923
align = "Center",
3024
offset = 10,
3125
alignOffset = 0,
3226
content,
3327
children,
34-
style,
3528
...props
3629
},
3730
ref
3831
) => {
39-
const anchorRef = useRef<HTMLElement | SVGElement | null>(null);
4032
const baseRef = useRef<HTMLDivElement>(null);
4133

4234
const positionPopOut = useCallback(() => {
43-
const anchor = anchorRef.current;
4435
const baseEl = baseRef.current;
45-
if (!anchor) return;
46-
if (!baseEl) return;
36+
if (!baseEl || !anchor) return;
4737

48-
const css = getRelativeFixedPosition(
49-
anchor.getBoundingClientRect(),
38+
const pCSS = getRelativeFixedPosition(
39+
anchor,
40+
baseEl.getBoundingClientRect(),
5041
position,
5142
align,
5243
offset,
53-
alignOffset,
54-
baseEl.getBoundingClientRect()
44+
alignOffset
5545
);
56-
baseEl.style.top = css.top;
57-
baseEl.style.bottom = css.bottom;
58-
baseEl.style.left = css.left;
59-
baseEl.style.right = css.right;
60-
baseEl.style.transform = css.transform;
61-
}, [position, align, offset, alignOffset]);
46+
baseEl.style.top = pCSS.top ?? "unset";
47+
baseEl.style.bottom = pCSS.bottom ?? "unset";
48+
baseEl.style.left = pCSS.left ?? "unset";
49+
baseEl.style.right = pCSS.right ?? "unset";
50+
}, [anchor, position, align, offset, alignOffset]);
6251

6352
useEffect(() => {
6453
window.addEventListener("resize", positionPopOut);
@@ -68,45 +57,21 @@ export const PopOut = as<"div", PopOutProps>(
6857
}, [positionPopOut]);
6958

7059
useLayoutEffect(() => {
71-
if (open) positionPopOut();
72-
}, [open, positionPopOut]);
73-
74-
const handleAnchorRef: RefCallback<HTMLElement | SVGElement> = useCallback((element) => {
75-
anchorRef.current = element;
76-
}, []);
60+
positionPopOut();
61+
}, [positionPopOut]);
7762

7863
return (
7964
<>
80-
{children(handleAnchorRef)}
81-
<Portal>
82-
{open && (
83-
<AsPopOut
84-
style={{
85-
position: "fixed",
86-
top: 0,
87-
right: 0,
88-
bottom: 0,
89-
left: 0,
90-
zIndex: config.zIndex.Max,
91-
...style,
92-
}}
93-
{...props}
94-
ref={ref}
95-
>
96-
<div
97-
ref={baseRef}
98-
style={{
99-
display: "inline-block",
100-
position: "fixed",
101-
maxWidth: "100vw",
102-
maxHeight: "100vh",
103-
}}
104-
>
65+
{children}
66+
{anchor && (
67+
<Portal>
68+
<AsPopOut className={classNames(css.PopOut, className)} {...props} ref={ref}>
69+
<div ref={baseRef} className={css.PopOutContainer}>
10570
{content}
10671
</div>
10772
</AsPopOut>
108-
)}
109-
</Portal>
73+
</Portal>
74+
)}
11075
</>
11176
);
11277
}

src/components/tooltip/Tooltip.css.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ComplexStyleRule, keyframes } from "@vanilla-extract/css";
1+
import { ComplexStyleRule, keyframes, style } from "@vanilla-extract/css";
22
import { recipe, RecipeVariants } from "@vanilla-extract/recipes";
33
import { color } from "../../theme/color.css";
44
import { config } from "../../theme/config.css";
@@ -53,3 +53,12 @@ export const Tooltip = recipe({
5353
});
5454

5555
export type TooltipVariants = RecipeVariants<typeof Tooltip>;
56+
57+
export const TooltipProvider = style({
58+
display: "inline-block",
59+
position: "fixed",
60+
maxWidth: "100vw",
61+
maxHeight: "100vh",
62+
zIndex: config.zIndex.Max,
63+
pointerEvents: "none",
64+
});

0 commit comments

Comments
 (0)