mirror of
https://github.com/nextcloud/server.git
synced 2026-06-09 08:44:07 -04:00
Add accesibility features to quick share dropdown
- 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 <fenn25.fn@gmail.com>
This commit is contained in:
parent
f75684ec7e
commit
faa76b94b8
1 changed files with 98 additions and 16 deletions
|
|
@ -1,16 +1,30 @@
|
|||
<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 }">
|
||||
<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"
|
||||
:aria-labelledby="dropdownId"
|
||||
tabindex="0"
|
||||
@keydown.down="handleArrowDown"
|
||||
@keydown.up="handleArrowUp"
|
||||
@keydown.esc="closeDropdown">
|
||||
<button v-for="option in options"
|
||||
:key="option"
|
||||
:class="{ 'dropdown-item': true, 'selected': option === selectedOption }"
|
||||
:aria-selected="option === selectedOption"
|
||||
@click="selectOption(option)">
|
||||
{{ option }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -26,6 +40,8 @@ import {
|
|||
ATOMIC_PERMISSIONS,
|
||||
} from '../lib/SharePermissionsToolBox.js'
|
||||
|
||||
import { createFocusTrap } from 'focus-trap'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DropdownIcon,
|
||||
|
|
@ -45,6 +61,7 @@ export default {
|
|||
return {
|
||||
selectedOption: '',
|
||||
showDropdown: this.toggle,
|
||||
focusTrap: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -102,6 +119,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) {
|
||||
|
|
@ -110,15 +131,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.clearFocusTrap()
|
||||
this.showDropdown = false
|
||||
},
|
||||
selectOption(option) {
|
||||
this.selectedOption = option
|
||||
|
|
@ -134,12 +166,50 @@ export default {
|
|||
this.selectedOption = this.preSelectedOption
|
||||
},
|
||||
handleClickOutside(event) {
|
||||
const dropdownElement = this.$refs.quickShareDropdown;
|
||||
const dropdownContainer = this.$refs.quickShareDropdownContainer
|
||||
|
||||
if (dropdownElement && !dropdownElement.contains(event.target)) {
|
||||
this.showDropdown = false;
|
||||
if (dropdownContainer && !dropdownContainer.contains(event.target)) {
|
||||
this.showDropdown = false
|
||||
}
|
||||
},
|
||||
},
|
||||
useFocusTrap() {
|
||||
// Create global stack if undefined
|
||||
// Use in with trapStack to avoid conflicting traps
|
||||
Object.assign(window, { _nc_focus_trap: window._nc_focus_trap || [] })
|
||||
const dropdownElement = this.$refs.quickShareDropdown
|
||||
this.focusTrap = createFocusTrap(dropdownElement, {
|
||||
allowOutsideClick: true,
|
||||
trapStack: window._nc_focus_trap,
|
||||
})
|
||||
|
||||
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()
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
|
|
@ -159,8 +229,10 @@ export default {
|
|||
color: var(--color-primary-element);
|
||||
}
|
||||
|
||||
.share-select-dropdown-container {
|
||||
.share-select-dropdown {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background-color: var(--color-main-background);
|
||||
|
|
@ -172,6 +244,16 @@ 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;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background-color: #f2f2f2;
|
||||
|
|
@ -184,13 +266,13 @@ export default {
|
|||
}
|
||||
|
||||
/* Optional: Add a transition effect for smoother dropdown animation */
|
||||
.share-select-dropdown-container {
|
||||
.share-select-dropdown {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
&.active .share-select-dropdown-container {
|
||||
&.active .share-select-dropdown {
|
||||
max-height: 200px;
|
||||
/* Adjust the value to your desired height */
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue