// Behaviour probe for PR #3890 (SideNav hidden collapse) at head 829da488. const {openStory, row} = require(process.env.HOME + '/astryx/probe-kit/lib.cjs'); const PORT = 6157; const STORY = 'core-sidenav--hidden-collapse'; // Instrument BEFORE the collapse: record every activeElement transition and the // moment `inert` appears, so a focus that touches cannot hide inside a // single after-the-fact read. async function instrument(page) { await page.evaluate(() => { const nav = document.querySelector('nav[role="navigation"]'); window.__log = {focus: [], inertAt: null, activeAtInert: null, t0: performance.now()}; const name = el => el == null ? 'null' : el === document.body ? 'BODY' : `${el.tagName.toLowerCase()}${el.getAttribute('aria-label') ? `[${el.getAttribute('aria-label')}]` : ''}${nav && nav.contains(el) ? ' (in nav)' : ' (outside nav)'}`; window.__name = name; document.addEventListener( 'focusin', e => window.__log.focus.push({t: Math.round(performance.now() - window.__log.t0), el: name(e.target)}), true, ); document.addEventListener( 'focusout', e => window.__log.focus.push({ t: Math.round(performance.now() - window.__log.t0), el: `OUT ${name(e.target)} -> ${name(e.relatedTarget)}`, }), true, ); new MutationObserver(muts => { for (const m of muts) { if (m.attributeName === 'inert' && nav.hasAttribute('inert') && window.__log.inertAt == null) { window.__log.inertAt = Math.round(performance.now() - window.__log.t0); window.__log.activeAtInert = name(document.activeElement); } } }).observe(nav, {attributes: true, attributeFilter: ['inert']}); }); } async function animationInventory(page) { return page.evaluate(() => document.getAnimations().map(a => { const t = a.effect?.target; const timing = a.effect?.getTiming?.() ?? {}; return { kind: a.constructor.name, property: a.transitionProperty ?? a.animationName ?? '?', target: t?.tagName?.toLowerCase() + (t?.getAttribute?.('role') ? `[${t.getAttribute('role')}]` : ''), duration: timing.duration, delay: timing.delay, playState: a.playState, }; }), ); } async function run({rtl = false, reducedMotion = 'no-preference'} = {}) { const {browser, page} = await openStory('chromium', STORY, { port: PORT, rtl, viewport: {width: 1000, height: 600}, ready: () => document.querySelector('nav[role="navigation"]') != null, launch: {headless: false}, }); if (reducedMotion !== 'no-preference') { await page.emulateMedia({reducedMotion}); await page.waitForTimeout(150); } const label = `${rtl ? 'RTL' : 'LTR'}/${reducedMotion}`; // Geometry before, including the constrained sibling that shares the flex row. const before = await page.evaluate(() => { const nav = document.querySelector('nav[role="navigation"]'); const layer = nav.firstElementChild; const sibling = nav.parentElement.children[1]; return { navWidth: nav.getBoundingClientRect().width, navX: Math.round(nav.getBoundingClientRect().x), layerTransform: getComputedStyle(layer).transform, layerX: Math.round(layer.getBoundingClientRect().x), siblingX: Math.round(sibling.getBoundingClientRect().x), siblingWidth: Math.round(sibling.getBoundingClientRect().width), inert: nav.hasAttribute('inert'), navChildren: nav.children.length, layerIsSlide: getComputedStyle(layer).transitionProperty, }; }); row(`${label} before`, before); await instrument(page); // Put focus on a link INSIDE the nav, then collapse without a pointer/keyboard // event on the toggle — element.click() does not move focus, so this is the // app-driven collapse the prior review asked about. const focusedInside = await page.evaluate(() => { const nav = document.querySelector('nav[role="navigation"]'); const link = nav.querySelector('a[href]'); link.focus(); return {focused: document.activeElement === link, label: link.textContent.trim()}; }); row(`${label} focus-in`, focusedInside); await page.evaluate(() => { const nav = document.querySelector('nav[role="navigation"]'); const toggle = [...document.querySelectorAll('button')].find( b => !nav.contains(b) && /sidebar/i.test(b.getAttribute('aria-label') || ''), ); if (!toggle) throw new Error('no outside collapse toggle found'); window.__toggleLabel = toggle.getAttribute('aria-label'); window.__log.t0 = performance.now(); toggle.click(); }); // Sample the animation inventory the instant the transitions are created. await page.waitForTimeout(8); const anims = await animationInventory(page); console.log(`${label} animations:`, JSON.stringify(anims, null, 1)); // Sample geometry across the slide. const samples = []; for (const at of [0, 40, 80, 124, 130, 260]) { await page.evaluate( ms => new Promise(r => { const wait = () => (performance.now() - window.__log.t0 >= ms ? r() : requestAnimationFrame(wait)); wait(); }), at, ); samples.push( await page.evaluate(at => { const nav = document.querySelector('nav[role="navigation"]'); const layer = nav.firstElementChild; const sibling = nav.parentElement.children[1]; return { at, t: Math.round(performance.now() - window.__log.t0), navW: Math.round(nav.getBoundingClientRect().width), layerX: Math.round(layer.getBoundingClientRect().x), siblingX: Math.round(sibling.getBoundingClientRect().x), siblingW: Math.round(sibling.getBoundingClientRect().width), inert: nav.hasAttribute('inert'), active: window.__name(document.activeElement), }; }, at), ); } console.table(samples); const log = await page.evaluate(() => window.__log); console.log(`${label} focus log:`, JSON.stringify(log, null, 1)); // Now expand again and check focus/inert come back. await page.evaluate(() => { const nav = document.querySelector('nav[role="navigation"]'); const toggle = [...document.querySelectorAll('button')].find( b => !nav.contains(b) && /sidebar/i.test(b.getAttribute('aria-label') || ''), ); toggle.click(); }); await page.waitForTimeout(300); const after = await page.evaluate(() => { const nav = document.querySelector('nav[role="navigation"]'); const link = nav.querySelector('a[href]'); return { navW: Math.round(nav.getBoundingClientRect().width), inert: nav.hasAttribute('inert'), linkTabbable: link.tabIndex >= 0 && !nav.hasAttribute('inert'), active: window.__name(document.activeElement), }; }); row(`${label} re-expanded`, after); await browser.close(); } (async () => { await run({}); await run({rtl: true}); await run({reducedMotion: 'reduce'}); })().catch(e => { console.error('PROBE FAILED', e); process.exit(1); });