Skip to content

Commit

Permalink
fix(NcAppSidebar): make sidebar a single node to allow using v-show
Browse files Browse the repository at this point in the history
Signed-off-by: Grigorii K. Shartsev <me@shgk.me>
  • Loading branch information
ShGKme committed Jun 25, 2024
1 parent b6f5c28 commit 296869a
Show file tree
Hide file tree
Showing 2 changed files with 262 additions and 11 deletions.
272 changes: 261 additions & 11 deletions src/components/NcAppSidebar/NcAppSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -361,24 +361,25 @@ A working alternative would be using an icon together with an `aria-label`:
</script>
```

### Conditionally show the sidebar
### Conditionally show the sidebar with `open`

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.
If the sidebar should be shown conditionally, you can use `open` prop to define sidebar visibility.
It automatically shows a toggle button to open the sidebar if it is closed.

You can also use `--app-sidebar-offset` CSS variable to preserve space for the toggle button, for example, in top bar of NcAppContent.
You can also use `--app-sidebar-offset` CSS variable to preserve space
for the toggle button, for example, in top bar of `NcAppContent`.

The built-in toggle button can be removed with `no-toggle` prop.

Note: the built-in toggle button is only available then NcAppSidebar is used in NcContent.

```vue
<template>
<!-- This is in most cases NcContent -->
<NcContent app-name="styleguidist" class="content-styleguidist">
<NcAppContent>
<div class="top-bar">
<NcButton @click.prevent="showSidebar = !showSidebar">
Toggle sidebar
</NcButton>
<NcButton type="primary">Start a call</NcButton>
</div>
</NcAppContent>
<!-- The sidebar -->
Expand Down Expand Up @@ -441,8 +442,16 @@ export default {
}
</style>
```
</docs>

### Conditionally show the sidebar programmatically with `v-if`

If the sidebar should be shown conditionally without any explicit toggle button, you can use `v-if`.

**Note about performance**: using `v-if` might result in bad performance and loosing sidebar content state.

**Note about `v-show`**: using `v-show` to hide sidebar will result in usability issues due to active focus trap on mobile.

```vue
<template>
<NcButton v-if="!open && !noToggle"
ref="toggle"
Expand Down Expand Up @@ -607,6 +616,225 @@ export default {
</transition>
</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-styleguidist {
position: relative !important;
/* 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;
}
}

.top-bar {
display: flex;
justify-content: flex-end;
/* preserve space for toggle button */
padding-inline-end: var(--app-sidebar-offset);
/* same as on toggle button, but doesn't have to be the same */
margin: var(--app-sidebar-padding);
}
</style>
```
</docs>

<template>
<transition appear
name="slide-right"
@before-enter="onBeforeEnter"
@after-enter="onAfterEnter"
@before-leave="onBeforeLeave"
@after-leave="onAfterLeave">
<aside v-show="open"
id="app-sidebar-vue"
ref="sidebar"
class="app-sidebar"
:aria-labelledby="`app-sidebar-vue-${uid}__header`"
@keydown.esc="onKeydownEsc">
<!--
We cannot render toggle button inside sidebar (aside#app-sidebar-vue), because it is hidden then the toggle is needed.
But we also need transition with the sidebar to be the root of this component to use it as a single UI element, allowing to use `v-show`.
So we cannot render the toggle button directly in this component.
As a simple solution - render it in the content to keep correct position.
-->
<Teleport v-if="ncContentSelector && !open && !noToggle" :selector="ncContentSelector">
<NcButton :aria-label="t('Open sidebar')"
class="app-sidebar__toggle"
:class="toggleClasses"
type="tertiary"
v-bind="toggleAttrs"
@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>
</Teleport>

<header :class="{
'app-sidebar-header--with-figure': hasFigure,
'app-sidebar-header--compact': compact,
}"
class="app-sidebar-header">
<!-- container for figure and description, allows easy switching to compact mode -->
<div class="app-sidebar-header__info">
<!-- sidebar header illustration/figure -->
<div v-if="hasFigure && !empty"
:class="{
'app-sidebar-header__figure--with-action': hasFigureClickListener
}"
class="app-sidebar-header__figure"
:style="{
backgroundImage: `url(${background})`
}"
tabindex="0"
@click="onFigureClick"
@keydown.enter="onFigureClick">
<slot class="app-sidebar-header__background" name="header" />
</div>

<!-- sidebar details -->
<div v-if="!empty"
:class="{
'app-sidebar-header__desc--with-tertiary-action': canStar || $slots['tertiary-actions'],
'app-sidebar-header__desc--editable': nameEditable && !subname,
'app-sidebar-header__desc--with-subname--editable': nameEditable && subname,
'app-sidebar-header__desc--without-actions': !$slots['secondary-actions'],
}"
class="app-sidebar-header__desc">
<!-- favourite icon -->
<div v-if="canStar || $slots['tertiary-actions']" class="app-sidebar-header__tertiary-actions">
<slot name="tertiary-actions">
<NcButton v-if="canStar"
:aria-label="favoriteTranslated"
:pressed="isStarred"
class="app-sidebar-header__star"
type="secondary"
@click.prevent="toggleStarred">
<template #icon>
<NcLoadingIcon v-if="starLoading" />
<Star v-else-if="isStarred" :size="20" />
<StarOutline v-else :size="20" />
</template>
</NcButton>
</slot>
</div>

<!-- name -->
<div class="app-sidebar-header__name-container">
<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 : -1"
@click.self="editName">
{{ name }}
</h2>
<template v-if="nameEditable">
<form v-click-outside="() => onSubmitName()"
class="app-sidebar-header__mainname-form"
@submit.prevent="onSubmitName">
<input ref="nameInput"
v-focus
class="app-sidebar-header__mainname-input"
type="text"
:placeholder="namePlaceholder"
:value="name"
@keydown.esc.stop="onDismissEditing"
@input="onNameInput">
<NcButton type="tertiary-no-background"
:aria-label="changeNameTranslated"
native-type="submit">
<template #icon>
<ArrowRight :size="20" />
</template>
</NcButton>
</form>
</template>
<!-- header main menu -->
<NcActions v-if="$slots['secondary-actions']"
class="app-sidebar-header__menu"
:force-menu="forceMenu">
<slot name="secondary-actions" />
</NcActions>
</div>
<!-- secondary name -->
<p v-if="subname.trim() !== '' || $slots['subname']"
:title="subtitle || undefined"
class="app-sidebar-header__subname">
<!-- @slot Alternative to the `subname` prop can be used for more complex conent. It will be rendered within a `p` tag. -->
<slot name="subname">
{{ subname }}
</slot>
</p>
</div>
</div>
</div>

<NcButton ref="closeButton"
:title="closeTranslated"
:aria-label="closeTranslated"
type="tertiary"
class="app-sidebar__close"
@click.prevent="closeSidebar">
<template #icon>
<Close :size="20" />
</template>
</NcButton>

<div v-if="$slots['description'] && !empty" class="app-sidebar-header__description">
<slot name="description" />
</div>
</header>

<NcAppSidebarTabs v-show="!loading"
ref="tabs"
:active="active"
@update:active="onUpdateActive">
<slot />
</NcAppSidebarTabs>

<NcEmptyContent v-if="loading">
<template #icon>
<NcLoadingIcon :size="64" />
</template>
</NcEmptyContent>
</aside>
</transition>
</template>

<script>
import NcAppSidebarTabs from './NcAppSidebarTabs.vue'
import NcActions from '../NcActions/index.js'
Expand Down Expand Up @@ -652,7 +880,12 @@ export default {
ClickOutside,
},

inheritAttrs: false,
inject: {
ncContentSelector: {
from: 'NcContent:selector',
default: undefined,
},
},

props: {
active: {
Expand Down Expand Up @@ -852,11 +1085,15 @@ export default {

open() {
this.toggleFocusTrap()

this.checkToggleButtonContainerAvailability()
},
},

created() {
this.preserveElementToReturnFocus()

this.checkToggleButtonContainerAvailability()
},

mounted() {
Expand Down Expand Up @@ -1040,6 +1277,19 @@ export default {
this.$refs.tabs.focusActiveTabContent()
},

/**
* Check if the toggle button container is available
*/
checkToggleButtonContainerAvailability() {
// Toggle button must be rendered, but there is no element to teleport it to
if (this.open === false && !this.noToggle && !this.ncContentSelector) {
console.warn(
'[NcAppSidebar] It looks like you want to use NcAppSidebar with the built-in toggle button. '
+ 'This feature is only available when NcAppSidebar is used in NcContent.',
)
}
},

/**
* Emit name change event to parent component
*
Expand Down
1 change: 1 addition & 0 deletions src/components/NcContent/NcContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export default {
provide() {
return {
'NcContent:setHasAppNavigation': this.setAppNavigation,
'NcContent:selector': '#content-vue',
}
},
props: {
Expand Down

0 comments on commit 296869a

Please sign in to comment.