Skip to content

Commit

Permalink
feat: added new popover-menu component
Browse files Browse the repository at this point in the history
  • Loading branch information
coderwelsch committed Aug 5, 2024
1 parent 63b248e commit 6a38c2d
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 0 deletions.
21 changes: 21 additions & 0 deletions src/components/popover-menu/popover-menu-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { PopoverButton as HeadlessUiPopoverButton } from "@headlessui/react";
import React from "react";
import { usePopoverMenuContext } from "./popover-menu-context";
import { Button, ButtonProps } from "../button/button";

export interface NavigationPopoverButtonProps extends ButtonProps {
children: React.ReactNode;
onClick?: () => void;
}

export const PopoverMenuButton = ({ onClick, ...restProps }: NavigationPopoverButtonProps) => {
const {
popoverButton: { setReferenceElement },
} = usePopoverMenuContext();

return (
<HeadlessUiPopoverButton ref={(el) => el && setReferenceElement(el)} onClick={onClick}>
<Button {...restProps} />
</HeadlessUiPopoverButton>
);
};
25 changes: 25 additions & 0 deletions src/components/popover-menu/popover-menu-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { CSSProperties, createContext, useContext } from "react";

export const PopoverMenuContext = createContext<{
popoverButton: {
setReferenceElement: React.Dispatch<React.SetStateAction<HTMLButtonElement | undefined>>;
};
popoverPanel: {
setPopperElement: React.Dispatch<React.SetStateAction<HTMLElement | undefined>>;
styles: CSSProperties;
attributes: { [key: string]: string } | undefined;
};
}>({
popoverButton: {
setReferenceElement: () => {},
},
popoverPanel: {
setPopperElement: () => {},
styles: {},
attributes: {},
},
});

export const usePopoverMenuContext = () => useContext(PopoverMenuContext);

export const PopoverMenuContextProvider = PopoverMenuContext.Provider;
6 changes: 6 additions & 0 deletions src/components/popover-menu/popover-menu-overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { PopoverBackdrop as HeadlessUiPopoverBackdrop } from "@headlessui/react";
import * as React from "react";

export const PopoverMenuOverlay = () => (
<HeadlessUiPopoverBackdrop className="fixed inset-0 z-30 bg-modal-background" />
);
47 changes: 47 additions & 0 deletions src/components/popover-menu/popover-menu-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { PopoverPanel as HeadlessUiPopoverPanel } from "@headlessui/react";
import React from "react";
import { usePopoverMenuContext } from "./popover-menu-context";

export interface PopoverMenuPanelItemProps {
children: React.ReactNode;
onClick?: () => void;
}

const PopoverMenuPanelItem = ({ children, onClick }: PopoverMenuPanelItemProps) => {
return (
<div
className="flex w-full cursor-pointer items-center overflow-hidden px-4 py-2 hover:bg-neutral-100 focus:ring-2 focus:ring-primary-200"
onClick={onClick}
onKeyDown={onClick}
tabIndex={0}
role="button"
>
<p className="text-sm font-normal">{children}</p>
</div>
);
};

export interface NavigationPopoverPanelProps {
children: React.ReactNode;
}

const PopoverMenuPanel = ({ children }: NavigationPopoverPanelProps) => {
const {
popoverPanel: { setPopperElement, styles, attributes },
} = usePopoverMenuContext();

return (
<HeadlessUiPopoverPanel
ref={(el) => el && setPopperElement(el)}
style={styles}
{...attributes}
className="z-40 ml-2 w-52 rounded bg-neutral-0 py-2 shadow"
>
{children}
</HeadlessUiPopoverPanel>
);
};

PopoverMenuPanel.Item = PopoverMenuPanelItem;

export { PopoverMenuPanel };
35 changes: 35 additions & 0 deletions src/components/popover-menu/popover-menu.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { PopoverMenu } from "./popover-menu";

const meta: Meta<typeof PopoverMenu> = {
title: "Popover Menu",
component: PopoverMenu,
parameters: {
options: {
showPanel: false,
},
},
};

export default meta;

type Story = StoryObj<typeof PopoverMenu>;

export const Default: Story = {
render: () => (
<div className="relative flex min-h-screen min-w-736 flex-col">
<PopoverMenu>
<PopoverMenu.Button variant="secondary">Open Popover Menu</PopoverMenu.Button>

<PopoverMenu.Overlay />

<PopoverMenu.Panel>
<PopoverMenu.Panel.Item>Item 1</PopoverMenu.Panel.Item>
<PopoverMenu.Panel.Item>Item 1</PopoverMenu.Panel.Item>
<PopoverMenu.Panel.Item>Item 1</PopoverMenu.Panel.Item>
</PopoverMenu.Panel>
</PopoverMenu>
</div>
),
};
42 changes: 42 additions & 0 deletions src/components/popover-menu/popover-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Popover } from "@headlessui/react";
import React, { useState } from "react";
import { usePopper } from "react-popper";
import { PopoverMenuButton } from "./popover-menu-button";
import { PopoverMenuContextProvider } from "./popover-menu-context";
import { PopoverMenuOverlay } from "./popover-menu-overlay";
import { PopoverMenuPanel } from "./popover-menu-panel";

export interface PopoverMenuProps {
children: React.ReactNode;
}

const PopoverMenu = ({ children }: PopoverMenuProps) => {
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement>();
const [popperElement, setPopperElement] = useState<HTMLElement>();
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "top-start",
});

const context = {
popoverButton: {
setReferenceElement,
},
popoverPanel: {
setPopperElement,
styles: styles.popper,
attributes: attributes.popper,
},
};

return (
<PopoverMenuContextProvider value={context}>
<Popover>{children}</Popover>
</PopoverMenuContextProvider>
);
};

PopoverMenu.Button = PopoverMenuButton;
PopoverMenu.Panel = PopoverMenuPanel;
PopoverMenu.Overlay = PopoverMenuOverlay;

export { PopoverMenu };

0 comments on commit 6a38c2d

Please sign in to comment.