Merge pull request #41199 from nextcloud/fix/apporder-accessible-details

fix(theming): Add accessible information to app order settings
This commit is contained in:
Ferdinand Thiessen 2023-10-31 22:11:03 +01:00 committed by GitHub
commit 16e97f05ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 98 additions and 25 deletions

View file

@ -1,21 +1,34 @@
<template>
<ol ref="listElement" data-cy-app-order class="order-selector">
<AppOrderSelectorElement v-for="app,index in appList"
:key="`${app.id}${renderCount}`"
ref="selectorElements"
:app="app"
:is-first="index === 0 || !!appList[index - 1].default"
:is-last="index === value.length - 1"
v-on="app.default ? {} : {
'move:up': () => moveUp(index),
'move:down': () => moveDown(index),
}" />
</ol>
<Fragment>
<div :id="statusInfoId"
aria-live="polite"
class="hidden-visually"
role="status">
{{ statusInfo }}
</div>
<ol ref="listElement" data-cy-app-order class="order-selector">
<AppOrderSelectorElement v-for="app,index in appList"
:key="`${app.id}${renderCount}`"
ref="selectorElements"
:app="app"
:aria-details="ariaDetails"
:aria-describedby="statusInfoId"
:is-first="index === 0 || !!appList[index - 1].default"
:is-last="index === value.length - 1"
v-on="app.default ? {} : {
'move:up': () => moveUp(index),
'move:down': () => moveDown(index),
'update:focus': () => updateStatusInfo(index),
}" />
</ol>
</Fragment>
</template>
<script lang="ts">
import { translate as t } from '@nextcloud/l10n'
import { useSortable } from '@vueuse/integrations/useSortable'
import { PropType, computed, defineComponent, onUpdated, ref } from 'vue'
import { Fragment } from 'vue-frag'
import AppOrderSelectorElement from './AppOrderSelectorElement.vue'
@ -32,8 +45,16 @@ export default defineComponent({
name: 'AppOrderSelector',
components: {
AppOrderSelectorElement,
Fragment,
},
props: {
/**
* Details like status information that need to be forwarded to the interactive elements
*/
ariaDetails: {
type: String,
default: null,
},
/**
* List of apps to reorder
*/
@ -125,6 +146,28 @@ export default defineComponent({
emit('update:value', [...before, props.value[index], ...after])
}
/**
* Additional status information to show to screen reader users for accessibility
*/
const statusInfo = ref('')
/**
* ID to be used on the status info element
*/
const statusInfoId = `sorting-status-info-${(Math.random() + 1).toString(36).substring(7)}`
/**
* Update the status information for the currently selected app
* @param index Index of the app that is currently selected
*/
const updateStatusInfo = (index: number) => {
statusInfo.value = t('theming', 'Current selected app: {app}, position {position} of {total}', {
app: props.value[index].label,
position: index + 1,
total: props.value.length,
})
}
return {
appList,
listElement,
@ -132,6 +175,10 @@ export default defineComponent({
moveDown,
moveUp,
statusInfoId,
statusInfo,
updateStatusInfo,
renderCount,
selectorElements,
}

View file

@ -3,7 +3,8 @@
:class="{
'order-selector-element': true,
'order-selector-element--disabled': app.default
}">
}"
@focusin="$emit('update:focus')">
<svg width="20"
height="20"
viewBox="0 0 20 20"
@ -25,6 +26,8 @@
<NcButton v-show="!isFirst && !app.default"
ref="buttonUp"
:aria-label="t('settings', 'Move up')"
:aria-describedby="ariaDescribedby"
:aria-details="ariaDetails"
data-cy-app-order-button="up"
type="tertiary-no-background"
@click="moveUp">
@ -36,6 +39,8 @@
<NcButton v-show="!isLast && !app.default"
ref="buttonDown"
:aria-label="t('settings', 'Move down')"
:aria-describedby="ariaDescribedby"
:aria-details="ariaDetails"
data-cy-app-order-button="down"
type="tertiary-no-background"
@click="moveDown">
@ -73,6 +78,17 @@ export default defineComponent({
NcButton,
},
props: {
/**
* Needs to be forwarded to the buttons (as interactive elements)
*/
ariaDescribedby: {
type: String,
default: null,
},
ariaDetails: {
type: String,
default: null,
},
app: {
type: Object as PropType<IApp>,
required: true,
@ -89,6 +105,10 @@ export default defineComponent({
emits: {
'move:up': () => true,
'move:down': () => true,
/**
* We need this as Sortable.js removes all native focus event listeners
*/
'update:focus': () => true,
},
setup(props, { emit }) {
const buttonUp = ref()

View file

@ -241,21 +241,27 @@ describe('User theming app order list accessibility', () => {
})
it('click the first button', () => {
cy.get('[data-cy-app-order] [data-cy-app-order-element]:first-of-type [data-cy-app-order-button="down"]').should('be.visible').click()
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="down"]').should('be.visible').focus()
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="down"]').click()
})
it('see the same app kept the focus', () => {
cy.get('[data-cy-app-order] [data-cy-app-order-element]:first-of-type [data-cy-app-order-button="down"]').should('not.have.focus')
cy.get('[data-cy-app-order] [data-cy-app-order-element]:last-of-type [data-cy-app-order-button="up"]').should('have.focus')
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="down"]').should('not.have.focus')
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('not.have.focus')
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="down"]').should('not.have.focus')
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="up"]').should('have.focus')
})
it('click the last button', () => {
cy.get('[data-cy-app-order] [data-cy-app-order-element]:last-of-type [data-cy-app-order-button="up"]').should('be.visible').click()
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="up"]').should('be.visible').focus()
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="up"]').click()
})
it('see the same app kept the focus', () => {
cy.get('[data-cy-app-order] [data-cy-app-order-element]:first-of-type [data-cy-app-order-button="down"]').should('have.focus')
cy.get('[data-cy-app-order] [data-cy-app-order-element]:last-of-type [data-cy-app-order-button="up"]').should('not.have.focus')
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="down"]').should('not.have.focus')
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('not.have.focus')
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="up"]').should('not.have.focus')
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="down"]').should('have.focus')
})
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long