@sandlada/mdc
Version:
@sandlada/mdc is an open source component library that follows the Material Design 3 design specifications.
47 lines • 11.1 kB
JavaScript
import"../../node_modules/lit/index.js";import{Duration as e}from"../../node_modules/@sandlada/mdk/build/domain/duration.js";import{Easing as t}from"../../node_modules/@sandlada/mdk/build/domain/easing.js";import"../../node_modules/@sandlada/mdk/build/index.js";
/**
* @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,
* })
* ```
*/
const n={Top:`top`,Bottom:`bottom`,Left:`left`,Right:`right`},r=e.ExpressiveFastSpatial.Value,i=t.Emphasized.ToCSSVariable(),a=e.ExpressiveFastEffects.Value,o=t.EmphasizedAccelerate.ToCSSVariable();var s=class{constructor(e,t){this._floating=!1,this._placement=`bottom`,this._autoHide=!1,this._scrollElementId=``,this._peekSize=24,this._quick=!1,this._isOpen=!1,this._isOpening=!1,this.generation=0,this.animationAbort=null,this.hoverTimer=null,this.scrollTarget=null,this.lastScrollTop=0,this.lastScrollLeft=0,this.lastScrollAction=null,this.scrollRafId=null,this.host=e,e.addController(this),this.handleMouseEnterBound=()=>this.handleMouseEnter(),this.handleMouseLeaveBound=()=>this.handleMouseLeave(),this.handleScrollBound=()=>this.scheduleScrollCheck(),t&&(t.floating!==void 0&&(this._floating=t.floating),t.placement!==void 0&&(this._placement=t.placement),t.autoHide!==void 0&&(this._autoHide=t.autoHide),t.scrollElementId!==void 0&&(this._scrollElementId=t.scrollElementId),t.peekSize!==void 0&&(this._peekSize=t.peekSize),t.quick!==void 0&&(this._quick=t.quick)),this.isConnectedPromise=this.createConnectedPromise()}get floating(){return this._floating}set floating(e){e!==this._floating&&(this._floating=e,!e&&this._isOpen===!1&&this.applyHiddenState(),this.host.requestUpdate())}get placement(){return this._placement}set placement(e){e!==this._placement&&(this._placement=e,this._isOpen||this.applyHiddenState(),this.rebindScroll(),this.host.requestUpdate())}get autoHide(){return this._autoHide}set autoHide(e){e!==this._autoHide&&(this._autoHide=e,this.rebindScroll(),this.host.requestUpdate())}get scrollElementId(){return this._scrollElementId}set scrollElementId(e){e!==this._scrollElementId&&(this._scrollElementId=e,this.rebindScroll(),this.host.requestUpdate())}get peekSize(){return this._peekSize}set peekSize(e){e!==this._peekSize&&(this._peekSize=e,this._isOpen||this.applyHiddenState(),this.host.requestUpdate())}get quick(){return this._quick}set quick(e){e!==this._quick&&(this._quick=e,this.host.requestUpdate())}get isOpen(){return this._isOpen}get isOpening(){return this._isOpening}hostConnected(){this.isConnectedPromiseResolve(),this.host.addEventListener(`mouseenter`,this.handleMouseEnterBound),this.host.addEventListener(`mouseleave`,this.handleMouseLeaveBound),this.rebindScroll()}hostDisconnected(){this.detachScroll(),this.cancelScrollRaf(),this.host.removeEventListener(`mouseenter`,this.handleMouseEnterBound),this.host.removeEventListener(`mouseleave`,this.handleMouseLeaveBound),this.clearHoverTimer(),++this.generation,this.animationAbort?.abort(),this.animationAbort=null,this.isConnectedPromise=this.createConnectedPromise()}rebindScroll(){this.detachScroll(),this._autoHide&&(this.scrollTarget=this.resolveScrollTarget(),this.scrollTarget&&(this.recordScrollBaseline(),this.lastScrollAction=null,this.scrollTarget.addEventListener(`scroll`,this.handleScrollBound,{passive:!0})))}async show(){let e=++this.generation;if(this.cleanupAnimations(),await this.isConnectedPromise,await this.host.updateComplete,e===this.generation){if(!this.host.dispatchEvent(new Event(`open`,{cancelable:!0}))){this._isOpen=!1;return}if(this._isOpening=!0,this._isOpen=!0,this.host.setAttribute(`open`,``),this._quick){this.applyShownState(),this._isOpening=!1,this.host.dispatchEvent(new Event(`opened`));return}await this.animateBar(this.getOpenKeyframes(),{duration:r,easing:i,fill:`forwards`}),e===this.generation&&(this.applyShownState(),this._isOpening=!1,this.host.dispatchEvent(new Event(`opened`)))}}async close(){let e=++this.generation;if(this.cleanupAnimations(),!this.host.isConnected){this._isOpen=!1,this.host.removeAttribute(`open`);return}if(await this.host.updateComplete,e===this.generation){if(!this.host.dispatchEvent(new Event(`close`,{cancelable:!0}))){this._isOpen=!0;return}if(this._isOpening=!1,this._isOpen=!1,this._quick){this.applyHiddenState(),this.host.removeAttribute(`open`),this.host.dispatchEvent(new Event(`closed`));return}await this.animateBar(this.getCloseKeyframes(),{duration:a,easing:o,fill:`forwards`}),e===this.generation&&(this.applyHiddenState(),this.host.removeAttribute(`open`),this.host.dispatchEvent(new Event(`closed`)))}}createConnectedPromise(){return new Promise(e=>{this.isConnectedPromiseResolve=e})}cleanupAnimations(){this.host.getAnimations({subtree:!1}).forEach(e=>e.cancel()),this.host.containerElement?.getAnimations({subtree:!1}).forEach(e=>e.cancel()),this.animationAbort?.abort(),this.animationAbort=null}async animateBar(e,t){this.animationAbort?.abort(),this.animationAbort=new AbortController;let n=this.host.containerElement;if(!n)return;let r=[],i=[],a=[];for(let t=0;t<e.length;t++)(t%2==0?i:a).push(e[t]);if(i.length>0){let e=this.host.animate(i,t);this.animationAbort.signal.addEventListener(`abort`,()=>{e.cancel()}),r.push(e)}if(a.length>0){let e=n.animate(a,t);this.animationAbort.signal.addEventListener(`abort`,()=>{e.cancel()}),r.push(e)}await Promise.all(r.map(e=>e.finished.catch(()=>{})))}getOpenKeyframes(){let e=this.collapseAxis(),t=this.naturalSize(),n=this.hiddenSize(),r=this.translateAxis(),i=this.openOffset(t);return[{[e]:`${n}px`},{[r]:`${i}px`},{[e]:`${t}px`},{[r]:`0px`}]}getCloseKeyframes(){let e=this.collapseAxis(),t=this.naturalSize(),n=this.hiddenSize(),r=this.translateAxis(),i=this.closeOffset(t);return[{[e]:`${t}px`},{[r]:`0px`},{[e]:`${n}px`},{[r]:`${i}px`}]}collapseAxis(){return this._placement===`top`||this._placement===`bottom`?`height`:`width`}translateAxis(){return this._placement===`top`||this._placement===`bottom`?`translateY`:`translateX`}naturalSize(){let e=this.collapseAxis();this.host.getAnimations({subtree:!1}).forEach(e=>e.cancel());let t=this.host.style.getPropertyValue(e);t&&this.host.style.removeProperty(e);let n=e===`height`?this.host.getBoundingClientRect().height:this.host.getBoundingClientRect().width;return t&&this.host.style.setProperty(e,t),n||64}hiddenSize(){return this._floating?Math.max(0,this._peekSize):0}closeOffset(e){if(!this._floating)switch(this._placement){case`bottom`:return+e;case`top`:return 0;case`left`:return+e;case`right`:return-e}let t=Math.max(0,e-Math.max(0,this._peekSize));switch(this._placement){case`bottom`:return 0;case`top`:return-t;case`left`:return-t;case`right`:return 0}}openOffset(e){if(!this._floating)switch(this._placement){case`bottom`:return-e;case`top`:return 0;case`left`:return-e;case`right`:return+e}return this.closeOffset(e)}applyShownState(){this.host.style.removeProperty(`height`),this.host.style.removeProperty(`width`),this.host.containerElement&&this.host.containerElement.style.removeProperty(`transform`)}applyHiddenState(){let e=this.collapseAxis(),t=this.hiddenSize();if(this.host.style.setProperty(e,`${t}px`),this.host.containerElement){let e=this.translateAxis(),t=this.closeOffset(this.naturalSize());this.host.containerElement.style.setProperty(`transform`,`${e}(${t}px)`)}}handleMouseEnter(){this.clearHoverTimer(),this._floating&&!this._isOpen&&this.show()}handleMouseLeave(){this._floating&&this._isOpen&&!this._isOpening&&(this.hoverTimer=setTimeout(()=>{this.close()},2e3))}clearHoverTimer(){this.hoverTimer!==null&&(clearTimeout(this.hoverTimer),this.hoverTimer=null)}resolveScrollTarget(){let e=this._scrollElementId?.trim()??``;return e.length>0?document.getElementById(e)??null:window}recordScrollBaseline(){let e=this.scrollTarget;if(!e)return;let t=this._placement===`top`||this._placement===`bottom`;if(e===window)t?this.lastScrollTop=window.scrollY??document.documentElement.scrollTop??0:this.lastScrollLeft=window.scrollX??document.documentElement.scrollLeft??0;else{let n=e;t?this.lastScrollTop=n.scrollTop:this.lastScrollLeft=n.scrollLeft}}detachScroll(){this.scrollTarget&&=(this.scrollTarget.removeEventListener(`scroll`,this.handleScrollBound),null)}cancelScrollRaf(){this.scrollRafId!==null&&(cancelAnimationFrame(this.scrollRafId),this.scrollRafId=null)}scheduleScrollCheck(){this.scrollRafId===null&&(this.scrollRafId=requestAnimationFrame(()=>{this.scrollRafId=null,this.checkScroll()}))}checkScroll(){let e=this.scrollTarget;if(!e)return;let t=this._placement===`top`||this._placement===`bottom`,n,r;if(e===window)t?(n=window.scrollY??document.documentElement.scrollTop??0,r=document.documentElement.scrollHeight-window.innerHeight,window.innerHeight):(n=window.scrollX??document.documentElement.scrollLeft??0,r=document.documentElement.scrollWidth-window.innerWidth,window.innerWidth);else{let i=e;t?(n=i.scrollTop,r=i.scrollHeight-i.clientHeight,i.clientHeight):(n=i.scrollLeft,r=i.scrollWidth-i.clientWidth,i.clientWidth)}let i=t?this.lastScrollTop:this.lastScrollLeft,a=n-i;if(n<=0){this.dispatchScrollAction(`show`),this.updateScrollBaseline(t,n);return}if(r<=0){this.dispatchScrollAction(`show`),this.updateScrollBaseline(t,n);return}if(Math.abs(a)<4)return;let o=a>0;this.dispatchScrollAction(o?`close`:`show`),this.updateScrollBaseline(t,n)}dispatchScrollAction(e){this.lastScrollAction!==e&&(this.lastScrollAction=e,e===`show`?this.show():this.close())}updateScrollBaseline(e,t){e?this.lastScrollTop=t:this.lastScrollLeft=t}};export{s as EdgeSlideController,n as Placement};
//# sourceMappingURL=edge-slide-controller.js.map