diff --git a/src/app/shared/motion/_reveal.scss b/src/app/shared/motion/_reveal.scss index 902e1a9..47b0863 100644 --- a/src/app/shared/motion/_reveal.scss +++ b/src/app/shared/motion/_reveal.scss @@ -1,10 +1,11 @@ @mixin reveal-target { + transition: + opacity var(--duration-base) var(--ease-standard), + transform var(--duration-base) var(--ease-standard); + &.reveal-pending { opacity: 0; transform: translateY(0.5rem); - transition: - opacity var(--duration-base) var(--ease-standard), - transform var(--duration-base) var(--ease-standard); } &.is-revealed { @@ -13,6 +14,7 @@ } @media (prefers-reduced-motion: reduce) { + &, &.reveal-pending, &.is-revealed { opacity: 1; diff --git a/src/app/shared/motion/reveal.styles.spec.ts b/src/app/shared/motion/reveal.styles.spec.ts new file mode 100644 index 0000000..3b68fc2 --- /dev/null +++ b/src/app/shared/motion/reveal.styles.spec.ts @@ -0,0 +1,36 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const REVEAL_SCSS = readFileSync(join(process.cwd(), 'src/app/shared/motion/_reveal.scss'), 'utf8'); + +function mixinBody(source: string): string { + const start = source.indexOf('@mixin reveal-target'); + expect(start).toBeGreaterThan(-1); + return source.slice(start); +} + +describe('reveal-target mixin', () => { + it('keeps transition on the base selector so the revealed state can animate', () => { + const mixin = mixinBody(REVEAL_SCSS); + const transitionIndex = mixin.search(/transition\s*:/); + const pendingIndex = mixin.indexOf('&.reveal-pending'); + const revealedIndex = mixin.indexOf('&.is-revealed'); + + expect(transitionIndex).toBeGreaterThan(-1); + expect(pendingIndex).toBeGreaterThan(-1); + expect(revealedIndex).toBeGreaterThan(pendingIndex); + expect(transitionIndex).toBeLessThan(pendingIndex); + + const pendingBlock = mixin.match(/&\.reveal-pending\s*\{([^}]*)\}/)?.[1] ?? ''; + expect(pendingBlock).toMatch(/opacity\s*:/); + expect(pendingBlock).not.toMatch(/transition\s*:/); + + const revealedBlock = mixin.match(/&\.is-revealed\s*\{([^}]*)\}/)?.[1] ?? ''; + expect(revealedBlock).toMatch(/opacity\s*:\s*1/); + expect(revealedBlock).not.toMatch(/transition\s*:/); + + expect(mixin).toMatch(/prefers-reduced-motion:\s*reduce/); + expect(mixin).toMatch(/&\s*,\s*&\.reveal-pending/); + expect(mixin).toMatch(/&\.is-revealed\s*\{[\s\S]*transition:\s*none/); + }); +});