]> source.dussan.org Git - nextcloud-server.git/commitdiff
Add accesibility features to quick share dropdown
authorfenn-cs <fenn25.fn@gmail.com>
Tue, 5 Sep 2023 15:51:58 +0000 (16:51 +0100)
committerArthur Schiwon <blizzz@arthur-schiwon.de>
Thu, 7 Sep 2023 13:17:50 +0000 (15:17 +0200)
- 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>
apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue

index 673038b620f432aa437b3d4eb5da750e2caaeb64..a83ef503048484d1e5232f737b6f6a3ebb1df6a3 100644 (file)
@@ -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 (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,
+                       })
 
-                       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()
+               },
        },
 
 }
@@ -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 */
        }