diff --git a/docs/pages/components/popover.mdx b/docs/pages/components/popover.mdx index aec213b9d8..d4c5d5bc8f 100644 --- a/docs/pages/components/popover.mdx +++ b/docs/pages/components/popover.mdx @@ -94,27 +94,27 @@ function() { } ``` -## Hover to open +## PopoverHover -Use `triggerMethod: 'hover'` on `usePopover` to open and close the popover by hovering it. +You can have a hover on Popover with `PopoverHover` and `usePopoverHover` from Popover. ```jsx function() { - const popover = usePopover({ triggerMethod: 'hover' }) + const popover = usePopoverHover() return ( <> - - Open Popover - - - Amazing title - + + Hover the button to open + + + Amazing title + Praesent sit amet quam ac velit faucibus dapibus.
Quisque sapien ligula, rutrum quis aliquam nec, convallis sit amet erat.
Mauris auctor blandit porta. -
-
+ + ) } @@ -122,15 +122,14 @@ function() { ## usePopover -We use `usePopover` from [Ariakit Popover](https://ariakit.org/reference/use-popover-store) for the state of the popover. +We use `usePopoverStore` from [Ariakit Popover](https://ariakit.org/reference/use-popover-store) for the state of the Popover and `useHovercardStore` from [Ariakit Hovercard](https://ariakit.org/reference/use-hovercard-store) for the state of the PopoverHover. -Pass options to `usePopover`: +Pass options to `usePopover` or `usePopoverHover`: - `defaultOpen`: e.g. `const popover = usePopover({ defaultOpen: true })` -- `triggerMethod`: `click` or `hover` - `withCloseButton`: `bool`, show/hide cross to close popover -When `triggerMethod` is set to hover +When you use `usePopoverHover` you can change: - `showTimeout`: `number` by default to `500`, show after x milliseconds on hover the trigger - `hideTimeout`: `number` by default to `300`, close after x milliseconds on mouse lease popover diff --git a/packages/Popover/package.json b/packages/Popover/package.json index 45a821f2fa..ee45a710c1 100644 --- a/packages/Popover/package.json +++ b/packages/Popover/package.json @@ -59,6 +59,6 @@ }, "gitHead": "974e7bfd71f8cfe846cbffd678c3860a8952f9e9", "sideEffects": false, - "component": "Popover, usePopover", + "component": "Popover, usePopover, PopoverHover, usePopoverHover", "homepage": "https://welcome-ui.com/components/popover" } diff --git a/packages/Popover/src/Arrow.tsx b/packages/Popover/src/Arrow.tsx new file mode 100644 index 0000000000..1622300d10 --- /dev/null +++ b/packages/Popover/src/Arrow.tsx @@ -0,0 +1,31 @@ +import React from 'react' + +import { UsePopover } from './usePopover' +import * as S from './styles' + +const transformMap = { + top: 'rotateZ(180deg)', + right: 'rotateZ(-90deg)', + bottom: 'rotateZ(360deg)', + left: 'rotateZ(90deg)', +} + +type ArrowProps = { + store: UsePopover +} + +export const Arrow = ({ store }: ArrowProps) => { + const placement = store.useState('currentPlacement') + + const [parentPlacement] = placement.split('-') + const transform = transformMap[parentPlacement as keyof typeof transformMap] + + return ( + + + + + + + ) +} diff --git a/packages/Popover/src/Content.tsx b/packages/Popover/src/Content.tsx new file mode 100644 index 0000000000..75565ee8b7 --- /dev/null +++ b/packages/Popover/src/Content.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { Box } from '@welcome-ui/box' +import { Button } from '@welcome-ui/button' +import { CrossIcon } from '@welcome-ui/icons' + +import { UsePopover, UsePopoverHover } from './usePopover' +import { Arrow } from './Arrow' +import { PopoverProps } from './Popover' + +export interface ContentOptions { + children: PopoverProps['children'] + /** call a function when popover closed */ + onClose?: () => void + store: UsePopover | UsePopoverHover +} + +export const Content = ({ children, onClose, store }: ContentOptions) => { + const handleClose = () => { + if (onClose) onClose() + store?.hide() + } + + const { withCloseButton } = store + + return ( + + + {children as React.ReactElement} + {withCloseButton && ( + + )} + + ) +} diff --git a/packages/Popover/src/Popover.tsx b/packages/Popover/src/Popover.tsx new file mode 100644 index 0000000000..27238361d6 --- /dev/null +++ b/packages/Popover/src/Popover.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { CreateWuiProps, forwardRef } from '@welcome-ui/system' +import * as Ariakit from '@ariakit/react' + +import * as S from './styles' +import { PopoverTrigger } from './Trigger' +import { UsePopover } from './usePopover' +import { Content } from './Content' + +export interface PopoverOptions extends Ariakit.PopoverProps { + /** call a function when popover closed */ + onClose?: () => void + store: UsePopover +} + +export type PopoverProps = CreateWuiProps<'div', PopoverOptions> + +const PopoverComponent = forwardRef<'div', PopoverProps>( + ({ children, onClose, store, ...rest }, ref) => { + const { withCloseButton } = store + + return ( + + + {children} + + + ) + } +) + +export const Popover = Object.assign(PopoverComponent, { + Content: S.Content, + Title: S.Title, + Trigger: PopoverTrigger, +}) diff --git a/packages/Popover/src/PopoverHover.tsx b/packages/Popover/src/PopoverHover.tsx new file mode 100644 index 0000000000..eada9ee9ae --- /dev/null +++ b/packages/Popover/src/PopoverHover.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { CreateWuiProps, forwardRef } from '@welcome-ui/system' +import * as Ariakit from '@ariakit/react' + +import * as S from './styles' +import { PopoverHoverTrigger } from './Trigger' +import { UsePopoverHover } from './usePopover' +import { Content } from './Content' + +export interface PopoverHoverOptions extends Ariakit.HovercardProps { + /** call a function when popover closed */ + onClose?: () => void + store: UsePopoverHover +} + +export type PopoverHoverProps = CreateWuiProps<'div', PopoverHoverOptions> + +const PopoverHoverComponent = forwardRef<'div', PopoverHoverProps>( + ({ children, onClose, store, ...rest }, ref) => { + const { withCloseButton } = store + + return ( + + + {children} + + + ) + } +) + +export const PopoverHover = Object.assign(PopoverHoverComponent, { + Content: S.Content, + Title: S.Title, + Trigger: PopoverHoverTrigger, +}) diff --git a/packages/Popover/src/Trigger.tsx b/packages/Popover/src/Trigger.tsx index e2af2ebb3b..12ebf5c4fe 100644 --- a/packages/Popover/src/Trigger.tsx +++ b/packages/Popover/src/Trigger.tsx @@ -1,57 +1,21 @@ import { CreateWuiProps, forwardRef } from '@welcome-ui/system' import React from 'react' -import { useIsomorphicLayoutEffect } from '@welcome-ui/utils' import { UsePopover } from './usePopover' import * as S from './styles' -export type TriggerProps = CreateWuiProps<'button', { store: UsePopover }> +export type PopoverTriggerProps = CreateWuiProps<'button', { store: UsePopover }> -export const Trigger = forwardRef<'button', TriggerProps>(({ as, store, ...rest }, ref) => { - const { triggerMethod } = store - const isHoverMethod = triggerMethod === 'hover' - const disclosureRef = store.useState('disclosureElement') - const popoverRef = store.useState('popoverElement') - - const showPopover: () => void = () => { - if (isHoverMethod) { - // remove listeners on mouseenter - disclosureRef?.removeEventListener('mouseenter', showPopover) - popoverRef?.removeEventListener('mouseenter', showPopover) - // add listeners on mouseleave - disclosureRef?.addEventListener('mouseleave', hidePopover) - popoverRef?.addEventListener('mouseleave', hidePopover) - // show popover - store.show() - } - } - - const hidePopover: () => void = () => { - if (isHoverMethod) { - // remove listeners on mouseleave - disclosureRef?.removeEventListener('mouseleave', hidePopover) - popoverRef?.removeEventListener('mouseleave', hidePopover) - // add listeners on mouseenter - disclosureRef?.addEventListener('mouseenter', showPopover) - popoverRef?.addEventListener('mouseenter', showPopover) - // hide popover - store.hide() - } +export const PopoverTrigger = forwardRef<'button', PopoverTriggerProps>( + ({ as, store, ...rest }, ref) => { + return } +) - useIsomorphicLayoutEffect(() => { - if (isHoverMethod && disclosureRef) { - // add listeners on mount - disclosureRef.addEventListener('mouseenter', showPopover) - disclosureRef.addEventListener('mouseleave', hidePopover) - return () => { - // remove listeners on unmount - disclosureRef.removeEventListener('mouseenter', showPopover) - disclosureRef.removeEventListener('mouseleave', hidePopover) - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [disclosureRef]) +export type PopoverHoverTriggerProps = CreateWuiProps<'button', { store: UsePopover }> - return -}) +export const PopoverHoverTrigger = forwardRef<'button', PopoverHoverTriggerProps>( + ({ as, store, ...rest }, ref) => { + return + } +) diff --git a/packages/Popover/src/index.tsx b/packages/Popover/src/index.tsx index 847c32bd35..0c3c1b02d0 100644 --- a/packages/Popover/src/index.tsx +++ b/packages/Popover/src/index.tsx @@ -1,76 +1,3 @@ -import React from 'react' -import { Box } from '@welcome-ui/box' -import { Button } from '@welcome-ui/button' -import { CrossIcon } from '@welcome-ui/icons' -import { CreateWuiProps, forwardRef } from '@welcome-ui/system' - -import * as S from './styles' -import { Trigger } from './Trigger' -import { UsePopover } from './usePopover' - -export interface PopoverOptions { - /** call a function when popover closed */ - onClose?: () => void - store: UsePopover -} - -export type PopoverProps = CreateWuiProps<'div', PopoverOptions> - -/* eslint-disable @typescript-eslint/no-unused-vars */ -export const PopoverComponent = forwardRef<'div', PopoverProps>( - ({ children, onClose, store, ...rest }, ref) => { - const closePopover = () => { - if (onClose) onClose() - store?.hide() - } - - const placement = store.useState('currentPlacement') - const { withCloseButton } = store - // get the correct transform style for arrow - const [parentPlacement] = placement.split('-') - const transformMap: { [key: string]: string } = { - top: 'rotateZ(180deg)', - right: 'rotateZ(-90deg)', - bottom: 'rotateZ(360deg)', - left: 'rotateZ(90deg)', - } - const transform = transformMap[parentPlacement] - - return ( - - - - - - - - - {children} - {withCloseButton && ( - - )} - - - ) - } -) - -export const Popover = Object.assign(PopoverComponent, { - Content: S.Content, - Title: S.Title, - Trigger: Trigger, -}) - +export * from './Popover' +export * from './PopoverHover' export * from './usePopover' diff --git a/packages/Popover/src/styles.ts b/packages/Popover/src/styles.ts index 2d8fb7edf5..e9c4c9e3ac 100644 --- a/packages/Popover/src/styles.ts +++ b/packages/Popover/src/styles.ts @@ -45,3 +45,7 @@ export const Popover = styled(Ariakit.Popover)<{ $withCloseButton: boolean }>( export const PopoverTrigger = styled(Ariakit.PopoverDisclosure)` ${system} ` + +export const PopoverHoverTrigger = styled(Ariakit.HovercardAnchor)` + ${system} +` diff --git a/packages/Popover/src/usePopover.ts b/packages/Popover/src/usePopover.ts index 407227ddbd..56d2c83fcf 100644 --- a/packages/Popover/src/usePopover.ts +++ b/packages/Popover/src/usePopover.ts @@ -1,32 +1,21 @@ -import { useCallback, useRef } from 'react' import * as Ariakit from '@ariakit/react' +type WithCloseButton = boolean + export interface UsePopoverProps extends Ariakit.PopoverStoreProps { - hideTimeout?: number - showTimeout?: number - triggerMethod?: 'hover' | 'click' - withCloseButton?: boolean + withCloseButton?: WithCloseButton } - -export type UsePopover = Ariakit.PopoverStore & - Pick & { - /** - * Custom hide function who call ariakit hide after a timeout if is hoverable, or not - **/ - hide: () => void - /** - * Custom show function who call ariakit show after a timeout if is hoverable, or not - **/ - show: () => void - } - +export type UsePopover = Ariakit.PopoverStore & Pick export type UsePopoverState = Ariakit.PopoverStoreState +export interface UsePopoverHoverProps extends Ariakit.HovercardStoreProps { + withCloseButton?: WithCloseButton +} +export type UsePopoverHover = Ariakit.HovercardStore & Pick +export type UsePopoverHoverState = Ariakit.HovercardStoreState + export const usePopover: (props?: UsePopoverProps) => UsePopover = ({ animated = 150, - hideTimeout = 300, - showTimeout = 500, - triggerMethod = 'click', withCloseButton = false, ...options } = {}) => { @@ -34,38 +23,29 @@ export const usePopover: (props?: UsePopoverProps) => UsePopover = ({ animated, ...options, }) - const isOpen = store.useState('open') - const closeCountdownRef = useRef() - const openCountdownRef = useRef() - const isHoverable = triggerMethod === 'hover' - const hide = useCallback(() => { - if (isHoverable) { - if (!isOpen && openCountdownRef.current) { - clearTimeout(openCountdownRef.current) - } - closeCountdownRef.current = setTimeout(() => store.hide(), hideTimeout) - } else { - store.hide() - } - }, [isHoverable, isOpen, hideTimeout, store]) + return { + ...store, + withCloseButton, + } +} - const show = useCallback(() => { - if (isHoverable) { - openCountdownRef.current = setTimeout(() => store.show(), showTimeout) - if (closeCountdownRef.current) { - clearTimeout(closeCountdownRef.current) - } - } else { - store.show() - } - }, [isHoverable, showTimeout, store]) +export const usePopoverHover: (props?: UsePopoverHoverProps) => UsePopoverHover = ({ + animated = 150, + hideTimeout = 300, + showTimeout = 500, + withCloseButton = false, + ...options +} = {}) => { + const store = Ariakit.useHovercardStore({ + animated, + hideTimeout, + showTimeout, + ...options, + }) return { ...store, - show, - hide, - triggerMethod, withCloseButton, } } diff --git a/packages/Popover/tests/hover.test.tsx b/packages/Popover/tests/hover.test.tsx new file mode 100644 index 0000000000..1bc6b06081 --- /dev/null +++ b/packages/Popover/tests/hover.test.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { fireEvent, screen } from '@testing-library/react' + +import { render } from '../../../utils/tests' +import { PopoverHover, usePopoverHover } from '../src' + +const contentText = 'Popover open' +const buttonText = 'open' + +const PopoverHoverWrapper = () => { + const store = usePopoverHover() + + return ( + <> + {buttonText} + + {contentText} + + + ) +} + +describe('', () => { + it('should render correctly on click on popover trigger button', () => { + render() + + expect(screen.queryByRole('dialog')).toBeNull() + + const button = screen.getByText(buttonText) + const dialog = screen.getByTestId('popover') + + expect(dialog).toHaveAttribute('hidden') + expect(dialog).toBeInTheDocument() + + fireEvent.mouseOver(button) + + /** we need to wait the showTimeout from component */ + setTimeout(() => { + expect(dialog).toHaveAttribute('data-enter') + }, 400) + }) +})