Skip to content

Commit

Permalink
Merge pull request #47121 from nextcloud/fix/a11y-inline-action
Browse files Browse the repository at this point in the history
fix(systemtags): Make inline tags list fully accessible
  • Loading branch information
susnux authored Aug 8, 2024
2 parents e21fc6c + d21c293 commit e49c55d
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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>"',
)
})

Expand All @@ -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>"',
)
})

Expand All @@ -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>"',
)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -63,29 +64,34 @@ 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
},

order: 0,
})

registerDavProperty('nc:system-tags')
registerFileAction(action)
30 changes: 30 additions & 0 deletions apps/systemtags/src/files_views/systemtagsView.ts
Original file line number Diff line number Diff line change
@@ -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,
}))
}
26 changes: 6 additions & 20 deletions apps/systemtags/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
155 changes: 155 additions & 0 deletions cypress/e2e/systemtags/files-inline-action.cy.ts
Original file line number Diff line number Diff line change
@@ -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])
})
})
})
44 changes: 44 additions & 0 deletions cypress/e2e/systemtags/files-sidebar.cy.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
4 changes: 2 additions & 2 deletions dist/systemtags-init.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/systemtags-init.js.map

Large diffs are not rendered by default.

0 comments on commit e49c55d

Please sign in to comment.