fix(a11y): fix number of headings (#2183)

Fixes #2162
This commit is contained in:
Nolan Lawson 2022-11-13 07:01:12 -08:00 committed by GitHub
parent 1c6387a0a4
commit f10e9dbcf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 94 additions and 13 deletions

View File

@ -680,5 +680,12 @@ export default {
statusOptions: 'Status options', statusOptions: 'Status options',
confirm: 'Confirm', confirm: 'Confirm',
closeDialog: 'Close dialog', closeDialog: 'Close dialog',
postPrivacy: 'Post privacy' postPrivacy: 'Post privacy',
homeOnInstance: 'Home on {instance}',
statusesTimelineOnInstance: 'Statuses: {timeline} timeline on {instance}',
statusesHashtag: 'Statuses: #{hashtag} hashtag',
statusesThread: 'Statuses: thread',
statusesAccountTimeline: 'Statuses: account timeline',
statusesList: 'Statuses: list',
notificationsOnInstance: 'Notifications on {instance}'
} }

View File

@ -0,0 +1,5 @@
{#if level === 2}
<h2 class={className || ''}><slot></slot></h2>
{:else}
<h1 class={className || ''}><slot></slot></h1>
{/if}

View File

@ -3,6 +3,7 @@
without a div wrapper due to sticky-positioned compose button. without a div wrapper due to sticky-positioned compose button.
TODO: this is a bit hacky due to code duplication TODO: this is a bit hacky due to code duplication
--> -->
<h1 class="sr-only">{headingLabel}</h1>
<div class="timeline-home-page" aria-busy={hideTimeline}> <div class="timeline-home-page" aria-busy={hideTimeline}>
{#if hidePage} {#if hidePage}
<LoadingPage /> <LoadingPage />
@ -30,6 +31,7 @@
import { store } from '../_store/store.js' import { store } from '../_store/store.js'
import LoadingPage from './LoadingPage.html' import LoadingPage from './LoadingPage.html'
import LazyComposeBox from './compose/LazyComposeBox.html' import LazyComposeBox from './compose/LazyComposeBox.html'
import { formatIntl } from '../_utils/formatIntl.js'
export default { export default {
oncreate () { oncreate () {
@ -40,7 +42,8 @@
}, },
computed: { computed: {
hidePage: ({ $timelineInitialized, $timelinePreinitialized }) => !$timelineInitialized && !$timelinePreinitialized, hidePage: ({ $timelineInitialized, $timelinePreinitialized }) => !$timelineInitialized && !$timelinePreinitialized,
hideTimeline: ({ $timelineInitialized }) => !$timelineInitialized hideTimeline: ({ $timelineInitialized }) => !$timelineInitialized,
headingLabel: ({ $currentInstance }) => formatIntl('intl.homeOnInstance', { instance: $currentInstance })
}, },
store: () => store, store: () => store,
components: { components: {

View File

@ -1,5 +1,5 @@
{#if realm === 'home'} {#if realm === 'home'}
<h1 class="sr-only">{intl.composeStatus}</h1> <h2 class="sr-only">{intl.composeStatus}</h2>
{/if} {/if}
<ComposeFileDrop {realm} > <ComposeFileDrop {realm} >
<div class="{computedClassName} {hideAndFadeIn}"> <div class="{computedClassName} {hideAndFadeIn}">

View File

@ -1,4 +1,4 @@
<h1 class="sr-only">{label}</h1> <DynamicHeading className="sr-only" level={headingLevel}>{label}</DynamicHeading>
<FocusRestoration realm={focusRealm}> <FocusRestoration realm={focusRealm}>
<div class="timeline" role="feed"> <div class="timeline" role="feed">
{#if components} {#if components}
@ -26,6 +26,7 @@
<ScrollListShortcuts /> <ScrollListShortcuts />
<script> <script>
import { store } from '../../_store/store.js' import { store } from '../../_store/store.js'
import DynamicHeading from '../DynamicHeading.html'
import Status from '../status/Status.html' import Status from '../status/Status.html'
import LoadingFooter from './LoadingFooter.html' import LoadingFooter from './LoadingFooter.html'
import MoreHeaderVirtualWrapper from './MoreHeaderVirtualWrapper.html' import MoreHeaderVirtualWrapper from './MoreHeaderVirtualWrapper.html'
@ -51,6 +52,7 @@
import { createMakeProps } from '../../_actions/createMakeProps.js' import { createMakeProps } from '../../_actions/createMakeProps.js'
import { showMoreAndScrollToTop } from '../../_actions/showMoreAndScrollToTop.js' import { showMoreAndScrollToTop } from '../../_actions/showMoreAndScrollToTop.js'
import FocusRestoration from '../FocusRestoration.html' import FocusRestoration from '../FocusRestoration.html'
import { formatIntl } from '../../_utils/formatIntl.js'
export default { export default {
oncreate () { oncreate () {
@ -89,20 +91,23 @@
), ),
label: ({ timeline, $currentInstance, timelineType, timelineValue }) => { label: ({ timeline, $currentInstance, timelineType, timelineValue }) => {
if (timelines[timeline]) { if (timelines[timeline]) {
return `Statuses: ${timelines[timeline].label} timeline on ${$currentInstance}` return formatIntl('intl.statusesTimelineOnInstance', {
timeline: timelines[timeline].label,
instance: $currentInstance
})
} }
switch (timelineType) { switch (timelineType) {
case 'tag': case 'tag':
return `Statuses: #${timelineValue} hashtag` return formatIntl('intl.statusesHashtag', { hashtag: timelineValue })
case 'status': case 'status':
return 'Statuses: thread' return 'intl.statusesThread'
case 'account': case 'account':
return 'Statuses: account timeline' return 'intl.statusesAccountTimeline'
case 'list': case 'list':
return 'Statuses: list' return 'intl.statusesList'
case 'notifications': case 'notifications':
return `Notifications on ${$currentInstance}` return formatIntl('intl.notificationsOnInstance', { instance: $currentInstance })
} }
}, },
timelineType: ({ $currentTimelineType }) => $currentTimelineType, timelineType: ({ $currentTimelineType }) => $currentTimelineType,
@ -127,7 +132,8 @@
onClick: showMoreItemsForCurrentTimeline onClick: showMoreItemsForCurrentTimeline
} }
}, },
focusRealm: ({ $currentInstance, timeline }) => `${$currentInstance}-${timeline}` focusRealm: ({ $currentInstance, timeline }) => `${$currentInstance}-${timeline}`,
headingLevel: ({ timeline, timelineType }) => timeline === 'home' || timelineType === 'status' ? 2 : 1
}, },
store: () => store, store: () => store,
methods: { methods: {
@ -232,7 +238,8 @@
components: { components: {
ScrollListShortcuts, ScrollListShortcuts,
Shortcut, Shortcut,
FocusRestoration FocusRestoration,
DynamicHeading
} }
} }
</script> </script>

View File

@ -1,6 +1,6 @@
{#if $isUserLoggedIn} {#if $isUserLoggedIn}
<h1 class="sr-only">{intl.community}</h1>
<div class="community-page"> <div class="community-page">
<FocusRestoration realm="community"> <FocusRestoration realm="community">
<RadioGroup <RadioGroup
id="pinnables" id="pinnables"

View File

@ -1,4 +1,5 @@
{#if $isUserLoggedIn} {#if $isUserLoggedIn}
<h1 class="sr-only">{intl.search}</h1>
<div class="search-page"> <div class="search-page">
<Search></Search> <Search></Search>
</div> </div>

View File

@ -0,0 +1,52 @@
import {
settingsNavButton,
notificationsNavButton,
localTimelineNavButton,
communityNavButton,
searchNavButton,
getNumElementsMatchingSelector,
getUrl, getNthStatus
} from '../utils'
import { loginAsFoobar } from '../roles'
fixture`042-headings.js`
.page`http://localhost:4002`
async function testHeadings (t, loggedIn) {
const navButtons = [
{ button: notificationsNavButton, url: 'notifications' },
{ button: localTimelineNavButton, url: 'local' },
{ button: communityNavButton, url: 'community' },
{ button: searchNavButton, url: 'search' },
{ button: settingsNavButton, url: 'settings' }
]
// home page
await t
.expect(getNumElementsMatchingSelector('h1')()).eql(1)
if (loggedIn) {
// status page
await t
.click(getNthStatus(1))
.expect(getUrl()).contains('status')
.expect(getNumElementsMatchingSelector('h1')()).eql(1)
}
// non-home pages
for (const { button, url } of navButtons) {
await t
.click(button)
.expect(getUrl()).contains(url)
.expect(getNumElementsMatchingSelector('h1')()).eql(1)
}
}
test('Only one <h1> when not logged in', async t => {
await testHeadings(t, false)
})
test('Only one <h1> when logged in', async t => {
await loginAsFoobar(t)
await testHeadings(t, true)
})

View File

@ -570,6 +570,12 @@ export function getNthPinnedStatusFavoriteButton (n) {
return $(`${getNthPinnedStatusSelector(n)} .status-toolbar button:nth-child(3)`) return $(`${getNthPinnedStatusSelector(n)} .status-toolbar button:nth-child(3)`)
} }
export const getNumElementsMatchingSelector = (selector) => (exec(() => {
return document.querySelectorAll(selector).length
}, {
dependencies: { selector }
}))
export async function validateTimeline (t, timeline) { export async function validateTimeline (t, timeline) {
const timeout = 30000 const timeout = 30000
for (let i = 0; i < timeline.length; i++) { for (let i = 0; i < timeline.length; i++) {