diff --git a/apps/sandbox/src/app/(sandbox)/pages/motion-lab/LabDemos.tsx b/apps/sandbox/src/app/(sandbox)/pages/motion-lab/LabDemos.tsx index 8738bab87e88b..1162b4dcc981c 100644 --- a/apps/sandbox/src/app/(sandbox)/pages/motion-lab/LabDemos.tsx +++ b/apps/sandbox/src/app/(sandbox)/pages/motion-lab/LabDemos.tsx @@ -40,6 +40,12 @@ import { } from './proposed/TabList'; import {Skeleton} from '@astryxdesign/core/Skeleton'; import {Token} from '@astryxdesign/core/Token'; +import { + TopNav, + TopNavHeading, + TopNavMegaMenu, + TopNavMegaMenuItem, +} from '@astryxdesign/core/TopNav'; import {HStack, VStack} from '@astryxdesign/core/Layout'; import {Text} from '@astryxdesign/core/Text'; import {useMotionLab} from './MotionLabStore'; @@ -897,6 +903,111 @@ export function OriginRig({kind}: {kind: 'zero' | 'centre' | 'anchored'}) { ); } +// --- TopNav mega-menu exit ---------------------------------------------------- + +const MEGA_MENU_LOOP_MS = 3200; +const MEGA_MENU_OPEN_MS = 1200; + +/** + * The real TopNav mega-menu, opened and closed on a loop through its own + * trigger. Before is core as it ships: the panel rises 4px and fades on open + * and close, and does the same under Reduce Motion. After is the same + * component with the missing branch as a sandbox prototype + * (MotionLab.module.css `.megaMenuProposed`): under Reduce Motion the panel + * fades within 200 ms on a touch screen and changes at once with a mouse + * (design:reduced-motion DR2, DR3); the lab's Degrade shows the fade, its + * Delete the cut. + * The two panes open half a loop apart: opening one auto popover closes any + * other, so in step they would close each other. + */ +export function TopNavExitRig({mode}: {mode: Mode}) { + const {reducedMotion} = useMotionLab(); + const rootRef = useRef(null); + const timersRef = useRef([]); + const setOpen = useCallback((isOpen: boolean) => { + const trigger = rootRef.current?.querySelector( + 'button[aria-expanded]', + ); + if (trigger == null || (trigger.ariaExpanded === 'true') === isOpen) { + return; + } + // A pointer-style click (detail 1): `click()` sends detail 0, which the + // menu reads as Enter or Space and answers by moving focus into the + // panel. Focus stays wherever the person left it. + const focused = document.activeElement; + trigger.dispatchEvent( + new MouseEvent('click', { + bubbles: true, + cancelable: true, + detail: 1, + view: window, + }), + ); + const moved = document.activeElement; + if (moved === focused || !(moved instanceof HTMLElement)) { + return; + } + if (focused instanceof HTMLElement && focused !== document.body) { + focused.focus({preventScroll: true}); + } else { + // Nothing had focus: leave nothing focused. + moved.blur(); + } + }, []); + useLoop(() => { + timersRef.current.forEach(window.clearTimeout); + const offset = mode === 'before' ? 0 : MEGA_MENU_LOOP_MS / 2; + timersRef.current = [ + window.setTimeout(() => setOpen(true), offset), + window.setTimeout(() => setOpen(false), offset + MEGA_MENU_OPEN_MS), + ]; + }, MEGA_MENU_LOOP_MS); + useEffect(() => () => timersRef.current.forEach(window.clearTimeout), []); + + return ( + +
+ } + startContent={ + + + + + + } + /> + } + /> +
+ + {mode === 'before' + ? 'An exit that travels, and the proof the pattern works: the panel rises 4px and fades on open and close. Under Reduce Motion it does exactly the same.' + : 'The same exit with the missing branch: under Reduce Motion the panel fades within 200 ms on a touch screen and changes at once with a mouse; the lab\u2019s Degrade shows the fade, Delete the cut. Turn either on to compare.'} + +
+ ); +} + // --- loops ------------------------------------------------------------------- export function LoopRig({policy}: {policy: 'today' | 'delete' | 'degrade'}) { diff --git a/apps/sandbox/src/app/(sandbox)/pages/motion-lab/MotionLab.module.css b/apps/sandbox/src/app/(sandbox)/pages/motion-lab/MotionLab.module.css index 382edee8ac3f5..6a68c91986276 100644 --- a/apps/sandbox/src/app/(sandbox)/pages/motion-lab/MotionLab.module.css +++ b/apps/sandbox/src/app/(sandbox)/pages/motion-lab/MotionLab.module.css @@ -753,6 +753,38 @@ } } +/* ------------------------------------------------- TopNav mega-menu ---- */ + +/* A sandbox prototype of the reduced-motion branch TopNav's mega-menu panel + lacks. Under Reduce Motion it never rises: on a touch screen it only + fades, no longer than the 200 ms cross-fade, and with a mouse it changes + at once (design:reduced-motion DR1-DR3). The panel stays a DOM descendant + in the top layer, so the scope reaches it. The lab's Degrade shows the + fade; Delete cuts. */ +@media (prefers-reduced-motion: reduce) and (pointer: coarse) { + .megaMenuProposed [popover] { + transform: none !important; + transition-duration: min(var(--duration-medium-min), 200ms) !important; + } +} + +@media (prefers-reduced-motion: reduce) and (not (pointer: coarse)) { + .megaMenuProposed [popover] { + transform: none !important; + transition-duration: 0s !important; + } +} + +.megaMenuProposed[data-reduced-motion='degrade'] [popover] { + transform: none !important; + transition-duration: min(var(--duration-medium-min), 200ms) !important; +} + +.megaMenuProposed[data-reduced-motion='delete'] [popover] { + transform: none !important; + transition-duration: 0s !important; +} + /* ---------------------------------------------------------------- tint ---- */ /* The Table hover tint, at whatever duration and curve is being argued over. diff --git a/apps/sandbox/src/app/(sandbox)/pages/motion-lab/previewPlan.ts b/apps/sandbox/src/app/(sandbox)/pages/motion-lab/previewPlan.ts index a46433a4a131d..d87f804111edf 100644 --- a/apps/sandbox/src/app/(sandbox)/pages/motion-lab/previewPlan.ts +++ b/apps/sandbox/src/app/(sandbox)/pages/motion-lab/previewPlan.ts @@ -571,7 +571,7 @@ export const PREVIEW_PLAN: ReadonlyArray = [ motionCost: 'idle', dirs: ['TopNav'], today: 'Mega-menu with a real exit, sub-lists, chevrons.', - gap: 'The only component animating a popover exit, and it never checks reduced motion.', + gap: 'Its panel exit travels, and it never checks reduced motion.', shows: 'Its exit as the existing proof the pattern works, plus the missing reduced-motion branch.', hardCase: @@ -582,8 +582,8 @@ export const PREVIEW_PLAN: ReadonlyArray = [ 'reduced motion', 'traverse speed', ], - status: 'specified', - rig: null, + status: 'built', + rig: 'TopNavExitRig', priority: 1, measurementNote: null, }, diff --git a/apps/sandbox/src/app/(sandbox)/pages/motion-lab/previews/page.tsx b/apps/sandbox/src/app/(sandbox)/pages/motion-lab/previews/page.tsx index 593cf55a52701..6c096bc531f2f 100644 --- a/apps/sandbox/src/app/(sandbox)/pages/motion-lab/previews/page.tsx +++ b/apps/sandbox/src/app/(sandbox)/pages/motion-lab/previews/page.tsx @@ -30,6 +30,7 @@ import { SkeletonSwapRig, StatCountRig, TabIndicatorRig, + TopNavExitRig, } from '../LabDemos'; export default function PreviewsPage() { @@ -196,6 +197,34 @@ export default function PreviewsPage() { + }> + , + }, + { + tone: 'after', + label: 'Proposed — Reduce Motion never rises', + content: , + }, + ]} + /> + + + Both panes are core’s real TopNav. The right one + carries the missing reduced-motion branch as a sandbox prototype in + CSS. Animator retargeting would come with the proposed public{' '} + useAnimator (AST-107, spec PR #8600). + + + +