Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/design-system/docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import './tailwind.css'
import DefaultTheme from 'vitepress/theme-without-fonts'
import './../../../src/styles/layers.css'
import { createGettext } from 'vue3-gettext'
import { createPinia } from 'pinia'
import * as components from './../../../src/components'
import * as directives from './../../../src/directives'
import './custom.scss'
Expand All @@ -12,7 +13,9 @@ export default {
extends: DefaultTheme,
enhanceApp({ app }) {
const gettext = createGettext()
const pinia = createPinia()
app.use(gettext)
app.use(pinia)

app.component('LiveCodeBlock', LiveCodeBlock)
app.component('ComponentApi', ComponentApi)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@
drawer-id="example-bottom-drawer-child"
toggle="#toggle-bottom-drawer-child"
title="Example Bottom Drawer Child"
:is-nested-element="true"
:nested-parent-ref="parent"
>
<oc-list class="flex flex-col p-2">
<oc-button justify-content="left" appearance="raw" no-hover aria-expanded="false">
Expand All @@ -44,8 +42,7 @@
</template>
<script setup lang="ts">
import { OcBottomDrawer } from '../../../src/components'
import { ref, useTemplateRef } from 'vue'
import { ref } from 'vue'

const parent = useTemplateRef('parent')
const renderChild = ref(false)
</script>
1 change: 1 addition & 0 deletions packages/design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"fuse.js": "^7.0.0",
"lodash-es": "^4.17.21",
"luxon": "^3.5.0",
"pinia": "^3.0.3",
"portal-vue": "^3.0.0",
"tippy.js": "^6.3.7",
"vue-inline-svg": "^4.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defaultPlugins, mount } from '@opencloud-eu/web-test-helpers'
import BottomDrawer from './OcBottomDrawer.vue'
import { defineComponent, nextTick } from 'vue'
import OcButton from '../OcButton/OcButton.vue'
import { useBottomDrawer } from '../../composables'

const selectors = {
toggle: '#button-drawer-toggle',
Expand Down Expand Up @@ -42,8 +43,8 @@ describe('OcBottomDrawer', () => {
expect(wrapper.find(selectors.drawer).exists()).toBe(true)

wrapper.find(selectors.closeButton).trigger('click')
await new Promise((r) => setTimeout(r, 160))
expect(wrapper.find(selectors.drawer).exists()).toBe(false)
const bottomDrawerStore = useBottomDrawer()
expect(bottomDrawerStore.closeAllDrawers).toHaveBeenCalled()
wrapper.unmount()
})

Expand All @@ -54,8 +55,8 @@ describe('OcBottomDrawer', () => {
expect(wrapper.find(selectors.drawer).exists()).toBe(true)

await wrapper.find(selectors.background).trigger('click')
await new Promise((r) => setTimeout(r, 160))
expect(wrapper.find(selectors.drawer).exists()).toBe(false)
const bottomDrawerStore = useBottomDrawer()
expect(bottomDrawerStore.closeAllDrawers).toHaveBeenCalled()
wrapper.unmount()
})

Expand All @@ -68,8 +69,8 @@ describe('OcBottomDrawer', () => {

const esc = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
document.dispatchEvent(esc)
await new Promise((r) => setTimeout(r, 160))
expect(wrapper.find(selectors.drawer).exists()).toBe(false)
const bottomDrawerStore = useBottomDrawer()
expect(bottomDrawerStore.closeAllDrawers).toHaveBeenCalled()
wrapper.unmount()
})

Expand All @@ -81,8 +82,8 @@ describe('OcBottomDrawer', () => {
expect(wrapper.find(selectors.drawer).exists()).toBe(true)

wrapper.find(selectors.actionButton).trigger('click')
await new Promise((r) => setTimeout(r, 160))
expect(wrapper.find(selectors.drawer).exists()).toBe(false)
const bottomDrawerStore = useBottomDrawer()
expect(bottomDrawerStore.closeDrawer).toHaveBeenCalled()
wrapper.unmount()
})

Expand All @@ -94,13 +95,21 @@ describe('OcBottomDrawer', () => {
expect(wrapper.find(selectors.drawer).exists()).toBe(true)

wrapper.find(selectors.actionButton).trigger('click')
await new Promise((r) => setTimeout(r, 160))
expect(wrapper.find(selectors.drawer).exists()).toBe(true)
const bottomDrawerStore = useBottomDrawer()
expect(bottomDrawerStore.closeDrawer).not.toHaveBeenCalled()
expect(bottomDrawerStore.closeAllDrawers).not.toHaveBeenCalled()
wrapper.unmount()
})
})

const getWrapper = (props = { closeOnClick: false }) => {
const plugins = [...defaultPlugins()]

const drawer = { id: '1' }
const bottomDrawerStore = useBottomDrawer()
vi.mocked(bottomDrawerStore.showDrawer).mockReturnValue(drawer)
bottomDrawerStore.drawers = [drawer]

const RootComponent = defineComponent({
components: { BottomDrawer, OcButton },
props: ['props'],
Expand Down Expand Up @@ -135,7 +144,7 @@ const getWrapper = (props = { closeOnClick: false }) => {
...props
},
attachTo: document.body,
global: { renderStubDefaultSlot: true, plugins: [...defaultPlugins()] }
global: { renderStubDefaultSlot: true, plugins }
})
}
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
<template>
<component
:is="usePortal ? 'portal' : 'div'"
v-if="isOpen"
v-if="isActive"
:to="usePortal ? portalTarget : undefined"
>
<div
v-if="isOpen"
ref="bottomDrawerRef"
class="oc-bottom-drawer-background fixed inset-0 z-[calc(var(--z-index-modal)+2)] top-0 left-0 bg-black/40 size-full"
role="button"
@click="onBackgroundClicked"
>
<focus-trap>
<focus-trap :active="isCurrentlyOnTop">
<div
:id="drawerId"
tabindex="0"
class="oc-bottom-drawer fixed inset-x-0 bg-role-surface-container-high rounded-t-sm w-full max-h-[66vh] overflow-y-auto bottom-[-100%] transition-all duration-200 [&.active]:bottom-0"
class="oc-bottom-drawer fixed inset-x-0 bg-role-surface-container-high rounded-t-sm w-full max-h-[66vh] overflow-y-auto transition-all duration-200 bottom-[-100%]"
:class="{
'[&.active]:bottom-0': isCurrentlyOnTop
}"
>
<oc-card class="bg-transparent" header-class="flex flex-row justify-between items-center">
<template #header>
<oc-button
v-if="isNestedElement"
v-if="drawers.length > 1"
appearance="raw"
class="raw-hover-surface oc-bottom-drawer-back-button"
:aria-label="$gettext('Open the parent context menu')"
@click="openParentDrawer"
@click="hide()"
>
<oc-icon name="arrow-left" fill-type="line" />
</oc-button>
Expand All @@ -33,7 +34,7 @@
appearance="raw"
class="raw-hover-surface oc-bottom-drawer-close-button"
:aria-label="$gettext('Close the context menu')"
@click="hide()"
@click="closeAllDrawers()"
>
<oc-icon name="close" fill-type="line" />
</oc-button>
Expand All @@ -49,13 +50,23 @@
</template>

<script setup lang="ts">
import { nextTick, onBeforeUnmount, onMounted, ref, unref, useTemplateRef } from 'vue'
import {
computed,
nextTick,
onBeforeUnmount,
onMounted,
ref,
unref,
useTemplateRef,
watch
} from 'vue'
import { useGettext } from 'vue3-gettext'
import { FocusTrap } from 'focus-trap-vue'
import { onKeyStroke } from '@vueuse/core'
import OcButton from '../OcButton/OcButton.vue'
import OcCard from '../OcCard/OcCard.vue'
import { NestedDrop } from '../../helpers'
import { BottomDrawer, useBottomDrawer } from '../../composables'
import { storeToRefs } from 'pinia'

export interface Props {
/**
Expand All @@ -80,22 +91,12 @@ export interface Props {
* @default false
*/
usePortal?: boolean

/**
* @docs
* The target of the portal, when in use.
* @default app.runtime.bottom.drawer
*/
portalTarget?: string
/**
* @docs Determines if the bottom drawer element is nested.
* @default false
*/
isNestedElement?: boolean
/**
* @docs The parent `OcBottomDrawer` ref of the nested bottom drawer.
*/
nestedParentRef?: NestedDrop | null
}

const {
Expand All @@ -104,8 +105,6 @@ const {
closeOnClick = false,
title = '',
usePortal = false,
isNestedElement = false,
nestedParentRef = null,
portalTarget = 'app.runtime.bottom.drawer'
} = defineProps<Props>()

Expand Down Expand Up @@ -133,79 +132,62 @@ export interface Slots {
defineSlots<Slots>()

const { $gettext } = useGettext()
const bottomDrawerStore = useBottomDrawer()
const { showDrawer, closeDrawer, closeAllDrawers } = bottomDrawerStore
const { drawers, currentDrawer } = storeToRefs(bottomDrawerStore)

const drawer = ref<BottomDrawer | null>(null)
const bottomDrawerCardBodyRef = useTemplateRef('bottomDrawerCardBodyRef')

const isActive = computed(() => {
// active means the drawer is in the stack, but not necessarily on top (visible)
return unref(drawers)
.map(({ id }) => id)
.includes(unref(drawer)?.id)
})

const isOpen = ref(false)
const bottomDrawerRef = useTemplateRef<HTMLElement | null>('bottomDrawerRef')
const bottomDrawerCardBodyRef = useTemplateRef<HTMLElement | null>('bottomDrawerCardBodyRef')
const isCurrentlyOnTop = computed(() => {
// the drawer that is currently shown
return unref(drawer)?.id && unref(currentDrawer)?.id === unref(drawer).id
})

const show = async () => {
if (isNestedElement) {
unref(nestedParentRef).getElement().classList.add('hidden')
}

isOpen.value = true
drawer.value = showDrawer()
emit('show')
await nextTick()
unref(bottomDrawerCardBodyRef).addEventListener('click', onChildClicked)

// set active class for the slide-in animation
const drawer = document.getElementById(drawerId)
drawer?.classList.add('active')
}

const openParentDrawer = () => {
hide({ hideParent: false })
unref(nestedParentRef).getElement().classList.remove('hidden')
unref(bottomDrawerCardBodyRef)?.addEventListener('click', onChildClicked)
}

const hide = async ({ hideParent = true } = {}) => {
if (isNestedElement && hideParent) {
unref(nestedParentRef).hide()
}

const hide = () => {
closeDrawer(unref(drawer)?.id)
unref(bottomDrawerCardBodyRef)?.removeEventListener('click', onChildClicked)

// remove active class for the slide-out animation
const drawer = document.getElementById(drawerId)
drawer?.classList.remove('active')
await new Promise((resolve) => setTimeout(resolve, 150)) // wait for the animation to finish

isOpen.value = false
emit('hide')
}

const onChildClicked = (event: MouseEvent) => {
const target = (event.target as HTMLElement).closest('a[href], button')

if (!target) {
return
}

if (target.hasAttribute('aria-expanded')) {
target.setAttribute('aria-expanded', 'true')
return
}

if (!closeOnClick) {
return
}

if (isNestedElement) {
unref(nestedParentRef).hide()
if (closeOnClick) {
hide()
closeAllDrawers()
}

hide()
}

const onBackgroundClicked = (event: MouseEvent) => {
if (event.target === event.currentTarget) {
hide()
closeAllDrawers()
}
}

onKeyStroke('Escape', (e) => {
e.preventDefault()
hide()
closeAllDrawers()
})

onMounted(() => {
Expand All @@ -218,13 +200,20 @@ onMounted(() => {

onBeforeUnmount(() => {
document.querySelector(toggle).removeEventListener('click', show)
if (unref(drawer)) {
closeDrawer(unref(drawer).id)
}
})

const getElement = () => {
return unref(bottomDrawerRef)
}
watch(isCurrentlyOnTop, async () => {
if (unref(isCurrentlyOnTop)) {
await nextTick()
const drawerEl = document.getElementById(drawerId)
drawerEl?.classList.add('active')
}
})

defineExpose({ show, hide, getElement })
defineExpose({ show, hide })
</script>
<style>
@reference '@opencloud-eu/design-system/tailwind';
Expand Down
Loading