import { animateTo, shimKeyframesHeightAuto, stopAnimations } from '../internal/animate'; import { getAnimation, setDefaultAnimation } from '../utilities/animation-registry'; import { waitForEvent } from '../internal/event'; import type { NileDetail } from './nile-detail'; export function getPreviewHeight(component: NileDetail): number { if (!component.preview) { return 0; } const pct = Math.min(100, Math.max(0, component.previewPercentage)); if (pct <= 0) { return 0; } return Math.max(1, Math.round((component.body.scrollHeight * pct) / 100)); } /* Runs the show animation on the detail body. In preview mode the animation starts from the preview height instead of 0. Emits nile-show (cancelable) before and nile-after-show after the animation. */ export async function animateShow(component: NileDetail): Promise { const nileShow = component.emit('nile-show', { cancelable: true }); if (nileShow.defaultPrevented) { component.open = false; return; } await stopAnimations(component.body); component.body.hidden = false; const previewHeight = getPreviewHeight(component); const { keyframes, options } = getAnimation(component, 'detail.show', { dir: 'ltr' }); const frames = previewHeight > 0 ? [ { height: `${previewHeight}px`, opacity: '1' }, { height: 'auto', opacity: '1' } ] : keyframes; await animateTo(component.body, shimKeyframesHeightAuto(frames, component.body.scrollHeight), options); component.body.style.height = 'auto'; component.emit('nile-after-show'); } /* Runs the hide animation on the detail body. In preview mode the body collapses to the preview height and stays visible. Emits nile-hide (cancelable) before and nile-after-hide after the animation. */ export async function animateHide(component: NileDetail): Promise { const nileHide = component.emit('nile-hide', { cancelable: true }); if (nileHide.defaultPrevented) { component.open = true; return; } await stopAnimations(component.body); const previewHeight = getPreviewHeight(component); const { keyframes, options } = getAnimation(component, 'detail.hide', { dir: 'ltr' }); if (previewHeight > 0) { const frames = [ { height: 'auto', opacity: '1' }, { height: `${previewHeight}px`, opacity: '1' } ]; await animateTo(component.body, shimKeyframesHeightAuto(frames, component.body.scrollHeight), options); component.body.style.height = `${previewHeight}px`; } else { await animateTo(component.body, shimKeyframesHeightAuto(keyframes, component.body.scrollHeight), options); component.body.hidden = true; component.body.style.height = 'auto'; } component.emit('nile-after-hide'); } /* Programmatically shows the detail if it is not already open or disabled. Returns a promise that resolves after the show animation completes. */ export async function showDetail(component: NileDetail): Promise { if (component.open || component.disabled) { return undefined; } component.open = true; return waitForEvent(component, 'nile-after-show'); } /* Programmatically hides the detail if it is not already closed or disabled. Returns a promise that resolves after the hide animation completes. */ export async function hideDetail(component: NileDetail): Promise { if (!component.open || component.disabled) { return undefined; } component.open = false; return waitForEvent(component, 'nile-after-hide'); } /* Handles click on the header summary row. Toggles the detail open/close and focuses the header. */ export function handleSummaryClick(component: NileDetail): void { if (!component.disabled) { if (component.open) { hideDetail(component); } else { showDetail(component); } } } /* Handles keyboard interaction on the header summary row. Enter/Space toggles, ArrowUp/Left closes, ArrowDown/Right opens. */ export function handleSummaryKeyDown(event: KeyboardEvent, component: NileDetail): void { event.preventDefault(); if (event.key === 'Enter' || event.key === ' ') { if (component.open) { hideDetail(component); } else { showDetail(component); } } if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') { hideDetail(component); } if (event.key === 'ArrowDown' || event.key === 'ArrowRight') { showDetail(component); } } /* Registers the default show and hide animations for the detail component. Show: animates from height 0 / opacity 0 to auto / 1 over 300ms. Hide: animates from auto / 1 to height 0 / opacity 0 over 300ms. */ export function registerDefaultAnimations(): void { setDefaultAnimation('detail.show', { keyframes: [ { height: '0', opacity: '0' }, { height: 'auto', opacity: '1' } ], options: { duration: 300, easing: 'ease' } }); setDefaultAnimation('detail.hide', { keyframes: [ { height: 'auto', opacity: '1' }, { height: '0', opacity: '0' } ], options: { duration: 300, easing: 'ease' } }); } registerDefaultAnimations();