diff --git a/.changeset/hungry-mammals-move.md b/.changeset/hungry-mammals-move.md new file mode 100644 index 000000000..c3f304754 --- /dev/null +++ b/.changeset/hungry-mammals-move.md @@ -0,0 +1,5 @@ +--- +"@launchpad-ui/components": patch +--- + +Add `HoverTrigger` diff --git a/packages/components/src/HoverTrigger.tsx b/packages/components/src/HoverTrigger.tsx new file mode 100644 index 000000000..7e3f8e9fd --- /dev/null +++ b/packages/components/src/HoverTrigger.tsx @@ -0,0 +1,55 @@ +import type { TooltipTriggerComponentProps } from 'react-aria-components'; + +import { PressResponder } from '@react-aria/interactions'; +import { useRef } from 'react'; +import { useHover } from 'react-aria'; +import { OverlayTriggerStateContext, PopoverContext, Provider } from 'react-aria-components'; +import { useTooltipTriggerState } from 'react-stately'; + +interface HoverTriggerProps extends TooltipTriggerComponentProps {} + +/** + * A hover popover allows sighted users to preview content available behind a link (inaccessible to keyboard users). + */ +const HoverTrigger = ({ children, ...props }: HoverTriggerProps) => { + const ref = useRef(null); + const triggerRef = useRef(null); + const { delay = 500, closeDelay = 250 } = props; + + const state = { + ...useTooltipTriggerState({ delay, closeDelay, ...props }), + setOpen: () => {}, + toggle: () => (state.isOpen ? state.close(true) : state.open(true)), + }; + + const { hoverProps } = useHover({ + onHoverStart: () => state?.open(), + onHoverEnd: () => state?.close(), + }); + + return ( + + state.close(true)} ref={triggerRef}> + + {children} + + + + ); +}; + +export { HoverTrigger }; +export type { HoverTriggerProps }; diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 0128325b2..d4bb19b6f 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -33,6 +33,7 @@ export type { FormProps } from './Form'; export type { GridListItemProps, GridListProps } from './GridList'; export type { GroupProps } from './Group'; export type { HeadingProps } from './Heading'; +export type { HoverTriggerProps } from './HoverTrigger'; export type { IconButtonProps } from './IconButton'; export type { InputProps } from './Input'; export type { LabelProps } from './Label'; @@ -160,6 +161,7 @@ export { export { Group, GroupContext, groupStyles } from './Group'; export { Header, HeaderContext, headerStyles } from './Header'; export { Heading, HeadingContext, headingStyles } from './Heading'; +export { HoverTrigger } from './HoverTrigger'; export { IconButton, IconButtonContext, iconButtonStyles } from './IconButton'; export { Input, InputContext, inputStyles } from './Input'; export { Keyboard } from './Keyboard'; diff --git a/packages/components/stories/Popover.stories.tsx b/packages/components/stories/Popover.stories.tsx index f47f015f2..a0f273ca0 100644 --- a/packages/components/stories/Popover.stories.tsx +++ b/packages/components/stories/Popover.stories.tsx @@ -7,6 +7,8 @@ import { expect, userEvent, within } from 'storybook/test'; import { Button } from '../src/Button'; import { Dialog, DialogTrigger } from '../src/Dialog'; import { Heading } from '../src/Heading'; +import { HoverTrigger } from '../src/HoverTrigger'; +import { Link } from '../src/Link'; import { OverlayArrow, Popover } from '../src/Popover'; import { Pressable } from '../src/Pressable'; @@ -101,3 +103,26 @@ export const CustomTrigger: Story = { }, play, }; + +export const Hover: Story = { + render: (args) => { + return ( + + Link + + Title +
Message
+ View more +
+
+ ); + }, + play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { + const canvas = within(canvasElement); + + await userEvent.hover(canvasElement); + await userEvent.hover(canvas.getByRole('link')); + const body = canvasElement.ownerDocument.body; + await expect(await within(body).findByTestId('popover')); + }, +};