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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
{#if $isUserLoggedIn}
<h1 class="sr-only">{intl.search}</h1>
<div class="search-page">
<Search></Search>
</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)`)
}
export const getNumElementsMatchingSelector = (selector) => (exec(() => {
return document.querySelectorAll(selector).length
}, {
dependencies: { selector }
}))
export async function validateTimeline (t, timeline) {
const timeout = 30000
for (let i = 0; i < timeline.length; i++) {