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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ window.ResizeObserver = MockResizeObserver as any
* @param options.defaultSlot - defaultSlot option.
* @param options.open - open option.
* @param options.focusOnOpen - focusOnOpen option.
* @param options.scrollLock - scrollLock option.
* @param options.contentClass - contentClass option.
* @param options.overlayClass - overlayClass option.
* @param options.referenceSelector - referenceSelector option.
Expand All @@ -35,6 +36,7 @@ function mountBaseModal(options: MountBaseModalOptions = {}) {
defaultSlot = '<span data-test="default-slot">Modal</span>',
open = false,
focusOnOpen = true,
scrollLock = true,
contentClass = '',
overlayClass = '',
referenceSelector = undefined,
Expand All @@ -47,17 +49,25 @@ function mountBaseModal(options: MountBaseModalOptions = {}) {
<BaseModal
:open="open"
:focusOnOpen="focusOnOpen"
:scrollLock="scrollLock"
:contentClass="contentClass"
:overlayClass="overlayClass"
:referenceSelector="referenceSelector"
>
<slot/>
</BaseModal>`,
components: { BaseModal },
props: ['open', 'focusOnOpen', 'contentClass', 'overlayClass', 'referenceSelector'],
props: [
'open',
'focusOnOpen',
'scrollLock',
'contentClass',
'overlayClass',
'referenceSelector',
],
},
{
propsData: { open, focusOnOpen, contentClass, overlayClass, referenceSelector },
propsData: { open, focusOnOpen, scrollLock, contentClass, overlayClass, referenceSelector },
global: { provide: ariaProvide(ariaLabels) },
slots: { default: defaultSlot },
},
Expand All @@ -68,7 +78,9 @@ function mountBaseModal(options: MountBaseModalOptions = {}) {

return {
wrapper: baseModalWrapper,
unmount: () => wrapper.unmount(),
getModalContent: () => baseModalWrapper.find(getDataTestSelector('modal-content')),
setScrollLock: async (scrollLock: boolean) => wrapper.setProps({ scrollLock }),
setOpen: async (open: boolean) => wrapper.setProps({ open }),
setReferenceSelector: async (referenceSelector: string) =>
wrapper.setProps({ referenceSelector }),
Expand Down Expand Up @@ -208,6 +220,71 @@ describe('testing Base Modal component', () => {
expect(focusedElementBeforeOpen).toEqual(document.activeElement)
})

describe('scroll lock', () => {
beforeEach(() => {
document.body.style.overflow = ''
document.documentElement.style.overflow = ''
})

it('disables the page scroll while open by default and restores it on close', async () => {
const { setOpen } = mountBaseModal()

await setOpen(true)
expect(document.body.style.overflow).toBe('hidden')
expect(document.documentElement.style.overflow).toBe('hidden')

await setOpen(false)
expect(document.body.style.overflow).toBe('')
expect(document.documentElement.style.overflow).toBe('')
})

it("doesn't touch the page scroll if the scrollLock prop is false", async () => {
document.body.style.overflow = 'auto'
const { setOpen } = mountBaseModal({ scrollLock: false })

await setOpen(true)
expect(document.body.style.overflow).toBe('auto')
expect(document.documentElement.style.overflow).toBe('')

await setOpen(false)
expect(document.body.style.overflow).toBe('auto')
})

it('releases and restores the lock when the scrollLock prop is toggled while open', async () => {
const { setOpen, setScrollLock } = mountBaseModal()
await setOpen(true)

await setScrollLock(false)
expect(document.body.style.overflow).toBe('')
expect(document.documentElement.style.overflow).toBe('')

await setScrollLock(true)
expect(document.body.style.overflow).toBe('hidden')

await setOpen(false)
expect(document.body.style.overflow).toBe('')
})

it("doesn't restore the overflow it didn't set when it closes", async () => {
const { setOpen } = mountBaseModal({ scrollLock: false })
await setOpen(true)
document.body.style.overflow = 'scroll'

await setOpen(false)

expect(document.body.style.overflow).toBe('scroll')
})

it('restores the page scroll when unmounted while open', () => {
const { unmount } = mountBaseModal({ open: true })
expect(document.body.style.overflow).toBe('hidden')

unmount()

expect(document.body.style.overflow).toBe('')
})
})

it('allows adding classes to the modal content', () => {
const { getModalContent } = mountBaseModal({
contentClass: 'test-class',
Expand Down Expand Up @@ -248,6 +325,8 @@ describe('testing Base Modal component', () => {
})

interface MountBaseModalOptions {
/** Whether the modal locks the page scroll while open. */
scrollLock?: boolean
/** The default slot content. */
defaultSlot?: string
/** Whether the modal is open. */
Expand Down
40 changes: 40 additions & 0 deletions packages/x-components/src/components/modals/base-modal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ export default defineComponent({
type: AnimationProp,
default: () => Fade,
},
/**
* Determines if the scroll of the body and the window is disabled while the modal is open.
* Set it to `false` for modals that must not lock the page scroll. It can be toggled while the
* modal is open.
*/
scrollLock: {
type: Boolean,
default: true,
},
/** Class inherited by content element. */
contentClass: String,
/** Class inherited by overlay element. */
Expand All @@ -102,15 +111,26 @@ export default defineComponent({
/** The reference element to use to find the modal's position. */
let referenceElement: HTMLElement | undefined

/** Whether this modal is currently holding the scroll lock, so it only restores what it set. */
let isScrollDisabled = false

/** Disables the scroll of both the body and the window. */
function disableScroll() {
if (!props.scrollLock || isScrollDisabled) {
return
}
isScrollDisabled = true
previousBodyOverflow.value = document.body.style.overflow
previousHTMLOverflow.value = document.documentElement.style.overflow
document.body.style.overflow = document.documentElement.style.overflow = 'hidden'
}

/** Restores the scroll of both the body and the window. */
function enableScroll() {
if (!isScrollDisabled) {
return
}
isScrollDisabled = false
document.body.style.overflow = previousBodyOverflow.value
document.documentElement.style.overflow = previousHTMLOverflow.value
}
Expand Down Expand Up @@ -210,6 +230,14 @@ export default defineComponent({

onMounted(() => {
watch(() => props.open, syncBody)
watch(
() => props.scrollLock,
shouldLockScroll => {
if (props.open) {
shouldLockScroll ? disableScroll() : enableScroll()
}
},
)
if (props.open) {
syncBody(true)
}
Expand Down Expand Up @@ -381,6 +409,18 @@ const fadeAndSlide = FadeAndSlide
</script>
```

#### Not locking the page scroll

By default (`scrollLock` is `true`) the modal sets `overflow: hidden` on the `body` and `html`
elements while it is open, and restores them when it closes. Set the `scrollLock` prop to `false`
to keep the page scrollable. The prop can be toggled while the modal is open.

```vue
<BaseModal :open="open" :scroll-lock="false">
<p>The page behind can still be scrolled.</p>
</BaseModal>
```

## Vue Events

A list of events that the component will emit:
Expand Down
Loading