Skip to content

Commit 4490340

Browse files
committed
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>
1 parent 300d6e1 commit 4490340

1 file changed

Lines changed: 92 additions & 13 deletions

File tree

apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,32 @@
11
<template>
2-
<div :class="{ 'active': showDropdown, 'share-select': true }" ref="quickShareDropdown">
3-
<span class="trigger-text" @click="toggleDropdown">
2+
<div ref="quickShareDropdownContainer"
3+
:class="{ 'active': showDropdown, 'share-select': true }"
4+
tabindex="0"
5+
@keydown.down="handleArrowDown"
6+
@keydown.up="handleArrowUp">
7+
<span :id="dropdownId"
8+
class="trigger-text"
9+
:aria-expanded="showDropdown"
10+
:aria-haspopup="true"
11+
aria-label="Quick share options dropdown"
12+
@click="toggleDropdown">
413
{{ selectedOption }}
514
<DropdownIcon :size="15" />
615
</span>
7-
<div v-if="showDropdown" class="share-select-dropdown-container">
8-
<div v-for="option in options"
16+
<div v-if="showDropdown"
17+
ref="quickShareDropdown"
18+
class="share-select-dropdown-container"
19+
:aria-labelledby="dropdownId"
20+
tabindex="0"
21+
@keydown.esc="closeDropdown">
22+
<button v-for="option in options"
923
:key="option"
1024
:class="{ 'dropdown-item': true, 'selected': option === selectedOption }"
25+
tabindex="0"
26+
:aria-selected="option === selectedOption"
1127
@click="selectOption(option)">
1228
{{ option }}
13-
</div>
29+
</button>
1430
</div>
1531
</div>
1632
</template>
@@ -26,6 +42,8 @@ import {
2642
ATOMIC_PERMISSIONS,
2743
} from '../lib/SharePermissionsToolBox.js'
2844
45+
import { createFocusTrap } from 'focus-trap'
46+
2947
export default {
3048
components: {
3149
DropdownIcon,
@@ -45,6 +63,7 @@ export default {
4563
return {
4664
selectedOption: '',
4765
showDropdown: this.toggle,
66+
focusTrap: null,
4867
}
4968
},
5069
computed: {
@@ -102,6 +121,10 @@ export default {
102121
return BUNDLED_PERMISSIONS.READ_ONLY
103122
}
104123
},
124+
dropdownId() {
125+
// Generate a unique ID for ARIA attributes
126+
return `dropdown-${Math.random().toString(36).substr(2, 9)}`
127+
},
105128
},
106129
watch: {
107130
toggle(toggleValue) {
@@ -110,15 +133,26 @@ export default {
110133
},
111134
mounted() {
112135
this.initializeComponent()
113-
window.addEventListener('click', this.handleClickOutside);
136+
window.addEventListener('click', this.handleClickOutside)
114137
},
115138
beforeDestroy() {
116-
// Remove the global click event listener to prevent memory leaks
117-
window.removeEventListener('click', this.handleClickOutside);
118-
},
139+
// Remove the global click event listener to prevent memory leaks
140+
window.removeEventListener('click', this.handleClickOutside)
141+
},
119142
methods: {
120143
toggleDropdown() {
121144
this.showDropdown = !this.showDropdown
145+
if (this.showDropdown) {
146+
this.$nextTick(() => {
147+
this._useFocusTrap()
148+
})
149+
} else {
150+
this._clearFocusTrap()
151+
}
152+
},
153+
closeDropdown() {
154+
this.showDropdown = false
155+
this._clearFocusTrap()
122156
},
123157
selectOption(option) {
124158
this.selectedOption = option
@@ -134,12 +168,46 @@ export default {
134168
this.selectedOption = this.preSelectedOption
135169
},
136170
handleClickOutside(event) {
137-
const dropdownElement = this.$refs.quickShareDropdown;
171+
const dropdownContainer = this.$refs.quickShareDropdownContainer
172+
173+
if (dropdownContainer && !dropdownContainer.contains(event.target)) {
174+
this.showDropdown = false
175+
}
176+
},
177+
_useFocusTrap() {
178+
const dropdownElement = this.$refs.quickShareDropdown
179+
this.focusTrap = createFocusTrap(dropdownElement, {
180+
allowOutsideClick: true,
181+
})
138182
139-
if (dropdownElement && !dropdownElement.contains(event.target)) {
140-
this.showDropdown = false;
183+
this.focusTrap.activate()
184+
},
185+
_clearFocusTrap() {
186+
this.focusTrap?.deactivate()
187+
this.focusTrap = null
188+
},
189+
shiftFocusForward() {
190+
const currentElement = document.activeElement
191+
let nextElement = currentElement.nextElementSibling
192+
if (nextElement) {
193+
nextElement = this.$refs.quickShareDropdown.firstElementChild
194+
}
195+
nextElement.focus()
196+
},
197+
shiftFocusBackward() {
198+
const currentElement = document.activeElement
199+
let previousElement = currentElement.previousElementSibling
200+
if (!previousElement) {
201+
previousElement = this.$refs.quickShareDropdown.lastElementChild
141202
}
142-
},
203+
previousElement.focus()
204+
},
205+
handleArrowUp() {
206+
this.shiftFocusBackward()
207+
},
208+
handleArrowDown() {
209+
this.shiftFocusForward()
210+
},
143211
},
144212
145213
}
@@ -161,6 +229,8 @@ export default {
161229
162230
.share-select-dropdown-container {
163231
position: absolute;
232+
display: flex;
233+
flex-direction: column;
164234
top: 100%;
165235
left: 0;
166236
background-color: var(--color-main-background);
@@ -172,6 +242,15 @@ export default {
172242
.dropdown-item {
173243
padding: 8px;
174244
font-size: 12px;
245+
background: none;
246+
border: none;
247+
border-radius: 0;
248+
font: inherit;
249+
cursor: pointer;
250+
color: inherit;
251+
outline: none;
252+
width: 100%;
253+
white-space: nowrap;
175254
176255
&:hover {
177256
background-color: #f2f2f2;

0 commit comments

Comments
 (0)