|
1 | 1 | import { Tooltip as KobalteTooltip } from "@kobalte/core/tooltip" |
2 | | -import { createSignal, Match, splitProps, Switch, type JSX } from "solid-js" |
| 2 | +import { createEffect, Match, onCleanup, splitProps, Switch, type JSX } from "solid-js" |
3 | 3 | import type { ComponentProps } from "solid-js" |
| 4 | +import { createStore } from "solid-js/store" |
4 | 5 |
|
5 | 6 | export interface TooltipProps extends ComponentProps<typeof KobalteTooltip> { |
6 | 7 | value: JSX.Element |
@@ -32,23 +33,101 @@ export function TooltipKeybind(props: TooltipKeybindProps) { |
32 | 33 | } |
33 | 34 |
|
34 | 35 | export function Tooltip(props: TooltipProps) { |
35 | | - const [open, setOpen] = createSignal(false) |
| 36 | + let ref: HTMLDivElement | undefined |
| 37 | + const [state, setState] = createStore({ |
| 38 | + open: false, |
| 39 | + block: false, |
| 40 | + expand: false, |
| 41 | + }) |
36 | 42 | const [local, others] = splitProps(props, [ |
37 | 43 | "children", |
38 | 44 | "class", |
39 | 45 | "contentClass", |
40 | 46 | "contentStyle", |
41 | 47 | "inactive", |
42 | 48 | "forceOpen", |
| 49 | + "ignoreSafeArea", |
43 | 50 | "value", |
44 | 51 | ]) |
45 | 52 |
|
| 53 | + const close = () => setState("open", false) |
| 54 | + |
| 55 | + const inside = () => { |
| 56 | + const active = document.activeElement |
| 57 | + if (!ref || !active) return false |
| 58 | + return ref.contains(active) |
| 59 | + } |
| 60 | + |
| 61 | + const drop = (expand = state.expand) => { |
| 62 | + if (expand) return |
| 63 | + if (ref?.matches(":hover")) return |
| 64 | + if (inside()) return |
| 65 | + setState("block", false) |
| 66 | + } |
| 67 | + |
| 68 | + const sync = () => { |
| 69 | + const expand = !!ref?.querySelector('[aria-expanded="true"], [data-expanded]') |
| 70 | + setState("expand", expand) |
| 71 | + if (expand) { |
| 72 | + setState("block", true) |
| 73 | + close() |
| 74 | + return |
| 75 | + } |
| 76 | + drop(expand) |
| 77 | + } |
| 78 | + |
| 79 | + const arm = () => { |
| 80 | + setState("block", true) |
| 81 | + close() |
| 82 | + } |
| 83 | + |
| 84 | + const leave = () => { |
| 85 | + if (!inside()) close() |
| 86 | + drop() |
| 87 | + } |
| 88 | + |
| 89 | + createEffect(() => { |
| 90 | + if (!ref) return |
| 91 | + sync() |
| 92 | + const obs = new MutationObserver(sync) |
| 93 | + obs.observe(ref, { |
| 94 | + subtree: true, |
| 95 | + childList: true, |
| 96 | + attributes: true, |
| 97 | + attributeFilter: ["aria-expanded", "data-expanded"], |
| 98 | + }) |
| 99 | + onCleanup(() => obs.disconnect()) |
| 100 | + }) |
| 101 | + |
46 | 102 | return ( |
47 | 103 | <Switch> |
48 | 104 | <Match when={local.inactive}>{local.children}</Match> |
49 | 105 | <Match when={true}> |
50 | | - <KobalteTooltip gutter={4} {...others} closeDelay={0} open={local.forceOpen || open()} onOpenChange={setOpen}> |
51 | | - <KobalteTooltip.Trigger as={"div"} data-component="tooltip-trigger" class={local.class}> |
| 106 | + <KobalteTooltip |
| 107 | + gutter={4} |
| 108 | + {...others} |
| 109 | + closeDelay={0} |
| 110 | + ignoreSafeArea={local.ignoreSafeArea ?? true} |
| 111 | + open={local.forceOpen || state.open} |
| 112 | + onOpenChange={(open) => { |
| 113 | + if (local.forceOpen) return |
| 114 | + if (state.block && open) return |
| 115 | + setState("open", open) |
| 116 | + }} |
| 117 | + > |
| 118 | + <KobalteTooltip.Trigger |
| 119 | + ref={ref} |
| 120 | + as={"div"} |
| 121 | + data-component="tooltip-trigger" |
| 122 | + class={local.class} |
| 123 | + onPointerDownCapture={arm} |
| 124 | + onKeyDownCapture={(event: KeyboardEvent) => { |
| 125 | + if (event.key !== "Enter" && event.key !== " ") return |
| 126 | + arm() |
| 127 | + }} |
| 128 | + onPointerLeave={leave} |
| 129 | + onFocusOut={() => requestAnimationFrame(() => drop())} |
| 130 | + > |
52 | 131 | {local.children} |
53 | 132 | </KobalteTooltip.Trigger> |
54 | 133 | <KobalteTooltip.Portal> |
|
0 commit comments