Skip to content

Commit 08ced9b

Browse files
committed
feat: integrate @headlessui/react into Popover component and update tests for new props
1 parent cd6be2d commit 08ced9b

6 files changed

Lines changed: 89 additions & 195 deletions

File tree

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@
1717
"/dist"
1818
],
1919
"peerDependencies": {
20+
"@headlessui/react": "1.7.5",
2021
"@phosphor-icons/react": "^2.1.10",
2122
"react": "^18.3.1",
2223
"react-dnd": "16.0.1",
2324
"react-dnd-html5-backend": "^16.0.1"
2425
},
2526
"devDependencies": {
2627
"@chromatic-com/storybook": "4.1.3",
28+
"@headlessui/react": "1.7.5",
2729
"@internxt/eslint-config-internxt": "2.0.1",
2830
"@internxt/prettier-config": "internxt/prettier-config#v1.0.2",
31+
"@storybook/addon-docs": "^10.1.4",
2932
"@storybook/addon-links": "^10.1.4",
3033
"@storybook/addon-onboarding": "^10.1.4",
3134
"@storybook/addon-themes": "^10.1.4",
@@ -66,8 +69,7 @@
6669
"vite-plugin-dts": "^4.5.4",
6770
"vite-plugin-svgr": "^4.5.0",
6871
"vite-tsconfig-paths": "^5.1.4",
69-
"vitest": "^3.2.4",
70-
"@storybook/addon-docs": "^10.1.4"
72+
"vitest": "^3.2.4"
7173
},
7274
"scripts": {
7375
"build:tsc": "tsc",

src/components/popover/Popover.tsx

Lines changed: 59 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
import { useState, useRef, useEffect } from 'react';
2-
import { ReactNode } from 'react';
1+
import { Popover as HPopover, Transition } from '@headlessui/react';
2+
import { CSSProperties, ElementType, HTMLAttributes, ReactNode } from 'react';
33

4-
export interface PopoverProps {
4+
export type PopoverPanel = ReactNode | ((closePopover: () => void) => ReactNode);
5+
6+
export interface PopoverProps extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
57
childrenButton: ReactNode;
6-
panel: (closePopover: () => void) => ReactNode;
8+
panel: PopoverPanel;
79
className?: string;
810
classButton?: string;
11+
buttonAs?: ElementType;
12+
buttonProps?: HTMLAttributes<HTMLElement>;
13+
panelClassName?: string;
14+
panelStyle?: CSSProperties;
15+
staticPanel?: boolean;
16+
'data-test'?: string;
917
}
1018

1119
/**
@@ -14,9 +22,9 @@ export interface PopoverProps {
1422
* @property {ReactNode} childrenButton
1523
* - The content to be displayed inside the trigger button.
1624
*
17-
* @property {(closePopover: () => void) => ReactNode} panel
18-
* - A function that returns the content to be displayed inside the popover panel.
19-
* It receives a `closePopover` function as a parameter, which can be used to programmatically close the popover.
25+
* @property {ReactNode | ((closePopover: () => void) => ReactNode)} panel
26+
* - Content to be displayed inside the popover panel. If a function is provided,
27+
* it receives a `closePopover` callback that can be used to close the popover.
2028
*
2129
* @property {string} [className]
2230
* - Additional custom classes for the outermost container of the popover.
@@ -29,72 +37,57 @@ export interface PopoverProps {
2937
* - The rendered Popover component.
3038
*/
3139

32-
const Popover = ({ childrenButton, panel, className, classButton }: PopoverProps): JSX.Element => {
33-
const [isOpen, setIsOpen] = useState(false);
34-
const panelRef = useRef<HTMLDivElement | null>(null);
35-
const [showContent, setShowContent] = useState(isOpen);
36-
const [transitionOpacity, setTransitionOpacity] = useState<string>('opacity-0');
37-
const [transitionScale, setTransitionScale] = useState<string>('scale-95');
38-
39-
const togglePopover = () => setIsOpen((prev) => !prev);
40-
41-
const handleMouseDown = (event: MouseEvent) => {
42-
if (panelRef.current && !panelRef.current.contains(event.target as Node)) {
43-
closePopover();
44-
}
45-
};
40+
const Popover = ({
41+
childrenButton,
42+
panel,
43+
className = '',
44+
classButton = '',
45+
buttonAs,
46+
buttonProps,
47+
panelClassName = '',
48+
panelStyle,
49+
staticPanel = false,
50+
style,
51+
...rest
52+
}: Readonly<PopoverProps>): JSX.Element => {
53+
const renderPanel = (closePopover: () => void) => (typeof panel === 'function' ? panel(closePopover) : panel);
54+
const { className: buttonPropsClassName = '', ...restButtonProps } = buttonProps ?? {};
4655

47-
const closePopover = () => {
48-
setIsOpen(false);
49-
};
50-
51-
useEffect(() => {
52-
if (isOpen) {
53-
const timeout = setTimeout(() => {
54-
setTransitionOpacity('opacity-100');
55-
setTransitionScale('scale-100');
56-
}, 10);
57-
setShowContent(true);
58-
return () => clearTimeout(timeout);
59-
} else {
60-
setTransitionOpacity('opacity-0');
61-
setTransitionScale('scale-95');
62-
const timeout = setTimeout(() => {
63-
setShowContent(false);
64-
}, 100);
65-
return () => clearTimeout(timeout);
66-
}
67-
}, [isOpen]);
68-
69-
useEffect(() => {
70-
document.addEventListener('mousedown', handleMouseDown);
71-
return () => {
72-
document.removeEventListener('mousedown', handleMouseDown);
73-
};
74-
}, []);
56+
const panelContent = (
57+
<HPopover.Panel
58+
className={`absolute right-0 z-50 mt-1 rounded-md border border-gray-10 bg-surface py-1.5 shadow-subtle dark:bg-gray-5 ${panelClassName}`}
59+
style={panelStyle}
60+
static={staticPanel}
61+
>
62+
{({ close }) => renderPanel(close)}
63+
</HPopover.Panel>
64+
);
7565

7666
return (
77-
<div style={{ lineHeight: 0 }} className={`relative ${className}`}>
78-
<button
79-
onClick={togglePopover}
80-
className={`cursor-pointer outline-none ${classButton}`}
81-
aria-expanded={isOpen}
82-
data-testid="popover-button"
67+
<HPopover style={{ lineHeight: 0, ...(style ?? {}) }} className={`relative ${className}`} {...rest}>
68+
<HPopover.Button
69+
as={buttonAs}
70+
className={`cursor-pointer outline-none ${classButton} ${buttonPropsClassName}`}
71+
{...restButtonProps}
8372
>
8473
{childrenButton}
85-
</button>
86-
{showContent && (
87-
<div
88-
ref={panelRef}
89-
className={
90-
'absolute right-0 z-50 mt-1 origin-top-right transform rounded-md border border-gray-10 ' +
91-
`bg-surface py-1.5 shadow-subtle duration-100 ease-out dark:bg-gray-5 ${transitionOpacity} ${transitionScale}`
92-
}
74+
</HPopover.Button>
75+
{staticPanel ? (
76+
panelContent
77+
) : (
78+
<Transition
79+
enter="transition duration-100 ease-out"
80+
enterFrom="scale-95 opacity-0"
81+
enterTo="scale-100 opacity-100"
82+
leave="transition duration-75 ease-out"
83+
leaveFrom="scale-100 opacity-100"
84+
leaveTo="scale-95 opacity-0"
85+
className="z-50"
9386
>
94-
{panel(closePopover)}
95-
</div>
87+
{panelContent}
88+
</Transition>
9689
)}
97-
</div>
90+
</HPopover>
9891
);
9992
};
10093

src/components/popover/__test__/Popover.test.tsx

Lines changed: 12 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,6 @@ describe('Popover', () => {
2121
vi.clearAllMocks();
2222
});
2323

24-
it('should match snapshot', () => {
25-
const popover = renderPopover();
26-
expect(popover).toMatchSnapshot();
27-
});
28-
2924
it('renders the button', () => {
3025
const { getByText } = renderPopover();
3126
expect(getByText('Open Popover')).toBeInTheDocument();
@@ -75,37 +70,34 @@ describe('Popover', () => {
7570
expect(button.parentElement).toHaveClass('custom-button');
7671
});
7772

78-
it('applies transition classes when opening the popover', async () => {
73+
it('applies panel styling classes when open', async () => {
7974
const { getByText, queryByText } = renderPopover();
8075

8176
const button = getByText('Open Popover');
8277
fireEvent.click(button);
8378

8479
await waitFor(() => {
8580
const popoverPanel = queryByText('Popover Content')?.parentElement?.parentElement;
86-
expect(popoverPanel).toHaveClass('opacity-100');
87-
expect(popoverPanel).toHaveClass('scale-100');
81+
expect(popoverPanel).toHaveClass('rounded-md');
82+
expect(popoverPanel).toHaveClass('border-gray-10');
83+
expect(popoverPanel).toHaveClass('bg-surface');
8884
});
8985
});
9086

91-
it('applies transition classes when closing the popover', async () => {
92-
const { getByText, queryByText } = renderPopover();
87+
it('applies custom panel classes and styles', async () => {
88+
const { getByText, queryByText } = renderPopover({
89+
panelClassName: 'custom-panel',
90+
panelStyle: { minWidth: '200px' },
91+
});
9392

9493
const button = getByText('Open Popover');
9594
fireEvent.click(button);
9695

97-
await waitFor(() => expect(getByText('Popover Content')).toBeInTheDocument());
98-
99-
const closeButton = getByText('Close Popover');
100-
fireEvent.click(closeButton);
101-
10296
await waitFor(() => {
103-
const popoverPanel = queryByText('Popover Content')?.parentElement?.parentElement;
104-
expect(popoverPanel).toHaveClass('opacity-0');
105-
expect(popoverPanel).toHaveClass('scale-95');
97+
const popoverPanel = queryByText('Popover Content')?.parentElement?.parentElement as HTMLElement | null;
98+
expect(popoverPanel).toHaveClass('custom-panel');
99+
expect(popoverPanel?.style.minWidth).toBe('200px');
106100
});
107-
108-
await waitFor(() => expect(queryByText('Popover Content')).not.toBeInTheDocument());
109101
});
110102

111103
it('open and close the popover when various main button clicks ', async () => {
@@ -122,17 +114,4 @@ describe('Popover', () => {
122114
fireEvent.click(button);
123115
await waitFor(() => expect(getByText('Popover Content')).toBeInTheDocument());
124116
});
125-
126-
it('should call onMouseDown stopPropagation when the button is clicked', () => {
127-
const stopPropagationSpy = vi.fn();
128-
const { getByText } = renderPopover();
129-
const button = getByText('Open Popover');
130-
131-
button.onmousedown = (e) => {
132-
e.stopPropagation = stopPropagationSpy;
133-
e.stopPropagation();
134-
};
135-
fireEvent.mouseDown(button);
136-
expect(stopPropagationSpy).toHaveBeenCalled();
137-
});
138117
});

src/components/popover/__test__/__snapshots__/Popover.test.tsx.snap

Lines changed: 0 additions & 92 deletions
This file was deleted.

src/stories/components/popover/Popover.stories.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ type Story = StoryObj<typeof meta>;
1717

1818
const defaultArgs = {
1919
childrenButton: (
20-
<button className="text-gray-800 p-2 rounded-full border border-gray-300">
20+
<div className="rounded-full border border-gray-300 p-2 text-gray-800">
2121
<UserCircle size={24} className="text-gray-800 dark:text-white" />
22-
</button>
22+
</div>
2323
),
2424
panel: (closePopover: () => void) => (
2525
<div className="p-4 text-sm">

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5652,3 +5652,15 @@ yocto-queue@^0.1.0:
56525652
version "0.1.0"
56535653
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
56545654
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
5655+
5656+
"@headlessui/react@1.7.5":
5657+
version "1.7.5"
5658+
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.5.tgz#c8864b0731d95dbb34aa6b3a60d0ee9ae6f8a7ca"
5659+
integrity sha512-UZSxOfA0CYKO7QDT5OGlFvesvlR1SKkawwSjwQJwt7XQItpzRKdE3ZUQxHcg4LEz3C0Wler2s9psdb872ynwrQ==
5660+
dependencies:
5661+
client-only "^0.0.1"
5662+
5663+
client-only@^0.0.1:
5664+
version "0.0.1"
5665+
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
5666+
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==

0 commit comments

Comments
 (0)