const {openStory, row} = require(process.env.HOME + '/astryx/probe-kit/lib.cjs'); const PORT = Number(process.env.SB_PORT || 6553); const STORY = 'hooks-usescrollabletabstop--only-what-can-actually-scroll'; async function rafs(page, count = 3) { await page.evaluate(async count => { for (let i = 0; i < count; i++) { await new Promise(resolve => requestAnimationFrame(resolve)); } }, count); } async function withStory(fn) { const {browser, page, sensorErrors} = await openStory('chromium', STORY, { port: PORT, viewport: {width: 1000, height: 500}, globals: {astryxTheme: 'neutral', colorMode: 'light', direction: 'ltr'}, waitUntil: 'domcontentloaded', launch: {headless: true}, ready: () => { const nodes = [...document.querySelectorAll('[aria-label^="overflow:"]')]; return nodes.length === 3 && nodes[0].getAttribute('tabindex') === '0' && !nodes[1].hasAttribute('tabindex') && !nodes[2].hasAttribute('tabindex'); }, }); try { const result = await fn(page); if (sensorErrors.length) result.sensorErrors = sensorErrors; return result; } finally { await browser.close(); } } (async () => { const transitions = await withStory(async page => { const before = await page.evaluate(() => { const el = document.querySelector('[aria-label="overflow: hidden"]'); return {tabIndex: el.getAttribute('tabindex'), overflow: getComputedStyle(el).overflowY}; }); await page.evaluate(() => { document.querySelector('[aria-label="overflow: hidden"]').style.overflow = 'auto'; }); await rafs(page); const afterStyleOnly = await page.evaluate(() => { const el = document.querySelector('[aria-label="overflow: hidden"]'); return {tabIndex: el.getAttribute('tabindex'), overflow: getComputedStyle(el).overflowY}; }); await page.evaluate(() => { document.querySelector('[aria-label="overflow: hidden"]').style.height = '141px'; }); await rafs(page); const afterResize = await page.evaluate(() => { const el = document.querySelector('[aria-label="overflow: hidden"]'); return {tabIndex: el.getAttribute('tabindex'), overflow: getComputedStyle(el).overflowY}; }); await page.evaluate(() => { document.querySelector('[aria-label="overflow: hidden"]').style.overflow = 'hidden'; }); await rafs(page); const hiddenStyleOnly = await page.evaluate(() => { const el = document.querySelector('[aria-label="overflow: hidden"]'); return {tabIndex: el.getAttribute('tabindex'), overflow: getComputedStyle(el).overflowY}; }); await page.evaluate(() => { document.querySelector('[aria-label="overflow: hidden"]').style.height = '142px'; }); await rafs(page); const hiddenAfterResize = await page.evaluate(() => { const el = document.querySelector('[aria-label="overflow: hidden"]'); return {tabIndex: el.getAttribute('tabindex'), overflow: getComputedStyle(el).overflowY}; }); return {before, afterStyleOnly, afterResize, hiddenStyleOnly, hiddenAfterResize}; }); row('transitions', { before: JSON.stringify(transitions.before), autoNoSignal: JSON.stringify(transitions.afterStyleOnly), autoResize: JSON.stringify(transitions.afterResize), hiddenNoSignal: JSON.stringify(transitions.hiddenStyleOnly), hiddenResize: JSON.stringify(transitions.hiddenAfterResize), }); const focusable = await withStory(async page => { await page.evaluate(() => { const el = document.querySelector('[aria-label="overflow: auto"]'); const before = document.createElement('button'); before.id = 'before-scroll'; before.textContent = 'Before'; const inside = document.createElement('button'); inside.id = 'inside-scroll'; inside.textContent = 'Inside'; el.parentElement.insertBefore(before, el); el.appendChild(inside); }); await rafs(page); const dom = await page.evaluate(() => { const el = document.querySelector('[aria-label="overflow: auto"]'); return {tabIndex: el.getAttribute('tabindex'), inside: !!el.querySelector('button')}; }); await page.locator('#before-scroll').focus(); await page.keyboard.press('Tab'); const first = await page.evaluate(() => ({ id: document.activeElement?.id || null, label: document.activeElement?.getAttribute?.('aria-label') || null, })); await page.keyboard.press('Tab'); const second = await page.evaluate(() => ({ id: document.activeElement?.id || null, label: document.activeElement?.getAttribute?.('aria-label') || null, })); return {dom, first, second}; }); row('focusables', { dom: JSON.stringify(focusable.dom), tab1: JSON.stringify(focusable.first), tab2: JSON.stringify(focusable.second), }); const ownership = await withStory(async page => { return page.evaluate(async () => { const el = document.querySelector('[aria-label="overflow: auto"]'); const initial = el.getAttribute('tabindex'); el.setAttribute('tabindex', '-1'); const consumer = el.getAttribute('tabindex'); el.style.overflow = 'hidden'; el.style.height = '141px'; await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); return {initial, consumer, afterHookMeasure: el.getAttribute('tabindex')}; }); }); row('ownership', ownership); for (const n of [20, 50, 200]) { const scale = await withStory(async page => { return page.evaluate(async n => { const el = document.querySelector('[aria-label="overflow: auto"]'); const original = window.getComputedStyle; let targetStyleReads = 0; window.getComputedStyle = function (node, pseudo) { if (node === el) targetStyleReads++; return original.call(window, node, pseudo); }; const children = []; for (let i = 0; i < n; i++) { const child = document.createElement('div'); child.textContent = String(i); child.style.height = '4px'; children.push(child); } el.replaceChildren(...children); await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); targetStyleReads = 0; for (const child of children) child.style.height = '8px'; await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); window.getComputedStyle = original; return {n, targetStyleReads, tabIndex: el.getAttribute('tabindex')}; }, n); }); row(`scale-${n}`, scale); } })().catch(error => { console.error(error); process.exitCode = 1; });