-
Notifications
You must be signed in to change notification settings - Fork 156
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[full-ci] Add keyboard selection / navigation (#7153)
* WIP selection via keyboard * WIP * Refactor keymap into own component * Add space, escape keyboard actions * Add select all shortcut * Refactor/slim code * Add latest selection checkbox outline, fix toggle multiple select issue * Add ctrl + left click selection * Add shift + click selection * Add keyboard up/down navigation * Fix navigation deselects first and last row bug * Fix shift+click doesnt set latest selected item * Bump ODS, Address PR issues * Fix ODS not updated, fix row click bug undefined error * Fix focus sidebar on selection killing keyboard shortcuts * Fix visibility observer * Remove wait for focus * Move keyboard actions into views * Add/update changelog, Resolve PR issues * Fix broken row click, Address PR issues * Make focus via keyboard tab work * Fix scroll to resource * Fix linting
- Loading branch information
Showing
17 changed files
with
307 additions
and
93 deletions.
There are no files selected for viewing
15 changes: 15 additions & 0 deletions
15
changelog/unreleased/enhancement-add-keyboard-navigation-selection
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
Enhancement: Add Keyboard navigation/selection | ||
|
||
We've added the possibility to navigate and select via keyboard. | ||
- Navigation: | ||
- via keyboard arrows up/down for moving up and down through the rows of the file list | ||
- Selection | ||
- via keyboard space bar: select / deselect the currently highlighted row | ||
- via keyboard shift + arrows up/down: add a series of rows | ||
- via keyboard cmd/ctrl + a: select all rows | ||
- via keyboard esc: deselect all rows | ||
- via mouse holding cmd/ctrl + left click on a row: add/remove the clicked item to/from the current selection model | ||
- via mouse holding shift + left click on a row: add the clicked row and the series of rows towards the most recently clicked row to the current selection model. | ||
|
||
https://github.com/owncloud/web/pull/7153 | ||
https://github.com/owncloud/web/issues/6029 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,15 @@ | ||
Enhancement: Update ODS to v14.0.0-alpha.2 | ||
Enhancement: Update ODS to v14.0.0-alpha.4 | ||
|
||
We updated the ownCloud Design System to version 14.0.0-alpha.2. Please refer to the full changelog in the ODS release (linked) for more details. Summary: | ||
We updated the ownCloud Design System to version 14.0.0-alpha.4. Please refer to the full changelog in the ODS release (linked) for more details. Summary: | ||
|
||
- Change - Remove OcAlert component: https://github.com/owncloud/owncloud-design-system/pull/2210 | ||
- Change - Remove transition animations: https://github.com/owncloud/owncloud-design-system/pull/2210 | ||
- Change - Revamp animations: https://github.com/owncloud/owncloud-design-system/pull/2210 | ||
- Enhancement - Progress bar indeterminate state: https://github.com/owncloud/owncloud-design-system/pull/2200 | ||
- Enhancement - Redesign notifications: https://github.com/owncloud/owncloud-design-system/pull/2210 | ||
- Bugfix - Remove click event on OcIcon: https://github.com/owncloud/owncloud-design-system/pull/2216 | ||
- Bugfix - Remove click event on OcIcon: #2216 | ||
- Change - Remove OcAlert component: #2210 | ||
- Change - Remove transition animations: #2210 | ||
- Change - Revamp animations: #2210 | ||
- Change - OcTable emit event data on row click: #2218 | ||
- Enhancement - OcCheckbox add outline: #2218 | ||
- Enhancement - Progress bar indeterminate state: #2200 | ||
- Enhancement - Redesign notifications: #2210 | ||
|
||
https://github.com/owncloud/web/pull/7139 | ||
https://github.com/owncloud/owncloud-design-system/releases/tag/14.0.0-alpha.2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
221 changes: 221 additions & 0 deletions
221
packages/web-app-files/src/components/FilesList/KeyboardActions.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,221 @@ | ||
<template> | ||
<div></div> | ||
</template> | ||
|
||
<script lang="ts"> | ||
import { bus } from 'web-pkg/src/instance' | ||
import { mapActions, mapState, mapMutations } from 'vuex' | ||
import { defineComponent } from '@vue/composition-api' | ||
import MixinFilesListScrolling from '../../mixins/filesListScrolling' | ||
export default defineComponent({ | ||
mixins: [MixinFilesListScrolling], | ||
props: { | ||
paginatedResources: { | ||
type: Array, | ||
required: true | ||
}, | ||
keybindOnElementId: { | ||
type: String, | ||
required: false, | ||
default: 'files-view' | ||
} | ||
}, | ||
data: () => { | ||
return { | ||
selectionCursor: 0 | ||
} | ||
}, | ||
computed: { | ||
...mapState('Files', ['latestSelectedId']) | ||
}, | ||
mounted() { | ||
const filesList = document.getElementById(this.keybindOnElementId) | ||
if (filesList) { | ||
filesList.addEventListener('keydown', this.handleShortcut, false) | ||
} | ||
const fileListClickedEvent = bus.subscribe('app.files.list.clicked', this.resetSelectionCursor) | ||
const fileListClickedMetaEvent = bus.subscribe( | ||
'app.files.list.clicked.meta', | ||
this.handleCtrlClickAction | ||
) | ||
const fileListClickedShiftEvent = bus.subscribe( | ||
'app.files.list.clicked.shift', | ||
this.handleShiftClickAction | ||
) | ||
this.$on('beforeDestroy', () => { | ||
bus.unsubscribe('app.files.list.clicked', fileListClickedEvent) | ||
bus.unsubscribe('app.files.list.clicked.meta', fileListClickedMetaEvent) | ||
bus.unsubscribe('app.files.list.clicked.shift', fileListClickedShiftEvent) | ||
filesList.removeEventListener('keydown', this.handleShortcut) | ||
}) | ||
}, | ||
methods: { | ||
...mapActions(['showMessage', 'createModal', 'hideModal']), | ||
...mapActions('Files', [ | ||
'copySelectedFiles', | ||
'cutSelectedFiles', | ||
'pasteSelectedFiles', | ||
'resetFileSelection', | ||
'toggleFileSelection' | ||
]), | ||
...mapMutations('Files', { | ||
upsertResource: 'UPSERT_RESOURCE', | ||
setLatestSelectedFile: 'SET_LATEST_SELECTED_FILE_ID', | ||
setFileSelection: 'SET_FILE_SELECTION', | ||
addFileSelection: 'ADD_FILE_SELECTION' | ||
}), | ||
handleShortcut(event) { | ||
const key = event.keyCode || event.which | ||
const ctrl = window.navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey | ||
const shift = event.shiftKey | ||
this.handleFileActionsShortcuts(key, ctrl) | ||
this.handleFileSelectionShortcuts(key, shift, ctrl, event) | ||
}, | ||
handleFileActionsShortcuts(key, ctrl) { | ||
const isCopyAction = key === 67 | ||
const isPasteAction = key === 86 | ||
const isCutAction = key === 88 | ||
if (isCopyAction && ctrl) return this.copySelectedFiles() | ||
if (isPasteAction && ctrl) return this.handlePasteAction() | ||
if (isCutAction && ctrl) return this.cutSelectedFiles() | ||
}, | ||
handleFileSelectionShortcuts(key, shift, ctrl, event) { | ||
const isUpPressed = key === 38 | ||
const isDownPressed = key === 40 | ||
const isEscapePressed = key === 27 | ||
const isSpacePressed = key === 32 | ||
const isAPressed = key === 65 | ||
if (isDownPressed && !shift) return this.handleNavigateAction(event) | ||
if (isUpPressed && !shift) return this.handleNavigateAction(event, true) | ||
if (isSpacePressed) return this.handleSpaceAction(event) | ||
if (isEscapePressed) return this.handleEscapeAction() | ||
if (isDownPressed && shift) return this.handleShiftDownAction(event) | ||
if (isUpPressed && shift) return this.handleShiftUpAction(event) | ||
if (isAPressed && ctrl) return this.handleSelectAllAction(event) | ||
}, | ||
handleNavigateAction(event, up = false) { | ||
event.preventDefault() | ||
if (!this.latestSelectedId) return | ||
const nextId = this.getNextResourceId(up) | ||
if (nextId === -1) return | ||
this.resetSelectionCursor() | ||
this.resetFileSelection() | ||
this.addFileSelection({ id: nextId }) | ||
this.scrollToResource({ id: nextId }) | ||
}, | ||
handleShiftClickAction(resource) { | ||
const parent = document.querySelectorAll(`[data-item-id='${resource.id}']`)[0] | ||
const resourceNodes = Object.values(parent.parentNode.childNodes) as HTMLElement[] | ||
const latestNode = resourceNodes.find( | ||
(r) => r.getAttribute('data-item-id') === this.latestSelectedId | ||
) | ||
const clickedNode = resourceNodes.find((r) => r.getAttribute('data-item-id') === resource.id) | ||
let latestNodeIndex = resourceNodes.indexOf(latestNode) | ||
latestNodeIndex = latestNodeIndex === -1 ? 0 : latestNodeIndex | ||
const clickedNodeIndex = resourceNodes.indexOf(clickedNode) | ||
const minIndex = Math.min(latestNodeIndex, clickedNodeIndex) | ||
const maxIndex = Math.max(latestNodeIndex, clickedNodeIndex) | ||
for (let i = minIndex; i <= maxIndex; i++) { | ||
const nodeId = resourceNodes[i].getAttribute('data-item-id') | ||
this.addFileSelection({ id: nodeId }) | ||
} | ||
this.setLatestSelectedFile(resource.id) | ||
}, | ||
handleCtrlClickAction(resource) { | ||
this.toggleFileSelection({ id: resource.id }) | ||
}, | ||
handleEscapeAction() { | ||
this.resetSelectionCursor() | ||
this.resetFileSelection() | ||
}, | ||
handleSelectAllAction(event) { | ||
event.preventDefault() | ||
this.resetSelectionCursor() | ||
this.setFileSelection(this.paginatedResources) | ||
}, | ||
handleSpaceAction(event) { | ||
event.preventDefault() | ||
this.toggleFileSelection({ id: this.latestSelectedId }) | ||
}, | ||
handleShiftUpAction() { | ||
const nextResourceId = this.getNextResourceId(true) | ||
if (nextResourceId === -1) return | ||
if (this.selectionCursor > 0) { | ||
// deselect | ||
this.toggleFileSelection({ id: this.latestSelectedId }) | ||
this.setLatestSelectedFile(nextResourceId) | ||
} else { | ||
// select | ||
this.addFileSelection({ id: nextResourceId }) | ||
} | ||
this.scrollToResource({ id: nextResourceId }) | ||
this.selectionCursor = this.selectionCursor - 1 | ||
}, | ||
handleShiftDownAction() { | ||
const nextResourceId = this.getNextResourceId() | ||
if (nextResourceId === -1) return | ||
if (this.selectionCursor < 0) { | ||
// deselect | ||
this.toggleFileSelection({ id: this.latestSelectedId }) | ||
this.setLatestSelectedFile(nextResourceId) | ||
} else { | ||
// select | ||
this.addFileSelection({ id: nextResourceId }) | ||
} | ||
this.scrollToResource({ id: nextResourceId }) | ||
this.selectionCursor = this.selectionCursor + 1 | ||
}, | ||
handlePasteAction() { | ||
this.pasteSelectedFiles({ | ||
client: this.$client, | ||
createModal: this.createModal, | ||
hideModal: this.hideModal, | ||
showMessage: this.showMessage, | ||
$gettext: this.$gettext, | ||
$gettextInterpolate: this.$gettextInterpolate, | ||
$ngettext: this.$ngettext, | ||
upsertResource: this.upsertResource | ||
}) | ||
}, | ||
resetSelectionCursor() { | ||
this.selectionCursor = 0 | ||
}, | ||
getNextResourceId(previous = false) { | ||
const latestSelectedRow = document.querySelectorAll( | ||
`[data-item-id='${this.latestSelectedId}']` | ||
)[0] | ||
const nextRow = ( | ||
previous ? latestSelectedRow.previousSibling : latestSelectedRow.nextSibling | ||
) as HTMLElement | ||
if (nextRow === null) return -1 | ||
const nextResourceId = nextRow.getAttribute('data-item-id') | ||
return nextResourceId | ||
} | ||
} | ||
}) | ||
</script> |
Oops, something went wrong.