fix(workspace): focus the page title on a route change

A sidebar or history navigation now focuses #page-title, as the concept
does on every hashchange (app.js 1168), so a screen reader announces the
page heading instead of the main region. The first render leaves focus
alone and the skip link still focuses main. The heading module's comment
now describes what the shell does.
This commit is contained in:
Vito Sansevero
2026-10-19 22:48:39 -07:00
parent 69d201d684
commit 64f14a3e59
3 changed files with 24 additions and 3 deletions
+19
View File
@@ -112,6 +112,25 @@ test('skip to content focuses the current view without changing its route', asyn
await expect(page.locator('#workspace-main h1')).toHaveText('Prompt library');
});
// The concept focuses its page title on every hashchange navigation (docs/concepts/workspace/app.js 1168), so a screen
// reader announces "<page>, heading level 1"; the first render leaves focus alone.
test('a sidebar route change focuses the page title, as the concept does', async ({ page }) => {
await page.goto(`${workspace}#/library`);
const title = page.locator('#page-title');
await expect(title).toHaveText('Prompt library');
await expect(title).not.toBeFocused();
await sidebarLink(page, 'Images').click();
await expect(title).toHaveText('Images');
await expect(title).toBeFocused();
await expect(page.locator('#workspace-main')).not.toBeFocused();
await sidebarLink(page, 'Tags & categories').click();
await expect(title).toHaveText('Tags & categories');
await expect(title).toBeFocused();
await page.goBack();
await expect(title).toHaveText('Images');
await expect(title).toBeFocused();
});
test('returning to overview refreshes persisted counts without duplicating navigation', async ({ page, request }) => {
await page.goto(`${workspace}#/overview`);
const prompts = statistic(page, 'Prompts');
+2 -1
View File
@@ -18,7 +18,8 @@ function partsOf(heading) {
/**
* Replace `main`'s content with `.heading` > div (`h1#page-title[tabindex=-1]`, `p.muted`) + `.heading-actions`.
* The h1 is the focus target after a save or a route change, as the concept's page title is.
* The h1 is the focus target after a save, and the shell focuses it on every route change after the first, as the
* concept focuses its page title on hashchange (app.js 1168). The skip link focuses `main` instead.
*/
export function renderHeading(main, { title = '', description = '', actions = [] } = {}) {
const heading = element('div', 'heading');
+3 -2
View File
@@ -89,8 +89,9 @@ function mountWorkspace() {
}
document.title = `${routeTitle(route)} · Prompt Manager`;
// The concept's heading(); each view fills its description and actions.
const { description } = renderHeading(main, { title: route.label });
if (!firstRender) main.focus();
// The concept focuses its page title on a hashchange navigation (app.js 1168); the skip link focuses `main`.
const { description, h1 } = renderHeading(main, { title: route.label });
if (!firstRender) h1.focus();
firstRender = false;
document.getElementById('workspace-breadcrumb').textContent = routeTitle(route);
if (route.id === 'images') {