tooltip.tsx 3.8 KB

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