Skip to content

Commit

Permalink
Unified animation
Browse files Browse the repository at this point in the history
Closed #108
  • Loading branch information
mantou132 committed Dec 24, 2023
1 parent 9eac59c commit 39e6840
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 32 deletions.
9 changes: 4 additions & 5 deletions packages/duoyun-ui/src/elements/drawer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { adoptedStyle, customElement } from '@mantou/gem/lib/decorators';
import { createCSSSheet, css } from '@mantou/gem/lib/utils';

import { slideInLeft, slideOutRight } from '../lib/animations';

import { DuoyunModalElement, ModalOptions } from './modal';

const style = createCSSSheet(css`
Expand All @@ -17,11 +19,6 @@ const style = createCSSSheet(css`
max-height: none;
border-radius: 0;
}
@keyframes showDialog {
from {
transform: translate(100%, 0);
}
}
`);

/**
Expand All @@ -33,6 +30,8 @@ export class DuoyunDrawerElement extends DuoyunModalElement {
constructor(options: ModalOptions) {
super(options);
this.addEventListener('maskclick', () => this.close(null));
this.openAnimation = slideInLeft;
this.closeAnimation = slideOutRight;
}
}

Expand Down
40 changes: 21 additions & 19 deletions packages/duoyun-ui/src/elements/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { theme } from '../lib/theme';
import { locale } from '../lib/locale';
import { hotkeys } from '../lib/hotkeys';
import { setBodyInert } from '../lib/utils';
import { commonAnimationOptions, fadeOut } from '../lib/animations';
import { commonAnimationOptions, fadeIn, fadeOut, slideInUp } from '../lib/animations';

import './button';
import './divider';
Expand Down Expand Up @@ -49,22 +49,9 @@ const style = createCSSSheet(css`
.mask {
inset: 0;
background-color: rgba(0, 0, 0, calc(${theme.maskAlpha} + 0.2));
animation: showMask 0.15s ${theme.timingFunction} forwards;
}
@keyframes showMask {
from {
opacity: 0;
}
}
.dialog {
outline: none;
animation: showDialog 0.15s ${theme.timingFunction} forwards;
}
@keyframes showDialog {
from {
transform: translate(0, 50%);
opacity: 0;
}
}
.main {
box-sizing: border-box;
Expand Down Expand Up @@ -171,7 +158,11 @@ export class DuoyunModalElement extends GemElement {

@property header?: string | TemplateResult;
@property body?: string | TemplateResult;
@property openAnimation: PropertyIndexedKeyframes | Keyframe[] = slideInUp;
@property closeAnimation: PropertyIndexedKeyframes | Keyframe[] = fadeOut;

@refobject maskRef: RefObject<HTMLElement>;
@refobject dialogRef: RefObject<HTMLElement>;
@refobject bodyRef: RefObject<HTMLElement>;

@part static dialog: string;
Expand Down Expand Up @@ -201,7 +192,7 @@ export class DuoyunModalElement extends GemElement {
});
}).finally(async () => {
restoreInert();
await modal.#closeAnimate().finished;
await modal.#closeAnimate();
modal.remove();
});
}
Expand Down Expand Up @@ -266,15 +257,25 @@ export class DuoyunModalElement extends GemElement {
);
};

#closeAnimate = () => this.animate(fadeOut, commonAnimationOptions);
#openAnimate = () => {
this.maskRef.element?.animate(fadeIn, commonAnimationOptions);
(this.dialogRef.element || this.bodyRef.element)?.animate(this.openAnimation, commonAnimationOptions);
};

#closeAnimate = () =>
Promise.all([
this.maskRef.element?.animate(fadeOut, commonAnimationOptions).finished,
(this.dialogRef.element || this.bodyRef.element)?.animate(this.closeAnimation, commonAnimationOptions).finished,
]);

mounted = () => {
this.effect(
async () => {
if (this.open) {
!this.shadowRoot?.activeElement && this.focus();
this.#openAnimate();
} else if (this.closing) {
await this.#closeAnimate().finished;
await this.#closeAnimate();
this.closing = false;
this.update();
}
Expand All @@ -289,22 +290,23 @@ export class DuoyunModalElement extends GemElement {
if (!this.open && !this.closing) return html``;

return html`
<div class="mask absolute" @click=${this.#onMaskClick}></div>
<div class="mask absolute" ref=${this.maskRef.ref} @click=${this.#onMaskClick}></div>
${this.customize
? html`
<div
ref=${this.bodyRef.ref}
part=${DuoyunModalElement.dialog}
role="dialog"
tabindex="0"
aria-modal="true"
class="dialog absolute"
ref=${this.bodyRef.ref}
>
${this.body || html`<slot name=${DuoyunModalElement.body}></slot>`}
</div>
`
: html`
<div
ref=${this.dialogRef.ref}
part=${DuoyunModalElement.dialog}
role="dialog"
tabindex="0"
Expand Down
32 changes: 28 additions & 4 deletions packages/duoyun-ui/src/lib/animations.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
import { themeStore } from './theme';

export const commonAnimationOptions: KeyframeAnimationOptions = {
export const commonAnimationOptions = {
easing: themeStore.timingFunction,
duration: 300,
};
duration: 150,
} satisfies KeyframeAnimationOptions;

export const fadeIn: Keyframe[] = [{ opacity: 0 }, { opacity: 1 }];
export const fadeOut: Keyframe[] = [{ opacity: 1 }, { opacity: 0 }];
export const fadeOut = [...fadeIn].reverse();

export const slideInUp: Keyframe[] = [
{
transform: 'translateY(50%)',
opacity: 0,
},
{
transform: 'translateY(0)',
opacity: 1,
},
];
export const slideOutDown = [...slideInUp].reverse();

export const slideInLeft: Keyframe[] = [
{
transform: 'translateX(50%)',
opacity: 0,
},
{
transform: 'translateX(0)',
opacity: 1,
},
];
export const slideOutRight = [...slideInLeft].reverse();
104 changes: 100 additions & 4 deletions packages/gem/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,24 +379,120 @@ export function objectMapToString<T = any>(

type StyleProp = keyof CSSStyleDeclaration | `--${string}`;

export type StyleObject = Partial<Record<StyleProp, string | number | undefined>>;
export type StyleObject = Partial<Record<StyleProp, string | number | undefined | null>>;

// 不支持 webkit 属性
export function styleMap(object: StyleObject) {
return objectMapToString(object, ';', (key, value) =>
value !== undefined ? `${camelToKebabCase(key)}:${value}` : '',
value !== undefined && value !== null ? `${camelToKebabCase(key)}:${value}` : '',
);
}

export function classMap(object: Record<string, boolean | string | number | undefined>) {
export function classMap(object: Record<string, boolean | string | number | undefined | null>) {
return objectMapToString(object, ' ', (key, value) => (value ? key : ''));
}

export const partMap = classMap;

// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/exportparts
export function exportPartsMap(object: Record<string, string | undefined | boolean>) {
export function exportPartsMap(object: Record<string, string | boolean | undefined | null>) {
return objectMapToString(object, ',', (key, value) =>
value === true || key === value ? key : value ? `${key}:${value}` : '',
);
}

declare global {
interface PropertyIndexedKeyframes extends StyleObject {}
interface Keyframe extends StyleObject {}
}

/**
* @example
* ```css
* animation: 150ms ease 0ms showMask;`
* @keyframes showMask {
* from {
* opacity: 0;
* }
* }
* ```
*/
// export function createCSSAnimation(
// keyframes: PropertyIndexedKeyframes | Keyframe[],
// options?: number | (KeyframeEffectOptions & { name: string }),
// ) {
// const frames = new Map<number, StyleObject>();
// if (Array.isArray(keyframes)) {
// keyframes.forEach((keyframe, index) => {
// const offset = keyframes.length === 1 ? 1 : keyframe.offset || index / (keyframes.length - 1);
// frames.set(offset, {
// ...keyframe,
// // https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Keyframe_Formats#attributes
// cssOffset: undefined,
// offset: keyframe.cssOffset,
// composite: undefined,
// animationComposition: keyframe.composite,
// easing: undefined,
// animationTimingFunction: keyframe.easing,
// } as StyleObject);
// });
// } else {
// const offsetList: (number | null)[] = Array.isArray(keyframes.offset)
// ? keyframes.offset
// : keyframes.offset === undefined
// ? []
// : [keyframes.offset];
// if (offsetList.length && offsetList.at(-1) !== 1) offsetList.push(1);

// const setStyle = (offset: number, key: string, value: string | number | null | undefined) => {
// const style = frames.get(offset) || {};
// switch (key) {
// case 'offset':
// break;
// case 'cssOffset':
// Reflect.set(style, 'offset', value);
// break;
// case 'composite':
// Reflect.set(style, 'animationComposition', value);
// break;
// case 'easing':
// Reflect.set(style, 'animationTimingFunction', value);
// break;
// default:
// Reflect.set(style, key, value);
// }
// frames.set(offset, style);
// };
// for (const key in keyframes) {
// const framesValue = keyframes[key];
// !Array.isArray(framesValue)
// ? setStyle(1, key, framesValue)
// : framesValue.length === 1
// ? setStyle(1, key, framesValue[0])
// : framesValue.forEach((value, index) =>
// setStyle(offsetList[index] ?? index / (framesValue.length - 1), key, value),
// );
// }
// }

// let framesStr = '';
// frames.forEach((rules, offset) => {
// framesStr += `${(offset * 100).toFixed()}%{${styleMap(rules)}}`;
// });

// if (options) {
// const {
// // 只能使用 ms 数字
// duration = 0,
// easing = '',
// delay = 0,
// iterations = 1,
// direction = '',
// fill = '',
// name = `ani-${randomStr()}`,
// } = typeof options === 'number' ? ({ duration: options } as Exclude<typeof options, number>) : options;
// return `${duration}ms ${easing} ${delay}ms ${iterations} ${direction} ${fill} ${name};@keyframes ${name}{${framesStr}}`;
// }

// return framesStr;
// }
25 changes: 25 additions & 0 deletions packages/gem/src/test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
classMap,
exportPartsMap,
absoluteLocation,
createCSSAnimation,
} from '../lib/utils';

declare global {
Expand Down Expand Up @@ -110,4 +111,28 @@ describe('utils 测试', () => {
expect(exportPartsMap({ foo: 'bar', content: 'content', false: false })).to.equal(`,foo:bar,content,`);
expect(exportPartsMap({ foo: 'bar', content: true })).to.equal(`,foo:bar,content,`);
});
it('createCSSAnimation', () => {
expect(createCSSAnimation([{ opacity: 0 }])).to.equal('100%{;opacity:0;}');
expect(createCSSAnimation([{ opacity: 1 }, { opacity: 0 }])).to.equal('0%{;opacity:1;}100%{;opacity:0;}');
expect(createCSSAnimation([{ opacity: 1 }, { opacity: 0.1, offset: 0.7 }, { opacity: 0 }])).to.equal(
'0%{;opacity:1;}70%{;opacity:0.1;}100%{;opacity:0;}',
);
expect(createCSSAnimation({ opacity: [0] })).to.equal('100%{;opacity:0;}');
expect(createCSSAnimation({ opacity: [1, 0] })).to.equal('0%{;opacity:1;}100%{;opacity:0;}');
expect(createCSSAnimation({ opacity: [1, 0], offset: [0, 0.7] })).to.equal(
'0%{;opacity:1;}70%{;opacity:0;}100%{;}',
);
expect(createCSSAnimation({ opacity: [1, 0], backgroundColor: ['red', 'yellow', 'green'] })).to.equal(
'0%{;opacity:1;background-color:red;}100%{;opacity:0;background-color:green;}50%{;background-color:yellow;}',
);
expect(
createCSSAnimation({
opacity: [1, 0],
backgroundColor: ['red', 'yellow', 'green'],
offset: [0, 0.7],
}),
).to.equal(
'0%{;opacity:1;background-color:red;}70%{;opacity:0;background-color:yellow;}100%{;background-color:green;}',
);
});
});

0 comments on commit 39e6840

Please sign in to comment.