mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 10:54:44 +00:00
feat: update package.json exports to support new module formats and types
This commit is contained in:
139
core/platform/src/browsers/animationLifecycle/index.test.ts
Normal file
139
core/platform/src/browsers/animationLifecycle/index.test.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
getAnimationName,
|
||||
isAnimatable,
|
||||
shouldSuspendUnmount,
|
||||
dispatchAnimationEvent,
|
||||
onAnimationSettle,
|
||||
} from '.';
|
||||
|
||||
describe('getAnimationName', () => {
|
||||
it('returns "none" for undefined element', () => {
|
||||
expect(getAnimationName(undefined)).toBe('none');
|
||||
});
|
||||
|
||||
it('returns the animation name from inline style', () => {
|
||||
const el = document.createElement('div');
|
||||
el.style.animationName = 'fadeIn';
|
||||
document.body.appendChild(el);
|
||||
|
||||
expect(getAnimationName(el)).toBe('fadeIn');
|
||||
|
||||
document.body.removeChild(el);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAnimatable', () => {
|
||||
it('returns false for undefined element', () => {
|
||||
expect(isAnimatable(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for element with no animation or transition', () => {
|
||||
const el = document.createElement('div');
|
||||
document.body.appendChild(el);
|
||||
|
||||
expect(isAnimatable(el)).toBe(false);
|
||||
|
||||
document.body.removeChild(el);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldSuspendUnmount', () => {
|
||||
it('returns false for undefined element', () => {
|
||||
expect(shouldSuspendUnmount(undefined, 'none')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for element with no animation/transition', () => {
|
||||
const el = document.createElement('div');
|
||||
document.body.appendChild(el);
|
||||
|
||||
expect(shouldSuspendUnmount(el, 'none')).toBe(false);
|
||||
|
||||
document.body.removeChild(el);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dispatchAnimationEvent', () => {
|
||||
it('dispatches a custom event on the element', () => {
|
||||
const el = document.createElement('div');
|
||||
const handler = vi.fn();
|
||||
|
||||
el.addEventListener('enter', handler);
|
||||
dispatchAnimationEvent(el, 'enter');
|
||||
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('does not throw for undefined element', () => {
|
||||
expect(() => dispatchAnimationEvent(undefined, 'leave')).not.toThrow();
|
||||
});
|
||||
|
||||
it('dispatches non-bubbling event', () => {
|
||||
const el = document.createElement('div');
|
||||
const parent = document.createElement('div');
|
||||
const handler = vi.fn();
|
||||
|
||||
parent.appendChild(el);
|
||||
parent.addEventListener('enter', handler);
|
||||
dispatchAnimationEvent(el, 'enter');
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onAnimationSettle', () => {
|
||||
it('returns a cleanup function', () => {
|
||||
const el = document.createElement('div');
|
||||
const cleanup = onAnimationSettle(el, { onSettle: vi.fn() });
|
||||
|
||||
expect(typeof cleanup).toBe('function');
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('calls onSettle callback on transitionend', () => {
|
||||
const el = document.createElement('div');
|
||||
const callback = vi.fn();
|
||||
|
||||
onAnimationSettle(el, { onSettle: callback });
|
||||
el.dispatchEvent(new Event('transitionend'));
|
||||
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('calls onSettle callback on transitioncancel', () => {
|
||||
const el = document.createElement('div');
|
||||
const callback = vi.fn();
|
||||
|
||||
onAnimationSettle(el, { onSettle: callback });
|
||||
el.dispatchEvent(new Event('transitioncancel'));
|
||||
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('calls onStart callback on animationstart', () => {
|
||||
const el = document.createElement('div');
|
||||
const startCallback = vi.fn();
|
||||
|
||||
onAnimationSettle(el, {
|
||||
onSettle: vi.fn(),
|
||||
onStart: startCallback,
|
||||
});
|
||||
|
||||
el.dispatchEvent(new Event('animationstart'));
|
||||
|
||||
expect(startCallback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('removes all listeners on cleanup', () => {
|
||||
const el = document.createElement('div');
|
||||
const callback = vi.fn();
|
||||
|
||||
const cleanup = onAnimationSettle(el, { onSettle: callback });
|
||||
cleanup();
|
||||
|
||||
el.dispatchEvent(new Event('transitionend'));
|
||||
el.dispatchEvent(new Event('transitioncancel'));
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
139
core/platform/src/browsers/animationLifecycle/index.ts
Normal file
139
core/platform/src/browsers/animationLifecycle/index.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
export type AnimationLifecycleEvent = 'enter' | 'after-enter' | 'leave' | 'after-leave';
|
||||
|
||||
export interface AnimationSettleCallbacks {
|
||||
onSettle: () => void;
|
||||
onStart?: (animationName: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name getAnimationName
|
||||
* @category Browsers
|
||||
* @description Returns the current CSS animation name(s) of an element
|
||||
*
|
||||
* @since 0.0.5
|
||||
*/
|
||||
export function getAnimationName(el: HTMLElement | undefined): string {
|
||||
return el ? getComputedStyle(el).animationName || 'none' : 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* @name isAnimatable
|
||||
* @category Browsers
|
||||
* @description Checks whether an element has a running CSS animation or transition
|
||||
*
|
||||
* @since 0.0.5
|
||||
*/
|
||||
export function isAnimatable(el: HTMLElement | undefined): boolean {
|
||||
if (!el) return false;
|
||||
|
||||
const style = getComputedStyle(el);
|
||||
const animationName = style.animationName || 'none';
|
||||
const transitionProperty = style.transitionProperty || 'none';
|
||||
|
||||
const hasAnimation = animationName !== 'none' && animationName !== '';
|
||||
const hasTransition = transitionProperty !== 'none' && transitionProperty !== '' && transitionProperty !== 'all';
|
||||
|
||||
return hasAnimation || hasTransition;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name shouldSuspendUnmount
|
||||
* @category Browsers
|
||||
* @description Determines whether unmounting should be delayed due to a running animation/transition change
|
||||
*
|
||||
* @since 0.0.5
|
||||
*/
|
||||
export function shouldSuspendUnmount(el: HTMLElement | undefined, prevAnimationName: string): boolean {
|
||||
if (!el) return false;
|
||||
|
||||
const style = getComputedStyle(el);
|
||||
|
||||
if (style.display === 'none') return false;
|
||||
|
||||
const animationName = style.animationName || 'none';
|
||||
const transitionProperty = style.transitionProperty || 'none';
|
||||
|
||||
const hasAnimation = animationName !== 'none' && animationName !== '';
|
||||
const hasTransition = transitionProperty !== 'none' && transitionProperty !== '' && transitionProperty !== 'all';
|
||||
|
||||
if (!hasAnimation && !hasTransition) return false;
|
||||
|
||||
return prevAnimationName !== animationName || hasTransition;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name dispatchAnimationEvent
|
||||
* @category Browsers
|
||||
* @description Dispatches a non-bubbling custom event on an element for animation lifecycle tracking
|
||||
*
|
||||
* @since 0.0.5
|
||||
*/
|
||||
export function dispatchAnimationEvent(el: HTMLElement | undefined, name: AnimationLifecycleEvent): void {
|
||||
el?.dispatchEvent(new CustomEvent(name, { bubbles: false, cancelable: false }));
|
||||
}
|
||||
|
||||
/**
|
||||
* @name onAnimationSettle
|
||||
* @category Browsers
|
||||
* @description Attaches animation/transition end listeners to an element with fill-mode flash prevention. Returns a cleanup function.
|
||||
*
|
||||
* @since 0.0.5
|
||||
*/
|
||||
export function onAnimationSettle(el: HTMLElement, callbacks: AnimationSettleCallbacks): () => void {
|
||||
let fillModeTimeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const handleAnimationEnd = (event: AnimationEvent) => {
|
||||
const currentAnimationName = getAnimationName(el);
|
||||
const isCurrentAnimation = currentAnimationName.includes(CSS.escape(event.animationName));
|
||||
|
||||
if (event.target === el && isCurrentAnimation) {
|
||||
callbacks.onSettle();
|
||||
|
||||
if (fillModeTimeoutId !== undefined) {
|
||||
clearTimeout(fillModeTimeoutId);
|
||||
}
|
||||
|
||||
const currentFillMode = el.style.animationFillMode;
|
||||
el.style.animationFillMode = 'forwards';
|
||||
|
||||
fillModeTimeoutId = setTimeout(() => {
|
||||
if (el.style.animationFillMode === 'forwards') {
|
||||
el.style.animationFillMode = currentFillMode;
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (event.target === el && currentAnimationName === 'none') {
|
||||
callbacks.onSettle();
|
||||
}
|
||||
};
|
||||
|
||||
const handleAnimationStart = (event: AnimationEvent) => {
|
||||
if (event.target === el) {
|
||||
callbacks.onStart?.(getAnimationName(el));
|
||||
}
|
||||
};
|
||||
|
||||
const handleTransitionEnd = (event: TransitionEvent) => {
|
||||
if (event.target === el) {
|
||||
callbacks.onSettle();
|
||||
}
|
||||
};
|
||||
|
||||
el.addEventListener('animationstart', handleAnimationStart, { passive: true });
|
||||
el.addEventListener('animationcancel', handleAnimationEnd, { passive: true });
|
||||
el.addEventListener('animationend', handleAnimationEnd, { passive: true });
|
||||
el.addEventListener('transitioncancel', handleTransitionEnd, { passive: true });
|
||||
el.addEventListener('transitionend', handleTransitionEnd, { passive: true });
|
||||
|
||||
return () => {
|
||||
el.removeEventListener('animationstart', handleAnimationStart);
|
||||
el.removeEventListener('animationcancel', handleAnimationEnd);
|
||||
el.removeEventListener('animationend', handleAnimationEnd);
|
||||
el.removeEventListener('transitioncancel', handleTransitionEnd);
|
||||
el.removeEventListener('transitionend', handleTransitionEnd);
|
||||
|
||||
if (fillModeTimeoutId !== undefined) {
|
||||
clearTimeout(fillModeTimeoutId);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './animationLifecycle';
|
||||
export * from './focusGuard';
|
||||
|
||||
Reference in New Issue
Block a user