Skip to content

Commit

Permalink
fix: glitch on Popover with an hover (#2282)
Browse files Browse the repository at this point in the history
* fix: glitch on Popover with an hover

* fix: wrong TS
  • Loading branch information
theo-mesnil authored Sep 26, 2023
1 parent d786f1a commit 72ad5a3
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 186 deletions.
29 changes: 14 additions & 15 deletions docs/pages/components/popover.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -94,43 +94,42 @@ 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 (
<>
<Popover.Trigger as={Button} store={popover}>
Open Popover
</Popover.Trigger>
<Popover aria-label="hover to open popover" store={popover}>
<Popover.Title>Amazing title</Popover.Title>
<Popover.Content>
<PopoverHover.Trigger as={Button} store={popover}>
Hover the button to open
</PopoverHover.Trigger>
<PopoverHover aria-label="hover to open popover" store={popover}>
<PopoverHover.Title>Amazing title</PopoverHover.Title>
<PopoverHover.Content>
Praesent sit amet quam ac velit faucibus dapibus.<br />
Quisque sapien ligula, rutrum quis aliquam nec, convallis sit amet erat.<br />
Mauris auctor blandit porta.
</Popover.Content>
</Popover>
</PopoverHover.Content>
</PopoverHover>
</>
)
}
```

## 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
Expand Down
2 changes: 1 addition & 1 deletion packages/Popover/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,6 @@
},
"gitHead": "974e7bfd71f8cfe846cbffd678c3860a8952f9e9",
"sideEffects": false,
"component": "Popover, usePopover",
"component": "Popover, usePopover, PopoverHover, usePopoverHover",
"homepage": "https://welcome-ui.com/components/popover"
}
31 changes: 31 additions & 0 deletions packages/Popover/src/Arrow.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<S.Arrow store={store}>
<S.ArrowItem $transform={transform} h={30} w={30} xmlns="http://www.w3.org/2000/svg">
<path d="M7 30L15 22L23 30H7Z" fill="currentColor" fillRule="nonzero" id="stroke" />
<path d="M8 30L15 23L22 30H8Z" fill="currentColor" fillRule="nonzero" />
</S.ArrowItem>
</S.Arrow>
)
}
46 changes: 46 additions & 0 deletions packages/Popover/src/Content.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Box position="relative">
<Arrow store={store} />
{children as React.ReactElement}
{withCloseButton && (
<Button
flex="0 0 auto"
ml="md"
onClick={handleClose}
position="absolute"
right={1}
shape="square"
size="xs"
top={1}
variant="secondary"
>
<CrossIcon />
</Button>
)}
</Box>
)
}
42 changes: 42 additions & 0 deletions packages/Popover/src/Popover.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<S.Popover
store={store}
{...rest}
$withCloseButton={withCloseButton}
as={undefined}
ref={ref}
>
<Content onClose={onClose} store={store}>
{children}
</Content>
</S.Popover>
)
}
)

export const Popover = Object.assign(PopoverComponent, {
Content: S.Content,
Title: S.Title,
Trigger: PopoverTrigger,
})
42 changes: 42 additions & 0 deletions packages/Popover/src/PopoverHover.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<S.Popover
as={Ariakit.Hovercard}
store={store}
{...rest}
$withCloseButton={withCloseButton}
ref={ref}
>
<Content onClose={onClose} store={store}>
{children}
</Content>
</S.Popover>
)
}
)

export const PopoverHover = Object.assign(PopoverHoverComponent, {
Content: S.Content,
Title: S.Title,
Trigger: PopoverHoverTrigger,
})
58 changes: 11 additions & 47 deletions packages/Popover/src/Trigger.tsx
Original file line number Diff line number Diff line change
@@ -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 <S.PopoverTrigger store={store} {...rest} forwardedAs={as} ref={ref} />
}
)

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 <S.PopoverTrigger store={store} {...rest} forwardedAs={as} ref={ref} />
})
export const PopoverHoverTrigger = forwardRef<'button', PopoverHoverTriggerProps>(
({ as, store, ...rest }, ref) => {
return <S.PopoverHoverTrigger store={store} {...rest} forwardedAs={as} ref={ref} />
}
)
77 changes: 2 additions & 75 deletions packages/Popover/src/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<S.Popover store={store} {...rest} $withCloseButton={withCloseButton} ref={ref}>
<Box position="relative">
<S.Arrow store={store}>
<S.ArrowItem $transform={transform} h={30} w={30} xmlns="http://www.w3.org/2000/svg">
<path d="M7 30L15 22L23 30H7Z" fill="currentColor" fillRule="nonzero" id="stroke" />
<path d="M8 30L15 23L22 30H8Z" fill="currentColor" fillRule="nonzero" />
</S.ArrowItem>
</S.Arrow>
{children}
{withCloseButton && (
<Button
flex="0 0 auto"
ml="md"
onClick={closePopover}
position="absolute"
right={1}
shape="square"
size="xs"
top={1}
variant="secondary"
>
<CrossIcon />
</Button>
)}
</Box>
</S.Popover>
)
}
)

export const Popover = Object.assign(PopoverComponent, {
Content: S.Content,
Title: S.Title,
Trigger: Trigger,
})

export * from './Popover'
export * from './PopoverHover'
export * from './usePopover'
4 changes: 4 additions & 0 deletions packages/Popover/src/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}
`
Loading

0 comments on commit 72ad5a3

Please sign in to comment.