Merge pull request #47121 from nextcloud/fix/a11y-inline-action

fix(systemtags): Make inline tags list fully accessible
pull/47139/head
Ferdinand Thiessen 2024-08-08 14:05:41 +07:00 committed by GitHub
commit e49c55df12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 266 additions and 56 deletions

@ -79,10 +79,8 @@ describe('Inline system tags action render tests', () => {
const result = await action.renderInline!(file, view)
expect(result).toBeInstanceOf(HTMLElement)
expect(result!.outerHTML).toBe(
'<ul class="files-list__system-tags" aria-label="This file has the tag Confidential">'
+ '<li class="files-list__system-tag">Confidential</li>'
+ '</ul>',
expect(result!.outerHTML).toMatchInlineSnapshot(
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags"><li class="files-list__system-tag">Confidential</li></ul>"',
)
})
@ -95,21 +93,15 @@ describe('Inline system tags action render tests', () => {
permissions: Permission.ALL,
attributes: {
'system-tags': {
'system-tag': [
'Important',
'Confidential',
],
'system-tag': ['Important', 'Confidential'],
},
},
})
const result = await action.renderInline!(file, view)
expect(result).toBeInstanceOf(HTMLElement)
expect(result!.outerHTML).toBe(
'<ul class="files-list__system-tags" aria-label="This file has the tags Important and Confidential">'
+ '<li class="files-list__system-tag">Important</li>'
+ '<li class="files-list__system-tag files-list__system-tag--more" title="Confidential">+1</li>'
+ '</ul>',
expect(result!.outerHTML).toMatchInlineSnapshot(
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags"><li class="files-list__system-tag">Important</li><li class="files-list__system-tag">Confidential</li></ul>"',
)
})
@ -134,11 +126,8 @@ describe('Inline system tags action render tests', () => {
const result = await action.renderInline!(file, view)
expect(result).toBeInstanceOf(HTMLElement)
expect(result!.outerHTML).toBe(
'<ul class="files-list__system-tags" aria-label="This file has the tags Important, Confidential, Secret and Classified">'
+ '<li class="files-list__system-tag">Important</li>'
+ '<li class="files-list__system-tag files-list__system-tag--more" title="Confidential, Secret, Classified">+3</li>'
+ '</ul>',
expect(result!.outerHTML).toMatchInlineSnapshot(
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags"><li class="files-list__system-tag">Important</li><li class="files-list__system-tag files-list__system-tag--more" title="Confidential, Secret, Classified" aria-hidden="true" role="presentation">+3</li><li class="files-list__system-tag hidden-visually">Confidential</li><li class="files-list__system-tag hidden-visually">Secret</li><li class="files-list__system-tag hidden-visually">Classified</li></ul>"',
)
})
})

@ -2,7 +2,8 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { FileAction, Node, registerDavProperty, registerFileAction } from '@nextcloud/files'
import type { Node } from '@nextcloud/files'
import { FileAction } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import '../css/fileEntryInlineSystemTags.scss'
@ -63,22 +64,30 @@ export const action = new FileAction({
const systemTagsElement = document.createElement('ul')
systemTagsElement.classList.add('files-list__system-tags')
if (tags.length === 1) {
systemTagsElement.setAttribute('aria-label', t('files', 'This file has the tag {tag}', { tag: tags[0] }))
} else {
const firstTags = tags.slice(0, -1).join(', ')
const lastTag = tags[tags.length - 1]
systemTagsElement.setAttribute('aria-label', t('files', 'This file has the tags {firstTags} and {lastTag}', { firstTags, lastTag }))
}
systemTagsElement.setAttribute('aria-label', t('files', 'Assigned collaborative tags'))
systemTagsElement.append(renderTag(tags[0]))
// More tags than the one we're showing
if (tags.length > 1) {
if (tags.length === 2) {
// Special case only two tags:
// the overflow fake tag would take the same space as this, so render it
systemTagsElement.append(renderTag(tags[1]))
} else if (tags.length > 1) {
// More tags than the one we're showing
// So we add a overflow element indicating there are more tags
const moreTagElement = renderTag('+' + (tags.length - 1), true)
moreTagElement.setAttribute('title', tags.slice(1).join(', '))
// because the title is not accessible we hide this element for screen readers (see alternative below)
moreTagElement.setAttribute('aria-hidden', 'true')
moreTagElement.setAttribute('role', 'presentation')
systemTagsElement.append(moreTagElement)
// For accessibility the tags are listed, as the title is not accessible
// but those tags are visually hidden
for (const tag of tags.slice(1)) {
const tagElement = renderTag(tag)
tagElement.classList.add('hidden-visually')
systemTagsElement.append(tagElement)
}
}
return systemTagsElement
@ -86,6 +95,3 @@ export const action = new FileAction({
order: 0,
})
registerDavProperty('nc:system-tags')
registerFileAction(action)

@ -0,0 +1,30 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { translate as t } from '@nextcloud/l10n'
import { View, getNavigation } from '@nextcloud/files'
import { getContents } from '../services/systemtags.js'
import svgTagMultiple from '@mdi/svg/svg/tag-multiple.svg?raw'
/**
* Register the system tags files view
*/
export function registerSystemTagsView() {
const Navigation = getNavigation()
Navigation.register(new View({
id: 'tags',
name: t('systemtags', 'Tags'),
caption: t('systemtags', 'List of tags and their associated files and folders.'),
emptyTitle: t('systemtags', 'No tags found'),
emptyCaption: t('systemtags', 'Tags you have created will show up here.'),
icon: svgTagMultiple,
order: 25,
getContents,
}))
}

@ -2,25 +2,11 @@
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import './actions/inlineSystemTagsAction.js'
import { registerDavProperty, registerFileAction } from '@nextcloud/files'
import { action as inlineSystemTagsAction } from './files_actions/inlineSystemTagsAction.js'
import { registerSystemTagsView } from './files_views/systemtagsView.js'
import { translate as t } from '@nextcloud/l10n'
import { View, getNavigation } from '@nextcloud/files'
import TagMultipleSvg from '@mdi/svg/svg/tag-multiple.svg?raw'
registerDavProperty('nc:system-tags')
registerFileAction(inlineSystemTagsAction)
import { getContents } from './services/systemtags.js'
const Navigation = getNavigation()
Navigation.register(new View({
id: 'tags',
name: t('systemtags', 'Tags'),
caption: t('systemtags', 'List of tags and their associated files and folders.'),
emptyTitle: t('systemtags', 'No tags found'),
emptyCaption: t('systemtags', 'Tags you have created will show up here.'),
icon: TagMultipleSvg,
order: 25,
getContents,
}))
registerSystemTagsView()

@ -0,0 +1,155 @@
/* eslint-disable no-unused-expressions */
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import { randomBytes } from 'crypto'
import { closeSidebar, getRowForFile, triggerActionForFile } from '../files/FilesUtils.ts'
describe('Systemtags: Files integration', { testIsolation: true }, () => {
let user: User
beforeEach(() => cy.createRandomUser().then(($user) => {
user = $user
cy.mkdir(user, '/folder')
cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt')
cy.login(user)
cy.visit('/apps/files')
}))
it('See first assigned tag in the file list', () => {
const tag = randomBytes(8).toString('base64')
getRowForFile('file.txt').should('be.visible')
triggerActionForFile('file.txt', 'details')
cy.get('[data-cy-sidebar]')
.should('be.visible')
.findByRole('button', { name: 'Actions' })
.should('be.visible')
.click()
cy.findByRole('menuitem', { name: 'Tags' })
.should('be.visible')
.click()
cy.intercept('PUT', '**/remote.php/dav/systemtags-relations/files/**').as('assignTag')
cy.get('[data-cy-sidebar]')
.findByRole('combobox', { name: /collaborative tags/i })
.should('be.visible')
.type(`${tag}{enter}`)
cy.wait('@assignTag')
closeSidebar()
cy.reload()
getRowForFile('file.txt')
.findByRole('list', { name: /collaborative tags/i })
.findByRole('listitem')
.should('be.visible')
.and('contain.text', tag)
})
it('See two assigned tags are also shown in the file list', () => {
const tag1 = randomBytes(5).toString('base64')
const tag2 = randomBytes(5).toString('base64')
getRowForFile('file.txt').should('be.visible')
triggerActionForFile('file.txt', 'details')
cy.get('[data-cy-sidebar]')
.should('be.visible')
.findByRole('button', { name: 'Actions' })
.should('be.visible')
.click()
cy.findByRole('menuitem', { name: 'Tags' })
.should('be.visible')
.click()
cy.intercept('PUT', '**/remote.php/dav/systemtags-relations/files/**').as('assignTag')
cy.get('[data-cy-sidebar]').within(() => {
cy.findByRole('combobox', { name: /collaborative tags/i })
.should('be.visible')
.type(`${tag1}{enter}`)
cy.wait('@assignTag')
cy.findByRole('combobox', { name: /collaborative tags/i })
.should('be.visible')
.type(`${tag2}{enter}`)
cy.wait('@assignTag')
})
closeSidebar()
cy.reload()
getRowForFile('file.txt')
.findByRole('list', { name: /collaborative tags/i })
.children()
.should('have.length', 2)
.should('contain.text', tag1)
.should('contain.text', tag2)
})
it.only('See three assigned tags result in overflow entry', () => {
const tag1 = randomBytes(4).toString('base64')
const tag2 = randomBytes(4).toString('base64')
const tag3 = randomBytes(4).toString('base64')
getRowForFile('file.txt').should('be.visible')
cy.intercept('PROPFIND', '**/remote.php/dav/**').as('sidebarLoaded')
triggerActionForFile('file.txt', 'details')
cy.wait('@sidebarLoaded')
cy.get('[data-cy-sidebar]')
.should('be.visible')
.findByRole('button', { name: 'Actions' })
.should('be.visible')
.click()
cy.findByRole('menuitem', { name: 'Tags' })
.should('be.visible')
.click()
cy.intercept('PUT', '**/remote.php/dav/systemtags-relations/files/**').as('assignTag')
cy.get('[data-cy-sidebar]').within(() => {
cy.findByRole('combobox', { name: /collaborative tags/i })
.should('be.visible')
.type(`${tag1}{enter}`)
cy.wait('@assignTag')
cy.findByRole('combobox', { name: /collaborative tags/i })
.should('be.visible')
.type(`${tag2}{enter}`)
cy.wait('@assignTag')
cy.findByRole('combobox', { name: /collaborative tags/i })
.should('be.visible')
.type(`${tag3}{enter}`)
cy.wait('@assignTag')
})
closeSidebar()
cy.reload()
getRowForFile('file.txt')
.findByRole('list', { name: /collaborative tags/i })
.children()
.then(($children) => {
expect($children.length).to.eq(4)
expect($children.get(0)).be.visible
expect($children.get(1)).be.visible
// not visible - just for accessibility
expect($children.get(2)).not.be.visible
expect($children.get(3)).not.be.visible
// Text content
expect($children.get(1)).contain.text('+2')
// Remove the '+x' element
const elements = [$children.get(0), ...$children.get().slice(2)]
.map((el) => el.innerText.trim())
expect(elements).to.have.members([tag1, tag2, tag3])
})
})
})

@ -0,0 +1,44 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import { randomBytes } from 'crypto'
import { getRowForFile, triggerActionForFile } from '../files/FilesUtils.ts'
describe('Systemtags: Files sidebar integration', { testIsolation: true }, () => {
let user: User
beforeEach(() => cy.createRandomUser().then(($user) => {
user = $user
cy.mkdir(user, '/folder')
cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt')
cy.login(user)
}))
it('Can assign tags using the sidebar', () => {
const tag = randomBytes(8).toString('base64')
cy.visit('/apps/files')
getRowForFile('file.txt').should('be.visible')
triggerActionForFile('file.txt', 'details')
cy.get('[data-cy-sidebar]')
.should('be.visible')
.findByRole('button', { name: 'Actions' })
.should('be.visible')
.click()
cy.findByRole('menuitem', { name: 'Tags' })
.click()
cy.intercept('PUT', '**/remote.php/dav/systemtags-relations/files/**').as('assignTag')
cy.get('[data-cy-sidebar]')
.findByRole('combobox', { name: /collaborative tags/i })
.should('be.visible')
.type(`${tag}{enter}`)
cy.wait('@assignTag')
})
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long