45 lines
1.6 KiB
TypeScript
45 lines
1.6 KiB
TypeScript
import type { TransitionConfig } from 'svelte/transition';
|
|
|
|
const prefersReducedMotion = () =>
|
|
typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
|
|
/**
|
|
* Fades a loading placeholder out *over* the content that replaces it.
|
|
*
|
|
* A plain `out:fade` is not usable on a skeleton: Svelte keeps the node in the
|
|
* layout for the length of the transition, so the real content stacks below the
|
|
* placeholder and the whole page jumps up the moment the placeholder finally
|
|
* unmounts. That is why these placeholders only ever faded in - the swap was
|
|
* abrupt, but at least nothing moved.
|
|
*
|
|
* This pins the placeholder to the box it already occupies and takes it out of
|
|
* flow for the fade instead. The content settles into its final position
|
|
* immediately and the skeleton dissolves on top of it.
|
|
*
|
|
* Requirements at the call site: the placeholder's parent must be positioned
|
|
* (`class="relative"`) and must be the same element that renders the real
|
|
* content, or the overlay lands somewhere else on the page.
|
|
*/
|
|
export function skeletonOut(node: HTMLElement, { duration = 260 } = {}): TransitionConfig {
|
|
if (prefersReducedMotion()) return { duration: 0 };
|
|
|
|
// measured while the node is still in flow, which is when Svelte builds the
|
|
// transition - the values below are what freeze it in place
|
|
const { offsetTop, offsetLeft, offsetWidth, offsetHeight } = node;
|
|
|
|
return {
|
|
duration,
|
|
css: (t) => `
|
|
opacity: ${t};
|
|
position: absolute;
|
|
top: ${offsetTop}px;
|
|
left: ${offsetLeft}px;
|
|
width: ${offsetWidth}px;
|
|
height: ${offsetHeight}px;
|
|
margin: 0;
|
|
pointer-events: none;
|
|
z-index: 5;
|
|
`
|
|
};
|
|
}
|