Skip to content

Commit

Permalink
Merge pull request #5219 from nextcloud-libraries/fix/nc-app-sidebar-…
Browse files Browse the repository at this point in the history
…-auto-return-focus

feat(NcAppSidebar): move focus to sidebar on open and auto return focus on close
  • Loading branch information
JuliaKirschenheuter committed Feb 7, 2024
2 parents 06fb526 + 6a9e2fa commit 7974b1b
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 5 deletions.
7 changes: 6 additions & 1 deletion src/components/NcActions/NcActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1249,11 +1249,16 @@ export default {
*/
this.$emit('open')
},
closeMenu(returnFocus = true) {
async closeMenu(returnFocus = true) {
if (!this.opened) {
return
}
// Wait for the next tick to keep the menu in DOM, allowing other components to find what button in what menu was used,
// for example, to implement auto set return focus.
// NcPopover will actually remove the menu from DOM also on the next tick.
await this.$nextTick()
this.opened = false
this.$refs.popover.clearFocusTrap({ returnFocus })
Expand Down
59 changes: 57 additions & 2 deletions src/components/NcAppSidebar/NcAppSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ export default {
<aside id="app-sidebar-vue"
ref="sidebar"
class="app-sidebar"
:aria-labelledby="`app-sidebar-vue-${uid}__header`"
@keydown.esc.stop="isMobile && closeSidebar()">
<header :class="{
'app-sidebar-header--with-figure': hasFigure,
Expand Down Expand Up @@ -403,11 +404,13 @@ export default {
<div class="app-sidebar-header__mainname-container">
<!-- main name -->
<h2 v-show="!nameEditable"
:id="`app-sidebar-vue-${uid}__header`"
ref="header"
v-linkify="{text: name, linkify: linkifyName}"
:aria-label="title"
:title="title"
class="app-sidebar-header__mainname"
:tabindex="nameEditable ? 0 : undefined"
:tabindex="nameEditable ? 0 : -1"
@click.self="editName">
{{ name }}
</h2>
Expand Down Expand Up @@ -492,6 +495,7 @@ import Focus from '../../directives/Focus/index.js'
import Linkify from '../../directives/Linkify/index.js'
import Tooltip from '../../directives/Tooltip/index.js'
import { useIsSmallMobile } from '../../composables/useIsMobile/index.js'
import GenRandomId from '../../utils/GenRandomId.js'
import { getTrapStack } from '../../utils/focusTrap.js'
import { t } from '../../l10n.js'
Expand Down Expand Up @@ -650,6 +654,7 @@ export default {
setup() {
return {
uid: GenRandomId(),
isMobile: useIsSmallMobile(),
}
},
Expand All @@ -661,6 +666,7 @@ export default {
favoriteTranslated: t('Favorite'),
isStarred: this.starred,
focusTrap: null,
elementToReturnFocus: null,
}
},
Expand All @@ -686,7 +692,16 @@ export default {
},
},
created() {
this.preserveElementToReturnFocus()
},
mounted() {
// Focus sidebar on open only if it was opened by a user interaction
if (this.elementToReturnFocus) {
this.focus()
}
this.toggleFocusTrap()
},
Expand All @@ -697,6 +712,23 @@ export default {
},
methods: {
preserveElementToReturnFocus() {
// Save the element that had focus before the sidebar was opened to return back on close
if (document.activeElement && document.activeElement !== document.body) {
this.elementToReturnFocus = document.activeElement
// Special case for menus (NcActions)
// If a sidebar was opened from a menu item, we want to return focus to the menu trigger instead of the item
if (this.elementToReturnFocus.getAttribute('role') === 'menuitem') {
const menu = this.elementToReturnFocus.closest('[role="menu"]')
if (menu) {
const menuTrigger = document.querySelector(`[aria-controls="${menu.id}"]`)
this.elementToReturnFocus = menuTrigger
}
}
}
},
initFocusTrap() {
if (this.focusTrap) {
return
Expand All @@ -721,7 +753,7 @@ export default {
/**
* Activate focus trap if it is currently needed, otherwise deactivate
*/
toggleFocusTrap() {
toggleFocusTrap() {
if (this.isMobile) {
this.initFocusTrap()
this.focusTrap.activate()
Expand Down Expand Up @@ -761,6 +793,10 @@ export default {
* @type {HTMLElement}
*/
this.$emit('closed', element)
// Return focus to the element that had focus before the sidebar was opened
this.elementToReturnFocus?.focus({ focusVisible: true })
this.elementToReturnFocus = null
},
/**
Expand Down Expand Up @@ -820,6 +856,25 @@ export default {
}
},
/**
* Focus the sidebar
* @public
*/
focus() {
this.$refs.header.focus()
},
/**
* Focus the active tab
* @public
*/
focusActiveTabContent() {
// If a tab is focused then probably a new trigger element moved the focus to the sidebar
this.preserveElementToReturnFocus()
this.$refs.tabs.focusActiveTabContent()
},
/**
* Emit name change event to parent component
*
Expand Down
4 changes: 2 additions & 2 deletions src/components/NcAppSidebarTab/NcAppSidebarTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
:aria-label="isTablistShown() ? undefined : name"
:aria-labelledby="isTablistShown() ? `tab-button-${id}` : undefined"
class="app-sidebar__tab"
tabindex="0"
role="tabpanel"
:tabindex="isTablistShown() ? 0 : -1"
:role="isTablistShown() ? 'tabpanel' : undefined"
@scroll="onScroll">
<h3 class="hidden-visually">
{{ name }}
Expand Down

0 comments on commit 7974b1b

Please sign in to comment.