Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(systemtags): Make inline tags list fully accessible #47121

Merged
merged 3 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

Loading