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

[full-ci] Add keyboard selection / navigation #7153

Merged
merged 31 commits into from
Jul 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4be1391
WIP selection via keyboard
lookacat Jun 21, 2022
9280d77
Merge branch 'master' of https://github.com/owncloud/web
lookacat Jun 21, 2022
9795fa8
Merge branch 'master' of https://github.com/owncloud/web
lookacat Jun 21, 2022
6dfd3a0
Merge branch 'master' of https://github.com/owncloud/web
lookacat Jun 24, 2022
b29aace
Merge branch 'master' of https://github.com/owncloud/web
lookacat Jun 27, 2022
352e9f9
Merge branch 'master' of https://github.com/owncloud/web
lookacat Jun 28, 2022
39bf95c
Merge branch 'master' of https://github.com/owncloud/web
lookacat Jun 29, 2022
c075204
Merge branch 'master' of https://github.com/owncloud/web
lookacat Jun 30, 2022
7c2839a
Merge branch 'master' of https://github.com/owncloud/web
lookacat Jul 8, 2022
a90545a
WIP
lookacat Jun 21, 2022
eabb850
Refactor keymap into own component
lookacat Jun 22, 2022
c578699
Add space, escape keyboard actions
lookacat Jun 22, 2022
6a3b9cf
Add select all shortcut
lookacat Jun 22, 2022
a0657f1
Refactor/slim code
lookacat Jun 22, 2022
b3eb6a4
Add latest selection checkbox outline, fix toggle multiple select issue
lookacat Jun 23, 2022
03dffbb
Add ctrl + left click selection
lookacat Jun 23, 2022
23ab860
Add shift + click selection
lookacat Jun 23, 2022
6706075
Add keyboard up/down navigation
lookacat Jun 23, 2022
9096d04
Fix navigation deselects first and last row bug
lookacat Jun 24, 2022
58454c1
Fix shift+click doesnt set latest selected item
lookacat Jun 24, 2022
cf34fc3
Bump ODS, Address PR issues
lookacat Jun 24, 2022
626a4a1
Fix ODS not updated, fix row click bug undefined error
lookacat Jun 27, 2022
9bc4379
Fix focus sidebar on selection killing keyboard shortcuts
lookacat Jun 27, 2022
f6963df
Fix visibility observer
lookacat Jun 28, 2022
fa53552
Remove wait for focus
lookacat Jun 29, 2022
e03d594
Move keyboard actions into views
lookacat Jun 30, 2022
59f44d8
Add/update changelog, Resolve PR issues
lookacat Jun 30, 2022
413b322
Fix broken row click, Address PR issues
lookacat Jun 30, 2022
61e37a9
Make focus via keyboard tab work
lookacat Jul 4, 2022
3d1ede8
Fix scroll to resource
lookacat Jul 4, 2022
e32f59b
Fix linting
lookacat Jul 8, 2022
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
15 changes: 15 additions & 0 deletions changelog/unreleased/enhancement-add-keyboard-navigation-selection
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
18 changes: 10 additions & 8 deletions changelog/unreleased/enhancement-update-ods
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
49 changes: 2 additions & 47 deletions packages/web-app-files/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<main id="files" class="oc-flex oc-height-1-1">
<div ref="filesListWrapper" tabindex="-1" class="files-list-wrapper oc-width-expand">
<router-view id="files-view" />
<router-view id="files-view" tabindex="0" />
</div>
<side-bar
v-if="showSidebar"
Expand All @@ -20,22 +20,15 @@
</template>
<script lang="ts">
import Mixins from './mixins'
import { mapActions, mapState, mapMutations } from 'vuex'
import { mapActions, mapState } from 'vuex'
import SideBar from './components/SideBar/SideBar.vue'
import { defineComponent } from '@vue/composition-api'
import { usePublicLinkPassword, useStore } from 'web-pkg/src/composables'

export default defineComponent({
components: {
SideBar
},
mixins: [Mixins],
setup() {
const store = useStore()
return {
publicLinkPassword: usePublicLinkPassword({ store })
}
},
computed: {
...mapState('Files/sidebar', {
sidebarClosed: 'closed',
Expand All @@ -62,50 +55,12 @@ export default defineComponent({
})
},

mounted() {
document.addEventListener('keydown', this.handleShortcut, false)
},

beforeDestroy() {
document.removeEventListener('keydown', this.handleShortcut)
},

methods: {
...mapActions('Files', ['resetFileSelection']),
...mapActions('Files/sidebar', {
closeSidebar: 'close',
setActiveSidebarPanel: 'setActivePanel'
}),
...mapActions(['showMessage', 'createModal', 'hideModal']),
...mapActions('Files', ['copySelectedFiles', 'cutSelectedFiles', 'pasteSelectedFiles']),
...mapMutations('Files', ['UPSERT_RESOURCE']),

handleShortcut(event) {
const key = event.keyCode || event.which
const ctr = window.navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey
if (!ctr /* CTRL | CMD */) return
const isCopyAction = key === 67
const isPasteAction = key === 86
const isCutAction = key === 88
if (isCopyAction) {
this.copySelectedFiles()
} else if (isPasteAction) {
this.pasteSelectedFiles({
client: this.$client,
createModal: this.createModal,
hideModal: this.hideModal,
showMessage: this.showMessage,
$gettext: this.$gettext,
$gettextInterpolate: this.$gettextInterpolate,
$ngettext: this.$ngettext,
routeContext: this.$route.name,
publicLinkPassword: this.publicLinkPassword,
upsertResource: this.UPSERT_RESOURCE
})
} else if (isCutAction) {
this.cutSelectedFiles()
}
},

focusSideBar(component, event) {
this.focus({
Expand Down
221 changes: 221 additions & 0 deletions packages/web-app-files/src/components/FilesList/KeyboardActions.vue
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>
Loading