UNPKG

@sandlada/mdc

Version:

@sandlada/mdc is an open source component library that follows the Material Design 3 design specifications.

262 lines 10.3 kB
/** * @license * Copyright 2026 Kai-Orion & Sandlada * SPDX-License-Identifier: MIT * * @fileoverview * ReactiveController that manages an element's show/close animation based on * edge-slide (in-flow collapse or floating peek) behavior, with optional * scroll-driven auto-show/hide. * * **Positioning model**: * - `floating = false`: The host stays in the document flow; on hide its * height/width animates to `0` (fully removed from layout). No blank gap * is left behind. * - `floating = true`: The host floats on the page (`position: fixed` must * be set by the host component). On hide it animates to `peekSize` — a * small sliver remains visible. Mouse hover expands the bar. * * **Direction mapping** (derived from `placement`): * - Vertical placements (`top` / `bottom`): * scroll down → hide, scroll up → show, at top → show. * - Horizontal placements (`left` / `right`): * scroll right → hide, scroll left → show, at left → show. * * **Animation**: * The host dimension height/width keyframes are interleaved with the inner * container translateX/translateY keyframes. The controller splits them and * animates each element independently via the Web Animations API. * * **Lifecycle** mirrors the FAB/dialog pattern: a `generation` counter * invalidates stale async operations, an `isConnectedPromise` defers DOM * work until connected, and `open` / `close` events are cancelable (followed * by non-cancelable `opened` / `closed`). * * @example * ```ts * const slide = new EdgeSlideController(this, { * placement: 'bottom', * floating: true, * autoHide: true, * peekSize: 24, * }) * ``` */ import { type ReactiveController, type ReactiveControllerHost } from 'lit'; /** * The viewport / document edge the host is docked to. The slide-out direction * is derived from this value: * - `top` → slides up (out the top) * - `bottom` → slides down (out the bottom) * - `left` → slides left (out the left) * - `right` → slides right (out the right) */ export type Placement = 'top' | 'bottom' | 'left' | 'right'; export declare const Placement: { readonly Top: 'top'; readonly Bottom: 'bottom'; readonly Left: 'left'; readonly Right: 'right'; }; /** * Host contract required by {@link EdgeSlideController}. * * The host must provide a `containerElement` — the inner element whose * `transform` is animated during show/close. */ export interface IEdgeSlideHost extends ReactiveControllerHost, HTMLElement { /** The inner element that is translated during show/close animation. */ containerElement: HTMLElement | null; } /** * Configuration options for {@link EdgeSlideController}. * All fields are optional — unspecified fields retain their defaults. */ export interface EdgeSlideOptions { /** * When `true` the host is expected to use `position: fixed` and floats * above the page content. When hiding it shrinks to `peekSize` (peek * mode). Mouse hover expands the bar. * * When `false` (default) the host stays in the document flow. When hiding * it collapses to `0` (full mode). */ floating?: boolean; /** * The edge the host is docked to. Drives the slide-out / slide-in * direction. * @default 'bottom' */ placement?: Placement; /** * When `true` the controller listens to scroll events on the target * element (resolved via `scrollElementId`, falling back to `window`) and * automatically hides or shows the host based on scroll direction. * @default false */ autoHide?: boolean; /** * The `id` of the scrollable element to observe. When empty or missing, * the controller falls back to `window`. * @default '' */ scrollElementId?: string; /** * Visible sliver size in pixels when hidden in `floating` (peek) mode. * @default 24 */ peekSize?: number; /** * When `true` the show/close transitions are skipped — the host jumps * directly to the target state. * @default false */ quick?: boolean; } export declare class EdgeSlideController implements ReactiveController { private readonly host; private _floating; private _placement; private _autoHide; private _scrollElementId; private _peekSize; private _quick; private _isOpen; private _isOpening; /** Generation counter — incremented before each `show()` / `close()` to * invalidate stale async operations. */ private generation; /** Resolved once in `hostConnected()`, re-created in `hostDisconnected()`. */ private isConnectedPromiseResolve; private isConnectedPromise; private animationAbort; private hoverTimer; private readonly handleMouseEnterBound; private readonly handleMouseLeaveBound; private scrollTarget; private lastScrollTop; private lastScrollLeft; /** Tracks the last scroll-driven action so we don't repeat it every frame. */ private lastScrollAction; private scrollRafId; private readonly handleScrollBound; constructor(host: IEdgeSlideHost, options?: EdgeSlideOptions); get floating(): boolean; set floating(value: boolean); get placement(): Placement; set placement(value: Placement); get autoHide(): boolean; set autoHide(value: boolean); get scrollElementId(): string; set scrollElementId(value: string); get peekSize(): number; set peekSize(value: number); get quick(): boolean; set quick(value: boolean); /** Whether the host is currently shown. */ get isOpen(): boolean; /** Whether the host is currently animating open. */ get isOpening(): boolean; hostConnected(): void; hostDisconnected(): void; /** * Re-resolve the scroll target and re-attach the listener. Call this when * `scrollElementId`, `autoHide`, or `placement` changes externally. */ rebindScroll(): void; /** * Imperatively show the host (animate in). * Fires cancelable `open` → animates → fires `opened`. */ show(): Promise<void>; /** * Imperatively hide the host (animate out). * Fires cancelable `close` → animates → fires `closed`. */ close(): Promise<void>; private createConnectedPromise; /** * Cancel all WAAPI animations on the host and container, and abort any * in-flight show/close. Call this before measuring the natural size or * starting a new animation cycle so stale `fill:forwards` doesn't * interfere with layout measurement or leave a ghost state behind. */ private cleanupAnimations; /** * Animate the host dimension + inner container translate. Splits the * interleaved keyframes array: even-indexed entries → host dimension * keyframes, odd-indexed entries → inner container translate keyframes. */ private animateBar; /** Build open keyframes: host grows to natural size, inner slides in. */ private getOpenKeyframes; /** Build close keyframes: host shrinks to hidden size, inner slides out. */ private getCloseKeyframes; /** Which host dimension collapses: vertical placements → height, horizontal → width. */ private collapseAxis; /** Which inner translate axis: vertical placements → translateY, horizontal → translateX. */ private translateAxis; /** * Natural (shown) host size in px along the collapse axis. Cancels any * stale fill:forwards and temporarily clears the inline hidden-size style * so the true natural layout size can be read via getBoundingClientRect. */ private naturalSize; /** * Target host size when hidden: * - `floating = true` (peek mode): `peekSize` * - `floating = false` (full mode): `0` */ private hiddenSize; /** * Container translate offset (px) when entering the hidden / peek state * (the CLOSE direction). The bar moves from shown (translate=0) toward * this offset so the un-docked edge disappears first: * * Full mode (!floating): * bottom → +natural (down) → 1→2→3 (top vanishes first) * top → 0 → 3→2→1 (host natural clip) * left → +natural (right) → c→b→a (right vanishes first) * right → -natural (left) → a→b→c (left vanishes first) * * Peek mode (floating, container slides **away** from the anchor so the * edge-opposite content stays visible in the clipped host): * bottom → 0 → shows top part via host clip * top → -(natural-peek) → shows bottom part * left → -(natural-peek) → shows right part * right → 0 → shows left part via host clip */ private closeOffset; /** * Container translate offset (px) when starting the SHOW animation. * The bar animates FROM this offset TO 0 while the host grows. * * Full mode (!floating) — the container starts on the opposite side of * the close direction so it slides **through** the host clip: * bottom → -natural (up) → slides down into view * top → 0 → no translation, host just grows * left → -natural (left) → slides right into view * right → +natural (right) → slides left into view * * Peek mode (floating) — same as `closeOffset` since the container is * already at the peek position and animates to 0 during show. */ private openOffset; /** Clear inline animation styles once shown (let CSS take over). */ private applyShownState; /** Apply the resting hidden state inline (host sized, inner translated). */ private applyHiddenState; /** Hover shows the full bar (floating / peek mode); mouse leave restores peek. */ private handleMouseEnter; private handleMouseLeave; private clearHoverTimer; private resolveScrollTarget; private recordScrollBaseline; private detachScroll; private cancelScrollRaf; private scheduleScrollCheck; private checkScroll; private dispatchScrollAction; private updateScrollBaseline; } //# sourceMappingURL=edge-slide-controller.d.ts.map