mirror of
https://github.com/nextcloud/server.git
synced 2026-03-27 12:54:24 -04:00
Merge pull request #47121 from nextcloud/fix/a11y-inline-action
fix(systemtags): Make inline tags list fully accessible
This commit is contained in:
commit
e49c55df12
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)
|
||||
30
apps/systemtags/src/files_views/systemtagsView.ts
Normal file
30
apps/systemtags/src/files_views/systemtagsView.ts
Normal file
|
|
@ -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()
|
||||
|
|
|
|||
155
cypress/e2e/systemtags/files-inline-action.cy.ts
Normal file
155
cypress/e2e/systemtags/files-inline-action.cy.ts
Normal file
|
|
@ -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
cypress/e2e/systemtags/files-sidebar.cy.ts
Normal file
44
cypress/e2e/systemtags/files-sidebar.cy.ts
Normal file
|
|
@ -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
dist/systemtags-init.js
vendored
4
dist/systemtags-init.js
vendored
File diff suppressed because one or more lines are too long
2
dist/systemtags-init.js.map
vendored
2
dist/systemtags-init.js.map
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue