UNPKG

tldraw

Version:

A tiny little drawing editor.

361 lines (360 loc) • 11.3 kB
import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import { assert, atom, tlenvReactive, uniqueId, useMaybeEditor, useValue } from "@tldraw/editor"; import { Tooltip as _Tooltip } from "radix-ui"; import React, { createContext, forwardRef, useContext, useEffect, useRef, useState } from "react"; import { useTldrawUiOrientation } from "./layout.mjs"; const DEFAULT_TOOLTIP_DELAY_MS = 700; class TooltipManager { static instance = null; state = atom("tooltip state", { name: "idle" }); static getInstance() { if (!TooltipManager.instance) { TooltipManager.instance = new TooltipManager(); } return TooltipManager.instance; } hideAllTooltips() { this.handleEvent({ type: "hide_all" }); } handleEvent(event) { const currentState = this.state.get(); switch (event.type) { case "pointer_down": { if (currentState.name === "waiting_to_hide") { clearTimeout(currentState.timeoutId); } this.state.set({ name: "pointer_down" }); break; } case "pointer_up": { if (currentState.name === "pointer_down") { this.state.set({ name: "idle" }); } break; } case "show": { if (currentState.name === "pointer_down") { return; } if (currentState.name === "waiting_to_hide") { clearTimeout(currentState.timeoutId); } this.state.set({ name: "showing", tooltip: event.tooltip }); break; } case "hide": { const { tooltipId, editor, instant } = event; if (currentState.name === "showing" && currentState.tooltip.id === tooltipId) { if (editor && !instant) { const timeoutId = editor.timers.setTimeout(() => { const state = this.state.get(); if (state.name === "waiting_to_hide" && state.tooltip.id === tooltipId) { this.state.set({ name: "idle" }); } }, 300); this.state.set({ name: "waiting_to_hide", tooltip: currentState.tooltip, timeoutId }); } else { this.state.set({ name: "idle" }); } } else if (currentState.name === "waiting_to_hide" && currentState.tooltip.id === tooltipId) { if (instant) { clearTimeout(currentState.timeoutId); this.state.set({ name: "idle" }); } } break; } case "hide_all": { if (currentState.name === "waiting_to_hide") { clearTimeout(currentState.timeoutId); } if (currentState.name === "pointer_down") { return; } this.state.set({ name: "idle" }); break; } } } getCurrentTooltipData() { const currentState = this.state.get(); let tooltip = null; if (currentState.name === "showing") { tooltip = currentState.tooltip; } else if (currentState.name === "waiting_to_hide") { tooltip = currentState.tooltip; } if (!tooltip) return null; if (tlenvReactive.get().isCoarsePointer && !tooltip.showOnMobile) return null; return tooltip; } } const tooltipManager = TooltipManager.getInstance(); function hideAllTooltips() { tooltipManager.hideAllTooltips(); } const TooltipSingletonContext = createContext(false); function TldrawUiTooltipProvider({ children }) { return /* @__PURE__ */ jsx(_Tooltip.Provider, { skipDelayDuration: 700, children: /* @__PURE__ */ jsxs(TooltipSingletonContext.Provider, { value: true, children: [ children, /* @__PURE__ */ jsx(TooltipSingleton, {}) ] }) }); } function TooltipSingleton() { const [isOpen, setIsOpen] = useState(false); const triggerRef = useRef(null); const isFirstShowRef = useRef(true); const editor = useMaybeEditor(); const currentTooltip = useValue( "current tooltip", () => tooltipManager.getCurrentTooltipData(), [] ); const cameraState = useValue("camera state", () => editor?.getCameraState(), [editor]); useEffect(() => { if (cameraState === "moving" && isOpen && currentTooltip) { tooltipManager.handleEvent({ type: "hide", tooltipId: currentTooltip.id, editor, instant: true }); } }, [cameraState, isOpen, currentTooltip, editor]); useEffect(() => { function handleKeyDown(event) { if (event.key === "Escape" && currentTooltip && isOpen) { hideAllTooltips(); event.stopPropagation(); } } document.addEventListener("keydown", handleKeyDown, { capture: true }); return () => { document.removeEventListener("keydown", handleKeyDown, { capture: true }); }; }, [currentTooltip, isOpen]); useEffect(() => { function handlePointerDown() { tooltipManager.handleEvent({ type: "pointer_down" }); } function handlePointerUp() { tooltipManager.handleEvent({ type: "pointer_up" }); } document.addEventListener("pointerdown", handlePointerDown, { capture: true }); document.addEventListener("pointerup", handlePointerUp, { capture: true }); document.addEventListener("pointercancel", handlePointerUp, { capture: true }); return () => { document.removeEventListener("pointerdown", handlePointerDown, { capture: true }); document.removeEventListener("pointerup", handlePointerUp, { capture: true }); document.removeEventListener("pointercancel", handlePointerUp, { capture: true }); tooltipManager.handleEvent({ type: "pointer_up" }); }; }, []); useEffect(() => { let timer = null; if (currentTooltip && triggerRef.current) { const activeRect = currentTooltip.targetElement.getBoundingClientRect(); const trigger = triggerRef.current; trigger.style.position = "fixed"; trigger.style.left = "0px"; trigger.style.top = "0px"; const cbOffset = trigger.getBoundingClientRect(); trigger.style.left = `${activeRect.left - cbOffset.left}px`; trigger.style.top = `${activeRect.top - cbOffset.top}px`; trigger.style.width = `${activeRect.width}px`; trigger.style.height = `${activeRect.height}px`; trigger.style.pointerEvents = "none"; trigger.style.zIndex = "9999"; if (isFirstShowRef.current) { timer = setTimeout(() => { setIsOpen(true); isFirstShowRef.current = false; }, currentTooltip.delayDuration); } else { setIsOpen(true); } } else { setIsOpen(false); isFirstShowRef.current = true; } return () => { if (timer !== null) { clearTimeout(timer); } }; }, [currentTooltip]); if (!currentTooltip) { return null; } return /* @__PURE__ */ jsxs(_Tooltip.Root, { open: isOpen, delayDuration: 0, children: [ /* @__PURE__ */ jsx(_Tooltip.Trigger, { asChild: true, children: /* @__PURE__ */ jsx("div", { ref: triggerRef }) }), /* @__PURE__ */ jsxs( _Tooltip.Content, { className: "tlui-tooltip", side: currentTooltip.side, sideOffset: currentTooltip.sideOffset, avoidCollisions: true, collisionPadding: 8, dir: "ltr", children: [ currentTooltip.content, /* @__PURE__ */ jsx(_Tooltip.Arrow, { className: "tlui-tooltip__arrow" }) ] } ) ] }); } const TldrawUiTooltip = forwardRef( ({ children, content, side, sideOffset = 5, disabled = false, showOnMobile = false, delayDuration }, ref) => { const editor = useMaybeEditor(); const tooltipId = useRef(uniqueId()); const hasProvider = useContext(TooltipSingletonContext); const enhancedA11yMode = useValue( "enhancedA11yMode", () => editor?.user.getEnhancedA11yMode(), [editor] ); const orientationCtx = useTldrawUiOrientation(); const sideToUse = side ?? orientationCtx.tooltipSide; useEffect(() => { const currentTooltipId = tooltipId.current; return () => { if (hasProvider) { tooltipManager.handleEvent({ type: "hide", tooltipId: currentTooltipId, editor, instant: true }); } }; }, [editor, hasProvider]); if (disabled || !content) { return /* @__PURE__ */ jsx(Fragment, { children }); } let delayDurationToUse; if (enhancedA11yMode) { delayDurationToUse = 0; } else { delayDurationToUse = delayDuration ?? (editor?.options.tooltipDelayMs || DEFAULT_TOOLTIP_DELAY_MS); } if (!hasProvider || enhancedA11yMode) { return /* @__PURE__ */ jsxs( _Tooltip.Root, { delayDuration: delayDurationToUse, disableHoverableContent: !enhancedA11yMode, children: [ /* @__PURE__ */ jsx(_Tooltip.Trigger, { asChild: true, ref, children }), /* @__PURE__ */ jsxs( _Tooltip.Content, { className: "tlui-tooltip", side: sideToUse, sideOffset, avoidCollisions: true, collisionPadding: 8, dir: "ltr", children: [ content, /* @__PURE__ */ jsx(_Tooltip.Arrow, { className: "tlui-tooltip__arrow" }) ] } ) ] } ); } const child = React.Children.only(children); assert(React.isValidElement(child), "TldrawUiTooltip children must be a single element"); const childElement = child; const handleMouseEnter = (event) => { childElement.props.onMouseEnter?.(event); tooltipManager.handleEvent({ type: "show", tooltip: { id: tooltipId.current, content, targetElement: event.currentTarget, side: sideToUse, sideOffset, showOnMobile, delayDuration: delayDurationToUse } }); }; const handleMouseLeave = (event) => { childElement.props.onMouseLeave?.(event); tooltipManager.handleEvent({ type: "hide", tooltipId: tooltipId.current, editor, instant: false }); }; const handleFocus = (event) => { childElement.props.onFocus?.(event); tooltipManager.handleEvent({ type: "show", tooltip: { id: tooltipId.current, content, targetElement: event.currentTarget, side: sideToUse, sideOffset, showOnMobile, delayDuration: delayDurationToUse } }); }; const handleBlur = (event) => { childElement.props.onBlur?.(event); tooltipManager.handleEvent({ type: "hide", tooltipId: tooltipId.current, editor, instant: false }); }; const childrenWithHandlers = React.cloneElement(childElement, { onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onFocus: handleFocus, onBlur: handleBlur }); return childrenWithHandlers; } ); export { TldrawUiTooltip, TldrawUiTooltipProvider, hideAllTooltips }; //# sourceMappingURL=TldrawUiTooltip.mjs.map