refactor(storybook): hide page headers and preserve accessibility

StoryPageHeader no longer shows the page title, kicker, description or the SW group notice; it renders only a visually hidden H1 with the Story title, so the preview keeps a page heading for screen readers while the first section starts at the top gutter (unchanged position).

- OrgChart verify-p8 / p9 / p9-1 / p9-2-composition wait for the first visible StoryPage content block and read the page H1 instead of .sb-page-title; existing expectations are unchanged.
- SW overview browser test clicks the page gutter as its outside-click target.
- Drop the unused .sb-page-description from the SW Storybook hide rules.
- Update the Storybook page frame rule in the project guideline.
This commit is contained in:
shkim4 committed 2026-11-11 15:50:39 +09:00
1 parent f22428548a
commit 63b3ab010b
11 files changed
+59 -105

No files matched your search

+12 -11
View File
@@ -46,8 +46,11 @@ page.setDefaultTimeout(20000);
async function settle(target = page) {
await target.evaluate(async () => { await document.fonts.ready; for (let i = 0; i < 5; i++) await new Promise(resolve => requestAnimationFrame(resolve)); });
}
// The Storybook page title is a visually hidden H1 (2026-11-11), so readiness waits for the first visible content block
// of the StoryPage frame, and page identity reads the H1 text (the Story's accessible page heading).
const pageContent = '.sb-page-inner > :not(h1)', pageHeading = '.sb-page-inner > h1';
async function ready(expected) {
await page.locator('.sb-page-title').waitFor();
await page.locator(pageContent).first().waitFor();
await page.waitForFunction(expected => {
const hosts = [...document.querySelectorAll('.hm-orgchart-host,.hm-orgchart-component-host')];
if (!hosts.length) return false;
@@ -76,10 +79,8 @@ async function commonStyle() {
const properties = {
'.sb-page': ['fontFamily', 'color', 'paddingTop', 'paddingBottom', 'boxSizing', 'width'],
'.sb-page-inner': ['width', 'boxSizing'],
'.sb-page-header': ['paddingBottom', 'borderBottomWidth', 'borderBottomColor', 'fontSize', 'lineHeight'],
'.sb-page-kicker': ['fontSize', 'fontWeight', 'color', 'marginBottom'],
'.sb-page-title': ['fontSize', 'fontWeight', 'lineHeight', 'marginTop', 'marginBottom'],
'.sb-page-description': ['fontSize', 'fontWeight', 'lineHeight', 'marginBottom'],
// The visible header (kicker, title, description) is no longer rendered; the page H1 stays visually hidden.
'.sb-page-inner > h1': ['position', 'width', 'height', 'overflow', 'clipPath'],
'.sb-page-section': ['marginTop', 'scrollMarginTop'],
'.sb-page-section-header': ['paddingBottom', 'marginBottom', 'borderBottomWidth', 'borderBottomColor'],
'.sb-page-section-title': ['fontSize', 'lineHeight', 'fontWeight', 'marginTop', 'marginBottom'],
@@ -105,7 +106,7 @@ async function pageGeometry() {
return page.evaluate(() => {
const rect = element => { if (!element) return null; const r = element.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height, right: r.right, bottom: r.bottom }; };
const documentGeometry = { scrollWidth: document.documentElement.scrollWidth, clientWidth: document.documentElement.clientWidth, innerWidth, scrollHeight: document.documentElement.scrollHeight, clientHeight: document.documentElement.clientHeight };
const frame = { page: rect(document.querySelector('.sb-page')), inner: rect(document.querySelector('.sb-page-inner')), title: document.querySelector('.sb-page-title')?.textContent };
const frame = { page: rect(document.querySelector('.sb-page')), inner: rect(document.querySelector('.sb-page-inner')), title: document.querySelector('.sb-page-inner > h1')?.textContent };
const shadow = document.querySelector('.hm-orgchart-host')?.shadowRoot;
const wrapper = shadow?.querySelector('.hm-orgchart-composition');
const stage = shadow?.querySelector('.hm-orgchart-stage');
@@ -159,10 +160,10 @@ try {
await manager.waitForFunction(() => {
const preview = document.querySelector('#storybook-preview-iframe')?.contentDocument;
const hosts = [...preview?.querySelectorAll('.hm-orgchart-component-host') ?? []];
return preview?.querySelector('.sb-page-title')?.textContent === 'Overview' && hosts.length > 0
return preview?.querySelector('.sb-page-inner > h1')?.textContent === 'Overview' && hosts.length > 0
&& hosts.every(host => host.shadowRoot && [...host.shadowRoot.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet));
});
await manager.frameLocator('#storybook-preview-iframe').locator('.sb-page-title').evaluate(async element => {
await manager.frameLocator('#storybook-preview-iframe').locator(pageHeading).evaluate(async element => {
const document = element.ownerDocument, view = document.defaultView;
await document.fonts.ready;
for (let i = 0; i < 5; i++) await new Promise(resolve => view.requestAnimationFrame(resolve));
@@ -198,7 +199,7 @@ try {
await gotoStory(story);
const rendered = await page.evaluate(({ selectors }) => {
const hosts = [...document.querySelectorAll('.hm-orgchart-host,.hm-orgchart-component-host')];
return { title: document.querySelector('.sb-page-title')?.textContent, hosts: hosts.length,
return { title: document.querySelector('.sb-page-inner > h1')?.textContent, hosts: hosts.length,
specimens: Object.fromEntries(selectors.map(selector => [selector, hosts.reduce((total, host) => total + host.shadowRoot.querySelectorAll(selector).length, 0)])),
employees: hosts.reduce((total, host) => total + host.shadowRoot.querySelectorAll('.hm-orgchart-employee').length, 0),
autoLayout: hosts.find(host => host.classList.contains('hm-orgchart-host'))?.shadowRoot.querySelector('.hm-orgchart-expanded')?.dataset.autoLayout };
@@ -214,7 +215,7 @@ try {
for (const id of patterns) {
await page.setViewportSize({ width: 1440, height: 1100 });
await page.goto(`${origin}/iframe.html?id=${id}&viewMode=story`);
await page.locator('.sb-page-title').waitFor(); await settle();
await page.locator(pageContent).first().waitFor(); await settle();
references.push({ id, style: await commonStyle() });
}
const reference = references[0].style;
@@ -268,7 +269,7 @@ try {
await section('full-repair', async () => {
await page.setViewportSize({ width: 1440, height: 1100 });
await page.goto(`${origin}/iframe.html?id=${patterns[0]}&viewMode=story`);
await page.locator('.sb-page-title').waitFor(); await settle();
await page.locator(pageContent).first().waitFor(); await settle();
const reference = await commonStyle();
await gotoStory(stories[2]);
const style = await commonStyle();
+4 -3
View File
@@ -39,7 +39,8 @@ async function settle() {
async function goto(name, size) {
await page.setViewportSize(size);
await page.goto(`${origin}/iframe.html?id=${stories[name]}&viewMode=story`);
await page.locator('.sb-page-title').waitFor();
// The page title is a visually hidden H1 (2026-11-11): wait for the first visible StoryPage content block instead.
await page.locator('.sb-page-inner > :not(h1)').first().waitFor();
await page.waitForFunction(() => {
const hosts = [...document.querySelectorAll('.hm-orgchart-host,.hm-orgchart-component-host')];
return hosts.length === 1 && hosts.every(host => host.shadowRoot && !host.shadowRoot.querySelector('[aria-busy="true"]') && [...host.shadowRoot.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet));
@@ -86,7 +87,7 @@ async function snapshot() {
const css = getComputedStyle(element);
return { scrollWidth: element.scrollWidth, clientWidth: element.clientWidth, scrollHeight: element.scrollHeight, clientHeight: element.clientHeight, left: element.scrollLeft, top: element.scrollTop, overflowX: css.overflowX, overflowY: css.overflowY };
};
const common = Object.fromEntries(['.sb-page', '.sb-page-inner', '.sb-page-title', '.sb-page-section-title', '.sb-page-section-header', '.sb-page-section-body'].map(selector => [selector, style(document.querySelector(selector))]));
const common = Object.fromEntries(['.sb-page', '.sb-page-inner', '.sb-page-inner > h1', '.sb-page-section-title', '.sb-page-section-header', '.sb-page-section-body'].map(selector => [selector, style(document.querySelector(selector))]));
// Page/section height is content-dependent; typography, borders and gutters are invariant.
for (const value of Object.values(common)) if (value) for (const key of ['height', 'minHeight']) delete value[key];
const documentScroll = { viewportWidth: innerWidth, viewportHeight: innerHeight, root: scroll(document.documentElement), body: scroll(document.body), scrollingElement: scroll(document.scrollingElement) };
@@ -145,7 +146,7 @@ async function snapshot() {
};
}
return {
title: document.querySelector('.sb-page-title')?.textContent, common, documentScroll, departments, unwanted,
title: document.querySelector('.sb-page-inner > h1')?.textContent, common, documentScroll, departments, unwanted,
optionsPresent: [...document.querySelectorAll('button')].filter(visible).some(button => /긴 이름 검증/.test(button.textContent)),
sectionTitles: [...document.querySelectorAll('.sb-page-section-title')].map(element => element.textContent), fixed,
};
@@ -33,7 +33,8 @@ async function settle() {
async function open(size) {
await page.setViewportSize(size);
await page.goto(`${origin}/iframe.html?id=orgchart--full-composition&viewMode=story`);
await page.locator('.sb-page-title').waitFor();
// The page title is a visually hidden H1 (2026-11-11): wait for the first visible StoryPage content block instead.
await page.locator('.sb-page-inner > :not(h1)').first().waitFor();
await page.waitForFunction(() => {
const shadow = document.querySelector('.hm-orgchart-host')?.shadowRoot;
return shadow?.querySelector('.hm-orgchart-stage[data-workdesk-artwork]') && !shadow.querySelector('[aria-busy="true"]') && [...shadow.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet);
+4 -3
View File
@@ -39,7 +39,8 @@ page.setDefaultTimeout(20000);
let expectedHosts = 1;
async function settle() { await page.evaluate(async () => { await document.fonts.ready; for (let i = 0; i < 5; i++) await new Promise(resolve => requestAnimationFrame(resolve)); }); }
async function ready(requirePage = true) {
if (requirePage) await page.locator('.sb-page-title').waitFor();
// The page title is a visually hidden H1 (2026-11-11): wait for the first visible StoryPage content block instead.
if (requirePage) await page.locator('.sb-page-inner > :not(h1)').first().waitFor();
await page.waitForFunction(expected => {
const hosts = [...document.querySelectorAll('.hm-orgchart-host,.hm-orgchart-component-host')];
return hosts.length === expected && hosts.every(host => host.shadowRoot && [...host.shadowRoot.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet) && !host.shadowRoot.querySelector('[aria-busy="true"]') && [...host.shadowRoot.querySelectorAll('img')].every(image => image.complete));
@@ -111,8 +112,8 @@ async function snapshot() {
cards: [...expanded.querySelectorAll('.hm-orgchart-employee')].map(card => { const css = getComputedStyle(card), direction = card.dataset.connectionDirection; return { company: card.dataset.companyId, direction, width: card.offsetWidth, height: card.offsetHeight, border: hex(css[direction === 'left' ? 'borderLeftColor' : direction === 'right' ? 'borderRightColor' : 'borderTopColor']), image: Boolean(card.querySelector('img')) }; }),
connectors: expanded.querySelectorAll('svg,.edge,.lines').length,
} : null;
const common = Object.fromEntries(['.sb-page-title', '.sb-page-description', '.sb-page-section-title', '.sb-page-section-header'].map(selector => [selector, style(document.querySelector(selector))]));
return { title: document.querySelector('.sb-page-title')?.textContent, titles: [...document.querySelectorAll('.sb-page-section-title')].map(element => element.textContent), document: { width: innerWidth, scrollWidth: document.documentElement.scrollWidth, scrollHeight: document.documentElement.scrollHeight, clientHeight: document.documentElement.clientHeight }, common, summary, affiliation, logos, departments, fixed, auto };
const common = Object.fromEntries(['.sb-page-inner > h1', '.sb-page-section-title', '.sb-page-section-header'].map(selector => [selector, style(document.querySelector(selector))]));
return { title: document.querySelector('.sb-page-inner > h1')?.textContent, titles: [...document.querySelectorAll('.sb-page-section-title')].map(element => element.textContent), document: { width: innerWidth, scrollWidth: document.documentElement.scrollWidth, scrollHeight: document.documentElement.scrollHeight, clientHeight: document.documentElement.clientHeight }, common, summary, affiliation, logos, departments, fixed, auto };
});
}
function noHorizontalOverflow(state) { return state.document.scrollWidth <= state.document.width + 1; }