Skip to content

Commit

Permalink
feat: make popout openable with anchor coordinates instead of anchor …
Browse files Browse the repository at this point in the history
…element (#38)

BREAKING CHANGE: popout children forward ref and open prop is now removed with anchor prop
  • Loading branch information
ajbura authored Apr 14, 2024
1 parent 3557a83 commit 9520729
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 41 deletions.
43 changes: 31 additions & 12 deletions src/components/pop-out/PopOut.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import FocusTrap from "focus-trap-react";
import React, { useState } from "react";
import React, { MouseEventHandler, useState } from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Text } from "../text";
import { PopOut } from "./PopOut";
Expand All @@ -8,25 +8,47 @@ import { Icon, Icons } from "../icon";
import { IconButton } from "../icon-button";
import { config } from "../../theme/config.css";
import { Box } from "../box";
import { RectCords } from "../util";

export default {
title: "PopOut",
component: PopOut,
} as ComponentMeta<typeof PopOut>;

const Template: ComponentStory<typeof PopOut> = (args) => {
const [open, setOpen] = useState(false);
const [anchor, setAnchor] = useState<RectCords>();

const handleOpen: MouseEventHandler<HTMLElement> = (evt) => {
const rect = evt.currentTarget?.getBoundingClientRect();
setAnchor(anchor ? undefined : rect);
};
const handleContextOpen: MouseEventHandler<HTMLElement> = (evt) => {
evt.preventDefault();
const rect = {
x: evt.clientX,
y: evt.clientY,
width: 0,
height: 0,
};
setAnchor(anchor ? undefined : rect);
};

return (
<Box justifyContent="Center" alignItems="Center" style={{ height: "100vh" }}>
<Box
onContextMenu={handleContextOpen}
justifyContent="Center"
alignItems="Center"
style={{ height: "100vh" }}
>
<PopOut
{...args}
open={open}
anchor={anchor}
offset={anchor?.width === 0 ? 0 : undefined}
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setOpen(false),
onDeactivate: () => setAnchor(undefined),
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === "ArrowDown",
isKeyBackward: (evt: KeyboardEvent) => evt.key === "ArrowUp",
Expand All @@ -45,13 +67,10 @@ const Template: ComponentStory<typeof PopOut> = (args) => {
</Menu>
</FocusTrap>
}
>
{(ref) => (
<IconButton variant="SurfaceVariant" onClick={() => setOpen((state) => !state)} ref={ref}>
<Icon src={Icons.VerticalDots} />
</IconButton>
)}
</PopOut>
/>
<IconButton variant="SurfaceVariant" onClick={handleOpen}>
<Icon src={Icons.VerticalDots} />
</IconButton>
</Box>
);
};
Expand Down
43 changes: 14 additions & 29 deletions src/components/pop-out/PopOut.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
import classNames from "classnames";
import React, {
ReactNode,
RefCallback,
useCallback,
useEffect,
useLayoutEffect,
useRef,
} from "react";
import React, { ReactNode, useCallback, useEffect, useLayoutEffect, useRef } from "react";
import { as } from "../as";
import { Portal } from "../portal";
import { Align, getRelativeFixedPosition, Position } from "../util";
import { Align, getRelativeFixedPosition, Position, RectCords } from "../util";
import * as css from "./PopOut.css";

export interface PopOutProps {
open: boolean;
anchor?: RectCords;
position?: Position;
align?: Align;
offset?: number;
alignOffset?: number;
content: ReactNode;
children: (anchorRef: RefCallback<HTMLElement | SVGElement>) => ReactNode;
}
export const PopOut = as<"div", PopOutProps>(
(
{
as: AsPopOut = "div",
className,
open,
anchor,
position = "Bottom",
align = "Center",
offset = 10,
Expand All @@ -37,17 +29,14 @@ export const PopOut = as<"div", PopOutProps>(
},
ref
) => {
const anchorRef = useRef<HTMLElement | SVGElement | null>(null);
const baseRef = useRef<HTMLDivElement>(null);

const positionPopOut = useCallback(() => {
const anchor = anchorRef.current;
const baseEl = baseRef.current;
if (!anchor) return;
if (!baseEl) return;
if (!baseEl || !anchor) return;

const pCSS = getRelativeFixedPosition(
anchor.getBoundingClientRect(),
anchor,
baseEl.getBoundingClientRect(),
position,
align,
Expand All @@ -58,7 +47,7 @@ export const PopOut = as<"div", PopOutProps>(
baseEl.style.bottom = pCSS.bottom ?? "unset";
baseEl.style.left = pCSS.left ?? "unset";
baseEl.style.right = pCSS.right ?? "unset";
}, [position, align, offset, alignOffset]);
}, [anchor, position, align, offset, alignOffset]);

useEffect(() => {
window.addEventListener("resize", positionPopOut);
Expand All @@ -68,25 +57,21 @@ export const PopOut = as<"div", PopOutProps>(
}, [positionPopOut]);

useLayoutEffect(() => {
if (open) positionPopOut();
}, [open, positionPopOut]);

const handleAnchorRef: RefCallback<HTMLElement | SVGElement> = useCallback((element) => {
anchorRef.current = element;
}, []);
positionPopOut();
}, [positionPopOut]);

return (
<>
{children(handleAnchorRef)}
<Portal>
{open && (
{children}
{anchor && (
<Portal>
<AsPopOut className={classNames(css.PopOut, className)} {...props} ref={ref}>
<div ref={baseRef} className={css.PopOutContainer}>
{content}
</div>
</AsPopOut>
)}
</Portal>
</Portal>
)}
</>
);
}
Expand Down

0 comments on commit 9520729

Please sign in to comment.