tldraw
Version:
A tiny little drawing editor.
377 lines (376 loc) • 13.6 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var TldrawUiTooltip_exports = {};
__export(TldrawUiTooltip_exports, {
TldrawUiTooltip: () => TldrawUiTooltip,
TldrawUiTooltipProvider: () => TldrawUiTooltipProvider,
hideAllTooltips: () => hideAllTooltips
});
module.exports = __toCommonJS(TldrawUiTooltip_exports);
var import_jsx_runtime = require("react/jsx-runtime");
var import_editor = require("@tldraw/editor");
var import_radix_ui = require("radix-ui");
var import_react = __toESM(require("react"), 1);
var import_layout = require("./layout");
const DEFAULT_TOOLTIP_DELAY_MS = 700;
class TooltipManager {
static instance = null;
state = (0, import_editor.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 (import_editor.tlenvReactive.get().isCoarsePointer && !tooltip.showOnMobile) return null;
return tooltip;
}
}
const tooltipManager = TooltipManager.getInstance();
function hideAllTooltips() {
tooltipManager.hideAllTooltips();
}
const TooltipSingletonContext = (0, import_react.createContext)(false);
function TldrawUiTooltipProvider({ children }) {
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_radix_ui.Tooltip.Provider, { skipDelayDuration: 700, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TooltipSingletonContext.Provider, { value: true, children: [
children,
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TooltipSingleton, {})
] }) });
}
function TooltipSingleton() {
const [isOpen, setIsOpen] = (0, import_react.useState)(false);
const triggerRef = (0, import_react.useRef)(null);
const isFirstShowRef = (0, import_react.useRef)(true);
const editor = (0, import_editor.useMaybeEditor)();
const currentTooltip = (0, import_editor.useValue)(
"current tooltip",
() => tooltipManager.getCurrentTooltipData(),
[]
);
const cameraState = (0, import_editor.useValue)("camera state", () => editor?.getCameraState(), [editor]);
(0, import_react.useEffect)(() => {
if (cameraState === "moving" && isOpen && currentTooltip) {
tooltipManager.handleEvent({
type: "hide",
tooltipId: currentTooltip.id,
editor,
instant: true
});
}
}, [cameraState, isOpen, currentTooltip, editor]);
(0, import_react.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]);
(0, import_react.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" });
};
}, []);
(0, import_react.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__ */ (0, import_jsx_runtime.jsxs)(import_radix_ui.Tooltip.Root, { open: isOpen, delayDuration: 0, children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_radix_ui.Tooltip.Trigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: triggerRef }) }),
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
import_radix_ui.Tooltip.Content,
{
className: "tlui-tooltip",
side: currentTooltip.side,
sideOffset: currentTooltip.sideOffset,
avoidCollisions: true,
collisionPadding: 8,
dir: "ltr",
children: [
currentTooltip.content,
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_radix_ui.Tooltip.Arrow, { className: "tlui-tooltip__arrow" })
]
}
)
] });
}
const TldrawUiTooltip = (0, import_react.forwardRef)(
({
children,
content,
side,
sideOffset = 5,
disabled = false,
showOnMobile = false,
delayDuration
}, ref) => {
const editor = (0, import_editor.useMaybeEditor)();
const tooltipId = (0, import_react.useRef)((0, import_editor.uniqueId)());
const hasProvider = (0, import_react.useContext)(TooltipSingletonContext);
const enhancedA11yMode = (0, import_editor.useValue)(
"enhancedA11yMode",
() => editor?.user.getEnhancedA11yMode(),
[editor]
);
const orientationCtx = (0, import_layout.useTldrawUiOrientation)();
const sideToUse = side ?? orientationCtx.tooltipSide;
(0, import_react.useEffect)(() => {
const currentTooltipId = tooltipId.current;
return () => {
if (hasProvider) {
tooltipManager.handleEvent({
type: "hide",
tooltipId: currentTooltipId,
editor,
instant: true
});
}
};
}, [editor, hasProvider]);
if (disabled || !content) {
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
}
let delayDurationToUse;
if (enhancedA11yMode) {
delayDurationToUse = 0;
} else {
delayDurationToUse = delayDuration ?? (editor?.options.tooltipDelayMs || DEFAULT_TOOLTIP_DELAY_MS);
}
if (!hasProvider || enhancedA11yMode) {
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
import_radix_ui.Tooltip.Root,
{
delayDuration: delayDurationToUse,
disableHoverableContent: !enhancedA11yMode,
children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_radix_ui.Tooltip.Trigger, { asChild: true, ref, children }),
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
import_radix_ui.Tooltip.Content,
{
className: "tlui-tooltip",
side: sideToUse,
sideOffset,
avoidCollisions: true,
collisionPadding: 8,
dir: "ltr",
children: [
content,
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_radix_ui.Tooltip.Arrow, { className: "tlui-tooltip__arrow" })
]
}
)
]
}
);
}
const child = import_react.default.Children.only(children);
(0, import_editor.assert)(import_react.default.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 = import_react.default.cloneElement(childElement, {
onMouseEnter: handleMouseEnter,
onMouseLeave: handleMouseLeave,
onFocus: handleFocus,
onBlur: handleBlur
});
return childrenWithHandlers;
}
);
//# sourceMappingURL=TldrawUiTooltip.js.map