feat(a11y): fix heading hierarchy across pages

- Home: greeting H2 → H1 (page needs a top-level heading)
- Task detail: task ID H1 → span (only title should be H1)
- Task detail: H6 breadcrumb → nav element
- App header: project title H1 → span (avoids duplicate H1)

Fixes WCAG 1.3.1 (Info and Relationships) and 2.4.6 (Headings).
This commit is contained in:
kolaente
2026-04-12 14:21:28 +02:00
committed by kolaente
parent c1f74ae9dc
commit 6f85a7fb6b
9 changed files with 18 additions and 17 deletions

View File

@@ -21,9 +21,9 @@
v-if="currentProject?.id"
class="project-title-wrapper"
>
<h1 class="project-title">
<span class="project-title">
{{ currentProject.title === '' ? $t('misc.loading') : getProjectTitle(currentProject) }}
</h1>
</span>
<BaseButton
v-if="!isEditorContentEmpty(currentProject.description)"

View File

@@ -7,9 +7,9 @@
:color="getHexColor(task.hexColor)"
/>
<BaseButton @click="copyUrl">
<h1 class="title task-id">
<span class="title task-id">
{{ textIdentifier }}
</h1>
</span>
</BaseButton>
</div>
<Done

View File

@@ -1,8 +1,8 @@
<template>
<div class="content has-text-centered">
<h2 v-if="salutation">
<h1 v-if="salutation">
{{ salutation }}
</h2>
</h1>
<Message
v-if="deletionScheduledAt !== null"

View File

@@ -28,8 +28,9 @@
@update:task="Object.assign(task, $event)"
@close="$emit('close')"
/>
<h6
<nav
v-if="project?.id"
aria-label="Breadcrumb"
class="subtitle"
>
<template
@@ -60,7 +61,7 @@
:can-write="canWrite"
@update:task="Object.assign(task, $event)"
/>
</h6>
</nav>
<ChecklistSummary :task="task" />

View File

@@ -278,8 +278,8 @@ test.describe('Task', () => {
await page.goto(`/tasks/${tasks[0].id}`)
await expect(page.locator('.task-view h1.title.input')).toContainText(tasks[0].title)
await expect(page.locator('.task-view h1.title.task-id')).toContainText('#1')
await expect(page.locator('.task-view h6.subtitle')).toContainText(projects[0].title)
await expect(page.locator('.task-view span.title.task-id')).toContainText('#1')
await expect(page.locator('.task-view nav.subtitle')).toContainText(projects[0].title)
await expect(page.locator('.task-view .details.content.description')).toContainText(tasks[0].description)
await expect(page.locator('.task-view .action-buttons p.created')).toContainText('Created')
})
@@ -328,7 +328,7 @@ test.describe('Task', () => {
await page.goto(`/tasks/${tasks[0].id}`)
await expect(page.locator('.task-view h1.title.task-id')).toContainText(`${projects[0].identifier}-${tasks[0].index}`)
await expect(page.locator('.task-view span.title.task-id')).toContainText(`${projects[0].identifier}-${tasks[0].index}`)
})
test('Can edit the description', async ({authenticatedPage: page}) => {
@@ -367,7 +367,7 @@ test.describe('Task', () => {
await page.locator('.task-view .details.content.description .tiptap button.done-edit', {timeout: 30_000}).click()
await page.locator('.task-view .details.content.description .tiptap__editor .tiptap.ProseMirror').fill('New Description')
await page.locator('.task-view h6.subtitle a').first().click()
await page.locator('.task-view nav.subtitle a').first().click()
await page.goto('/tasks/1')
await expect(page.locator('.task-view .details.content.description')).toContainText('New Description')
@@ -443,7 +443,7 @@ test.describe('Task', () => {
await expect(page.locator('.task-view .content.details .field .multiselect.control .search-results')).toBeVisible({timeout: 5000})
await page.locator('.task-view .content.details .field .multiselect.control .search-results').locator('> *').first().click()
await expect(page.locator('.task-view h6.subtitle')).toContainText(projects[1].title)
await expect(page.locator('.task-view nav.subtitle')).toContainText(projects[1].title)
await expect(page.locator('.global-notification')).toContainText('Success')
})

View File

@@ -45,7 +45,7 @@ test.describe('Login', () => {
test('Should log in with the right credentials', async ({page}) => {
await page.goto('/login')
await login(page)
await expect(page.locator('main h2')).toContainText(credentials.username)
await expect(page.locator('main h1')).toContainText(credentials.username)
})
test('Should fail with a bad password', async ({page}) => {

View File

@@ -15,7 +15,7 @@ test.describe('OpenID Login', () => {
// Should redirect back to the app
await expect(page).toHaveURL(/\//)
await expect(page.locator('main.app-content .content h2')).toContainText('test')
await expect(page.locator('main.app-content .content h1')).toContainText('test')
await expect(page.locator('.show-tasks h3')).toContainText('Current Tasks')
})
})

View File

@@ -27,7 +27,7 @@ test.describe('Registration', () => {
await page.locator('#password').fill(fixture.password)
await page.locator('#register-submit').click()
await expect(page).toHaveURL('/')
await expect(page.locator('main h2')).toContainText(fixture.username)
await expect(page.locator('main h1')).toContainText(fixture.username)
})
test('Should fail', async ({page, apiContext}) => {

View File

@@ -11,7 +11,7 @@ async function loginViaBrowser(page, username: string) {
await page.locator('input[id=password]').fill(TEST_PASSWORD)
await page.locator('.button').filter({hasText: 'Login'}).click()
await expect(page).toHaveURL('/')
await expect(page.locator('main h2')).toContainText(username)
await expect(page.locator('main h1')).toContainText(username)
// Wait for the proactive refresh (from useRenewTokenOnFocus) to complete
// so it doesn't race with our test assertions.