mirror of
https://github.com/nextcloud/server.git
synced 2026-06-11 09:42:09 -04:00
Merge pull request #41199 from nextcloud/fix/apporder-accessible-details
fix(theming): Add accessible information to app order settings
This commit is contained in:
commit
16e97f05ce
7 changed files with 98 additions and 25 deletions
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
4
dist/theming-admin-theming.js
vendored
4
dist/theming-admin-theming.js
vendored
File diff suppressed because one or more lines are too long
2
dist/theming-admin-theming.js.map
vendored
2
dist/theming-admin-theming.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/theming-personal-theming.js
vendored
4
dist/theming-personal-theming.js
vendored
File diff suppressed because one or more lines are too long
2
dist/theming-personal-theming.js.map
vendored
2
dist/theming-personal-theming.js.map
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue