Skip to content

Commit

Permalink
Add accesibility features to quick share dropdown
Browse files Browse the repository at this point in the history
- Adds appropriate aria attributes
- Uses button element for dropdown items as it's more semantically correct
- Uses trap-focus lib to trap focus when the drowpdown is active
- Adds custom handling for arrow up and down

Signed-off-by: fenn-cs <[email protected]>
Signed-off-by: nextcloud-command <[email protected]>
  • Loading branch information
nfebe authored and nextcloud-command committed Sep 6, 2023
1 parent 300d6e1 commit 486ed0f
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 20 deletions.
105 changes: 92 additions & 13 deletions apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
<template>
<div :class="{ 'active': showDropdown, 'share-select': true }" ref="quickShareDropdown">
<span class="trigger-text" @click="toggleDropdown">
<div ref="quickShareDropdownContainer"
:class="{ 'active': showDropdown, 'share-select': true }"
tabindex="0"
@keydown.down="handleArrowDown"
@keydown.up="handleArrowUp">
<span :id="dropdownId"
class="trigger-text"
:aria-expanded="showDropdown"
:aria-haspopup="true"
aria-label="Quick share options dropdown"
@click="toggleDropdown">
{{ selectedOption }}
<DropdownIcon :size="15" />
</span>
<div v-if="showDropdown" class="share-select-dropdown-container">
<div v-for="option in options"
<div v-if="showDropdown"
ref="quickShareDropdown"
class="share-select-dropdown-container"
:aria-labelledby="dropdownId"
tabindex="0"
@keydown.esc="closeDropdown">
<button v-for="option in options"
:key="option"
:class="{ 'dropdown-item': true, 'selected': option === selectedOption }"
tabindex="0"
:aria-selected="option === selectedOption"
@click="selectOption(option)">
{{ option }}
</div>
</button>
</div>
</div>
</template>
Expand All @@ -26,6 +42,8 @@ import {
ATOMIC_PERMISSIONS,
} from '../lib/SharePermissionsToolBox.js'
import { createFocusTrap } from 'focus-trap'
export default {
components: {
DropdownIcon,
Expand All @@ -45,6 +63,7 @@ export default {
return {
selectedOption: '',
showDropdown: this.toggle,
focusTrap: null,
}
},
computed: {
Expand Down Expand Up @@ -102,6 +121,10 @@ export default {
return BUNDLED_PERMISSIONS.READ_ONLY
}
},
dropdownId() {
// Generate a unique ID for ARIA attributes
return `dropdown-${Math.random().toString(36).substr(2, 9)}`
},
},
watch: {
toggle(toggleValue) {
Expand All @@ -110,15 +133,26 @@ export default {
},
mounted() {
this.initializeComponent()
window.addEventListener('click', this.handleClickOutside);
window.addEventListener('click', this.handleClickOutside)
},
beforeDestroy() {
// Remove the global click event listener to prevent memory leaks
window.removeEventListener('click', this.handleClickOutside);
},
// Remove the global click event listener to prevent memory leaks
window.removeEventListener('click', this.handleClickOutside)
},
methods: {
toggleDropdown() {
this.showDropdown = !this.showDropdown
if (this.showDropdown) {
this.$nextTick(() => {
this._useFocusTrap()
})
} else {
this._clearFocusTrap()
}
},
closeDropdown() {
this.showDropdown = false
this._clearFocusTrap()
},
selectOption(option) {
this.selectedOption = option
Expand All @@ -134,12 +168,46 @@ export default {
this.selectedOption = this.preSelectedOption
},
handleClickOutside(event) {
const dropdownElement = this.$refs.quickShareDropdown;
const dropdownContainer = this.$refs.quickShareDropdownContainer
if (dropdownContainer && !dropdownContainer.contains(event.target)) {
this.showDropdown = false
}
},
_useFocusTrap() {
const dropdownElement = this.$refs.quickShareDropdown
this.focusTrap = createFocusTrap(dropdownElement, {
allowOutsideClick: true,
})
if (dropdownElement && !dropdownElement.contains(event.target)) {
this.showDropdown = false;
this.focusTrap.activate()
},
_clearFocusTrap() {
this.focusTrap?.deactivate()
this.focusTrap = null
},
shiftFocusForward() {
const currentElement = document.activeElement
let nextElement = currentElement.nextElementSibling
if (nextElement) {
nextElement = this.$refs.quickShareDropdown.firstElementChild
}
nextElement.focus()
},
shiftFocusBackward() {
const currentElement = document.activeElement
let previousElement = currentElement.previousElementSibling
if (!previousElement) {
previousElement = this.$refs.quickShareDropdown.lastElementChild
}
},
previousElement.focus()
},
handleArrowUp() {
this.shiftFocusBackward()
},
handleArrowDown() {
this.shiftFocusForward()
},
},
}
Expand All @@ -161,6 +229,8 @@ export default {
.share-select-dropdown-container {
position: absolute;
display: flex;
flex-direction: column;
top: 100%;
left: 0;
background-color: var(--color-main-background);
Expand All @@ -172,6 +242,15 @@ export default {
.dropdown-item {
padding: 8px;
font-size: 12px;
background: none;
border: none;
border-radius: 0;
font: inherit;
cursor: pointer;
color: inherit;
outline: none;
width: 100%;
white-space: nowrap;
&:hover {
background-color: #f2f2f2;
Expand Down
3 changes: 3 additions & 0 deletions dist/3441-3441.js

Large diffs are not rendered by default.

File renamed without changes.
1 change: 1 addition & 0 deletions dist/3441-3441.js.map

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions dist/894-894.js

This file was deleted.

1 change: 0 additions & 1 deletion dist/894-894.js.map

This file was deleted.

4 changes: 2 additions & 2 deletions dist/files_sharing-files_sharing_tab.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/files_sharing-files_sharing_tab.js.map

Large diffs are not rendered by default.

0 comments on commit 486ed0f

Please sign in to comment.