Skip to content

Commit

Permalink
Add support for nested popovers (#928)
Browse files Browse the repository at this point in the history
Co-authored-by: Mattias Andersson <[email protected]>
  • Loading branch information
Limpaan and mattias800 authored Nov 26, 2024
1 parent 82f2aae commit fe28600
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 72 deletions.
96 changes: 64 additions & 32 deletions packages/tooltip/src/components/popover/ControlledPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import {
flip,
FloatingArrow,
FloatingFocusManager,
FloatingNode,
FloatingPortal,
FloatingTree,
offset,
shift,
useDismiss,
useFloating,
useFloatingNodeId,
useFloatingParentNodeId,
useInteractions,
useRole,
useTransitionStyles,
Expand Down Expand Up @@ -38,6 +42,29 @@ const ARROW_HEIGHT = 8;
const GAP = 2;

export const ControlledPopover: React.FC<ControlledPopoverProps> = ({
children,
...props
}) => {
const parentNodeId = useFloatingParentNodeId();

if (parentNodeId == null) {
return (
<FloatingTree>
<ControlledPopoverComponent {...props}>
{children}
</ControlledPopoverComponent>
</FloatingTree>
);
}

return (
<ControlledPopoverComponent {...props}>
{children}
</ControlledPopoverComponent>
);
};

const ControlledPopoverComponent: React.FC<ControlledPopoverProps> = ({
children,
placement = "bottom",
hideArrow,
Expand All @@ -51,6 +78,8 @@ export const ControlledPopover: React.FC<ControlledPopoverProps> = ({
appendTo,
zIndex,
}) => {
const nodeId = useFloatingNodeId();

const arrowRef = useRef(null);

const onOpenChange = useCallback(
Expand All @@ -63,6 +92,7 @@ export const ControlledPopover: React.FC<ControlledPopoverProps> = ({
);

const { refs, floatingStyles, context } = useFloating({
nodeId,
open,
onOpenChange,
placement,
Expand Down Expand Up @@ -93,42 +123,44 @@ export const ControlledPopover: React.FC<ControlledPopoverProps> = ({
<>
{renderTrigger({ ref: refs.setReference, ...getReferenceProps() })}

{isMounted && (
<FloatingPortal root={appendTo}>
<FloatingFocusManager
context={context}
modal={false}
restoreFocus={restoreFocus}
returnFocus={returnFocus}
initialFocus={initialFocus}
>
<div
ref={refs.setFloating}
style={{ zIndex, ...floatingStyles }}
{...getFloatingProps}
<FloatingNode id={nodeId}>
{isMounted && (
<FloatingPortal root={appendTo}>
<FloatingFocusManager
context={context}
modal={false}
restoreFocus={restoreFocus}
returnFocus={returnFocus}
initialFocus={initialFocus}
>
<div
style={transitionStyles}
className={cx(
moduleStyles.floating,
disablePadding && moduleStyles.disablePadding,
)}
ref={refs.setFloating}
style={{ zIndex, ...floatingStyles }}
{...getFloatingProps()}
>
{children}
{!hideArrow && (
<FloatingArrow
ref={arrowRef}
context={context}
width={ARROW_WIDTH}
height={ARROW_HEIGHT}
fill={"white"}
/>
)}
<div
style={transitionStyles}
className={cx(
moduleStyles.floating,
disablePadding && moduleStyles.disablePadding,
)}
>
{children}
{!hideArrow && (
<FloatingArrow
ref={arrowRef}
context={context}
width={ARROW_WIDTH}
height={ARROW_HEIGHT}
fill={"white"}
/>
)}
</div>
</div>
</div>
</FloatingFocusManager>
</FloatingPortal>
)}
</FloatingFocusManager>
</FloatingPortal>
)}
</FloatingNode>
</>
);
};
50 changes: 49 additions & 1 deletion packages/tooltip/src/components/popover/Popover.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import {
} from "@stenajs-webui/core";
import { FlatButton, Icon, stenaTrash } from "@stenajs-webui/elements";
import * as React from "react";
import { useState } from "react";
import { ActionPrompt } from "./ActionPrompt";
import { Popover } from "./Popover";
import { useState } from "react";
import { ControlledPopover } from "./ControlledPopover";

export default {
title: "tooltip/Popover",
Expand Down Expand Up @@ -147,3 +148,50 @@ export const Variants = () => (
</Popover>
</Box>
);

export const ControlledPopoverInPopover = () => {
const [outerOpen, setOuterOpen] = useState(false);
const [innerOpen, setInnerOpen] = useState(false);

return (
<Row alignItems={"center"}>
<FlatButton label={"Open outer"} onClick={() => setOuterOpen(true)} />
<Space />
<ControlledPopover
renderTrigger={(props) => <Text {...props}>Outer</Text>}
open={outerOpen}
onRequestClose={() => setOuterOpen(false)}
>
<FlatButton label={"Open inner"} onClick={() => setInnerOpen(true)} />
<Space />
<ControlledPopover
renderTrigger={(props) => <Text {...props}>Inner</Text>}
open={innerOpen}
onRequestClose={() => setInnerOpen(false)}
>
<FlatButton label={"I can be clicked"} />
</ControlledPopover>
</ControlledPopover>
</Row>
);
};

export const PopoverInPopover = () => (
<Box display={"inline-block"}>
<Popover
renderTrigger={(props) => <FlatButton label={"Open outer"} {...props} />}
placement={"right"}
>
<Column>
<Popover
renderTrigger={(props) => (
<FlatButton label={"Open inner"} {...props} />
)}
placement={"right"}
>
<FlatButton label={"I can be clicked"} />
</Popover>
</Column>
</Popover>
</Box>
);
101 changes: 62 additions & 39 deletions packages/tooltip/src/components/popover/Popover.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
import * as React from "react";
import { ReactNode, useCallback, useRef, useState } from "react";
import {
arrow,
autoUpdate,
flip,
FloatingArrow,
FloatingFocusManager,
FloatingNode,
FloatingPortal,
FloatingTree,
offset,
safePolygon,
shift,
useClick,
useDismiss,
useFloating,
useFloatingNodeId,
useFloatingParentNodeId,
useFocus,
useHover,
useInteractions,
useRole,
useTransitionStyles,
} from "@floating-ui/react";
import * as React from "react";
import { ReactNode, useCallback, useRef, useState } from "react";
import cx from "classnames";
import moduleStyles from "./Popover.module.css";
import moduleStyles from "../popover/Popover.module.css";
import { Placement } from "../../types/Placement";

export type PopoverVariant =
Expand Down Expand Up @@ -65,7 +69,21 @@ const ARROW_WIDTH = 12;
const ARROW_HEIGHT = 8;
const GAP = 2;

export const Popover: React.FC<PopoverProps> = ({
export const Popover: React.FC<PopoverProps> = ({ children, ...props }) => {
const parentNodeId = useFloatingParentNodeId();

if (parentNodeId == null) {
return (
<FloatingTree>
<PopoverComponent {...props}>{children}</PopoverComponent>
</FloatingTree>
);
}

return <PopoverComponent {...props}>{children}</PopoverComponent>;
};

const PopoverComponent: React.FC<PopoverProps> = ({
children,
variant,
trigger = "hover",
Expand All @@ -83,6 +101,8 @@ export const Popover: React.FC<PopoverProps> = ({
}) => {
const [isOpen, setIsOpen] = useState(false);

const nodeId = useFloatingNodeId();

const arrowRef = useRef(null);

const onOpenChange = useCallback(
Expand All @@ -99,6 +119,7 @@ export const Popover: React.FC<PopoverProps> = ({
);

const { refs, floatingStyles, context } = useFloating({
nodeId,
open: isOpen,
onOpenChange,
placement,
Expand Down Expand Up @@ -143,45 +164,47 @@ export const Popover: React.FC<PopoverProps> = ({
<>
{renderTrigger({ ref: refs.setReference, ...getReferenceProps() })}

{isMounted && (
<FloatingPortal root={appendTo}>
<FloatingFocusManager
context={context}
modal={false}
restoreFocus={restoreFocus}
returnFocus={returnFocus}
initialFocus={initialFocus}
>
<div
ref={refs.setFloating}
style={{ zIndex, ...floatingStyles }}
{...getFloatingProps}
<FloatingNode id={nodeId}>
{isMounted && (
<FloatingPortal root={appendTo}>
<FloatingFocusManager
context={context}
modal={false}
restoreFocus={restoreFocus}
returnFocus={returnFocus}
initialFocus={initialFocus}
>
<div
style={transitionStyles}
className={cx(
moduleStyles.floating,
disablePadding && moduleStyles.disablePadding,
variant && moduleStyles.withIcon,
)}
ref={refs.setFloating}
style={{ zIndex, ...floatingStyles }}
{...getFloatingProps()}
>
{typeof children === "function"
? children({ onRequestClose })
: children}
{!hideArrow && (
<FloatingArrow
ref={arrowRef}
context={context}
width={ARROW_WIDTH}
height={ARROW_HEIGHT}
fill={"white"}
/>
)}
<div
style={transitionStyles}
className={cx(
moduleStyles.floating,
disablePadding && moduleStyles.disablePadding,
variant && moduleStyles.withIcon,
)}
>
{typeof children === "function"
? children({ onRequestClose })
: children}
{!hideArrow && (
<FloatingArrow
ref={arrowRef}
context={context}
width={ARROW_WIDTH}
height={ARROW_HEIGHT}
fill={"white"}
/>
)}
</div>
</div>
</div>
</FloatingFocusManager>
</FloatingPortal>
)}
</FloatingFocusManager>
</FloatingPortal>
)}
</FloatingNode>
</>
);
};
Expand Down

0 comments on commit fe28600

Please sign in to comment.