Merge pull request #5606 from nextcloud-libraries/feat/backport-app-sidebar

[next]  feat(NcAppSidebar): Allow to set open state to prevent focus trap issues on mobile #5584
This commit is contained in:
Ferdinand Thiessen 2024-05-17 17:08:15 +02:00 коммит произвёл GitHub
Родитель 1f36a3bc72 f3356bca50
Коммит 7daf58978a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
2 изменённых файлов: 156 добавлений и 40 удалений

Просмотреть файл

@ -238,6 +238,9 @@ msgstr ""
msgid "Open navigation"
msgstr ""
msgid "Open sidebar"
msgstr ""
msgid "Options"
msgstr ""

Просмотреть файл

@ -27,6 +27,10 @@ This component provides a way to include the standardised sidebar.
The standard properties like name, subname, starred, etc. allow to automatically
include a standard-header like it's used by the files app.
To conditionally show the sidebar either use `v-if` on the sidebar component,
or use the `open` property of the component to controll the state.
Using `v-show` directly will result in usability issues due to internal focus trap handling.
### Standard usage
```vue
@ -373,16 +377,98 @@ A working alternative would be using an icon together with an `aria-label`:
}
</script>
```
### Conditionally show the sidebar
If the sidebar should be shown conditionally (e.g. using a button)
and the users are expected to open and close the sidebar multiple times,
then using `v-if` might result in bad performance.
So instead use the `open` property:
```vue
<template>
<!-- This is in most cases NcContent -->
<div class="content-wrapper">
<!-- The main content - In most cases NcAppContent -->
<div>
<NcButton @click.prevent="showSidebar = !showSidebar">
Toggle sidebar
</NcButton>
</div>
<!-- The sidebar -->
<NcAppSidebar
v-model:open="showSidebar"
name="cat-picture.jpg"
subname="last edited 3 weeks ago">
<NcAppSidebarTab name="Settings" id="settings-tab">
<template #icon>
<Cog :size="20" />
</template>
Single tab content
</NcAppSidebarTab>
</NcAppSidebar>
</div>
</template>
<script>
import Cog from 'vue-material-design-icons/Cog'
export default {
components: {
Cog,
},
data() {
return {
showSidebar: true,
}
},
}
</script>
<style scoped>
/* This styles just mock NcContent and NcAppContent */
.content-wrapper {
position: relative;
/* Just to prevent jumping when the sidebar is hidden */
min-height: 360px;
}
.main-content {
position: absolute;
height: 100%;
width: 100%;
}
/* Fix styles on this style guide page */
@media only screen and (max-width: 512px) {
:deep(aside) {
width: calc(100vw - 64px) !important;
}
}
</style>
```
</docs>
<template>
<NcButton v-if="!open"
:aria-label="t('Open sidebar')"
class="app-sidebar__toggle"
:class="toggleClasses"
type="tertiary"
ref="toggle"
@click="$emit('update:open', true)">
<template #icon>
<!-- @slot Custom icon for the toggle button, defaults to the dock-right icon from MDI -->
<slot name="toggle-icon">
<IconDockRight :size="20" />
</slot>
</template>
</NcButton>
<transition appear
name="slide-right"
@before-enter="onBeforeEnter"
v-bind="$attrs"
@after-enter="onAfterEnter"
@before-leave="onBeforeLeave"
@after-leave="onAfterLeave">
<aside id="app-sidebar-vue"
<aside v-show="open"
id="app-sidebar-vue"
ref="sidebar"
class="app-sidebar"
:aria-labelledby="`app-sidebar-vue-${uid}__header`"
@ -429,8 +515,8 @@ A working alternative would be using an icon together with an `aria-label`:
@click.prevent="toggleStarred">
<template #icon>
<NcLoadingIcon v-if="starLoading" />
<Star v-else-if="isStarred" :size="20" />
<StarOutline v-else :size="20" />
<IconStar v-else-if="isStarred" :size="20" />
<IconStarOutline v-else :size="20" />
</template>
</NcButton>
</slot>
@ -467,7 +553,7 @@ A working alternative would be using an icon together with an `aria-label`:
:aria-label="changeNameTranslated"
native-type="submit">
<template #icon>
<ArrowRight :size="20" />
<IconArrowRight :size="20" />
</template>
</NcButton>
</form>
@ -499,7 +585,7 @@ A working alternative would be using an icon together with an `aria-label`:
class="app-sidebar__close"
@click.prevent="closeSidebar">
<template #icon>
<Close :size="20" />
<IconClose :size="20" />
</template>
</NcButton>
@ -532,17 +618,17 @@ import NcButton from '../NcButton/index.ts'
import NcEmptyContent from '../NcEmptyContent/index.js'
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'
import isSlotPopulated from '../../utils/isSlotPopulated.ts'
import ArrowRight from 'vue-material-design-icons/ArrowRight.vue'
import Close from 'vue-material-design-icons/Close.vue'
import Star from 'vue-material-design-icons/Star.vue'
import StarOutline from 'vue-material-design-icons/StarOutline.vue'
import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue'
import IconClose from 'vue-material-design-icons/Close.vue'
import IconDockRight from 'vue-material-design-icons/DockRight.vue'
import IconStar from 'vue-material-design-icons/Star.vue'
import IconStarOutline from 'vue-material-design-icons/StarOutline.vue'
import { vOnClickOutside as ClickOutside } from '@vueuse/components'
import { createFocusTrap } from 'focus-trap'
@ -553,22 +639,24 @@ export default {
components: {
NcActions,
NcAppSidebarTabs,
ArrowRight,
NcButton,
NcLoadingIcon,
NcEmptyContent,
Close,
Star,
StarOutline,
IconArrowRight,
IconClose,
IconDockRight,
IconStar,
IconStarOutline,
},
directives: {
focus: Focus,
linkify: Linkify,
ClickOutside,
Tooltip,
},
inheritAttrs: false,
props: {
active: {
type: String,
@ -675,19 +763,40 @@ export default {
type: String,
default: '',
},
/**
* Allow to conditionally show the sidebar
* You can also use `v-if` on the sidebar, but using the open prop allow to keep
* the sidebar inside the DOM for performance if it is opened and closed multple times.
*
* When using the `open` property to close the sidebar a built-in toggle button will be shown to reopen it,
* similar to the app navigation.
*/
open: {
type: Boolean,
default: true,
},
/**
* Custom classes to assign to the sidebar toggle button
* If needed this can be used to assign styles to the button using `:deep()` selector.
*/
toggleClasses: {
type: [String, Array, Object],
default: '',
},
},
emits: [
'close',
'closing',
'closed',
'opening',
'opened',
// 'figure-click', not emitted on purpose to make "hasFigureClickListener" work
'update:starred',
'update:nameEditable',
'update:name',
'update:active',
'update:name',
'update:nameEditable',
'update:open',
'update:starred',
'submit-name',
'dismiss-editing',
],
@ -727,6 +836,10 @@ export default {
isMobile() {
this.toggleFocusTrap()
},
open() {
this.toggleFocusTrap()
},
},
created() {
@ -751,6 +864,8 @@ export default {
methods: {
isSlotPopulated,
t,
preserveElementToReturnFocus() {
// Save the element that had focus before the sidebar was opened to return back on close
if (document.activeElement && document.activeElement !== document.body) {
@ -793,7 +908,7 @@ export default {
* Activate focus trap if it is currently needed, otherwise deactivate
*/
toggleFocusTrap() {
if (this.isMobile) {
if (this.open && this.isMobile) {
this.initFocusTrap()
this.focusTrap.activate()
} else {
@ -813,14 +928,6 @@ export default {
}
},
onBeforeEnter(element) {
/**
* The sidebar is opening and the transition is in progress
*
* @type {HTMLElement}
*/
this.$emit('opening', element)
},
onAfterEnter(element) {
/**
* The sidebar is opened and the transition is complete
@ -829,14 +936,6 @@ export default {
*/
this.$emit('opened', element)
},
onBeforeLeave(element) {
/**
* The sidebar is closing and the transition is in progress
*
* @type {HTMLElement}
*/
this.$emit('closing', element)
},
onAfterLeave(element) {
/**
* The sidebar is closed and the transition is complete
@ -862,6 +961,11 @@ export default {
* @type {Event}
*/
this.$emit('close', e)
/**
* Current open state emitted after the transitions are finished
* @type {boolean}
*/
this.$emit('update:open', false)
},
/**
@ -912,7 +1016,7 @@ export default {
* @public
*/
focus() {
this.$refs.header.focus()
(this.$refs.header ?? this.$refs.toggle)?.focus()
},
/**
@ -1012,6 +1116,15 @@ $top-buttons-spacing: 6px;
border-left: 1px solid var(--color-border);
background: var(--color-main-background);
&__toggle {
--sidebar-toggle-position: #{$app-navigation-padding};
position: absolute !important;
inset-block-start: var(--sidebar-toggle-position);
inset-inline-end: var(--sidebar-toggle-position);
// app-content has z-index 1000 so we need 1001
z-index: 1001;
}
.app-sidebar-header {
> .app-sidebar__close {
position: absolute;