Skip to content

Commit

Permalink
refactor: useBodyScrollLock (unovue#516)
Browse files Browse the repository at this point in the history
* refactor: useBodyScrollLock

* fix: body scroll unlocking prematurely

* chore: test on nuxt playground
  • Loading branch information
zernonia authored Nov 13, 2023
1 parent b3964ce commit 657d9d8
Show file tree
Hide file tree
Showing 9 changed files with 1,985 additions and 1,769 deletions.
2 changes: 1 addition & 1 deletion .histoire/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"devDependencies": {
"@antfu/eslint-config": "^0.39.7",
"@floating-ui/dom": "^1.4.2",
"@floating-ui/vue": "^1.0.1",
"@floating-ui/vue": "^1.0.2",
"@histoire/plugin-vue": "^0.16.1",
"@iconify/vue": "^4.1.1",
"@radix-ui/colors": "^1.0.0",
Expand Down
71 changes: 71 additions & 0 deletions packages/radix-vue/src/Dialog/story/DialogProgrammatic.story.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script setup lang="ts">
import { ref } from 'vue'
import { DialogContent, DialogOverlay, DialogRoot } from '../'
const dialogAOpen = ref(false)
const dialogBOpen = ref(false)
function openA() {
dialogBOpen.value = false
dialogAOpen.value = true
}
function openB() {
dialogAOpen.value = false
dialogBOpen.value = true
}
function closeA(opened: boolean) {
if (opened)
dialogAOpen.value = false
}
function closeB(opened: boolean) {
if (opened)
dialogBOpen.value = false
}
</script>

<template>
<Story title="Dialog/Programmatic" :layout="{ type: 'single', iframe: true }">
<Variant title="default">
<div class="h-[300vh]">
<button
class="text-black9 bg-blackA9 hover:bg-blackA10 rounded-[4px] text-white p-2"
@click="openA"
>
Open A
</button>
<div v-if="dialogAOpen">
<DialogRoot v-model:open="dialogAOpen" @update:open="closeA">
<DialogOverlay class="bg-blackA9 fixed inset-0" />
<DialogContent
class="fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] rounded-[6px] bg-white p-[25px]"
>
<h1>Dialog A</h1>
<button
class="text-black9 bg-blackA9 hover:bg-blackA10 rounded-[4px] text-white p-2"
@click="openB"
>
Open B
</button>
</DialogContent>
</DialogRoot>
</div>
<div v-if="dialogBOpen">
<DialogRoot v-model:open="dialogBOpen" @update:open="closeB">
<DialogOverlay class="bg-blackA9 fixed inset-0" />
<DialogContent
class="fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] rounded-[6px] bg-white p-[25px]"
>
<h1>Dialog B</h1>
<button
class="text-black9 bg-blackA9 hover:bg-blackA10 rounded-[4px] text-white p-2"
@click="openA"
>
Open A
</button>
</DialogContent>
</DialogRoot>
</div>
</div>
</Variant>
</Story>
</template>
118 changes: 57 additions & 61 deletions packages/radix-vue/src/shared/useBodyScrollLock.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import {
createGlobalState,
defaultDocument,
useScrollLock,
} from '@vueuse/core'
import { isClient } from '@vueuse/shared'
import { computed, nextTick, onBeforeUnmount, ref } from 'vue'
import { nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { defu } from 'defu'
import { injectConfigProviderContext } from '@/ConfigProvider/ConfigProvider.vue'

const useInitialOverflowStyle = createGlobalState(() => ref<string | undefined>())
const useBodyLockStackCount = createGlobalState(() => ref(0))

export function useBodyScrollLock(initialState?: boolean | undefined) {
Expand All @@ -16,69 +15,66 @@ export function useBodyScrollLock(initialState?: boolean | undefined) {
})

const stack = useBodyLockStackCount()
const locked = useScrollLock(defaultDocument?.body, false)

const writableLock = computed<boolean>({
get() {
return locked.value
},
set(newLocked) {
if (!isClient)
return

if (newLocked) {
const verticalScrollbarWidth
= window.innerWidth - document.documentElement.clientWidth

const defaultConfig = { padding: verticalScrollbarWidth, margin: 0 }

const config = context.scrollBody?.value
? typeof context.scrollBody.value === 'object'
? defu({
padding: context.scrollBody.value.padding === true ? verticalScrollbarWidth : context.scrollBody.value.padding,
margin: context.scrollBody.value.margin === true ? verticalScrollbarWidth : context.scrollBody.value.margin,
}, defaultConfig)
: defaultConfig
: ({ padding: 0, margin: 0 })

if (verticalScrollbarWidth > 0) {
document.body.style.paddingRight = `${config.padding}px`
document.body.style.marginRight = `${config.margin}px`
document.body.style.setProperty('--scrollbar-width', `${verticalScrollbarWidth}px`)
}

// let dismissibleLayer set previous pointerEvent first
nextTick(() => {
document.body.style.pointerEvents = 'none'
locked.value = true
})
}
else {
document.body.style.paddingRight = ''
document.body.style.marginRight = ''
document.body.style.pointerEvents = ''
document.body.style.removeProperty('--scrollbar-width')
locked.value = false
}
},
})
const initialOverflow = useInitialOverflowStyle()

if (initialState) {
stack.value++
writableLock.value = initialState
const locked = ref(initialState)

const resetBodyStyle = () => {
document.body.style.paddingRight = ''
document.body.style.marginRight = ''
document.body.style.pointerEvents = ''
document.body.style.removeProperty('--scrollbar-width')
document.body.style.overflow = initialOverflow.value ?? ''
initialOverflow.value = undefined
}

onBeforeUnmount(() => {
if (!initialState)
if (initialState)
stack.value++

watch(locked, (val) => {
if (!isClient)
return
stack.value--
if (stack.value === 0) {
document.body.style.paddingRight = ''
document.body.style.marginRight = ''
document.body.style.pointerEvents = ''
document.body.style.removeProperty('--scrollbar-width')

if (val) {
if (initialOverflow.value === undefined)
initialOverflow.value = document.body.style.overflow

const verticalScrollbarWidth
= window.innerWidth - document.documentElement.clientWidth

const defaultConfig = { padding: verticalScrollbarWidth, margin: 0 }

const config = context.scrollBody?.value
? typeof context.scrollBody.value === 'object'
? defu({
padding: context.scrollBody.value.padding === true ? verticalScrollbarWidth : context.scrollBody.value.padding,
margin: context.scrollBody.value.margin === true ? verticalScrollbarWidth : context.scrollBody.value.margin,
}, defaultConfig)
: defaultConfig
: ({ padding: 0, margin: 0 })

if (verticalScrollbarWidth > 0) {
document.body.style.paddingRight = `${config.padding}px`
document.body.style.marginRight = `${config.margin}px`
document.body.style.setProperty('--scrollbar-width', `${verticalScrollbarWidth}px`)
document.body.style.overflow = 'hidden'
}

// let dismissibleLayer set previous pointerEvent first
nextTick(() => {
document.body.style.pointerEvents = 'none'
document.body.style.overflow = 'hidden'
})
}
}, { immediate: true })

onBeforeUnmount(() => {
if (initialState)
stack.value--

if (stack.value === 0)
resetBodyStyle()
})

return writableLock
return locked
}
2 changes: 1 addition & 1 deletion playground/nuxt/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Card from './components/Card.vue'

<template>
<div class="w-full flex flex-col items-center">
<div class="max-w-6xl w-full grid grid-cols-3 gap-4 pt-40 pb-40">
<div class="max-w-6xl w-full flex flex-col lg:grid lg:grid-cols-3 gap-4 pt-40 pb-40">
<Card>
<Accordion />
</Card>
Expand Down
4 changes: 2 additions & 2 deletions playground/nuxt/components/Card.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="relative flex min-h-[300px]">
<div class="h-full from-teal9 to-green9 bg-gradient-to-br w-full rounded-xl flex relative items-center justify-center resize">
<div class="relative flex flex-col min-h-[300px] ">
<div class="h-full flex-1 from-teal9 to-green9 bg-gradient-to-br w-full rounded-xl flex relative items-center justify-center resize">
<div class="w-full max-w-[700px] flex flex-col items-center justify-center">
<slot />
</div>
Expand Down
32 changes: 25 additions & 7 deletions playground/nuxt/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
modules: ['radix-vue/nuxt', '@nuxtjs/tailwindcss'],
components: [
{
path: '../../docs/components/demo',
modules: ['@nuxtjs/tailwindcss'],
// components: [
// {
// path: '../../docs/components/demo',
// },
// ],
hooks: {
'components:dirs': (dirs) => {
dirs.unshift({
path: '../../docs/components/demo',
ignore: ['**/css/**'],
// this is required else Nuxt will autoImport `.ts` file
extensions: ['.vue'],
// prefix for your components, eg: UiButton
prefix: '',
// prevent adding another prefix component by it's path.
pathPrefix: true,
extendComponent(component) {
return {
...component,
kebabName: component.kebabName.replace('-tailwind', ''),
pascalName: component.pascalName.replace('Tailwind', ''),
}
},
})
},
],
radix: {
components: true,
},
})
1 change: 1 addition & 0 deletions playground/nuxt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"radix-vue": "link:../../packages/radix-vue"
},
"devDependencies": {
"@floating-ui/vue": "^1.0.2",
"@iconify/vue": "^4.1.1",
"@nuxt/devtools": "latest",
"@nuxtjs/tailwindcss": "^6.8.0",
Expand Down
Loading

0 comments on commit 657d9d8

Please sign in to comment.