11import type { ReactiveController , ReactiveControllerHost } from 'lit' ;
22import type { Ref } from 'lit/directives/ref.js' ;
3-
43import { isElement } from '../components/common/util.js' ;
54import type { AnimationReferenceMetadata } from './types.js' ;
65
7- const listenerOptions = { once : true } ;
6+ /**
7+ * Defines the result of an optional View Transition start.
8+ */
9+ type ViewTransitionResult = {
10+ transition ?: ViewTransition ;
11+ } ;
12+
13+ const LISTENER_OPTIONS = { once : true } as const ;
814
9- function getPrefersReducedMotion ( ) {
15+ /**
16+ * Checks the user's preference for reduced motion.
17+ */
18+ function getPrefersReducedMotion ( ) : boolean {
1019 return globalThis ?. matchMedia ( '(prefers-reduced-motion: reduce)' ) . matches ;
1120}
1221
22+ /**
23+ * A ReactiveController for managing Web Animation API (WAAPI) playback
24+ * on a host element or a specified target element.
25+ *
26+ * It provides methods to play, stop, and coordinate animations, including
27+ * support for 'height: auto' transitions and reduced motion preference.
28+ */
1329class AnimationController implements ReactiveController {
14- protected get target ( ) {
15- if ( isElement ( this . _target ) ) {
16- return this . _target ;
30+ private readonly _host : ReactiveControllerHost & HTMLElement ;
31+ private readonly _ref ?: Ref < HTMLElement > | HTMLElement ;
32+
33+ /**
34+ * The actual HTMLElement target for the animations.
35+ * Prioritizes a passed-in Ref value, then a direct HTMLElement, falling back to the host.
36+ */
37+ protected get _target ( ) : HTMLElement {
38+ if ( isElement ( this . _ref ) ) {
39+ return this . _ref ;
1740 }
18- return this . _target ?. value ?? this . host ;
41+
42+ return this . _ref ?. value ?? this . _host ;
1943 }
2044
2145 constructor (
22- private readonly host : ReactiveControllerHost & HTMLElement ,
23- private _target ?: Ref < HTMLElement > | HTMLElement
46+ host : ReactiveControllerHost & HTMLElement ,
47+ ref ?: Ref < HTMLElement > | HTMLElement
2448 ) {
25- this . host . addController ( this ) ;
49+ this . _host = host ;
50+ this . _ref = ref ;
51+ this . _host . addController ( this ) ;
2652 }
2753
28- private parseKeyframes ( keyframes : Keyframe [ ] ) {
29- return keyframes . map ( ( keyframe ) => {
30- if ( ! keyframe . height ) return keyframe ;
31-
32- return {
33- ...keyframe ,
34- height :
35- keyframe . height === 'auto'
36- ? `${ this . target . scrollHeight } px`
37- : keyframe . height ,
38- } ;
54+ /** Pre-processes keyframes, specifically resolving 'auto' height to the element's scrollHeight. */
55+ private _parseKeyframes ( keyframes : Keyframe [ ] ) : Keyframe [ ] {
56+ const target = this . _target ;
57+
58+ return keyframes . map ( ( frame ) => {
59+ return frame . height === 'auto'
60+ ? { ...frame , height : `${ target . scrollHeight } px` }
61+ : frame ;
3962 } ) ;
4063 }
4164
42- public async play ( animation : AnimationReferenceMetadata ) {
65+ /** @internal */
66+ public hostConnected ( ) : void { }
67+
68+ /** Plays a sequence of keyframes, first cancelling all existing animations on the target. */
69+ public async playExclusive (
70+ animation : AnimationReferenceMetadata
71+ ) : Promise < boolean > {
72+ await this . cancelAll ( ) ;
73+
74+ const event = await this . play ( animation ) ;
75+ return event . type === 'finish' ;
76+ }
77+
78+ /**
79+ * Plays a sequence of keyframes using WAAPI.
80+ * Automatically sets duration to 0 if 'prefers-reduced-motion' is set.
81+ */
82+ public async play (
83+ animation : AnimationReferenceMetadata
84+ ) : Promise < AnimationPlaybackEvent > {
4385 const { steps, options } = animation ;
86+ const duration = getPrefersReducedMotion ( ) ? 0 : ( options ?. duration ?? 0 ) ;
4487
45- if ( options ?. duration === Number . POSITIVE_INFINITY ) {
88+ if ( ! Number . isFinite ( duration ) ) {
4689 throw new Error ( 'Promise-based animations must be finite.' ) ;
4790 }
4891
4992 return new Promise < AnimationPlaybackEvent > ( ( resolve ) => {
50- const animation = this . target . animate ( this . parseKeyframes ( steps ) , {
93+ const animation = this . _target . animate ( this . _parseKeyframes ( steps ) , {
5194 ...options ,
52- duration : getPrefersReducedMotion ( ) ? 0 : options ! . duration ,
95+ duration,
5396 } ) ;
5497
55- animation . addEventListener ( 'cancel' , resolve , listenerOptions ) ;
56- animation . addEventListener ( 'finish' , resolve , listenerOptions ) ;
98+ animation . addEventListener ( 'cancel' , resolve , LISTENER_OPTIONS ) ;
99+ animation . addEventListener ( 'finish' , resolve , LISTENER_OPTIONS ) ;
57100 } ) ;
58101 }
59102
60- public stopAll ( ) {
61- return Promise . all (
62- this . target . getAnimations ( ) . map ( ( animation ) => {
63- return new Promise ( ( resolve ) => {
64- const resolver = ( ) => requestAnimationFrame ( resolve ) ;
65- animation . addEventListener ( 'cancel' , resolver , listenerOptions ) ;
66- animation . addEventListener ( 'finish' , resolver , listenerOptions ) ;
67-
68- animation . cancel ( ) ;
69- } ) ;
70- } )
71- ) ;
72- }
73-
74- public async playExclusive ( animation : AnimationReferenceMetadata ) {
75- const [ _ , event ] = await Promise . all ( [
76- this . stopAll ( ) ,
77- this . play ( animation ) ,
78- ] ) ;
103+ /** Cancels all active animations on the target element. */
104+ public cancelAll ( ) : Promise < void > {
105+ for ( const animation of this . _target . getAnimations ( ) ) {
106+ animation . cancel ( ) ;
107+ }
79108
80- return event . type === 'finish' ;
109+ return Promise . resolve ( ) ;
81110 }
82-
83- public hostConnected ( ) { }
84111}
85112
86113/**
@@ -91,14 +118,14 @@ class AnimationController implements ReactiveController {
91118export function addAnimationController (
92119 host : ReactiveControllerHost & HTMLElement ,
93120 target ?: Ref < HTMLElement > | HTMLElement
94- ) {
121+ ) : AnimationController {
95122 return new AnimationController ( host , target ) ;
96123}
97124
98- type ViewTransitionResult = {
99- transition ?: ViewTransition ;
100- } ;
101-
125+ /**
126+ * Initiates a View Transition if supported by the browser and not suppressed by
127+ * the 'prefers-reduced-motion' setting.
128+ */
102129export function startViewTransition (
103130 callback ?: ViewTransitionUpdateCallback
104131) : ViewTransitionResult {
0 commit comments