Skip to content

Commit 418ecf1

Browse files
kristian-zendatobackportbot[bot]
authored andcommitted
fix: multiple file selection by keyboard
fix: multiple file selection by keyboard Signed-off-by: kristian-zendato <kristian.zendato@nextcloud.com> [skip ci]
1 parent b11c68e commit 418ecf1

4 files changed

Lines changed: 79 additions & 127 deletions

File tree

apps/files/src/components/FilesListTableHeader.vue

Lines changed: 62 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,39 @@
33
- SPDX-License-Identifier: AGPL-3.0-or-later
44
-->
55
<template>
6-
<tr class="files-list__row-head">
6+
<tr
7+
class="files-list__row-head"
8+
:class="{ 'files-list__row-head--selected': !isNoneSelected }">
79
<th
810
class="files-list__column files-list__row-checkbox"
911
@keyup.esc.exact="resetSelection">
1012
<NcCheckboxRadioSwitch
1113
v-bind="selectAllBind"
12-
:id="FILES_LIST_HEADER_SELECT_ALL_CHECKBOX_ID"
1314
data-cy-files-list-selection-checkbox
1415
@update:model-value="onToggleAll" />
16+
17+
<!-- Batch selection UI lives in the select-all cell so column headers
18+
stay valid and Tab order stays in document flow
19+
(select-all → actions → rows). -->
20+
<template v-if="!isNoneSelected">
21+
<span class="files-list__selected">
22+
{{ n('files', '{count} selected', '{count} selected', selectedNodes.length, { count: selectedNodes.length }) }}
23+
</span>
24+
<FilesListTableHeaderActions
25+
:current-view="currentView"
26+
:selected-nodes="selectedNodes" />
27+
</template>
1528
</th>
1629

17-
<!-- Columns display -->
30+
<!-- Columns display (always present so headers match body cells;
31+
visually hidden while a selection is active) -->
1832

1933
<!-- Link to file -->
2034
<th
2135
class="files-list__column files-list__row-name files-list__column--sortable"
22-
:aria-sort="ariaSortForMode('basename')">
36+
:class="{ 'hidden-visually': !isNoneSelected }"
37+
:aria-sort="ariaSortForMode('basename')"
38+
:inert="!isNoneSelected">
2339
<!-- Icon or preview -->
2440
<span class="files-list__row-icon" />
2541

@@ -28,7 +44,10 @@
2844
</th>
2945

3046
<!-- Actions -->
31-
<th class="files-list__row-actions">
47+
<th
48+
class="files-list__row-actions"
49+
:class="{ 'hidden-visually': !isNoneSelected }"
50+
:inert="!isNoneSelected">
3251
<span class="hidden-visually">
3352
{{ t('files', 'Actions') }}
3453
</span>
@@ -38,26 +57,38 @@
3857
<th
3958
v-if="isMimeAvailable"
4059
class="files-list__column files-list__row-mime"
41-
:class="{ 'files-list__column--sortable': isMimeAvailable }"
42-
:aria-sort="ariaSortForMode('mime')">
60+
:class="{
61+
'files-list__column--sortable': isMimeAvailable,
62+
'hidden-visually': !isNoneSelected,
63+
}"
64+
:aria-sort="ariaSortForMode('mime')"
65+
:inert="!isNoneSelected">
4366
<FilesListTableHeaderButton :name="t('files', 'File type')" mode="mime" />
4467
</th>
4568

4669
<!-- Size -->
4770
<th
4871
v-if="isSizeAvailable"
4972
class="files-list__column files-list__row-size"
50-
:class="{ 'files-list__column--sortable': isSizeAvailable }"
51-
:aria-sort="ariaSortForMode('size')">
73+
:class="{
74+
'files-list__column--sortable': isSizeAvailable,
75+
'hidden-visually': !isNoneSelected,
76+
}"
77+
:aria-sort="ariaSortForMode('size')"
78+
:inert="!isNoneSelected">
5279
<FilesListTableHeaderButton :name="t('files', 'Size')" mode="size" />
5380
</th>
5481

5582
<!-- Mtime -->
5683
<th
5784
v-if="isMtimeAvailable"
5885
class="files-list__column files-list__row-mtime"
59-
:class="{ 'files-list__column--sortable': isMtimeAvailable }"
60-
:aria-sort="ariaSortForMode('mtime')">
86+
:class="{
87+
'files-list__column--sortable': isMtimeAvailable,
88+
'hidden-visually': !isNoneSelected,
89+
}"
90+
:aria-sort="ariaSortForMode('mtime')"
91+
:inert="!isNoneSelected">
6192
<FilesListTableHeaderButton :name="t('files', 'Modified')" mode="mtime" />
6293
</th>
6394

@@ -66,7 +97,8 @@
6697
v-for="column in columns"
6798
:key="column.id"
6899
:class="classForColumn(column)"
69-
:aria-sort="ariaSortForMode(column.id)">
100+
:aria-sort="ariaSortForMode(column.id)"
101+
:inert="!isNoneSelected">
70102
<FilesListTableHeaderButton v-if="!!column.sort" :name="column.title" :mode="column.id" />
71103
<span v-else>
72104
{{ column.title }}
@@ -76,15 +108,15 @@
76108
</template>
77109

78110
<script lang="ts">
79-
import type { Node } from '@nextcloud/files'
111+
import type { Node, View } from '@nextcloud/files'
80112
import type { PropType } from 'vue'
81113
import type { FileSource } from '../types.ts'
82114
83-
import { t } from '@nextcloud/l10n'
115+
import { n, t } from '@nextcloud/l10n'
84116
import { useHotKey } from '@nextcloud/vue/composables/useHotKey'
85117
import { defineComponent } from 'vue'
86118
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
87-
import { FILE_LIST_HEAD_FIRST_BATCH_ACTION_ID } from './FilesListTableHeaderActions.vue'
119+
import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue'
88120
import FilesListTableHeaderButton from './FilesListTableHeaderButton.vue'
89121
import { useFileListWidth } from '../composables/useFileListWidth.ts'
90122
import { useRouteParameters } from '../composables/useRouteParameters.ts'
@@ -94,12 +126,11 @@ import { useFilesStore } from '../store/files.ts'
94126
import { useSelectionStore } from '../store/selection.ts'
95127
import { logger } from '../utils/logger.ts'
96128
97-
export const FILES_LIST_HEADER_SELECT_ALL_CHECKBOX_ID = 'files-list-header-select-all-checkbox'
98-
99129
export default defineComponent({
100130
name: 'FilesListTableHeader',
101131
102132
components: {
133+
FilesListTableHeaderActions,
103134
FilesListTableHeaderButton,
104135
NcCheckboxRadioSwitch,
105136
},
@@ -109,6 +140,11 @@ export default defineComponent({
109140
],
110141
111142
props: {
143+
currentView: {
144+
type: Object as PropType<View>,
145+
required: true,
146+
},
147+
112148
isMimeAvailable: {
113149
type: Boolean,
114150
default: false,
@@ -145,8 +181,6 @@ export default defineComponent({
145181
146182
directory,
147183
isNarrow,
148-
149-
FILES_LIST_HEADER_SELECT_ALL_CHECKBOX_ID,
150184
}
151185
},
152186
@@ -206,11 +240,6 @@ export default defineComponent({
206240
})
207241
},
208242
209-
mounted() {
210-
const selectAllCheckbox = document.getElementById(FILES_LIST_HEADER_SELECT_ALL_CHECKBOX_ID)
211-
selectAllCheckbox?.addEventListener('keydown', this.onSelectAllCheckboxFocusOut)
212-
},
213-
214243
methods: {
215244
ariaSortForMode(mode: string): 'ascending' | 'descending' | undefined {
216245
if (this.sortingMode === mode) {
@@ -224,6 +253,7 @@ export default defineComponent({
224253
'files-list__column--sortable': !!column.sort,
225254
'files-list__row-column-custom': true,
226255
[`files-list__row-${this.activeStore.activeView?.id}-${column.id}`]: true,
256+
'hidden-visually': !this.isNoneSelected,
227257
}
228258
},
229259
@@ -246,18 +276,7 @@ export default defineComponent({
246276
this.selectionStore.reset()
247277
},
248278
249-
onSelectAllCheckboxFocusOut(event: KeyboardEvent) {
250-
// If the user tabbed further and we have a batch action to tab to
251-
const firstBatchActionButton = document.getElementById(FILE_LIST_HEAD_FIRST_BATCH_ACTION_ID)
252-
if (event.code === 'Tab' && !event.shiftKey && !event.metaKey && firstBatchActionButton) {
253-
event.preventDefault()
254-
event.stopPropagation()
255-
256-
firstBatchActionButton.focus()
257-
logger.debug('Focusing first batch action button')
258-
}
259-
},
260-
279+
n,
261280
t,
262281
},
263282
})
@@ -274,4 +293,11 @@ export default defineComponent({
274293
}
275294
}
276295
296+
.files-list__selected {
297+
padding-inline-end: 12px;
298+
white-space: nowrap;
299+
font-variant-numeric: tabular-nums;
300+
flex-shrink: 0;
301+
color: var(--color-main-text);
302+
}
277303
</style>

apps/files/src/components/FilesListTableHeaderActions.vue

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
@close="openedSubmenu = null">
1717
<!-- Default actions list-->
1818
<NcActionButton
19-
v-for="(action, idx) in enabledMenuActions"
20-
:id="idx === 0 ? FILE_LIST_HEAD_FIRST_BATCH_ACTION_ID : undefined"
19+
v-for="(action) in enabledMenuActions"
2120
:key="action.id"
2221
:ref="`action-batch-${action.id}`"
2322
:class="{
@@ -85,7 +84,6 @@ import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator'
8584
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
8685
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
8786
import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue'
88-
import { FILES_LIST_HEADER_SELECT_ALL_CHECKBOX_ID } from './FilesListTableHeader.vue'
8987
import { useFileActions } from '../composables/useFileActions.ts'
9088
import { useFileListWidth } from '../composables/useFileListWidth.ts'
9189
import actionsMixins from '../mixins/actionsMixin.ts'
@@ -95,8 +93,6 @@ import { useFilesStore } from '../store/files.ts'
9593
import { useSelectionStore } from '../store/selection.ts'
9694
import { logger } from '../utils/logger.ts'
9795
98-
export const FILE_LIST_HEAD_FIRST_BATCH_ACTION_ID = 'files-list-head-first-batch-action'
99-
10096
export default defineComponent({
10197
name: 'FilesListTableHeaderActions',
10298
@@ -153,8 +149,6 @@ export default defineComponent({
153149
154150
boundariesElement,
155151
inlineActions,
156-
157-
FILE_LIST_HEAD_FIRST_BATCH_ACTION_ID,
158152
}
159153
},
160154
@@ -275,17 +269,6 @@ export default defineComponent({
275269
},
276270
},
277271
278-
mounted() {
279-
const firstActionId = this.enabledMenuActions.at(0)?.id
280-
const firstButton = this.$refs.actionsMenu?.$refs?.[`action-batch-${firstActionId}`]
281-
if (firstButton) {
282-
firstButton.$el.focus()
283-
logger.debug('Focusing first batch action button')
284-
285-
firstButton.$el.addEventListener('focusout', this.onFirstButtonFocusOut)
286-
}
287-
},
288-
289272
methods: {
290273
/**
291274
* Get a cached note from the store
@@ -360,20 +343,6 @@ export default defineComponent({
360343
}
361344
},
362345
363-
// When focusing out the first button outside the header actions
364-
// we can return back to the select all checkbox
365-
onFirstButtonFocusOut(event: FocusEvent) {
366-
// If the focus is still within this component, do nothing
367-
if (this.$el.contains(event.relatedTarget)) {
368-
return
369-
}
370-
371-
event.preventDefault()
372-
event.stopPropagation()
373-
document.getElementById(FILES_LIST_HEADER_SELECT_ALL_CHECKBOX_ID)?.focus()
374-
logger.debug('Focusing select all checkbox again')
375-
},
376-
377346
t: translate,
378347
},
379348
})

0 commit comments

Comments
 (0)