diff options
author | John Molakvoæ <skjnldsv@users.noreply.github.com> | 2023-10-15 13:52:14 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-15 13:52:14 +0200 |
commit | 7e2c51204b05f869d9dcfe9608d9927e3db1bd0f (patch) | |
tree | 2ba4a1a7555ee82eb5633d2be0927db70a84da30 /apps | |
parent | 562f19a49e654673468549d84711b0bfdf4fa8d0 (diff) | |
parent | 459e05223715a70405d8d7ae37129578d0dea77d (diff) | |
download | nextcloud-server-7e2c51204b05f869d9dcfe9608d9927e3db1bd0f.tar.gz nextcloud-server-7e2c51204b05f869d9dcfe9608d9927e3db1bd0f.zip |
Merge pull request #40893 from nextcloud/enh/a11y/files-header-sort
Diffstat (limited to 'apps')
-rw-r--r-- | apps/files/src/components/FilesListFooter.vue | 174 | ||||
-rw-r--r-- | apps/files/src/components/FilesListHeaderActions.vue | 226 | ||||
-rw-r--r-- | apps/files/src/components/FilesListHeaderButton.vue | 123 | ||||
-rw-r--r-- | apps/files/src/components/FilesListTableHeader.vue | 17 | ||||
-rw-r--r-- | apps/files/src/components/FilesListTableHeaderButton.vue | 6 |
5 files changed, 15 insertions, 531 deletions
diff --git a/apps/files/src/components/FilesListFooter.vue b/apps/files/src/components/FilesListFooter.vue deleted file mode 100644 index 51b04179b8c..00000000000 --- a/apps/files/src/components/FilesListFooter.vue +++ /dev/null @@ -1,174 +0,0 @@ -<!-- - - @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> - - - - @author John Molakvoæ <skjnldsv@protonmail.com> - - - - @license GNU AGPL version 3 or any later version - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU Affero General Public License as - - published by the Free Software Foundation, either version 3 of the - - License, or (at your option) any later version. - - - - This program is distributed in the hope that it will be useful, - - but WITHOUT ANY WARRANTY; without even the implied warranty of - - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - - GNU Affero General Public License for more details. - - - - You should have received a copy of the GNU Affero General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - - --> -<template> - <tr> - <th class="files-list__row-checkbox"> - <span class="hidden-visually">{{ t('files', 'Total rows summary') }}</span> - </th> - - <!-- Link to file --> - <td class="files-list__row-name"> - <!-- Icon or preview --> - <span class="files-list__row-icon" /> - - <!-- Summary --> - <span>{{ summary }}</span> - </td> - - <!-- Actions --> - <td class="files-list__row-actions" /> - - <!-- Size --> - <td v-if="isSizeAvailable" - class="files-list__column files-list__row-size"> - <span>{{ totalSize }}</span> - </td> - - <!-- Mtime --> - <td v-if="isMtimeAvailable" - class="files-list__column files-list__row-mtime" /> - - <!-- Custom views columns --> - <th v-for="column in columns" - :key="column.id" - :class="classForColumn(column)"> - <span>{{ column.summary?.(nodes, currentView) }}</span> - </th> - </tr> -</template> - -<script lang="ts"> -import Vue from 'vue' -import { formatFileSize } from '@nextcloud/files' -import { translate } from '@nextcloud/l10n' - -import { useFilesStore } from '../store/files.ts' -import { usePathsStore } from '../store/paths.ts' - -export default Vue.extend({ - name: 'FilesListFooter', - - components: { - }, - - props: { - isMtimeAvailable: { - type: Boolean, - default: false, - }, - isSizeAvailable: { - type: Boolean, - default: false, - }, - nodes: { - type: Array, - required: true, - }, - summary: { - type: String, - default: '', - }, - filesListWidth: { - type: Number, - default: 0, - }, - }, - - setup() { - const pathsStore = usePathsStore() - const filesStore = useFilesStore() - return { - filesStore, - pathsStore, - } - }, - - computed: { - currentView() { - return this.$navigation.active - }, - - dir() { - // Remove any trailing slash but leave root slash - return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1') - }, - - currentFolder() { - if (!this.currentView?.id) { - return - } - - if (this.dir === '/') { - return this.filesStore.getRoot(this.currentView.id) - } - const fileId = this.pathsStore.getPath(this.currentView.id, this.dir) - return this.filesStore.getNode(fileId) - }, - - columns() { - // Hide columns if the list is too small - if (this.filesListWidth < 512) { - return [] - } - return this.currentView?.columns || [] - }, - - totalSize() { - // If we have the size already, let's use it - if (this.currentFolder?.size) { - return formatFileSize(this.currentFolder.size, true) - } - - // Otherwise let's compute it - return formatFileSize(this.nodes.reduce((total, node) => total + node.size || 0, 0), true) - }, - }, - - methods: { - classForColumn(column) { - return { - 'files-list__row-column-custom': true, - [`files-list__row-${this.currentView.id}-${column.id}`]: true, - } - }, - - t: translate, - }, -}) -</script> - -<style scoped lang="scss"> -// Scoped row -tr { - border-top: 1px solid var(--color-border); - // Prevent hover effect on the whole row - background-color: transparent !important; - border-bottom: none !important; -} - -td { - user-select: none; - // Make sure the cell colors don't apply to column headers - color: var(--color-text-maxcontrast) !important; -} - -</style> diff --git a/apps/files/src/components/FilesListHeaderActions.vue b/apps/files/src/components/FilesListHeaderActions.vue deleted file mode 100644 index 4d6dcdd0399..00000000000 --- a/apps/files/src/components/FilesListHeaderActions.vue +++ /dev/null @@ -1,226 +0,0 @@ -<!-- - - @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> - - - - @author John Molakvoæ <skjnldsv@protonmail.com> - - - - @license GNU AGPL version 3 or any later version - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU Affero General Public License as - - published by the Free Software Foundation, either version 3 of the - - License, or (at your option) any later version. - - - - This program is distributed in the hope that it will be useful, - - but WITHOUT ANY WARRANTY; without even the implied warranty of - - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - - GNU Affero General Public License for more details. - - - - You should have received a copy of the GNU Affero General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - - --> -<template> - <th class="files-list__column files-list__row-actions-batch" colspan="2"> - <NcActions ref="actionsMenu" - :disabled="!!loading || areSomeNodesLoading" - :force-name="true" - :inline="inlineActions" - :menu-name="inlineActions <= 1 ? t('files', 'Actions') : null" - :open.sync="openedMenu"> - <NcActionButton v-for="action in enabledActions" - :key="action.id" - :class="'files-list__row-actions-batch-' + action.id" - @click="onActionClick(action)"> - <template #icon> - <NcLoadingIcon v-if="loading === action.id" :size="18" /> - <NcIconSvgWrapper v-else :svg="action.iconSvgInline(nodes, currentView)" /> - </template> - {{ action.displayName(nodes, currentView) }} - </NcActionButton> - </NcActions> - </th> -</template> - -<script lang="ts"> -import { showError, showSuccess } from '@nextcloud/dialogs' -import { translate } from '@nextcloud/l10n' -import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' -import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' -import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' -import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' -import Vue from 'vue' - -import { getFileActions, useActionsMenuStore } from '../store/actionsmenu.ts' -import { useFilesStore } from '../store/files.ts' -import { useSelectionStore } from '../store/selection.ts' -import filesListWidthMixin from '../mixins/filesListWidth.ts' -import logger from '../logger.js' -import { NodeStatus } from '@nextcloud/files' - -// The registered actions list -const actions = getFileActions() - -export default Vue.extend({ - name: 'FilesListHeaderActions', - - components: { - NcActions, - NcActionButton, - NcIconSvgWrapper, - NcLoadingIcon, - }, - - mixins: [ - filesListWidthMixin, - ], - - props: { - currentView: { - type: Object, - required: true, - }, - selectedNodes: { - type: Array, - default: () => ([]), - }, - }, - - setup() { - const actionsMenuStore = useActionsMenuStore() - const filesStore = useFilesStore() - const selectionStore = useSelectionStore() - return { - actionsMenuStore, - filesStore, - selectionStore, - } - }, - - data() { - return { - loading: null, - } - }, - - computed: { - dir() { - // Remove any trailing slash but leave root slash - return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1') - }, - enabledActions() { - return actions - .filter(action => action.execBatch) - .filter(action => !action.enabled || action.enabled(this.nodes, this.currentView)) - .sort((a, b) => (a.order || 0) - (b.order || 0)) - }, - - nodes() { - return this.selectedNodes - .map(fileid => this.getNode(fileid)) - .filter(node => node) - }, - - areSomeNodesLoading() { - return this.nodes.some(node => node.status === NodeStatus.LOADING) - }, - - openedMenu: { - get() { - return this.actionsMenuStore.opened === 'global' - }, - set(opened) { - this.actionsMenuStore.opened = opened ? 'global' : null - }, - }, - - inlineActions() { - if (this.filesListWidth < 512) { - return 0 - } - if (this.filesListWidth < 768) { - return 1 - } - if (this.filesListWidth < 1024) { - return 2 - } - return 3 - }, - }, - - methods: { - /** - * Get a cached note from the store - * - * @param {number} fileId the file id to get - * @return {Folder|File} - */ - getNode(fileId) { - return this.filesStore.getNode(fileId) - }, - - async onActionClick(action) { - const displayName = action.displayName(this.nodes, this.currentView) - const selectionIds = this.selectedNodes - try { - // Set loading markers - this.loading = action.id - this.nodes.forEach(node => { - Vue.set(node, 'status', NodeStatus.LOADING) - }) - - // Dispatch action execution - const results = await action.execBatch(this.nodes, this.currentView, this.dir) - - // Check if all actions returned null - if (!results.some(result => result !== null)) { - // If the actions returned null, we stay silent - this.selectionStore.reset() - return - } - - // Handle potential failures - if (results.some(result => result === false)) { - // Remove the failed ids from the selection - const failedIds = selectionIds - .filter((fileid, index) => results[index] === false) - this.selectionStore.set(failedIds) - - showError(this.t('files', '"{displayName}" failed on some elements ', { displayName })) - return - } - - // Show success message and clear selection - showSuccess(this.t('files', '"{displayName}" batch action executed successfully', { displayName })) - this.selectionStore.reset() - } catch (e) { - logger.error('Error while executing action', { action, e }) - showError(this.t('files', '"{displayName}" action failed', { displayName })) - } finally { - // Remove loading markers - this.loading = null - this.nodes.forEach(node => { - Vue.set(node, 'status', undefined) - }) - } - }, - - t: translate, - }, -}) -</script> - -<style scoped lang="scss"> -.files-list__row-actions-batch { - flex: 1 1 100% !important; - - // Remove when https://github.com/nextcloud/nextcloud-vue/pull/3936 is merged - ::v-deep .button-vue__wrapper { - width: 100%; - span.button-vue__text { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } -} -</style> diff --git a/apps/files/src/components/FilesListHeaderButton.vue b/apps/files/src/components/FilesListHeaderButton.vue deleted file mode 100644 index bc85e2cdd7f..00000000000 --- a/apps/files/src/components/FilesListHeaderButton.vue +++ /dev/null @@ -1,123 +0,0 @@ -<!-- - - @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> - - - - @author John Molakvoæ <skjnldsv@protonmail.com> - - - - @license GNU AGPL version 3 or any later version - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU Affero General Public License as - - published by the Free Software Foundation, either version 3 of the - - License, or (at your option) any later version. - - - - This program is distributed in the hope that it will be useful, - - but WITHOUT ANY WARRANTY; without even the implied warranty of - - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - - GNU Affero General Public License for more details. - - - - You should have received a copy of the GNU Affero General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - - --> -<template> - <NcButton :aria-label="sortAriaLabel(name)" - :class="{'files-list__column-sort-button--active': sortingMode === mode}" - :alignment="mode !== 'size' ? 'start-reverse' : ''" - class="files-list__column-sort-button" - type="tertiary" - @click.stop.prevent="toggleSortBy(mode)"> - <!-- Sort icon before text as size is align right --> - <MenuUp v-if="sortingMode !== mode || isAscSorting" slot="icon" /> - <MenuDown v-else slot="icon" /> - {{ name }} - </NcButton> -</template> - -<script lang="ts"> -import { translate } from '@nextcloud/l10n' -import MenuDown from 'vue-material-design-icons/MenuDown.vue' -import MenuUp from 'vue-material-design-icons/MenuUp.vue' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import Vue from 'vue' - -import filesSortingMixin from '../mixins/filesSorting.ts' - -export default Vue.extend({ - name: 'FilesListHeaderButton', - - components: { - MenuDown, - MenuUp, - NcButton, - }, - - mixins: [ - filesSortingMixin, - ], - - props: { - name: { - type: String, - required: true, - }, - mode: { - type: String, - required: true, - }, - }, - - methods: { - sortAriaLabel(column) { - const direction = this.isAscSorting - ? this.t('files', 'ascending') - : this.t('files', 'descending') - return this.t('files', 'Sort list by {column} ({direction})', { - column, - direction, - }) - }, - - t: translate, - }, -}) -</script> - -<style lang="scss"> -.files-list__column-sort-button { - // Compensate for cells margin - margin: 0 calc(var(--cell-margin) * -1); - // Reverse padding - padding: 0 4px 0 16px !important; - - // Icon after text - .button-vue__wrapper { - flex-direction: row-reverse; - // Take max inner width for text overflow ellipsis - // Remove when https://github.com/nextcloud/nextcloud-vue/pull/3936 is merged - width: 100%; - } - - .button-vue__icon { - transition-timing-function: linear; - transition-duration: .1s; - transition-property: opacity; - opacity: 0; - } - - // Remove when https://github.com/nextcloud/nextcloud-vue/pull/3936 is merged - .button-vue__text { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - &--active, - &:hover, - &:focus, - &:active { - .button-vue__icon { - opacity: 1 !important; - } - } -} -</style> diff --git a/apps/files/src/components/FilesListTableHeader.vue b/apps/files/src/components/FilesListTableHeader.vue index 52060d2589e..e619acf0623 100644 --- a/apps/files/src/components/FilesListTableHeader.vue +++ b/apps/files/src/components/FilesListTableHeader.vue @@ -34,6 +34,7 @@ <template v-else> <!-- Link to file --> <th class="files-list__column files-list__row-name files-list__column--sortable" + :aria-sort="ariaSortForMode('basename')" @click.stop.prevent="toggleSortBy('basename')"> <!-- Icon or preview --> <span class="files-list__row-icon" /> @@ -48,21 +49,24 @@ <!-- Size --> <th v-if="isSizeAvailable" :class="{'files-list__column--sortable': isSizeAvailable}" - class="files-list__column files-list__row-size"> + class="files-list__column files-list__row-size" + :aria-sort="ariaSortForMode('size')"> <FilesListTableHeaderButton :name="t('files', 'Size')" mode="size" /> </th> <!-- Mtime --> <th v-if="isMtimeAvailable" :class="{'files-list__column--sortable': isMtimeAvailable}" - class="files-list__column files-list__row-mtime"> + class="files-list__column files-list__row-mtime" + :aria-sort="ariaSortForMode('mtime')"> <FilesListTableHeaderButton :name="t('files', 'Modified')" mode="mtime" /> </th> <!-- Custom views columns --> <th v-for="column in columns" :key="column.id" - :class="classForColumn(column)"> + :class="classForColumn(column)" + :aria-sort="ariaSortForMode(column.id)"> <FilesListTableHeaderButton v-if="!!column.sort" :name="column.title" :mode="column.id" /> <span v-else> {{ column.title }} @@ -173,6 +177,13 @@ export default Vue.extend({ }, methods: { + ariaSortForMode(mode: string): ARIAMixin['ariaSort'] { + if (this.sortingMode === mode) { + return this.isAscSorting ? 'ascending' : 'descending' + } + return null + }, + classForColumn(column) { return { 'files-list__column': true, diff --git a/apps/files/src/components/FilesListTableHeaderButton.vue b/apps/files/src/components/FilesListTableHeaderButton.vue index 11d7e63f772..203c5b307a3 100644 --- a/apps/files/src/components/FilesListTableHeaderButton.vue +++ b/apps/files/src/components/FilesListTableHeaderButton.vue @@ -68,12 +68,8 @@ export default Vue.extend({ methods: { sortAriaLabel(column) { - const direction = this.isAscSorting - ? this.t('files', 'ascending') - : this.t('files', 'descending') - return this.t('files', 'Sort list by {column} ({direction})', { + return this.t('files', 'Sort list by {column}', { column, - direction, }) }, |