Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hungry-mammals-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@launchpad-ui/components": patch
---

Add `HoverTrigger`
55 changes: 55 additions & 0 deletions packages/components/src/HoverTrigger.tsx
Original file line number Diff line number Diff line change
@@ -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 }),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we expose closeDelay here, I think I'd expect to see openDelay instead of delay. Or maybe both? If delay is defined it overrides *Delay? Wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API is from RAC. delay is their version of "openDelay".

setOpen: () => {},
toggle: () => (state.isOpen ? state.close(true) : state.open(true)),
};

const { hoverProps } = useHover({
onHoverStart: () => state?.open(),
onHoverEnd: () => state?.close(),
});

return (
<Provider
values={[
[OverlayTriggerStateContext, state],
[
PopoverContext,
{
triggerRef,
isNonModal: true,
trigger: 'DialogTrigger',
UNSTABLE_portalContainer: ref.current ?? undefined,
},
],
]}
>
<PressResponder onPress={() => state.close(true)} ref={triggerRef}>
<span {...hoverProps} ref={ref} style={{ display: 'contents' }}>
{children}
</span>
</PressResponder>
</Provider>
);
};

export { HoverTrigger };
export type { HoverTriggerProps };
2 changes: 2 additions & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type { FormProps } from './Form';
export type { GridListProps, GridListItemProps } from './GridList';
export type { GroupProps } from './Group';
export type { HeadingProps } from './Heading';
export type { HoverTriggerProps } from './HoverTrigger';
export type { InputProps } from './Input';
export type { IconButtonProps } from './IconButton';
export type { LabelProps } from './Label';
Expand Down Expand Up @@ -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';
Expand Down
25 changes: 25 additions & 0 deletions packages/components/stories/Popover.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -101,3 +103,26 @@ export const CustomTrigger: Story = {
},
play,
};

export const Hover: Story = {
render: (args) => {
return (
<HoverTrigger>
<Link href="/test">Link</Link>
<Popover {...args} data-testid="popover">
<Heading slot="title">Title</Heading>
<div>Message</div>
<Link href="/more">View more</Link>
</Popover>
</HoverTrigger>
);
},
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'));
},
};
Loading