From f21b27b60f8731d97fbf2958dddb0aa831306852 Mon Sep 17 00:00:00 2001 From: sivanova Date: Wed, 18 Jan 2023 09:38:03 +0200 Subject: [PATCH 01/13] feat(dialog/snackbar): animate component states --- src/animations/presets/fade/index.ts | 14 ++++++++++++++ src/animations/presets/index.ts | 1 + src/components/dialog/dialog.ts | 28 ++++++++++++++++++++++++---- src/components/snackbar/snackbar.ts | 24 +++++++++++++++++++++++- 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 src/animations/presets/fade/index.ts diff --git a/src/animations/presets/fade/index.ts b/src/animations/presets/fade/index.ts new file mode 100644 index 000000000..2a844af93 --- /dev/null +++ b/src/animations/presets/fade/index.ts @@ -0,0 +1,14 @@ +import { EaseOut } from '../../easings.js'; +import { animation } from '../../types.js'; + +const fadeIn = animation([{ opacity: 0 }, { opacity: 1 }], { + duration: 350, + easing: EaseOut.Sine, +}); + +const fadeOut = animation([{ opacity: 1 }, { opacity: 0 }], { + duration: 350, + easing: EaseOut.Sine, +}); + +export { fadeIn, fadeOut }; diff --git a/src/animations/presets/index.ts b/src/animations/presets/index.ts index 8b0261847..dc3204c53 100644 --- a/src/animations/presets/index.ts +++ b/src/animations/presets/index.ts @@ -1 +1,2 @@ export * from './grow/index.js'; +export * from './fade/index.js'; diff --git a/src/components/dialog/dialog.ts b/src/components/dialog/dialog.ts index bea24a500..f4e60a38a 100644 --- a/src/components/dialog/dialog.ts +++ b/src/components/dialog/dialog.ts @@ -14,6 +14,7 @@ import { styles as material } from './themes/light/dialog.material.css.js'; import { themes } from '../../theming/theming-decorator.js'; import { defineComponents } from '../common/definitions/defineComponents.js'; import IgcButtonComponent from '../button/button.js'; +import { AnimationPlayer, fadeIn, fadeOut } from '../../animations/index.js'; defineComponents(IgcButtonComponent); @@ -50,6 +51,7 @@ export default class IgcDialogComponent extends EventEmitterMixin< private static readonly increment = createCounter(); private titleId = `title-${IgcDialogComponent.increment()}`; + private animationPlayer!: AnimationPlayer; @query('dialog', true) private dialog!: HTMLDialogElement; @@ -102,28 +104,43 @@ export default class IgcDialogComponent extends EventEmitterMixin< } protected override async firstUpdated() { + this.animationPlayer = new AnimationPlayer(this.dialog); await this.updateComplete; if (this.open) { this.dialog.showModal(); } } + private async toggleAnimation(dir: 'open' | 'close') { + const animation = dir === 'open' ? fadeIn : fadeOut; + + const [_, event] = await Promise.all([ + this.animationPlayer.stopAll(), + this.animationPlayer.play(animation), + ]); + + return event.type === 'finish'; + } + /** Opens the dialog. */ public show() { if (this.open) { return; } + this.toggleAnimation('open'); this.open = true; } /** Closes the dialog. */ - public hide() { + public async hide() { if (!this.open) { return; } - this.open = false; + if (await this.toggleAnimation('close')) { + this.open = false; + } } /** Toggles the open state of the dialog. */ @@ -140,9 +157,12 @@ export default class IgcDialogComponent extends EventEmitterMixin< return; } - this.open = false; + if (await this.toggleAnimation('close')) { + this.open = false; + this.emitEvent('igcClosed'); + } + await this.updateComplete; - this.emitEvent('igcClosed'); } private handleCancel(event: Event) { diff --git a/src/components/snackbar/snackbar.ts b/src/components/snackbar/snackbar.ts index b097a9566..b6fa7e134 100644 --- a/src/components/snackbar/snackbar.ts +++ b/src/components/snackbar/snackbar.ts @@ -1,5 +1,5 @@ import { html, LitElement, nothing } from 'lit'; -import { property } from 'lit/decorators.js'; +import { property, query } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { themes } from '../../theming/theming-decorator.js'; @@ -12,6 +12,7 @@ import { styles as indigo } from './themes/light/snackbar.indigo.css.js'; import { defineComponents } from '../common/definitions/defineComponents.js'; import IgcButtonComponent from '../button/button.js'; +import { AnimationPlayer, fadeIn, fadeOut } from '../../animations/index.js'; defineComponents(IgcButtonComponent); @@ -45,6 +46,10 @@ export default class IgcSnackbarComponent extends EventEmitterMixin< public static styles = styles; private autoHideTimeout!: number; + private animationPlayer!: AnimationPlayer; + + @query('[part~="base"]', true) + protected content!: HTMLElement; /** * Determines whether the snackbar is opened. @@ -74,12 +79,28 @@ export default class IgcSnackbarComponent extends EventEmitterMixin< @property({ attribute: 'action-text' }) public actionText!: string; + public override firstUpdated() { + this.animationPlayer = new AnimationPlayer(this.content); + } + + private async toggleAnimation(dir: 'open' | 'close') { + const animation = dir === 'open' ? fadeIn : fadeOut; + + const [_, event] = await Promise.all([ + this.animationPlayer.stopAll(), + this.animationPlayer.play(animation), + ]); + + return event.type === 'finish'; + } + /** Opens the snackbar. */ public show() { if (this.open) { return; } + this.toggleAnimation('open'); this.open = true; clearTimeout(this.autoHideTimeout); @@ -97,6 +118,7 @@ export default class IgcSnackbarComponent extends EventEmitterMixin< return; } + this.toggleAnimation('close'); this.open = false; clearTimeout(this.autoHideTimeout); } From 1cdf3e51043126676219563bd92f5be350e80806 Mon Sep 17 00:00:00 2001 From: sivanova Date: Wed, 18 Jan 2023 13:58:35 +0200 Subject: [PATCH 02/13] feat(toast): animate component states --- src/components/dialog/dialog.spec.ts | 11 +++++++---- src/components/dialog/dialog.ts | 5 ++--- src/components/toast/toast.spec.ts | 5 +++-- src/components/toast/toast.ts | 24 ++++++++++++++++++++++-- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/components/dialog/dialog.spec.ts b/src/components/dialog/dialog.spec.ts index 791d25415..30dc0d106 100644 --- a/src/components/dialog/dialog.spec.ts +++ b/src/components/dialog/dialog.spec.ts @@ -4,6 +4,7 @@ import { fixture, html, unsafeStatic, + waitUntil, } from '@open-wc/testing'; import sinon from 'sinon'; import { defineComponents, IgcDialogComponent } from '../../index.js'; @@ -154,7 +155,7 @@ describe('Dialog component', () => { await elementUpdated(dialog); dialog.hide(); - await elementUpdated(dialog); + await waitUntil(() => !dialog.open); expect(dialog.open).to.eq(false); }); @@ -163,7 +164,7 @@ describe('Dialog component', () => { await elementUpdated(dialog); dialog.toggle(); - await elementUpdated(dialog); + await waitUntil(() => !dialog.open); expect(dialog.open).to.eq(false); dialog.open = false; @@ -205,7 +206,7 @@ describe('Dialog component', () => { expect(spy.callCount).to.equal(0); dialog.hide(); - await elementUpdated(dialog); + await waitUntil(() => !dialog.open); expect(dialog.open).to.be.false; expect(spy.callCount).to.equal(0); @@ -314,7 +315,9 @@ describe('Dialog component', () => { clientY: y - 1, }) ); - await elementUpdated(dialog); + + const eventSpy = sinon.spy(dialog, 'emitEvent'); + await waitUntil(() => eventSpy.calledWith('igcClosed')); expect(dialog.open).to.be.false; }); diff --git a/src/components/dialog/dialog.ts b/src/components/dialog/dialog.ts index f4e60a38a..d6ffb36d5 100644 --- a/src/components/dialog/dialog.ts +++ b/src/components/dialog/dialog.ts @@ -138,9 +138,8 @@ export default class IgcDialogComponent extends EventEmitterMixin< return; } - if (await this.toggleAnimation('close')) { - this.open = false; - } + await this.toggleAnimation('close'); + this.open = false; } /** Toggles the open state of the dialog. */ diff --git a/src/components/toast/toast.spec.ts b/src/components/toast/toast.spec.ts index 79b5afb16..d6c706656 100644 --- a/src/components/toast/toast.spec.ts +++ b/src/components/toast/toast.spec.ts @@ -4,6 +4,7 @@ import { expect, elementUpdated, unsafeStatic, + waitUntil, } from '@open-wc/testing'; import { defineComponents, IgcToastComponent } from '../../index.js'; @@ -68,7 +69,7 @@ describe('Toast', () => { await elementUpdated(toast); toast.hide(); - await elementUpdated(toast); + await waitUntil(() => !toast.open); expect(toast.open).to.eq(false); }); @@ -77,7 +78,7 @@ describe('Toast', () => { await elementUpdated(toast); toast.toggle(); - await elementUpdated(toast); + await waitUntil(() => !toast.open); expect(toast.open).to.eq(false); toast.open = false; diff --git a/src/components/toast/toast.ts b/src/components/toast/toast.ts index a600f9fe2..b0148bcfe 100644 --- a/src/components/toast/toast.ts +++ b/src/components/toast/toast.ts @@ -5,6 +5,7 @@ import { styles } from './themes/toast.base.css.js'; import { styles as bootstrap } from './themes/toast.bootstrap.css.js'; import { styles as fluent } from './themes/toast.fluent.css.js'; import { styles as indigo } from './themes/toast.indigo.css.js'; +import { AnimationPlayer, fadeIn, fadeOut } from '../../animations/index.js'; /** * A toast component is used to show a notification @@ -20,6 +21,7 @@ export default class IgcToastComponent extends LitElement { public static override styles = [styles]; private displayTimeout!: ReturnType; + private animationPlayer!: AnimationPlayer; /** * Determines whether the toast is opened. @@ -42,9 +44,25 @@ export default class IgcToastComponent extends LitElement { @property({ type: Boolean, reflect: true, attribute: 'keep-open' }) public keepOpen = false; + public override firstUpdated() { + this.animationPlayer = new AnimationPlayer(this); + } + + private async toggleAnimation(dir: 'open' | 'close') { + const animation = dir === 'open' ? fadeIn : fadeOut; + + const [_, event] = await Promise.all([ + this.animationPlayer.stopAll(), + this.animationPlayer.play(animation), + ]); + + return event.type === 'finish'; + } + /** Closes the toast. */ - public hide() { + public async hide() { if (this.open) { + await this.toggleAnimation('close'); this.open = false; } } @@ -54,11 +72,13 @@ export default class IgcToastComponent extends LitElement { window.clearTimeout(this.displayTimeout); if (!this.open) { + this.toggleAnimation('open'); this.open = true; } if (this.keepOpen === false) { - this.displayTimeout = setTimeout(() => { + this.displayTimeout = setTimeout(async () => { + await this.toggleAnimation('close'); this.open = false; }, this.displayTime); } From 740bf8338fd7a1c3373ac5fa6eb249f81cd81e16 Mon Sep 17 00:00:00 2001 From: sivanova Date: Wed, 18 Jan 2023 15:14:33 +0200 Subject: [PATCH 03/13] fix(dialog): failing tests --- src/components/dialog/dialog.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/dialog/dialog.spec.ts b/src/components/dialog/dialog.spec.ts index 30dc0d106..c40039ab6 100644 --- a/src/components/dialog/dialog.spec.ts +++ b/src/components/dialog/dialog.spec.ts @@ -230,7 +230,7 @@ describe('Dialog component', () => { await elementUpdated(dialog); dialog.shadowRoot!.querySelector('igc-button')!.click(); - await elementUpdated(dialog); + await waitUntil(() => !dialog.open); expect(spy.callCount).to.equal(2); expect(spy.firstCall).calledWith('igcClosing'); @@ -337,7 +337,7 @@ describe('Dialog component', () => { const form = document.getElementById('form'); form?.dispatchEvent(new Event('igcSubmit')); - await elementUpdated(dialog); + await waitUntil(() => !dialog.open); expect(dialog.open).to.eq(false); }); From d0a87d25c30613bd3f884313a9cea8de5072ef12 Mon Sep 17 00:00:00 2001 From: sivanova Date: Wed, 18 Jan 2023 15:52:40 +0200 Subject: [PATCH 04/13] fix(snackbar): hide animation --- src/components/snackbar/snackbar.spec.ts | 4 +++- src/components/snackbar/snackbar.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/snackbar/snackbar.spec.ts b/src/components/snackbar/snackbar.spec.ts index 3244c9071..dc8f3b6d3 100644 --- a/src/components/snackbar/snackbar.spec.ts +++ b/src/components/snackbar/snackbar.spec.ts @@ -5,6 +5,7 @@ import { elementUpdated, unsafeStatic, aTimeout, + waitUntil, } from '@open-wc/testing'; import sinon from 'sinon'; import { defineComponents, IgcSnackbarComponent } from '../../index.js'; @@ -81,7 +82,7 @@ describe('Snackbar', () => { expect(el).dom.to.equal(``); el.hide(); - await elementUpdated(el); + await waitUntil(() => !el.open); expect(el.open).to.equal(false); expect(el).dom.to.equal(``); }); @@ -96,6 +97,7 @@ describe('Snackbar', () => { await elementUpdated(el); expect(el.open).to.equal(true); await aTimeout(1000); + await waitUntil(() => !el.open); expect(el.open).to.equal(false); }); diff --git a/src/components/snackbar/snackbar.ts b/src/components/snackbar/snackbar.ts index b6fa7e134..feb91ac46 100644 --- a/src/components/snackbar/snackbar.ts +++ b/src/components/snackbar/snackbar.ts @@ -113,12 +113,12 @@ export default class IgcSnackbarComponent extends EventEmitterMixin< } /** Closes the snackbar. */ - public hide() { + public async hide() { if (!this.open) { return; } - this.toggleAnimation('close'); + await this.toggleAnimation('close'); this.open = false; clearTimeout(this.autoHideTimeout); } From 33dccf4b54da2b6be3948e076648280088cf2da1 Mon Sep 17 00:00:00 2001 From: sivanova Date: Fri, 28 Apr 2023 09:54:10 +0300 Subject: [PATCH 05/13] fix(dialog): animate backdrop --- src/components/dialog/dialog.ts | 8 +++++++- src/components/dialog/themes/light/dialog.base.scss | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/dialog/dialog.ts b/src/components/dialog/dialog.ts index 06c11b689..70d5f1ab9 100644 --- a/src/components/dialog/dialog.ts +++ b/src/components/dialog/dialog.ts @@ -56,6 +56,9 @@ export default class IgcDialogComponent extends EventEmitterMixin< @query('dialog', true) private dialog!: HTMLDialogElement; + @query('[part~="backdrop"]', true) + private backdrop!: HTMLElement; + /* blazorSuppress */ /** * Whether the dialog should be closed when pressing the 'ESCAPE' button. @@ -142,6 +145,7 @@ export default class IgcDialogComponent extends EventEmitterMixin< return; } + this.backdrop.setAttribute('aria-hidden', 'false'); this.toggleAnimation('open'); this.open = true; } @@ -152,6 +156,7 @@ export default class IgcDialogComponent extends EventEmitterMixin< return; } + this.backdrop.setAttribute('aria-hidden', 'true'); await this.toggleAnimation('close'); this.open = false; } @@ -162,6 +167,7 @@ export default class IgcDialogComponent extends EventEmitterMixin< } protected async hideWithEvent() { + this.backdrop.setAttribute('aria-hidden', 'true'); if (!this.open) { return; } @@ -226,7 +232,7 @@ export default class IgcDialogComponent extends EventEmitterMixin< const labelledby = label ? undefined : this.titleId; return html` - +
Date: Fri, 28 Apr 2023 09:57:15 +0300 Subject: [PATCH 06/13] Update dialog.base.scss --- src/components/dialog/themes/light/dialog.base.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dialog/themes/light/dialog.base.scss b/src/components/dialog/themes/light/dialog.base.scss index d3e36d9ec..eee31394b 100644 --- a/src/components/dialog/themes/light/dialog.base.scss +++ b/src/components/dialog/themes/light/dialog.base.scss @@ -73,4 +73,4 @@ slot[name='footer'] { [part='backdrop'][aria-hidden='false'] { opacity: 1; -} \ No newline at end of file +} From 6ce3952873b5fcd89910eeca5684087f0c2bbe70 Mon Sep 17 00:00:00 2001 From: sivanova Date: Tue, 23 May 2023 14:53:18 +0300 Subject: [PATCH 07/13] feat(stepper): provide component animations --- src/animations/presets/index.ts | 1 + src/animations/presets/slide/index.ts | 26 +++++ src/components/stepper/step.ts | 95 ++++++++++++++++++- .../stepper/themes/step/light/step.base.scss | 14 ++- .../stepper/themes/stepper/stepper.base.scss | 14 ++- 5 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 src/animations/presets/slide/index.ts diff --git a/src/animations/presets/index.ts b/src/animations/presets/index.ts index dc3204c53..6b2ccb8c2 100644 --- a/src/animations/presets/index.ts +++ b/src/animations/presets/index.ts @@ -1,2 +1,3 @@ export * from './grow/index.js'; export * from './fade/index.js'; +export * from './slide/index.js'; diff --git a/src/animations/presets/slide/index.ts b/src/animations/presets/slide/index.ts new file mode 100644 index 000000000..d3de053da --- /dev/null +++ b/src/animations/presets/slide/index.ts @@ -0,0 +1,26 @@ +import { EaseOut } from '../../easings.js'; +import { animation } from '../../types.js'; + +const slideInLeft = animation( + [ + { opacity: 0, transform: 'translateX(-500px)' }, + { opacity: 1, transform: 'translateX(0)' }, + ], + { + duration: 350, + easing: EaseOut.Quad, + } +); + +const slideInRight = animation( + [ + { opacity: 0, transform: 'translateX(500px)' }, + { opacity: 1, transform: 'translateX(0)' }, + ], + { + duration: 350, + easing: EaseOut.Quad, + } +); + +export { slideInLeft, slideInRight }; diff --git a/src/components/stepper/step.ts b/src/components/stepper/step.ts index 43b1054c5..bc959c3f1 100644 --- a/src/components/stepper/step.ts +++ b/src/components/stepper/step.ts @@ -3,12 +3,21 @@ import { property, query, queryAssignedElements } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; import { watch } from '../common/decorators/watch.js'; import { partNameMap } from '../common/util.js'; +import { + AnimationPlayer, + growVerIn, + growVerOut, + fadeIn, + fadeOut, + EaseOut, +} from '../../animations/index.js'; import { themes } from '../../theming/theming-decorator.js'; import { styles } from './themes/step/light/step.base.css.js'; import { styles as bootstrap } from './themes/step/light/step.bootstrap.css.js'; import { styles as indigo } from './themes/step/light/step.indigo.css.js'; import { styles as fluent } from './themes/step/light/step.fluent.css.js'; import { styles as material } from './themes/step/light/step.material.css.js'; +import { animation } from '../../animations/types.js'; /** * The step component is used within the `igc-stepper` element and it holds the content of each step. @@ -46,6 +55,7 @@ export default class IgcStepComponent extends LitElement { public static readonly tagName = 'igc-step'; /** @private */ public static override styles = styles; + private animationPlayer!: AnimationPlayer; @queryAssignedElements({ slot: 'title' }) private _titleChildren!: Array; @@ -61,6 +71,17 @@ export default class IgcStepComponent extends LitElement { @query('[part~="body"]') public contentBody!: HTMLElement; + /* blazorSuppress */ + @query('[part~="content"]') + public content!: HTMLElement; + + /** + * Determines the stepper animation duration in ms. + * @attr duration + */ + @property({ type: Number, attribute: 'animation-duration' }) + public animationDuration = 320; + /** Gets/sets whether the step is invalid. */ @property({ reflect: true, type: Boolean }) public invalid = false; @@ -107,6 +128,14 @@ export default class IgcStepComponent extends LitElement { @property({ attribute: false }) public orientation: 'horizontal' | 'vertical' = 'horizontal'; + /** @private */ + @property({ attribute: false }) + public verticalAnimation: 'grow' | 'fade' | 'none' = 'grow'; + + /** @private */ + @property({ attribute: false }) + public horizontalAnimation: 'slide' | 'fade' | 'none' = 'slide'; + /** @private */ @property({ attribute: false }) public index = -1; @@ -123,9 +152,73 @@ export default class IgcStepComponent extends LitElement { @property({ attribute: false }) public visited = false; + public override firstUpdated() { + this.animationPlayer = new AnimationPlayer(this.content); + } + + private async toggleAnimation(dir: 'open' | 'close') { + const slideInLeftFrames: Keyframe[] = [ + { opacity: 0, transform: 'translateX(-500px)' }, + { opacity: 1, transform: 'translateX(0)' }, + ]; + + const slideInRightFrames: Keyframe[] = [ + { opacity: 0, transform: 'translateX(500px)' }, + { opacity: 1, transform: 'translateX(0)' }, + ]; + + const opts: KeyframeAnimationOptions = { + duration: this.animationDuration, + easing: EaseOut.Quad, + }; + + const customSlideInLeft = animation(slideInLeftFrames, opts); + const customSlideInRight = animation(slideInRightFrames, opts); + + let customAnimation = + dir === 'open' ? customSlideInLeft : customSlideInRight; + + if (this.orientation == 'vertical') { + switch (this.verticalAnimation) { + case 'grow': + customAnimation = dir === 'open' ? growVerIn : growVerOut; + break; + case 'fade': + customAnimation = dir === 'open' ? fadeIn : fadeOut; + break; + case 'none': + this.animationDuration = 0; + break; + } + } + + if (this.orientation == 'horizontal') { + switch (this.horizontalAnimation) { + case 'slide': + customAnimation = + dir === 'open' ? customSlideInLeft : customSlideInRight; + break; + case 'fade': + customAnimation = dir === 'open' ? fadeIn : fadeOut; + break; + case 'none': + this.animationDuration = 0; + break; + } + } + + const [_, event] = await Promise.all([ + this.animationPlayer.stopAll(), + this.animationPlayer.play(customAnimation), + ]); + + return event.type === 'finish'; + } + @watch('active', { waitUntilFirstUpdate: true }) protected activeChange() { if (this.active) { + this.toggleAnimation('open'); this.dispatchEvent( new CustomEvent('stepActiveChanged', { bubbles: true, detail: false }) ); @@ -236,7 +329,7 @@ export default class IgcStepComponent extends LitElement { role="tabpanel" aria-labelledby="igc-step-header-${this.index}" > -
+
`; diff --git a/src/components/stepper/themes/step/light/step.base.scss b/src/components/stepper/themes/step/light/step.base.scss index 087b936af..76ad9d681 100644 --- a/src/components/stepper/themes/step/light/step.base.scss +++ b/src/components/stepper/themes/step/light/step.base.scss @@ -159,6 +159,7 @@ [part~='body'] { display: var(--vertical-body-disply--last-of-type, var(--vertical-body-disply, none)); + // height: var(--vertical-body-disply--last-of-type, var(--vertical-body-disply, none)); grid-row-start: var(--body-top, 2); grid-column: span var(--steps-count); margin-inline-start: calc(var(--body-indent--vertical) / 2); @@ -188,7 +189,12 @@ } [part='content'] { - display: var(--horizontal-content-disply, none); + overflow: hidden; + pointer-events: none; + + &[aria-hidden="true"] { + height: 0; + } } [part~='top'] { @@ -383,6 +389,7 @@ :host([active]) { [part~='body'] { display: var(--vertical-body-disply, block); + height: auto; } [part~='body bottom'] { @@ -392,8 +399,11 @@ } [part='content'] { - display: var(--horizontal-content-disply, block); pointer-events: all; + + &[aria-hidden="false"] { + height: auto; + } } [part~='indicator'] { diff --git a/src/components/stepper/themes/stepper/stepper.base.scss b/src/components/stepper/themes/stepper/stepper.base.scss index ed821ef03..94e378492 100644 --- a/src/components/stepper/themes/stepper/stepper.base.scss +++ b/src/components/stepper/themes/stepper/stepper.base.scss @@ -77,9 +77,18 @@ --horizontal-separator-display--first-of-type: none; } +:host([orientation='vertical']) { + --vertical-body-disply: auto; + + ::slotted(igc-step:last-of-type) { + --vertical-body-disply--last-of-type: 0; + } +} + // LAST STEP ::slotted(igc-step:last-of-type) { - --vertical-body-disply--last-of-type: none; + // --vertical-body-disply--last-of-type: none; + // --vertical-body-disply--last-of-type: 0; --horizontal-separator-display--last-of-type: none; } @@ -115,7 +124,8 @@ // STEPPER [VERTICAL] :host(:not([orientation='horizontal'])) { --vertical-header-z-index: 2; - --vertical-body-disply: block; + // --vertical-body-disply: block; + // --vertical-body-disply: auto; --header-width-vertical: 100%; --hide-horizontal-separator: none; From 9c5f0501ad9356c784f5ff95dbe93cb8b14a2e9a Mon Sep 17 00:00:00 2001 From: Silvia Ivanova <59446295+SisIvanova@users.noreply.github.com> Date: Tue, 18 Jul 2023 01:05:19 +0300 Subject: [PATCH 08/13] Update dialog.ts --- src/components/dialog/dialog.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/dialog/dialog.ts b/src/components/dialog/dialog.ts index acc35e2e9..c14263789 100644 --- a/src/components/dialog/dialog.ts +++ b/src/components/dialog/dialog.ts @@ -56,9 +56,6 @@ export default class IgcDialogComponent extends EventEmitterMixin< @query('dialog', true) private dialog!: HTMLDialogElement; - @query('[part~="backdrop"]', true) - private backdrop!: HTMLElement; - /* blazorSuppress */ /** * Whether the dialog should be closed when pressing the 'ESCAPE' button. @@ -176,7 +173,6 @@ export default class IgcDialogComponent extends EventEmitterMixin< } protected async hideWithEvent() { - this.backdrop.setAttribute('aria-hidden', 'true'); if (!this.open) { return; } From 68ce0c6d4fe0fce0e063017a56e720a28a8bda6c Mon Sep 17 00:00:00 2001 From: sivanova Date: Tue, 1 Aug 2023 11:25:38 +0300 Subject: [PATCH 09/13] feat(stepper): implement animations --- src/components/stepper/step.ts | 92 +++++++++++-------- src/components/stepper/stepper.ts | 55 ++++++++++- .../stepper/themes/step/light/step.base.scss | 6 +- .../stepper/themes/stepper/stepper.base.scss | 4 +- stories/stepper.stories.ts | 43 ++++++++- 5 files changed, 156 insertions(+), 44 deletions(-) diff --git a/src/components/stepper/step.ts b/src/components/stepper/step.ts index e69ac0221..5390fa02b 100644 --- a/src/components/stepper/step.ts +++ b/src/components/stepper/step.ts @@ -3,21 +3,17 @@ import { property, query, queryAssignedElements } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; import { watch } from '../common/decorators/watch.js'; import { partNameMap } from '../common/util.js'; -import { - AnimationPlayer, - growVerIn, - growVerOut, - fadeIn, - fadeOut, - EaseOut, -} from '../../animations/index.js'; +import { AnimationPlayer, EaseOut } from '../../animations/index.js'; import { themes } from '../../theming/theming-decorator.js'; import { styles } from './themes/step/light/step.base.css.js'; import { styles as bootstrap } from './themes/step/light/step.bootstrap.css.js'; import { styles as indigo } from './themes/step/light/step.indigo.css.js'; import { styles as fluent } from './themes/step/light/step.fluent.css.js'; import { styles as material } from './themes/step/light/step.material.css.js'; -import { animation } from '../../animations/types.js'; +import { + animation, + AnimationReferenceMetadata, +} from '../../animations/types.js'; /** * The step component is used within the `igc-stepper` element and it holds the content of each step. @@ -56,6 +52,7 @@ export default class IgcStepComponent extends LitElement { /** @private */ public static override styles = styles; private animationPlayer!: AnimationPlayer; + private animationType!: AnimationReferenceMetadata; @queryAssignedElements({ slot: 'title' }) private _titleChildren!: Array; @@ -75,13 +72,6 @@ export default class IgcStepComponent extends LitElement { @query('[part~="content"]') public content!: HTMLElement; - /** - * Determines the stepper animation duration in ms. - * @attr duration - */ - @property({ type: Number, attribute: 'animation-duration' }) - public animationDuration = 320; - /** Gets/sets whether the step is invalid. */ @property({ reflect: true, type: Boolean }) public invalid = false; @@ -128,6 +118,10 @@ export default class IgcStepComponent extends LitElement { @property({ attribute: false }) public orientation: 'horizontal' | 'vertical' = 'horizontal'; + /** @private */ + @property({ attribute: false }) + public animationDuration = 320; + /** @private */ @property({ attribute: false }) public verticalAnimation: 'grow' | 'fade' | 'none' = 'grow'; @@ -167,6 +161,16 @@ export default class IgcStepComponent extends LitElement { { opacity: 1, transform: 'translateX(0)' }, ]; + const growVerInFrames: Keyframe[] = [ + { opacity: 0, height: 0 }, + { opacity: 1, height: 'auto' }, + ]; + + const growVerOutFrames: Keyframe[] = [ + { opacity: 1, height: 'auto' }, + { opacity: 0, height: 0 }, + ]; + const opts: KeyframeAnimationOptions = { duration: this.animationDuration, easing: EaseOut.Quad, @@ -174,54 +178,68 @@ export default class IgcStepComponent extends LitElement { const customSlideInLeft = animation(slideInLeftFrames, opts); const customSlideInRight = animation(slideInRightFrames, opts); - - let customAnimation = + const customSlideAnimation = dir === 'open' ? customSlideInLeft : customSlideInRight; + const customGrowVerIn = animation(growVerInFrames, opts); + const customGrowVerOut = animation(growVerOutFrames, opts); + const customGrowAnimation = + dir === 'open' ? customGrowVerIn : customGrowVerOut; + + const fadeInFrames: Keyframe[] = [{ opacity: 0 }, { opacity: 1 }]; + + const fadeOpts: KeyframeAnimationOptions = { + duration: this.animationDuration, + easing: EaseOut.Sine, + }; + + const customFadeAnimation = animation(fadeInFrames, fadeOpts); + if (this.orientation == 'vertical') { switch (this.verticalAnimation) { - case 'grow': - customAnimation = dir === 'open' ? growVerIn : growVerOut; - break; case 'fade': - customAnimation = dir === 'open' ? fadeIn : fadeOut; + this.animationType = customFadeAnimation; break; case 'none': - this.animationDuration = 0; - break; + return; + default: + this.animationType = customGrowAnimation; } } if (this.orientation == 'horizontal') { switch (this.horizontalAnimation) { - case 'slide': - customAnimation = - dir === 'open' ? customSlideInLeft : customSlideInRight; - break; case 'fade': - customAnimation = dir === 'open' ? fadeIn : fadeOut; + this.animationType = customFadeAnimation; break; case 'none': - this.animationDuration = 0; - break; + return; + default: + this.animationType = customSlideAnimation; } } const [_, event] = await Promise.all([ this.animationPlayer.stopAll(), - this.animationPlayer.play(customAnimation), + this.animationPlayer.play(this.animationType), ]); return event.type === 'finish'; } @watch('active', { waitUntilFirstUpdate: true }) - protected activeChange() { + protected async activeChange() { if (this.active) { - this.toggleAnimation('open'); - this.dispatchEvent( - new CustomEvent('stepActiveChanged', { bubbles: true, detail: false }) - ); + if ( + (await this.toggleAnimation('open')) || + this.verticalAnimation == 'none' || + this.horizontalAnimation == 'none' + ) { + this.dispatchEvent( + new CustomEvent('stepActiveChanged', { bubbles: true, detail: false }) + ); + console.log('ready'); + } } } diff --git a/src/components/stepper/stepper.ts b/src/components/stepper/stepper.ts index b4bef7679..6fa5ffa31 100644 --- a/src/components/stepper/stepper.ts +++ b/src/components/stepper/stepper.ts @@ -104,11 +104,37 @@ export default class IgcStepperComponent extends EventEmitterMixin< * @remarks * The default value is undefined. * When the stepper is horizontally orientated the title is positioned below the indicator. - * When the stepper is horizontally orientated the title is positioned on the right side of the indicator. + * When the stepper is vertically orientated the title is positioned on the right side of the indicator. */ @property({ reflect: false, attribute: 'title-position' }) public titlePosition?: 'bottom' | 'top' | 'end' | 'start'; + /** + * Determines the stepper animation duration in ms. + * @remarks + * Default value is `320ms`. + */ + @property({ type: Number, attribute: 'animation-duration' }) + public animationDuration = 320; + + /** + * Get/Set the vertical animation of the stepper. + * + * @remarks + * Default value is `grow`. + */ + @property({ attribute: false }) + public verticalAnimation: 'grow' | 'fade' | 'none' = 'grow'; + + /** + * Get/Set the horizontal animation of the stepper. + * + * @remarks + * Default value is `slide`. + */ + @property({ attribute: false }) + public horizontalAnimation: 'slide' | 'fade' | 'none' = 'slide'; + @watch('orientation', { waitUntilFirstUpdate: true }) protected orientationChange(): void { this.setAttribute('aria-orientation', this.orientation); @@ -117,6 +143,30 @@ export default class IgcStepperComponent extends EventEmitterMixin< ); } + @watch('animationDuration', { waitUntilFirstUpdate: true }) + protected animationDurationChange(): void { + this.steps.forEach( + (step: IgcStepComponent) => + (step.animationDuration = this.animationDuration) + ); + } + + @watch('verticalAnimation', { waitUntilFirstUpdate: true }) + protected verticalAnimationChange(): void { + this.steps.forEach( + (step: IgcStepComponent) => + (step.verticalAnimation = this.verticalAnimation) + ); + } + + @watch('horizontalAnimation', { waitUntilFirstUpdate: true }) + protected horizontalAnimationChange(): void { + this.steps.forEach( + (step: IgcStepComponent) => + (step.horizontalAnimation = this.horizontalAnimation) + ); + } + @watch('stepType', { waitUntilFirstUpdate: true }) protected stepTypeChange(): void { this.steps.forEach( @@ -382,6 +432,9 @@ export default class IgcStepperComponent extends EventEmitterMixin< private syncProperties(): void { this.steps.forEach((step: IgcStepComponent, index: number) => { step.orientation = this.orientation; + step.animationDuration = this.animationDuration; + step.horizontalAnimation = this.horizontalAnimation; + step.verticalAnimation = this.verticalAnimation; step.stepType = this.stepType; step.titlePosition = this.titlePosition; step.contentTop = this.contentTop; diff --git a/src/components/stepper/themes/step/light/step.base.scss b/src/components/stepper/themes/step/light/step.base.scss index f98740352..f12d9bf1c 100644 --- a/src/components/stepper/themes/step/light/step.base.scss +++ b/src/components/stepper/themes/step/light/step.base.scss @@ -197,9 +197,9 @@ height: 0; } - display: var(--horizontal-content-disply, none); - height: 100%; - overflow: auto; + // display: var(--horizontal-content-disply, none); + // height: 100%; + // overflow: auto; } [part~='top'] { diff --git a/src/components/stepper/themes/stepper/stepper.base.scss b/src/components/stepper/themes/stepper/stepper.base.scss index bc5ad5daf..53176ca2b 100644 --- a/src/components/stepper/themes/stepper/stepper.base.scss +++ b/src/components/stepper/themes/stepper/stepper.base.scss @@ -87,7 +87,7 @@ // LAST STEP ::slotted(igc-step:last-of-type) { - // --vertical-body-disply--last-of-type: none; + --vertical-body-disply--last-of-type: none; // --vertical-body-disply--last-of-type: 0; --horizontal-separator-display--last-of-type: none; } @@ -124,7 +124,7 @@ // STEPPER [VERTICAL] :host(:not([orientation='horizontal'])) { --vertical-header-z-index: 2; - // --vertical-body-disply: block; + --vertical-body-disply: block; // --vertical-body-disply: auto; --header-width-vertical: 100%; --hide-horizontal-separator: none; diff --git a/stories/stepper.stories.ts b/stories/stepper.stories.ts index d063606ba..20c70c457 100644 --- a/stories/stepper.stories.ts +++ b/stories/stepper.stories.ts @@ -54,12 +54,35 @@ const metadata: Meta = { options: ['start', 'end', 'top', 'bottom', 'undefined'], control: { type: 'select' }, }, + animationDuration: { + type: 'number', + description: 'Determines the stepper animation duration in ms.', + control: 'number', + defaultValue: 320, + }, + verticalAnimation: { + type: '"none" | "grow" | "fade"', + description: 'Get/Set the vertical animation of the stepper.', + options: ['none', 'grow', 'fade'], + control: { type: 'inline-radio' }, + defaultValue: 'grow', + }, + horizontalAnimation: { + type: '"none" | "fade" | "slide"', + description: 'Get/Set the horizontal animation of the stepper.', + options: ['none', 'fade', 'slide'], + control: { type: 'inline-radio' }, + defaultValue: 'slide', + }, }, args: { orientation: 'horizontal', stepType: 'full', linear: false, contentTop: false, + animationDuration: 320, + verticalAnimation: 'grow', + horizontalAnimation: 'slide', }, }; @@ -76,12 +99,27 @@ interface IgcStepperArgs { contentTop: boolean; /** Get/Set the position of the steps title. */ titlePosition: 'start' | 'end' | 'top' | 'bottom' | undefined; + /** Determines the stepper animation duration in ms. */ + animationDuration: number; + /** Get/Set the vertical animation of the stepper. */ + verticalAnimation: 'none' | 'grow' | 'fade'; + /** Get/Set the horizontal animation of the stepper. */ + horizontalAnimation: 'none' | 'fade' | 'slide'; } type Story = StoryObj; // endregion const BasicTemplate = ( - { orientation, stepType, titlePosition, linear, contentTop }: IgcStepperArgs, + { + orientation, + stepType, + titlePosition, + linear, + contentTop, + animationDuration, + horizontalAnimation, + verticalAnimation, + }: IgcStepperArgs, { globals: { direction } }: Context ) => { const next = () => { @@ -105,6 +143,9 @@ const BasicTemplate = ( .linear=${linear} .contentTop=${contentTop} .dir=${direction} + .animationDuration=${animationDuration} + .horizontalAnimation=${horizontalAnimation} + .verticalAnimation=${verticalAnimation} > Step1 From a585001647b705d3c75a84d030f94743d1ff28d3 Mon Sep 17 00:00:00 2001 From: sivanova Date: Tue, 1 Aug 2023 11:50:44 +0300 Subject: [PATCH 10/13] fix(stepper): lint errors --- src/components/stepper/themes/step/light/step.base.scss | 9 ++------- src/components/stepper/themes/stepper/stepper.base.scss | 2 -- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/components/stepper/themes/step/light/step.base.scss b/src/components/stepper/themes/step/light/step.base.scss index f12d9bf1c..3ed7bd455 100644 --- a/src/components/stepper/themes/step/light/step.base.scss +++ b/src/components/stepper/themes/step/light/step.base.scss @@ -159,7 +159,6 @@ [part~='body'] { display: var(--vertical-body-disply--last-of-type, var(--vertical-body-disply, none)); - // height: var(--vertical-body-disply--last-of-type, var(--vertical-body-disply, none)); grid-row-start: var(--body-top, 2); grid-column: span var(--steps-count); margin-inline-start: calc(var(--body-indent--vertical) / 2); @@ -193,13 +192,9 @@ overflow: hidden; pointer-events: none; - &[aria-hidden="true"] { + &[aria-hidden='true'] { height: 0; } - - // display: var(--horizontal-content-disply, none); - // height: 100%; - // overflow: auto; } [part~='top'] { @@ -405,7 +400,7 @@ [part='content'] { pointer-events: all; - &[aria-hidden="false"] { + &[aria-hidden='false'] { height: auto; } } diff --git a/src/components/stepper/themes/stepper/stepper.base.scss b/src/components/stepper/themes/stepper/stepper.base.scss index 53176ca2b..144dffd9b 100644 --- a/src/components/stepper/themes/stepper/stepper.base.scss +++ b/src/components/stepper/themes/stepper/stepper.base.scss @@ -88,7 +88,6 @@ // LAST STEP ::slotted(igc-step:last-of-type) { --vertical-body-disply--last-of-type: none; - // --vertical-body-disply--last-of-type: 0; --horizontal-separator-display--last-of-type: none; } @@ -125,7 +124,6 @@ :host(:not([orientation='horizontal'])) { --vertical-header-z-index: 2; --vertical-body-disply: block; - // --vertical-body-disply: auto; --header-width-vertical: 100%; --hide-horizontal-separator: none; From 8a1c8dc6aa1e6c50b150fc95735da10c4ed7b94c Mon Sep 17 00:00:00 2001 From: Silvia Ivanova <59446295+SisIvanova@users.noreply.github.com> Date: Tue, 1 Aug 2023 11:59:54 +0300 Subject: [PATCH 11/13] Update step.ts --- src/components/stepper/step.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/stepper/step.ts b/src/components/stepper/step.ts index 5390fa02b..982f26a30 100644 --- a/src/components/stepper/step.ts +++ b/src/components/stepper/step.ts @@ -238,7 +238,6 @@ export default class IgcStepComponent extends LitElement { this.dispatchEvent( new CustomEvent('stepActiveChanged', { bubbles: true, detail: false }) ); - console.log('ready'); } } } From 04ea794d1f071b9604a65ef6cdea65d7728d125d Mon Sep 17 00:00:00 2001 From: sivanova Date: Tue, 8 Aug 2023 15:58:29 +0300 Subject: [PATCH 12/13] feat(stepper): improve animatios --- src/components/stepper/step.ts | 24 +++++++++--------------- src/components/stepper/stepper.ts | 23 ++++++++++++++++++++++- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/components/stepper/step.ts b/src/components/stepper/step.ts index 982f26a30..0385c9b92 100644 --- a/src/components/stepper/step.ts +++ b/src/components/stepper/step.ts @@ -150,15 +150,15 @@ export default class IgcStepComponent extends LitElement { this.animationPlayer = new AnimationPlayer(this.content); } - private async toggleAnimation(dir: 'open' | 'close') { + public async toggleAnimation(dir: 'open' | 'close') { const slideInLeftFrames: Keyframe[] = [ - { opacity: 0, transform: 'translateX(-500px)' }, - { opacity: 1, transform: 'translateX(0)' }, + { opacity: 1, transform: 'translateX(-100%)' }, + { opacity: 1, transform: 'translateX(0%)' }, ]; const slideInRightFrames: Keyframe[] = [ - { opacity: 0, transform: 'translateX(500px)' }, - { opacity: 1, transform: 'translateX(0)' }, + { opacity: 1, transform: 'translateX(0%)' }, + { opacity: 1, transform: 'translateX(-100%)' }, ]; const growVerInFrames: Keyframe[] = [ @@ -179,7 +179,7 @@ export default class IgcStepComponent extends LitElement { const customSlideInLeft = animation(slideInLeftFrames, opts); const customSlideInRight = animation(slideInRightFrames, opts); const customSlideAnimation = - dir === 'open' ? customSlideInLeft : customSlideInRight; + dir === 'open' ? customSlideInRight : customSlideInLeft; const customGrowVerIn = animation(growVerInFrames, opts); const customGrowVerOut = animation(growVerOutFrames, opts); @@ -230,15 +230,9 @@ export default class IgcStepComponent extends LitElement { @watch('active', { waitUntilFirstUpdate: true }) protected async activeChange() { if (this.active) { - if ( - (await this.toggleAnimation('open')) || - this.verticalAnimation == 'none' || - this.horizontalAnimation == 'none' - ) { - this.dispatchEvent( - new CustomEvent('stepActiveChanged', { bubbles: true, detail: false }) - ); - } + this.dispatchEvent( + new CustomEvent('stepActiveChanged', { bubbles: true, detail: false }) + ); } } diff --git a/src/components/stepper/stepper.ts b/src/components/stepper/stepper.ts index 6fa5ffa31..06a97481a 100644 --- a/src/components/stepper/stepper.ts +++ b/src/components/stepper/stepper.ts @@ -256,7 +256,7 @@ export default class IgcStepperComponent extends EventEmitterMixin< } } - private activateStep(step: IgcStepComponent, shouldEmit = true) { + private async activateStep(step: IgcStepComponent, shouldEmit = true) { if (step === this.activeStep) { return; } @@ -271,6 +271,27 @@ export default class IgcStepperComponent extends EventEmitterMixin< cancelable: true, }; + if ( + this.orientation == 'horizontal' && + this.horizontalAnimation == 'slide' + ) { + if (step.index < this.activeStep.index) { + await this.activeStep.toggleAnimation('close'); + step.toggleAnimation('close'); + } else { + await this.activeStep.toggleAnimation('open'); + step.toggleAnimation('open'); + } + } else if ( + this.orientation == 'vertical' && + this.verticalAnimation == 'grow' + ) { + this.activeStep.toggleAnimation('close'); + step.toggleAnimation('open'); + } else { + step.toggleAnimation('open'); + } + const allowed = this.emitEvent('igcActiveStepChanging', args); if (!allowed) { From 899b234addf2118f4d3f158d074abea4f15fd831 Mon Sep 17 00:00:00 2001 From: sivanova Date: Fri, 11 Aug 2023 16:02:20 +0300 Subject: [PATCH 13/13] refactor(stepper): animations --- src/components/stepper/step.ts | 8 ++++++-- src/components/stepper/stepper.ts | 20 +++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/components/stepper/step.ts b/src/components/stepper/step.ts index 0385c9b92..ae2ac433c 100644 --- a/src/components/stepper/step.ts +++ b/src/components/stepper/step.ts @@ -53,6 +53,7 @@ export default class IgcStepComponent extends LitElement { public static override styles = styles; private animationPlayer!: AnimationPlayer; private animationType!: AnimationReferenceMetadata; + public direction = -1; @queryAssignedElements({ slot: 'title' }) private _titleChildren!: Array; @@ -151,14 +152,17 @@ export default class IgcStepComponent extends LitElement { } public async toggleAnimation(dir: 'open' | 'close') { + const position = 100 * this.direction; + // const endPosition = 100 * this.direction; + const slideInLeftFrames: Keyframe[] = [ - { opacity: 1, transform: 'translateX(-100%)' }, + { opacity: 1, transform: `translateX(${position}%)` }, { opacity: 1, transform: 'translateX(0%)' }, ]; const slideInRightFrames: Keyframe[] = [ { opacity: 1, transform: 'translateX(0%)' }, - { opacity: 1, transform: 'translateX(-100%)' }, + { opacity: 1, transform: `translateX(${position}%)` }, ]; const growVerInFrames: Keyframe[] = [ diff --git a/src/components/stepper/stepper.ts b/src/components/stepper/stepper.ts index 06a97481a..946628677 100644 --- a/src/components/stepper/stepper.ts +++ b/src/components/stepper/stepper.ts @@ -276,11 +276,25 @@ export default class IgcStepperComponent extends EventEmitterMixin< this.horizontalAnimation == 'slide' ) { if (step.index < this.activeStep.index) { - await this.activeStep.toggleAnimation('close'); + // this.activeStep.direction = 1; + // this.activeStep.toggleAnimation('open'); step.toggleAnimation('close'); + console.log( + 'previous step', + `next: ${step.index}`, + `active: ${this.activeStep.index}` + ); + // this.activeStep.direction = -1; } else { - await this.activeStep.toggleAnimation('open'); - step.toggleAnimation('open'); + this.activeStep.toggleAnimation('open'); // right to left + console.log( + 'next step', + `next: ${step.index}`, + `active: ${this.activeStep.index}` + ); + // step.direction = 1; + await step.toggleAnimation('close'); + // step.direction = -1; } } else if ( this.orientation == 'vertical' &&