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:
parent
0515133ece
commit
153e4f4fcd
|
@ -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(', ')
|
||||||
|
|
|
@ -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')) ||
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
Loading…
Reference in a new issue