Skip to content

Commit

Permalink
feat(core): clean up clarity motion decorator
Browse files Browse the repository at this point in the history
Signed-off-by: Scott Mathis <smathis@vmware.com>
  • Loading branch information
Scott Mathis authored and mathisscott committed Dec 14, 2021
1 parent 7c052b1 commit c77f0a8
Show file tree
Hide file tree
Showing 14 changed files with 50 additions and 56 deletions.
3 changes: 1 addition & 2 deletions packages/core/src/accordion/accordion-panel.element.ts
Expand Up @@ -8,7 +8,6 @@ import { html, LitElement } from 'lit';
import {
AnimationAccordionPanelOpenName,
reverseAnimation,
Animatable,
animate,
baseStyles,
event,
Expand Down Expand Up @@ -56,7 +55,7 @@ import styles from './accordion-panel.element.scss';
false: reverseAnimation(AnimationAccordionPanelOpenName),
},
})
export class CdsAccordionPanel extends LitElement implements Animatable {
export class CdsAccordionPanel extends LitElement {
@property({ type: String })
cdsMotion = 'on';

Expand Down
Expand Up @@ -5,7 +5,6 @@
*/

import {
Animatable,
AnimationModalEnterName,
animate,
baseStyles,
Expand Down Expand Up @@ -223,7 +222,7 @@ export class CdsInternalStaticOverlay extends CdsBaseFocusTrap {
false: AnimationModalEnterName,
},
})
export class CdsInternalOverlay extends CdsInternalStaticOverlay implements Animatable {
export class CdsInternalOverlay extends CdsInternalStaticOverlay {
@property({ type: String })
cdsMotion = 'on';

Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/internal-components/popup/popup.element.ts
Expand Up @@ -6,7 +6,6 @@

import {
animate,
Animatable,
AnimationResponsivePopupEnterName,
AriaPopupController,
AxisAligns,
Expand Down Expand Up @@ -87,7 +86,7 @@ import { CdsInternalPointer } from './pointer.element.js';
false: AnimationResponsivePopupEnterName,
},
})
export class CdsInternalPopup extends CdsInternalStaticOverlay implements Animatable, PositionableElement {
export class CdsInternalPopup extends CdsInternalStaticOverlay implements PositionableElement {
// --- mixins/composables ---

protected ariaPopupController = new AriaPopupController(this);
Expand Down
17 changes: 5 additions & 12 deletions packages/core/src/internal/decorators/animate.spec.ts
Expand Up @@ -5,14 +5,7 @@
*/

import { animate } from './animate.js';
import {
Animatable,
AnimatableElement,
event,
EventEmitter,
property,
registerElementSafely,
} from '@cds/core/internal';
import { AnimatableElement, event, EventEmitter, property, registerElementSafely } from '@cds/core/internal';
import { html, LitElement } from 'lit';
import { componentIsStable, createTestElement, removeTestElement } from '@cds/core/test';

Expand All @@ -29,7 +22,7 @@ declare global {
no: 'nothing',
},
})
class TestAnimateElement extends LitElement implements Animatable {
class TestAnimateElement extends LitElement {
@property({ type: String })
cdsMotion = 'on';

Expand All @@ -43,7 +36,7 @@ class TestAnimateElement extends LitElement implements Animatable {

// sanity check if bad data comes through
@animate(null)
class TestEmptyAnimateElement extends LitElement implements Animatable {
class TestEmptyAnimateElement extends LitElement {
@property({ type: String })
cdsMotion = 'on';

Expand Down Expand Up @@ -78,13 +71,13 @@ describe('animate decorator', () => {

it('should set _animations hidden property', async () => {
await componentIsStable(component);
expect(component._animations).toBeDefined();
expect(component._animations).not.toBeNull();
expect(component._animations.isValid.yes).toBe('something');
expect(component._animations.isValid.no).toBe('nothing');
});

it('should handle bad data', async () => {
await componentIsStable(emptyComponent);
expect(emptyComponent._animations).toBeUndefined();
expect(emptyComponent._animations).toBeNull();
});
});
34 changes: 22 additions & 12 deletions packages/core/src/internal/decorators/animate.ts
Expand Up @@ -12,26 +12,36 @@ import {
} from '../motion/interfaces.js';
import { runPropertyAnimations } from '../motion/utils.js';

// https://www.typescriptlang.org/docs/handbook/decorators.html#class-decorators
// decorator factory that extends the component constructor to inject animations code into it
export function animate(config: PropertyDrivenAnimation) {
// eslint-disable-next-line @typescript-eslint/ban-types
return function _DecoratorName<T extends { new (...args: any[]): {} }>(constr: T) {
const _constr = constr as any;
return (class extends _constr {
_animations: any;
return function _animationDecorator<T extends { new (...args: any[]): AnimatableElement }>(constructor: T) {
return (class extends constructor {
private _animationReady = false;

private _animationDemoMode = false;

_animations = config;

updated(props: Map<string, any>) {
super.updated(props);

const self = (this as unknown) as AnimatableElement & { _animations: PropertyDrivenAnimation };
self._animations = config || void 0;
if (!this._animationReady && !this.hasAttribute(PRIVATE_ANIMATION_STATUS_ATTR_NAME)) {
// this prevents animations from loading onpageload
this.setAttribute(PRIVATE_ANIMATION_STATUS_ATTR_NAME, AnimationStatus.ready);
this._animationReady = true;
return;
}

if (!self.hasAttribute(PRIVATE_ANIMATION_STATUS_ATTR_NAME)) {
self.setAttribute(PRIVATE_ANIMATION_STATUS_ATTR_NAME, AnimationStatus.ready);
} else if (!self.hasAttribute('_demo-mode')) {
// ignore if element has the _demo-mode attribute set; _demo-mode is used for docs, static examples, and stories
runPropertyAnimations(props, self);
if (this._animationDemoMode === true || this.hasAttribute('_demo-mode')) {
// ignore if element has the _demo-mode attribute set;
// _demo-mode is used for docs, static examples, and stories
// _demo-mode is not intended for external use
this._animationDemoMode = true;
return;
}

runPropertyAnimations(props, this);
}
} as unknown) as T;
};
Expand Down
9 changes: 5 additions & 4 deletions packages/core/src/internal/motion/interfaces.ts
Expand Up @@ -4,6 +4,7 @@
* The full license information can be found in LICENSE in the root directory of this project.
*/

import { ReactiveElement } from 'lit';
import { EventEmitter } from '../decorators/event';

/* GLOBALS */
Expand Down Expand Up @@ -64,9 +65,9 @@ export interface MotionRegistry {

/* ANIMATED COMPONENTS */

export interface Animatable {
export type AnimatableElement = ReactiveElement & {
cdsMotion: string;
cdsMotionChange: EventEmitter<string>;
}

export type AnimatableElement = HTMLElement & Animatable & { [key: string]: any; _animations: PropertyDrivenAnimation };
_animations?: PropertyDrivenAnimation;
'_cds-animation-status'?: string;
};
13 changes: 5 additions & 8 deletions packages/core/src/internal/motion/motion.stories.mdx
Expand Up @@ -34,13 +34,12 @@ Clarity Motion is customizable at several levels:

## Installation

To animate a component with ClarityMotion, import the `animate` decorator and `Animatable` interface in your JavaScript. The `Animatable` interface will require the animated component to have a `cdsMotion` property and `motionChange` event emitter.
To animate a component with ClarityMotion, import the `animate` decorator. The `@animate` decorator will require the component to have a `cdsMotion` property and `cdsMotionChange` event emitter.

```typescript
// custom-animated-component/custom-animated-component.element.ts

import {
Animatable,
animate,
event,
property,
Expand All @@ -53,12 +52,12 @@ import { LitElement } from 'lit';
false: 'shake-me'
},
})
export class CustomAnimatedComponent extends LitElement implements Animatable {
export class CustomAnimatedComponent extends LitElement {
@property({ type: String })
cdsMotion = 'on';

@event()
motionChange: EventEmitter<string>;
cdsMotionChange: EventEmitter<string>;

@property({ type: Boolean })
isValid = true;
Expand Down Expand Up @@ -110,7 +109,6 @@ Saying an animation is _property-driven_ means that the thing which makes the an
// custom-animated-component/custom-animated-component.element.ts

import {
Animatable,
animate,
event,
property,
Expand Down Expand Up @@ -142,7 +140,7 @@ import { LitElement } from 'lit';
false: 'shake-me'
},
})
export class CustomAnimatedComponent extends LitElement implements Animatable {
export class CustomAnimatedComponent extends LitElement {
@property({ type: Boolean })
isValid = true; // This is the property that will trigger the shake animation

Expand Down Expand Up @@ -349,7 +347,6 @@ In the next example, we have extended the modal so that it will slide in and out
// custom-sliding-modal/custom-sliding-modal.element.ts

import {
Animatable,
animate,
event,
property
Expand All @@ -362,7 +359,7 @@ import { CdsModal } from '@cds/core/modal';
false: 'modal-slide-left',
},
})
export class CustomSlidingModal extends CdsModal implements Animatable {
export class CustomSlidingModal extends CdsModal {
@property({ type: String })
cdsMotion = 'on';

Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/internal/motion/utils.spec.ts
Expand Up @@ -10,7 +10,6 @@ import { componentIsStable, createTestElement, removeTestElement } from '@cds/co
import { registerElementSafely, property, event, EventEmitter } from '@cds/core/internal';
import { animate } from '../decorators/animate.js';
import {
Animatable,
AnimatableElement,
CLARITY_MOTION_FALLBACK_EASING,
CLARITY_MOTION_FALLBACK_DURATION_IN_MS,
Expand Down Expand Up @@ -57,7 +56,7 @@ type PropMap = Map<string, any>;
false: 'nothing',
},
})
export class TestAnimateUtilsElement extends LitElement implements Animatable {
export class TestAnimateUtilsElement extends LitElement {
@property({ type: String })
cdsMotion = 'on';

Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/internal/motion/utils.ts
Expand Up @@ -61,13 +61,13 @@ export async function runPropertyAnimations(props: Map<string, any>, hostEl: Ani
propertyAnimations.map((propNameAnimationTuple: TargetedAnimationAsPropertyTuple) => {
const [propname, propertyAnimationsByValue] = propNameAnimationTuple;

if (props.get(propname) === hostEl[propname]) {
if (props.get(propname) === (hostEl as any)[propname]) {
// a weird/unlikely state where an update is sent but the property value didn't actually change
return false;
}

// gets animations to run based on the property's value
const animatedPropertyValueAsString = propertyAnimationsByValue[hostEl[propname].toString()];
const animatedPropertyValueAsString = propertyAnimationsByValue[(hostEl as any)[propname].toString()];
const cdsMotionValue = hostEl.cdsMotion;

// looping through each tuple value in order, getAnimationConfigForPropertyValue()
Expand All @@ -77,7 +77,7 @@ export async function runPropertyAnimations(props: Map<string, any>, hostEl: Ani

const [motionName, returnedMotion] = getAnimationConfigForPropertyValue(
animatedPropertyValueAsString,
getInlineOverride(cdsMotionValue, propname, hostEl[propname].toString())
getInlineOverride(cdsMotionValue, propname, (hostEl as any)[propname].toString())
);
let motionForMyValue = clone(returnedMotion); // have to jump through this hoop to keep typescript happy

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/internal/utils/global.ts
Expand Up @@ -8,6 +8,7 @@ import { isBrowser } from './exists.js';
import { getAngularVersion, getReactVersion, getVueVersion, getAngularJSVersion } from './framework.js';
import { FeatureSupportMatrix, browserFeatures } from './supports.js';
import { LogService } from '../services/log.service.js';
import { MotionRegistry } from '../motion/interfaces.js';

export interface CDSGlobal {
_version: string[];
Expand All @@ -28,7 +29,7 @@ export interface CDSState {
i18nRegistry: Readonly<Record<string, unknown>>;
elementRegistry: Readonly<{ [key: string]: any }>;
iconRegistry: Readonly<Record<string, unknown>>;
motionRegistry: Readonly<Record<string, any>>;
motionRegistry: Readonly<MotionRegistry>;
}

declare global {
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/modal/modal.element.ts
Expand Up @@ -8,7 +8,6 @@ import { html, TemplateResult } from 'lit';
import { query } from 'lit/decorators/query.js';
import {
animate,
Animatable,
AnimationModalEnterName,
i18n,
I18nService,
Expand Down Expand Up @@ -69,7 +68,7 @@ import { CdsModalActions } from './modal-actions.element';
false: AnimationModalEnterName,
},
})
export class CdsModal extends CdsInternalOverlay implements Animatable {
export class CdsModal extends CdsInternalOverlay {
protected get customBumpers(): [HTMLElement, HTMLElement] {
return [this.modalHeader, this.modalFooter];
}
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/navigation/navigation-group.element.ts
Expand Up @@ -7,7 +7,6 @@
import { html, LitElement, PropertyValues } from 'lit';

import {
Animatable,
animate,
AnimationNavigationGroupOpenName,
baseStyles,
Expand Down Expand Up @@ -58,7 +57,7 @@ export const CdsNavigationGroupTagName = 'cds-navigation-group';
false: reverseAnimation(AnimationNavigationGroupOpenName),
},
})
export class CdsNavigationGroup extends LitElement implements Animatable {
export class CdsNavigationGroup extends LitElement {
@property({ type: String })
cdsMotion = 'on';

Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/navigation/navigation.element.ts
Expand Up @@ -6,7 +6,6 @@

import { html, LitElement, PropertyValues } from 'lit';
import {
Animatable,
animate,
baseStyles,
event,
Expand Down Expand Up @@ -78,7 +77,7 @@ export const CdsNavigationTagName = 'cds-navigation';
false: reverseAnimation(AnimationNavigationOpenName),
},
})
export class CdsNavigation extends LitElement implements Animatable {
export class CdsNavigation extends LitElement {
expandedRoot = false;

@property({ type: String })
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/tree-view/tree-item.element.ts
Expand Up @@ -5,7 +5,6 @@
*/

import {
Animatable,
animate,
AnimationTreeItemExpandName,
baseStyles,
Expand Down Expand Up @@ -58,7 +57,7 @@ import styles from './tree-item.element.scss';
false: reverseAnimation(AnimationTreeItemExpandName),
},
})
export class CdsTreeItem extends LitElement implements Animatable {
export class CdsTreeItem extends LitElement {
@i18n() i18n = I18nService.keys.treeview;

@property({ type: String })
Expand Down

0 comments on commit c77f0a8

Please sign in to comment.