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)
+ })
+})