aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorJohn Molakvoæ <skjnldsv@users.noreply.github.com>2023-10-15 13:52:14 +0200
committerGitHub <noreply@github.com>2023-10-15 13:52:14 +0200
commit7e2c51204b05f869d9dcfe9608d9927e3db1bd0f (patch)
tree2ba4a1a7555ee82eb5633d2be0927db70a84da30 /apps
parent562f19a49e654673468549d84711b0bfdf4fa8d0 (diff)
parent459e05223715a70405d8d7ae37129578d0dea77d (diff)
downloadnextcloud-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.vue174
-rw-r--r--apps/files/src/components/FilesListHeaderActions.vue226
-rw-r--r--apps/files/src/components/FilesListHeaderButton.vue123
-rw-r--r--apps/files/src/components/FilesListTableHeader.vue17
-rw-r--r--apps/files/src/components/FilesListTableHeaderButton.vue6
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,
})
},