From c02baf75955a377329f3835c7880361b2f61d395 Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Mon, 1 Jun 2026 10:49:50 -0500 Subject: [PATCH 1/2] style: adopt pine motion duration tokens for component transitions --- libs/core/src/components/pds-combobox/pds-combobox.scss | 2 +- .../src/components/pds-filters/pds-filter/pds-filter.scss | 2 +- libs/core/src/components/pds-input/pds-input.scss | 2 +- libs/core/src/components/pds-multiselect/pds-multiselect.scss | 2 +- libs/core/src/components/pds-radio/pds-radio.scss | 2 +- libs/core/src/components/pds-toast/pds-toast.scss | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/libs/core/src/components/pds-combobox/pds-combobox.scss b/libs/core/src/components/pds-combobox/pds-combobox.scss index 379e479a3..de708eedb 100644 --- a/libs/core/src/components/pds-combobox/pds-combobox.scss +++ b/libs/core/src/components/pds-combobox/pds-combobox.scss @@ -39,7 +39,7 @@ flex: 1; font: var(--pine-typography-body-medium); padding: var(--pine-dimension-xs) var(--pine-dimension-450) var(--pine-dimension-xs) var(--pine-dimension-150); - transition: border-color 0.2s ease; + transition: border-color var(--pine-motion-duration-base) ease; width: 100%; &:hover:not(:disabled) { diff --git a/libs/core/src/components/pds-filters/pds-filter/pds-filter.scss b/libs/core/src/components/pds-filters/pds-filter/pds-filter.scss index 405938671..9d04d1c56 100644 --- a/libs/core/src/components/pds-filters/pds-filter/pds-filter.scss +++ b/libs/core/src/components/pds-filters/pds-filter/pds-filter.scss @@ -25,7 +25,7 @@ line-height: var(--pine-line-height-150); padding: var(--pine-dimension-025) var(--pine-dimension-125); position: relative; - transition: all 0.2s ease; + transition: all var(--pine-motion-duration-base) ease; pds-icon { block-size: var(--pine-font-size-100); diff --git a/libs/core/src/components/pds-input/pds-input.scss b/libs/core/src/components/pds-input/pds-input.scss index d2d8d4458..664da6ca7 100644 --- a/libs/core/src/components/pds-input/pds-input.scss +++ b/libs/core/src/components/pds-input/pds-input.scss @@ -240,7 +240,7 @@ min-height: var(--pds-input-field-min-height); min-width: var(--pine-dimension-none); padding: var(--pds-input-padding-y) var(--pds-input-padding-x); - transition: border-color 0.2s ease; + transition: border-color var(--pine-motion-duration-base) ease; width: 100%; &:hover:not(:disabled) { diff --git a/libs/core/src/components/pds-multiselect/pds-multiselect.scss b/libs/core/src/components/pds-multiselect/pds-multiselect.scss index 5cf4c0c20..e288421cb 100644 --- a/libs/core/src/components/pds-multiselect/pds-multiselect.scss +++ b/libs/core/src/components/pds-multiselect/pds-multiselect.scss @@ -46,7 +46,7 @@ padding: var(--pine-dimension-xs) var(--pine-dimension-sm); position: relative; text-align: start; - transition: border-color 0.2s ease, box-shadow 0.2s ease; + transition: border-color var(--pine-motion-duration-base) ease, box-shadow var(--pine-motion-duration-base) ease; width: 100%; &:hover:not(.pds-multiselect__trigger--disabled) { diff --git a/libs/core/src/components/pds-radio/pds-radio.scss b/libs/core/src/components/pds-radio/pds-radio.scss index bacb96163..eae06b266 100644 --- a/libs/core/src/components/pds-radio/pds-radio.scss +++ b/libs/core/src/components/pds-radio/pds-radio.scss @@ -259,7 +259,7 @@ label:has(input:disabled) { justify-content: center; overflow: hidden; position: relative; - transition: all 0.2s ease; + transition: all var(--pine-motion-duration-base) ease; &:hover { border-color: var(--pine-color-border-hover); diff --git a/libs/core/src/components/pds-toast/pds-toast.scss b/libs/core/src/components/pds-toast/pds-toast.scss index e68a52ace..00dfafe09 100644 --- a/libs/core/src/components/pds-toast/pds-toast.scss +++ b/libs/core/src/components/pds-toast/pds-toast.scss @@ -1,6 +1,6 @@ :host { - --animation-duration: 0.3s; - --animation-timing: cubic-bezier(0.4, 0, 0.2, 1); + --animation-duration: var(--pine-motion-duration-slow); + --animation-timing: var(--pine-motion-easing-in-out); --padding-inline: var(--pine-dimension-md); --padding-inline-desktop: var(--pine-dimension-2xl); --sizing-height-default: 68px; From c0ffd3cb22e79965811757ce7e09126869f4d2a8 Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Wed, 3 Jun 2026 08:33:19 -0500 Subject: [PATCH 2/2] fix(pds-toast): align dismiss timer with reduced-motion CSS duration --- .../src/components/pds-toast/pds-toast.tsx | 52 +++++++++++++++++-- .../pds-toast/test/pds-toast.spec.tsx | 46 ++++++++++++++++ 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/libs/core/src/components/pds-toast/pds-toast.tsx b/libs/core/src/components/pds-toast/pds-toast.tsx index 35a139b88..99a40818a 100644 --- a/libs/core/src/components/pds-toast/pds-toast.tsx +++ b/libs/core/src/components/pds-toast/pds-toast.tsx @@ -1,4 +1,7 @@ -import { Component, Event, EventEmitter, h, Host, Method, Prop, State, Watch } from '@stencil/core'; +import { Component, Element, Event, EventEmitter, h, Host, Method, Prop, State, Watch } from '@stencil/core'; + +/** Fallback when computed `--animation-duration` is unavailable (matches `--pine-motion-duration-slow`). */ +const TOAST_DISMISS_ANIMATION_MS = 300; /** * @part dismiss @@ -9,6 +12,8 @@ import { Component, Event, EventEmitter, h, Host, Method, Prop, State, Watch } f shadow: true, }) export class PdsToast { + @Element() el!: HTMLPdsToastElement; + // Props /** * A unique identifier used for the underlying component `id` attribute. @@ -90,8 +95,7 @@ export class PdsToast { // Start the animation out this.isAnimatingOut = true; - // Wait for animation to complete before hiding and cleanup - await new Promise((resolve) => setTimeout(resolve, 300)); // Match --animation-duration + await this.waitForDismissAnimation(); this.isVisible = false; this.cleanup(); @@ -99,6 +103,48 @@ export class PdsToast { } // Private methods + private waitForDismissAnimation(): Promise { + const durationMs = this.getDismissAnimationDurationMs(); + if (durationMs <= 0) { + return Promise.resolve(); + } + return new Promise((resolve) => window.setTimeout(resolve, durationMs)); + } + + private getDismissAnimationDurationMs(): number { + if (typeof window === 'undefined') { + return TOAST_DISMISS_ANIMATION_MS; + } + + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + return 0; + } + + const fromCss = this.parseCssDurationToMs( + getComputedStyle(this.el).getPropertyValue('--animation-duration').trim(), + ); + + return fromCss ?? TOAST_DISMISS_ANIMATION_MS; + } + + private parseCssDurationToMs(value: string): number | undefined { + if (!value) { + return undefined; + } + if (value === '0' || value === '0ms' || value === '0s') { + return 0; + } + if (value.endsWith('ms')) { + const ms = Number.parseFloat(value); + return Number.isFinite(ms) ? ms : undefined; + } + if (value.endsWith('s')) { + const seconds = Number.parseFloat(value); + return Number.isFinite(seconds) ? seconds * 1000 : undefined; + } + return undefined; + } + private cleanup(): void { if (this.dismissTimer) { window.clearTimeout(this.dismissTimer); diff --git a/libs/core/src/components/pds-toast/test/pds-toast.spec.tsx b/libs/core/src/components/pds-toast/test/pds-toast.spec.tsx index a99658d26..da70c14aa 100644 --- a/libs/core/src/components/pds-toast/test/pds-toast.spec.tsx +++ b/libs/core/src/components/pds-toast/test/pds-toast.spec.tsx @@ -169,6 +169,52 @@ describe('pds-toast', () => { expect(dismissSpy).toHaveBeenCalledWith({ componentId: 'test-toast' }); }); + it('should dismiss without exit-animation delay when prefers-reduced-motion is reduce', async () => { + const originalMatchMedia = window.matchMedia; + const setTimeoutSpy = jest.spyOn(window, 'setTimeout'); + + Object.defineProperty(window, 'matchMedia', { + configurable: true, + writable: true, + value: (query: string) => + ({ + matches: query.includes('prefers-reduced-motion'), + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + }) as MediaQueryList, + }); + + try { + const page = await newSpecPage({ + components: [PdsToast], + html: ``, + }); + + const component = page.rootInstance as PdsToast; + + await component.dismiss(); + + const exitAnimationDelays = setTimeoutSpy.mock.calls + .map(([, delay]) => delay) + .filter((delay) => delay === 300); + + expect(exitAnimationDelays).toHaveLength(0); + expect(component.isVisible).toBe(false); + } finally { + setTimeoutSpy.mockRestore(); + Object.defineProperty(window, 'matchMedia', { + configurable: true, + writable: true, + value: originalMatchMedia, + }); + } + }); + // Test for button onClick calling dismiss it('should dismiss when dismiss button is clicked', async () => { const page = await newSpecPage({