feat(a11y): add option for short article aria labels (#705)

Actually fixes #694 by providing an option to make the labels like they used to be.
This commit is contained in:
Nolan Lawson 2018-12-01 11:53:20 -08:00 committed by GitHub
parent 0515133ece
commit 153e4f4fcd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 93 additions and 65 deletions

View file

@ -2,9 +2,7 @@ import { getAccountAccessibleName } from './getAccountAccessibleName'
import { POST_PRIVACY_OPTIONS } from '../_static/statuses' import { POST_PRIVACY_OPTIONS } from '../_static/statuses'
import { htmlToPlainText } from '../_utils/htmlToPlainText' import { htmlToPlainText } from '../_utils/htmlToPlainText'
const MAX_TEXT_LENGTH = 150 function getNotificationText (notification, omitEmojiInDisplayNames) {
function notificationText (notification, omitEmojiInDisplayNames) {
if (!notification) { if (!notification) {
return return
} }
@ -16,7 +14,7 @@ function notificationText (notification, omitEmojiInDisplayNames) {
} }
} }
function privacyText (visibility) { function getPrivacyText (visibility) {
for (let option of POST_PRIVACY_OPTIONS) { for (let option of POST_PRIVACY_OPTIONS) {
if (option.key === visibility) { if (option.key === visibility) {
return option.label return option.label
@ -24,7 +22,7 @@ function privacyText (visibility) {
} }
} }
function reblogText (reblog, account, omitEmojiInDisplayNames) { function getReblogText (reblog, account, omitEmojiInDisplayNames) {
if (!reblog) { if (!reblog) {
return return
} }
@ -32,32 +30,34 @@ function reblogText (reblog, account, omitEmojiInDisplayNames) {
return `Boosted by ${accountDisplayName}` return `Boosted by ${accountDisplayName}`
} }
// Works around a bug in NVDA where it may crash if the string is too long function cleanupText (text) {
// https://github.com/nolanlawson/pinafore/issues/694
function truncateTextForSRs (text) {
if (text.length > MAX_TEXT_LENGTH) {
text = text.substring(0, MAX_TEXT_LENGTH)
text = text.replace(/\S+$/, '') + ' (truncated)'
}
return text.replace(/\s+/g, ' ').trim() return text.replace(/\s+/g, ' ').trim()
} }
export function getAccessibleLabelForStatus (originalAccount, account, content, export function getAccessibleLabelForStatus (originalAccount, account, content,
timeagoFormattedDate, spoilerText, showContent, timeagoFormattedDate, spoilerText, showContent,
reblog, notification, visibility, omitEmojiInDisplayNames) { reblog, notification, visibility, omitEmojiInDisplayNames,
disableLongAriaLabels) {
let originalAccountDisplayName = getAccountAccessibleName(originalAccount, omitEmojiInDisplayNames) let originalAccountDisplayName = getAccountAccessibleName(originalAccount, omitEmojiInDisplayNames)
let contentTextToShow = (showContent || !spoilerText) let contentTextToShow = (showContent || !spoilerText)
? truncateTextForSRs(htmlToPlainText(content)) ? cleanupText(htmlToPlainText(content))
: `Content warning: ${truncateTextForSRs(spoilerText)}` : `Content warning: ${cleanupText(spoilerText)}`
let privacyText = getPrivacyText(visibility)
if (disableLongAriaLabels) {
// Long text can crash NVDA; allow users to shorten it like we had it before.
// https://github.com/nolanlawson/pinafore/issues/694
return `${privacyText} status by ${originalAccountDisplayName}`
}
let values = [ let values = [
notificationText(notification, omitEmojiInDisplayNames), getNotificationText(notification, omitEmojiInDisplayNames),
originalAccountDisplayName, originalAccountDisplayName,
contentTextToShow, contentTextToShow,
timeagoFormattedDate, timeagoFormattedDate,
`@${originalAccount.acct}`, `@${originalAccount.acct}`,
privacyText(visibility), privacyText,
reblogText(reblog, account, omitEmojiInDisplayNames) getReblogText(reblog, account, omitEmojiInDisplayNames)
].filter(Boolean) ].filter(Boolean)
return values.join(', ') return values.join(', ')

View file

@ -220,10 +220,10 @@
timeagoFormattedDate: ({ createdAtDate }) => formatTimeagoDate(createdAtDate), timeagoFormattedDate: ({ createdAtDate }) => formatTimeagoDate(createdAtDate),
reblog: ({ status }) => status.reblog, reblog: ({ status }) => status.reblog,
ariaLabel: ({ originalAccount, account, content, timeagoFormattedDate, spoilerText, ariaLabel: ({ originalAccount, account, content, timeagoFormattedDate, spoilerText,
showContent, reblog, notification, visibility, $omitEmojiInDisplayNames }) => ( showContent, reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels }) => (
getAccessibleLabelForStatus(originalAccount, account, content, getAccessibleLabelForStatus(originalAccount, account, content,
timeagoFormattedDate, spoilerText, showContent, timeagoFormattedDate, spoilerText, showContent,
reblog, notification, visibility, $omitEmojiInDisplayNames) reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels)
), ),
showHeader: ({ notification, status, timelineType }) => ( showHeader: ({ notification, status, timelineType }) => (
(notification && (notification.type === 'reblog' || notification.type === 'favourite')) || (notification && (notification.type === 'reblog' || notification.type === 'favourite')) ||

View file

@ -28,6 +28,11 @@
bind:checked="$disableCustomScrollbars" on:change="$save()"> bind:checked="$disableCustomScrollbars" on:change="$save()">
<label for="choice-disable-custom-scrollbars">Disable custom scrollbars</label> <label for="choice-disable-custom-scrollbars">Disable custom scrollbars</label>
</div> </div>
<div class="setting-group">
<input type="checkbox" id="choice-disable-long-aria-labels"
bind:checked="$disableLongAriaLabels" on:change="$save()">
<label for="choice-disable-long-aria-labels">Use short article ARIA labels</label>
</div>
</form> </form>
<h2>Themes <h2>Themes

View file

@ -4,58 +4,52 @@ import { mixins } from './mixins/mixins'
import { LocalStorageStore } from './LocalStorageStore' import { LocalStorageStore } from './LocalStorageStore'
import { observe } from 'svelte-extras' import { observe } from 'svelte-extras'
const KEYS_TO_STORE_IN_LOCAL_STORAGE = new Set([ const persistedState = {
'currentInstance', autoplayGifs: false,
'currentRegisteredInstance', composeData: {},
'currentRegisteredInstanceName', currentInstance: null,
'instanceNameInSearch', currentRegisteredInstanceName: undefined,
'instanceThemes', currentRegisteredInstance: undefined,
'loggedInInstances', disableCustomScrollbars: false,
'loggedInInstancesInOrder', disableLongAriaLabels: false,
'autoplayGifs', instanceNameInSearch: '',
'markMediaAsSensitive', instanceThemes: {},
'reduceMotion', loggedInInstances: {},
'disableCustomScrollbars', loggedInInstancesInOrder: [],
'omitEmojiInDisplayNames', markMediaAsSensitive: false,
'pinnedPages', omitEmojiInDisplayNames: undefined,
'composeData', pinnedPages: {},
'pushSubscription' pushSubscription: null,
]) reduceMotion: !process.browser || window.matchMedia('(prefers-reduced-motion: reduce)').matches
}
const nonPersistedState = {
customEmoji: {},
instanceInfos: {},
instanceLists: {},
online: !process.browser || navigator.onLine,
pinnedStatuses: {},
pushNotificationsSupport: process.browser && ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in window.PushSubscription.prototype),
queryInSearch: '',
repliesShown: {},
sensitivesShown: {},
spoilersShown: {},
statusModifications: {},
verifyCredentials: {}
}
const state = Object.assign({}, persistedState, nonPersistedState)
const keysToStoreInLocalStorage = new Set(Object.keys(persistedState))
class PinaforeStore extends LocalStorageStore { class PinaforeStore extends LocalStorageStore {
constructor (state) { constructor (state) {
super(state, KEYS_TO_STORE_IN_LOCAL_STORAGE) super(state, keysToStoreInLocalStorage)
} }
} }
PinaforeStore.prototype.observe = observe PinaforeStore.prototype.observe = observe
export const store = new PinaforeStore({ export const store = new PinaforeStore(state)
instanceNameInSearch: '',
queryInSearch: '',
currentInstance: null,
loggedInInstances: {},
loggedInInstancesInOrder: [],
instanceThemes: {},
spoilersShown: {},
sensitivesShown: {},
repliesShown: {},
autoplayGifs: false,
markMediaAsSensitive: false,
reduceMotion: !process.browser || window.matchMedia('(prefers-reduced-motion: reduce)').matches,
disableCustomScrollbars: false,
pinnedPages: {},
instanceLists: {},
pinnedStatuses: {},
instanceInfos: {},
statusModifications: {},
customEmoji: {},
composeData: {},
verifyCredentials: {},
online: !process.browser || navigator.onLine,
pushNotificationsSupport: process.browser && ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in window.PushSubscription.prototype),
pushSubscription: null
})
mixins(PinaforeStore) mixins(PinaforeStore)
computations(store) computations(store)

View file

@ -1,5 +1,13 @@
import { loginAsFoobar } from '../roles' import { loginAsFoobar } from '../roles'
import { getNthShowOrHideButton, getNthStatus, notificationsNavButton, scrollToStatus } from '../utils' import {
generalSettingsButton,
getNthShowOrHideButton,
getNthStatus, homeNavButton,
notificationsNavButton,
scrollToStatus,
settingsNavButton
} from '../utils'
import { Selector as $ } from 'testcafe'
import { indexWhere } from '../../routes/_utils/arrays' import { indexWhere } from '../../routes/_utils/arrays'
import { homeTimeline } from '../fixtures' import { homeTimeline } from '../fixtures'
@ -67,3 +75,24 @@ test('aria-labels for notifications', async t => {
/quux followed you, @quux/i /quux followed you, @quux/i
) )
}) })
test('can shorten aria-labels', async t => {
await loginAsFoobar(t)
await t
.click(settingsNavButton)
.click(generalSettingsButton)
.click($('#choice-disable-long-aria-labels'))
.click(homeNavButton)
.hover(getNthStatus(0))
.expect(getNthStatus(0).getAttribute('aria-label')).match(
/Unlisted status by quux/
)
.click(settingsNavButton)
.click(generalSettingsButton)
.click($('#choice-disable-long-aria-labels'))
.click(homeNavButton)
.hover(getNthStatus(0))
.expect(getNthStatus(0).getAttribute('aria-label')).match(
/quux, pinned toot 1, .+ ago, @quux, Unlisted, Boosted by admin/i
)
})