Skip to content

Commit

Permalink
Merge pull request #11600 from owncloud/fix/more-a11y-fixes
Browse files Browse the repository at this point in the history
fix(a11y): keyboard navigation
  • Loading branch information
JammingBen authored Sep 19, 2024
2 parents 8a69d47 + 93b9d81 commit 22b7944
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 111 deletions.
4 changes: 4 additions & 0 deletions changelog/unreleased/enhancement-a11y-improvements
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ The following accessibility improvements have been made:
- An issue where the current focus would not change when navigating the resource table/tiles with the keyboard has been fixed.
- The contrasts of input borders have been aligned with the official WCAG requirements.
- The space member search in the right sidebar now has a label. Also, the search has been moved from the top of the sharing panel to the members section where all members are listed.
- Leaving dropdown menus via the keyboard navigation now closes them.
- When the sidebar is open and uses 100% of the screen width (mobile view), it is no longer possible to focus elements in the background.

https://github.com/owncloud/web/pull/11574
https://github.com/owncloud/web/pull/11591
https://github.com/owncloud/web/pull/11600
https://github.com/owncloud/web/issues/10725
https://github.com/owncloud/web/issues/10729
https://github.com/owncloud/web/issues/10724
https://github.com/owncloud/web/issues/10733
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const dom = ({ position = 'auto', mode = 'click', paddingSize = 'medium' } = {})
}
)
const drop = wrapper.findComponent({ name: 'oc-drop' })
const tippy = drop.vm.$data.tippy
const tippy = drop.vm.tippy

return { wrapper, drop, tippy }
}
Expand Down
70 changes: 36 additions & 34 deletions packages/design-system/src/components/OcDrop/OcDrop.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
<template>
<div
:id="dropId"
ref="drop"
class="oc-drop oc-box-shadow-medium oc-rounded"
@click="$_ocDrop_close"
>
<div :id="dropId" ref="drop" class="oc-drop oc-box-shadow-medium oc-rounded" @click="onClick">
<div
v-if="$slots.default"
:class="['oc-card oc-card-body oc-background-secondary', paddingClass]"
Expand All @@ -23,7 +18,7 @@ import { destroy, hideOnEsc } from '../../directives/OcTooltip'
import { AVAILABLE_SIZES } from '../../helpers/constants'
import uniqueId from '../../utils/uniqueId'
import { getSizeClass } from '../../utils/sizeClasses'
import { defineComponent } from 'vue'
import { defineComponent, onBeforeUnmount, onMounted, ref, unref, useTemplateRef } from 'vue'
/**
* Position any element in relation to another element.
Expand Down Expand Up @@ -125,8 +120,40 @@ export default defineComponent({
}
},
emits: ['hideDrop', 'showDrop'],
data() {
return { tippy: null }
setup(props) {
const drop = useTemplateRef<HTMLElement>('drop')
const tippy = ref(null)
const show = (duration?: number) => {
unref(tippy)?.show(duration)
}
const hide = (duration?: number) => {
unref(tippy)?.hide(duration)
}
const onClick = () => {
if (props.closeOnClick) {
hide()
}
}
const onFocusOut = (event: FocusEvent) => {
const focusLeft = event.relatedTarget && !unref(drop).contains(event.relatedTarget as Node)
if (focusLeft) {
// close drop when the focus leaves it
hide()
}
}
onMounted(() => {
unref(drop).addEventListener('focusout', onFocusOut)
})
onBeforeUnmount(() => {
unref(drop).removeEventListener('focusout', onFocusOut)
})
return { drop, tippy, show, hide, onClick }
},
computed: {
triggerMapping() {
Expand Down Expand Up @@ -227,31 +254,6 @@ export default defineComponent({
}
this.tippy = tippy(to, config)
},
methods: {
$_ocDrop_close() {
if (this.closeOnClick) {
this.tippy.hide()
}
},
/**
* Programatically show the drop
*
* @public
*/
show(duration?: number) {
this.tippy.show(duration)
},
/**
* Programatically hide the drop
*
* @public
*/
hide(duration?: number) {
this.tippy.hide(duration)
}
}
})
</script>
Expand Down
10 changes: 8 additions & 2 deletions packages/web-app-admin-settings/src/views/Users.vue
Original file line number Diff line number Diff line change
Expand Up @@ -345,14 +345,20 @@ export default defineComponent({
const filterGroups = (groups: Group[]) => {
filters.groups.ids.value = groups.map((g) => g.id)
loadUsersTask.perform()
userSettingsStore.setSelectedUsers([])
if (userSettingsStore.selectedUsers.length) {
// only reset selection if there are selected users because is messes with the focus otherwise
userSettingsStore.setSelectedUsers([])
}
additionalUserDataLoadedForUserIds.value = []
return resetPagination()
}
const filterRoles = (roles: AppRole[]) => {
filters.roles.ids.value = roles.map((r) => r.id)
loadUsersTask.perform()
userSettingsStore.setSelectedUsers([])
if (userSettingsStore.selectedUsers.length) {
// only reset selection if there are selected users because is messes with the focus otherwise
userSettingsStore.setSelectedUsers([])
}
additionalUserDataLoadedForUserIds.value = []
return resetPagination()
}
Expand Down
2 changes: 1 addition & 1 deletion packages/web-pkg/src/components/ItemFilter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export default defineComponent({
const showDrop = async () => {
setDisplayedItems(props.items)
await nextTick()
unref(filterInputRef).focus()
unref(filterInputRef)?.focus()
}
watch(filterTerm, () => {
Expand Down
153 changes: 88 additions & 65 deletions packages/web-pkg/src/components/SideBar/SideBar.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<template>
<div
id="app-sidebar"
ref="appSideBar"
data-testid="app-sidebar"
:class="{
'has-active-sub-panel': hasActiveSubPanel,
'oc-flex oc-flex-center oc-flex-middle': loading
'oc-flex oc-flex-center oc-flex-middle': loading,
'app-sidebar-full-width': fullWidthSideBar
}"
>
<oc-spinner v-if="loading" />
Expand Down Expand Up @@ -95,7 +97,17 @@

<script lang="ts">
import { VisibilityObserver } from '../../observer'
import { computed, defineComponent, nextTick, PropType, ref, unref, watch } from 'vue'
import {
computed,
defineComponent,
nextTick,
onBeforeUnmount,
PropType,
ref,
unref,
useTemplateRef,
watch
} from 'vue'
import { SideBarPanel, SideBarPanelContext } from './types'
import { useGettext } from 'vue3-gettext'
Expand Down Expand Up @@ -129,6 +141,8 @@ export default defineComponent({
emits: ['close', 'selectPanel'],
setup(props) {
const { $gettext } = useGettext()
const appSideBar = useTemplateRef<HTMLElement>('appSideBar')
const panelContainer = useTemplateRef<HTMLElement[]>('panelContainer')
const rootPanels = computed(() => {
return props.availablePanels.filter(
Expand Down Expand Up @@ -201,59 +215,7 @@ export default defineComponent({
return $gettext('Back to main panels')
})
return {
displayPanels,
rootPanels,
subPanels,
activeSubPanelName,
activePanelName,
oldPanelName,
clearOldPanelName,
setOldPanelName,
hasActiveSubPanel,
hasActiveRootPanel,
accessibleLabelBack,
focussedElementId
}
},
data() {
return {
selectedFile: {}
}
},
watch: {
isOpen: {
handler: function (isOpen) {
if (!isOpen) {
return
}
this.$nextTick(() => {
this.initVisibilityObserver()
})
},
immediate: true
}
},
beforeUnmount() {
visibilityObserver.disconnect()
hiddenObserver.disconnect()
},
methods: {
setSidebarPanel(panel: string) {
this.$emit('selectPanel', panel)
},
resetSidebarPanel() {
this.$emit('selectPanel', null)
},
closeSidebar() {
this.$emit('close')
},
initVisibilityObserver() {
const initVisibilityObserver = () => {
visibilityObserver = new VisibilityObserver({
root: document.querySelector('#app-sidebar'),
threshold: 0.9
Expand All @@ -263,31 +225,93 @@ export default defineComponent({
threshold: 0.05
})
const doFocus = () => {
if (!this.focussedElementId) {
if (!unref(focussedElementId)) {
return
}
const element = document.getElementById(this.focussedElementId)
const element = document.getElementById(unref(focussedElementId))
if (!element) {
return
}
element.focus()
}
if (!this.$refs.panelContainer) {
if (!unref(panelContainer)) {
return
}
visibilityObserver.disconnect()
hiddenObserver.disconnect()
;(this.$refs.panelContainer as HTMLElement[]).forEach((panel) => {
unref(panelContainer).forEach((panel) => {
visibilityObserver.observe(panel, {
onEnter: doFocus,
onExit: doFocus
})
hiddenObserver.observe(panel, {
onExit: this.clearOldPanelName
onExit: clearOldPanelName
})
})
}
const fullWidthSideBar = computed(() => window.innerWidth <= 960)
const backgroundContentEl = computed(() => {
return unref(appSideBar).parentElement.querySelector('div') as HTMLElement
})
watch(
() => props.isOpen,
async (isOpen) => {
if (!isOpen) {
return
}
await nextTick()
initVisibilityObserver()
if (unref(fullWidthSideBar) && unref(backgroundContentEl)) {
// hide content behind sidebar when it has full width to avoid focusable elements
unref(backgroundContentEl).style.visibility = 'hidden'
}
},
{ immediate: true }
)
onBeforeUnmount(() => {
visibilityObserver.disconnect()
hiddenObserver.disconnect()
if (unref(backgroundContentEl)) {
unref(backgroundContentEl).style.visibility = 'visible'
}
})
return {
appSideBar,
displayPanels,
rootPanels,
subPanels,
activeSubPanelName,
activePanelName,
oldPanelName,
clearOldPanelName,
setOldPanelName,
hasActiveSubPanel,
hasActiveRootPanel,
accessibleLabelBack,
focussedElementId,
fullWidthSideBar
}
},
methods: {
setSidebarPanel(panel: string) {
this.$emit('selectPanel', panel)
},
resetSidebarPanel() {
this.$emit('selectPanel', null)
},
closeSidebar() {
this.$emit('close')
},
openPanel(panel: string) {
Expand All @@ -311,13 +335,12 @@ export default defineComponent({
min-width: 440px;
width: 440px;
}
.app-sidebar-full-width {
min-width: 100% !important;
width: 100% !important;
}
@media only screen and (max-width: 960px) {
#app-sidebar {
min-width: 100%;
width: 100%;
}
.files-wrapper {
flex-wrap: nowrap !important;
}
Expand Down
Loading

0 comments on commit 22b7944

Please sign in to comment.