move tsx kit -> uikit
This commit is contained in:
178
web/uikit/Tooltips.tsx
Normal file
178
web/uikit/Tooltips.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import { createSignal, JSXElement, onCleanup, Show } from "solid-js";
|
||||
import { FloatingRoot, FloatingTrigger, FloatingContent, useFloatingContext, Placement } from "./Floating.tsx";
|
||||
|
||||
const TOOLTIP_DEFAULT_OFFSET = 8;
|
||||
const TOOLTIP_CLS = "bg-neutral-800 text-white text-sm px-2.5 py-1.5 rounded-default shadow-lg max-w-80 relative";
|
||||
|
||||
function arrowCls(base: string) {
|
||||
const common = "absolute w-0 h-0";
|
||||
switch (base) {
|
||||
case "top":
|
||||
return common + " -bottom-[6px] left-1/2 -translate-x-1/2 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-t-[6px] border-t-neutral-800";
|
||||
case "bottom":
|
||||
return common + " -top-[6px] left-1/2 -translate-x-1/2 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-b-[6px] border-b-neutral-800";
|
||||
case "left":
|
||||
return common + " -right-[6px] top-1/2 -translate-y-1/2 border-t-[6px] border-t-transparent border-b-[6px] border-b-transparent border-l-[6px] border-l-neutral-800";
|
||||
case "right":
|
||||
return common + " -left-[6px] top-1/2 -translate-y-1/2 border-t-[6px] border-t-transparent border-b-[6px] border-b-transparent border-r-[6px] border-r-neutral-800";
|
||||
default:
|
||||
return common;
|
||||
}
|
||||
}
|
||||
|
||||
interface TooltipProps {
|
||||
content: JSXElement;
|
||||
trigger?: "hover" | "focus";
|
||||
placement?: "top" | "bottom" | "left" | "right";
|
||||
offset?: number;
|
||||
delay?: number;
|
||||
children?: JSXElement;
|
||||
}
|
||||
|
||||
export function Tooltip(props: TooltipProps) {
|
||||
return (
|
||||
<Show when={props.trigger === "focus"} fallback={
|
||||
<HoverTooltip content={props.content} placement={props.placement ?? "top"} offset={props.offset ?? TOOLTIP_DEFAULT_OFFSET} delay={props.delay ?? 200}>
|
||||
{props.children}
|
||||
</HoverTooltip>
|
||||
}>
|
||||
<FocusTooltip content={props.content} placement={props.placement ?? "top"} offset={props.offset ?? TOOLTIP_DEFAULT_OFFSET}>
|
||||
{props.children}
|
||||
</FocusTooltip>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
interface HoverTooltipProps {
|
||||
content: JSXElement;
|
||||
placement: Placement;
|
||||
offset: number;
|
||||
delay: number;
|
||||
children?: JSXElement;
|
||||
}
|
||||
|
||||
export function HoverTooltip(props: HoverTooltipProps) {
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
|
||||
return (
|
||||
<FloatingRoot open={isOpen()} onOpenChange={setIsOpen} placement={props.placement} offset={props.offset} flip={true} shift={true}>
|
||||
<HoverTooltipTrigger delay={props.delay} setIsOpen={setIsOpen}>
|
||||
{props.children}
|
||||
</HoverTooltipTrigger>
|
||||
<TooltipContentWithArrow placement={props.placement} setIsOpen={setIsOpen}>
|
||||
{props.content}
|
||||
</TooltipContentWithArrow>
|
||||
</FloatingRoot>
|
||||
);
|
||||
}
|
||||
|
||||
interface HoverTriggerProps {
|
||||
delay: number;
|
||||
setIsOpen: (v: boolean) => void;
|
||||
children?: JSXElement;
|
||||
}
|
||||
|
||||
function HoverTooltipTrigger(props: HoverTriggerProps) {
|
||||
const { setTriggerRef } = useFloatingContext();
|
||||
let hoverOpenTimeoutRef: ReturnType<typeof setTimeout> | null = null;
|
||||
let hoverCloseTimeoutRef: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const clearTimeouts = () => {
|
||||
if (hoverOpenTimeoutRef) { clearTimeout(hoverOpenTimeoutRef); hoverOpenTimeoutRef = null; }
|
||||
if (hoverCloseTimeoutRef) { clearTimeout(hoverCloseTimeoutRef); hoverCloseTimeoutRef = null; }
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
clearTimeouts();
|
||||
hoverOpenTimeoutRef = setTimeout(() => props.setIsOpen(true), props.delay);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
clearTimeouts();
|
||||
hoverCloseTimeoutRef = setTimeout(() => props.setIsOpen(false), 100);
|
||||
};
|
||||
|
||||
onCleanup(clearTimeouts);
|
||||
|
||||
return (
|
||||
<span ref={(el: HTMLElement) => setTriggerRef(el)} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} style={{ display: "inline-block" }}>
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface FocusTooltipProps {
|
||||
content: JSXElement;
|
||||
placement: Placement;
|
||||
offset: number;
|
||||
children?: JSXElement;
|
||||
}
|
||||
|
||||
export function FocusTooltip(props: FocusTooltipProps) {
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
|
||||
return (
|
||||
<FloatingRoot open={isOpen()} onOpenChange={setIsOpen} placement={props.placement ?? "top"} offset={props.offset ?? TOOLTIP_DEFAULT_OFFSET} flip={true} shift={true}>
|
||||
<FocusTooltipTrigger setIsOpen={setIsOpen}>
|
||||
{props.children}
|
||||
</FocusTooltipTrigger>
|
||||
<TooltipContentWithArrow placement={props.placement ?? "top"}>
|
||||
{props.content}
|
||||
</TooltipContentWithArrow>
|
||||
</FloatingRoot>
|
||||
);
|
||||
}
|
||||
|
||||
interface FocusTriggerProps {
|
||||
setIsOpen: (v: boolean) => void;
|
||||
children?: JSXElement;
|
||||
}
|
||||
|
||||
function FocusTooltipTrigger(props: FocusTriggerProps) {
|
||||
const { setTriggerRef } = useFloatingContext();
|
||||
|
||||
return (
|
||||
<div ref={(el: HTMLElement) => setTriggerRef(el)} onFocusIn={() => props.setIsOpen(true)} onFocusOut={() => props.setIsOpen(false)}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TooltipContentProps {
|
||||
placement: string;
|
||||
setIsOpen?: (v: boolean) => void;
|
||||
onMouseEnter?: () => void;
|
||||
onMouseLeave?: () => void;
|
||||
children?: JSXElement;
|
||||
}
|
||||
|
||||
function TooltipContentWithArrow(props: TooltipContentProps) {
|
||||
const { position } = useFloatingContext();
|
||||
let hoverCloseTimeoutRef: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (hoverCloseTimeoutRef) { clearTimeout(hoverCloseTimeoutRef); hoverCloseTimeoutRef = null; }
|
||||
props.onMouseEnter?.();
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (props.setIsOpen) {
|
||||
hoverCloseTimeoutRef = setTimeout(() => props.setIsOpen!(false), 50);
|
||||
}
|
||||
props.onMouseLeave?.();
|
||||
};
|
||||
|
||||
onCleanup(() => {
|
||||
if (hoverCloseTimeoutRef) clearTimeout(hoverCloseTimeoutRef);
|
||||
});
|
||||
|
||||
const actualPlacement = () => position()?.placement ?? props.placement;
|
||||
const basePlacement = () => actualPlacement().split("-")[0];
|
||||
|
||||
return (
|
||||
<FloatingContent class={TOOLTIP_CLS} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
{props.children}
|
||||
<div class={arrowCls(basePlacement())}/>
|
||||
</FloatingContent>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user