Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions docs/content/react/components/help-bubble.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ npx @seed-design/cli@latest add ui:help-bubble
```
</ComponentExample>

### Hover Trigger

`trigger="hover"`를 설정하면 클릭 대신 포인터 호버로 Help Bubble이 열리고 닫힙니다. `openDelay`와 `closeDelay`로 열림/닫힘 지연(ms)을 조절할 수 있습니다.

터치 디바이스에서도 동작하며, 디바이스 환경에 따라 동작이 자동 전환되지는 않습니다. 필요에 따라 명시적으로 `trigger` 값을 지정해야 합니다.

<ComponentExample name="react/help-bubble/hover">
```json doc-gen:file
{
"file": "examples/react/help-bubble/hover.tsx",
"codeblock": true
}
```
</ComponentExample>

### Anchor

`HelpBubbleAnchor`의 `children`은 Help Bubble이 위치를 잡는 데에만 사용되며 클릭으로 열고 닫는 동작은 없습니다.
Expand Down
19 changes: 19 additions & 0 deletions docs/examples/react/help-bubble/hover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IconILowercaseSerifCircleLine } from "@karrotmarket/react-monochrome-icon";
import { Icon } from "@seed-design/react";
import { HelpBubbleTrigger } from "seed-design/ui/help-bubble";
import { ActionButton } from "seed-design/ui/action-button";

export default function HelpBubbleHover() {
return (
<HelpBubbleTrigger
trigger="hover"
title="Hover trigger"
description="포인터가 트리거 위에 있을 때만 열립니다."
placement="right"
>
<ActionButton variant="ghost" size="small" layout="iconOnly" aria-label="도움말">
<Icon svg={<IconILowercaseSerifCircleLine />} />
</ActionButton>
</HelpBubbleTrigger>
);
}
55 changes: 47 additions & 8 deletions packages/react-headless/popover/src/usePopover.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
useClick,
useDismiss,
useHover,
useInteractions,
useRole,
useTransitionStatus,
Expand All @@ -9,19 +10,55 @@ import { buttonProps, dataAttr, elementProps } from "@seed-design/dom-utils";
import { useMemo } from "react";
import { usePositionedFloating, type UsePositionedFloatingProps } from "./floating";

// TODO: useRole이 임의로 id를 생성하는 문제가 있음. 동작만 참고하고 role="dialog"에 맞게 aria attribute 설정을 직접 해야 함.

export interface UsePopoverProps extends UsePositionedFloatingProps {
/**
* Whether to close the popover when clicking outside of it.
* @default true
*/
closeOnInteractOutside?: boolean;
/**
* How the popover opens. `"hover"` uses pointer enter/leave (touch included);
* dismiss behavior (outside press, escape) still applies in both modes.
* @default "click"
*/
trigger?: "click" | "hover";
/**
* Delay in milliseconds before the popover opens on hover.
* Only applies when `trigger` is `"hover"`.
* @default 0
*/
openDelay?: number;
/**
* Delay in milliseconds before the popover closes on hover leave.
* Only applies when `trigger` is `"hover"`.
* @default 0
*/
closeDelay?: number;
/**
* ARIA role of the floating element, which also controls the aria
* attributes emitted on the trigger.
*
* - `"dialog"`: trigger gets `aria-haspopup="dialog"` and `aria-expanded`;
* the floating element gets `role="dialog"`.
* - `"tooltip"`: trigger gets `aria-describedby` while open; the floating
* element gets `role="tooltip"`. Per WAI-ARIA, tooltip content must not
* contain focusable children.
*
* @default "dialog"
*/
role?: "dialog" | "tooltip";
}

export type UsePopoverReturn = ReturnType<typeof usePopover>;

export function usePopover({ closeOnInteractOutside, ...props }: UsePopoverProps = {}) {
export function usePopover({
closeOnInteractOutside,
trigger = "click",
openDelay,
closeDelay,
role: roleProp,
...props
}: UsePopoverProps = {}) {
const {
open,
onOpenChange,
Expand All @@ -35,14 +72,18 @@ export function usePopover({ closeOnInteractOutside, ...props }: UsePopoverProps
rects,
} = usePositionedFloating(props);

const role = useRole(context);
const click = useClick(context);
const role = useRole(context, { role: roleProp ?? "dialog" });
const click = useClick(context, { enabled: trigger === "click" });
const hover = useHover(context, {
enabled: trigger === "hover",
delay: { open: openDelay, close: closeDelay },
});
const dismiss = useDismiss(context, {
outsidePress: closeOnInteractOutside ?? true,
});

const { status } = useTransitionStatus(context);
const triggerInteractions = useInteractions([role, click, dismiss]);
const triggerInteractions = useInteractions([role, click, hover, dismiss]);
const anchorInteractions = useInteractions([role, dismiss]);

const stateProps = useMemo(
Expand Down Expand Up @@ -71,8 +112,6 @@ export function usePopover({ closeOnInteractOutside, ...props }: UsePopoverProps
stateProps,
anchorProps: elementProps({ ...anchorInteractions.getReferenceProps(), ...stateProps }),
triggerProps: elementProps({
"aria-haspopup": "dialog",
"aria-expanded": open,
...triggerInteractions.getReferenceProps(),
...stateProps,
}),
Expand Down