a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 178 lines 5.2 kB view raw
1/** 2 * View Transitions API integration with CSS fallback 3 * Provides progressive enhancement for smooth DOM transitions 4 */ 5 6import { prefersReducedMotion } from "$core/transitions"; 7import type { Optional } from "$types/helpers"; 8import type { ViewTransitionOptions as ViewTransitionOpts } from "$types/volt"; 9 10type StartViewTransitionResult = { 11 finished: Promise<void>; 12 ready: Promise<void>; 13 updateCbDone: Promise<void>; 14 skipTransition(): void; 15}; 16 17/** 18 * Extended Document type with View Transitions API support 19 */ 20type DocumentWithViewTransition = Document & { 21 startViewTransition(updateCb: () => void | Promise<void>): StartViewTransitionResult; 22}; 23 24/** 25 * Check if the browser supports the View Transitions API 26 * 27 * @returns true if document.startViewTransition is available 28 */ 29export function supportsViewTransitions(): boolean { 30 return typeof document !== "undefined" && "startViewTransition" in document; 31} 32 33/** 34 * Execute a DOM update with View Transitions API. 35 * Falls back to direct execution if unsupported or reduced motion is preferred. 36 * 37 * @param cb - Function that performs DOM updates 38 * @param opts - Optional configuration for the transition 39 * @returns Promise that resolves when transition completes 40 * 41 * @example 42 * ```typescript 43 * // Simple transition 44 * await startViewTransition(() => { 45 * element.textContent = 'Updated!'; 46 * }); 47 * 48 * // Named transition for specific element 49 * await startViewTransition(() => { 50 * element.classList.add('active'); 51 * }, { name: 'card-flip', elements: [element] }); 52 * ``` 53 */ 54export async function startViewTransition( 55 cb: () => void | Promise<void>, 56 opts: ViewTransitionOpts = {}, 57): Promise<void> { 58 const { respectReducedMotion = true, forceFallback = false } = opts; 59 60 if (respectReducedMotion && prefersReducedMotion()) { 61 await cb(); 62 return; 63 } 64 65 if (!forceFallback && supportsViewTransitions()) { 66 const namedElements = applyViewTransitionNames(opts.name, opts.elements); 67 68 try { 69 const transition = (document as DocumentWithViewTransition).startViewTransition(cb); 70 await transition.finished; 71 } finally { 72 removeViewTransitionNames(namedElements); 73 } 74 } else { 75 await cb(); 76 } 77} 78 79/** 80 * Execute a transition with a specific named view transition. 81 * This is a convenience wrapper around startViewTransition for named transitions. 82 * 83 * @param name - View transition name (maps to view-transition-name CSS property) 84 * @param elements - Elements to apply the named transition to 85 * @param cb - Function that performs DOM updates 86 * @returns Promise that resolves when transition completes 87 * 88 * @example 89 * ```typescript 90 * const card = document.querySelector('.card'); 91 * await namedViewTransition('card-flip', [card], () => { 92 * card.classList.toggle('flipped'); 93 * }); 94 * ``` 95 */ 96export async function namedViewTransition( 97 name: string, 98 elements: HTMLElement[], 99 cb: () => void | Promise<void>, 100): Promise<void> { 101 return startViewTransition(cb, { name, elements }); 102} 103 104/** 105 * Apply view-transition-name CSS property to elements. 106 * Returns a map of elements to their original view-transition-name values 107 * for later restoration. 108 * 109 * @param baseName - Base name for the transition (suffixed with index if multiple elements) 110 * @param elements - Elements to apply names to 111 * @returns Map of elements to their original view-transition-name values 112 * 113 * @internal 114 */ 115function applyViewTransitionNames( 116 baseName: Optional<string>, 117 elements: Optional<HTMLElement[]>, 118): Map<HTMLElement, string> { 119 const originalNames = new Map<HTMLElement, string>(); 120 121 if (!baseName || !elements || elements.length === 0) { 122 return originalNames; 123 } 124 125 for (const [index, element] of elements.entries()) { 126 const originalValue = element.style.viewTransitionName; 127 originalNames.set(element, originalValue); 128 129 const transitionName = elements.length === 1 ? baseName : `${baseName}-${index}`; 130 element.style.viewTransitionName = transitionName; 131 } 132 133 return originalNames; 134} 135 136/** 137 * Remove view-transition-name CSS properties and restore original values. 138 * 139 * @param namedElements - Map of elements to their original view-transition-name values 140 * 141 * @internal 142 */ 143function removeViewTransitionNames(namedElements: Map<HTMLElement, string>): void { 144 for (const [element, originalValue] of namedElements) { 145 if (originalValue) { 146 element.style.viewTransitionName = originalValue; 147 } else { 148 element.style.viewTransitionName = ""; 149 } 150 } 151} 152 153/** 154 * Wraps a callback with View Transitions API if supported. 155 * This is a simpler version without named transitions support. 156 * 157 * @param cb - Function to execute 158 * @param respectReducedMotion - Skip transition if prefers-reduced-motion 159 * 160 * @example 161 * ```typescript 162 * withViewTransition(() => { 163 * element.remove(); 164 * }); 165 * ``` 166 */ 167export function withViewTransition(cb: () => void, respectReducedMotion = true): void { 168 if (respectReducedMotion && prefersReducedMotion()) { 169 cb(); 170 return; 171 } 172 173 if (supportsViewTransitions()) { 174 (document as DocumentWithViewTransition).startViewTransition(cb); 175 } else { 176 cb(); 177 } 178}