import { ParentComponent, createContext, createEffect, createMemo, createRenderEffect, createSignal, useContext, type Signal, } from "solid-js"; import { css } from "solid-styled"; const TabListContext = /* @__PURE__ */ createContext<{ focusOn: Signal; }>(); export function useTabListContext() { const result = useContext(TabListContext); if (!result) { throw new TypeError("tab list context is not found"); } return result; } const ANIM_SPEED = 160 / 110; // 160px/110ms const TABLIST_FOCUS_CLASS = "tablist-focus"; const Tabs: ParentComponent<{ offset?: number; onFocusChanged?: (element: HTMLElement[]) => void; }> = (props) => { let self: HTMLDivElement; const [focusOn, setFocusOn] = createSignal([]); createRenderEffect((lastFocusElement) => { const current = focusOn(); if (lastFocusElement) { for (const e of lastFocusElement) { e.classList.remove(TABLIST_FOCUS_CLASS); } } for (const e of current) { e.classList.add("tablist-focus"); } return current; }); createRenderEffect(() => { const callback = props.onFocusChanged; if (!callback) return; callback(focusOn()); }); let lastLeft = 0; let lastWidth = 0; const getNearestDistance = ( srcRect: { x: number; width: number }, prevEl: Element | null, nextEl: Element | null, offset?: number, ) => { if (!offset || offset === 0) return [0, 0] as const; if (offset > 0) { if (!nextEl) return [0, 0] as const; const rect = nextEl.getBoundingClientRect(); return [ (rect.x - srcRect.x) * offset, (rect.width - srcRect.width) * offset, ] as const; } else { if (!prevEl) return [0, 0] as const; const rect = prevEl.getBoundingClientRect(); return [ (rect.x - srcRect.x) * offset, (srcRect.width - rect.width) * offset, ] as const; } }; const focusBoundingClientRect = () => { return focusOn() .map((x) => x.getBoundingClientRect()) .reduce( (p, c) => { return { x: Math.min(p.x, c.x), width: p.width + c.width, }; }, { x: +Infinity, width: 0 }, ); }; const focusSiblings = () => { const rects = focusOn().map((x) => [x, x.getBoundingClientRect()] as const); if (rects.length === 0) return [null, null] as const; rects.sort(([, rect1], [, rect2]) => rect1.x - rect2.x); return [ rects[0][0].previousElementSibling, rects[rects.length - 1][0].nextElementSibling, ] as const; }; const indicator = () => { const el = focusOn(); if (!el) { return ["0px", "0px", "110ms", "110ms"] as const; } const rect = focusBoundingClientRect(); const rootRect = self.getBoundingClientRect(); const left = rect.x - rootRect.x; const width = rect.width; const [prevEl, nextEl] = focusSiblings(); const [offset, widthChange] = getNearestDistance( rect, prevEl, nextEl, props.offset, ); const result = [ `${left + offset}px`, `${width + widthChange}px`, `${Math.max(Math.floor(Math.abs(left + offset - lastLeft)), 160) * ANIM_SPEED}ms`, `${Math.max(Math.floor(Math.abs(width - lastWidth)), 160) * ANIM_SPEED}ms`, ] as const; lastLeft = left; lastWidth = width; return result; }; css` .tablist { width: 100%; position: relative; white-space: nowrap; overflow-x: auto; &::after { transition: left ${indicator()[2]} var(--tutu-anim-curve-std), width ${indicator()[3]} var(--tutu-anim-curve-std); position: absolute; content: ""; display: block; background-color: white; height: 2px; width: ${indicator()[1]}; left: ${indicator()[0]}; bottom: 0; } } `; return (
{props.children}
); }; export default Tabs;