diff --git a/docs/content/react/components/help-bubble.mdx b/docs/content/react/components/help-bubble.mdx
index b259ff51d3..3f5738a440 100644
--- a/docs/content/react/components/help-bubble.mdx
+++ b/docs/content/react/components/help-bubble.mdx
@@ -61,6 +61,21 @@ npx @seed-design/cli@latest add ui:help-bubble
```
+### Hover Trigger
+
+`trigger="hover"`를 설정하면 클릭 대신 포인터 호버로 Help Bubble이 열리고 닫힙니다. `openDelay`와 `closeDelay`로 열림/닫힘 지연(ms)을 조절할 수 있습니다.
+
+터치 디바이스에서도 동작하며, 디바이스 환경에 따라 동작이 자동 전환되지는 않습니다. 필요에 따라 명시적으로 `trigger` 값을 지정해야 합니다.
+
+
+ ```json doc-gen:file
+ {
+ "file": "examples/react/help-bubble/hover.tsx",
+ "codeblock": true
+ }
+ ```
+
+
### Anchor
`HelpBubbleAnchor`의 `children`은 Help Bubble이 위치를 잡는 데에만 사용되며 클릭으로 열고 닫는 동작은 없습니다.
diff --git a/docs/examples/react/help-bubble/hover.tsx b/docs/examples/react/help-bubble/hover.tsx
new file mode 100644
index 0000000000..ed9445c7a1
--- /dev/null
+++ b/docs/examples/react/help-bubble/hover.tsx
@@ -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 (
+
+
+ } />
+
+
+ );
+}
diff --git a/packages/react-headless/popover/src/usePopover.ts b/packages/react-headless/popover/src/usePopover.ts
index 58dcdf9a15..aeafaf5ad6 100644
--- a/packages/react-headless/popover/src/usePopover.ts
+++ b/packages/react-headless/popover/src/usePopover.ts
@@ -1,6 +1,7 @@
import {
useClick,
useDismiss,
+ useHover,
useInteractions,
useRole,
useTransitionStatus,
@@ -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;
-export function usePopover({ closeOnInteractOutside, ...props }: UsePopoverProps = {}) {
+export function usePopover({
+ closeOnInteractOutside,
+ trigger = "click",
+ openDelay,
+ closeDelay,
+ role: roleProp,
+ ...props
+}: UsePopoverProps = {}) {
const {
open,
onOpenChange,
@@ -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(
@@ -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,
}),