Skip to content
Closed
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
230 changes: 229 additions & 1 deletion packages/core/src/DismissableLayer/DismissableLayer.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { DOMWrapper, VueWrapper } from '@vue/test-utils'
import { fireEvent } from '@testing-library/vue'
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it } from 'vitest'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { nextTick } from 'vue'
import { sleep } from '@/test'
import DismissableLayer from './story/_DismissableLayer.vue'
import ShadowRootContainer from './story/shadowRoot/ShadowRootContainer.vue'

const OPEN_LABEL = 'Open'
const CLOSE_LABEL = 'Close'
Expand Down Expand Up @@ -70,3 +72,229 @@ describe('given a default DismissableLayer', () => {
})
})
})

describe('given a Dialog inside a ShadowRoot', () => {
let wrapper: VueWrapper<InstanceType<typeof ShadowRootContainer>>
let shadowHost: HTMLElement
let shadowRoot: ShadowRoot

globalThis.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}

beforeEach(async () => {
document.body.innerHTML = ''
wrapper = mount(ShadowRootContainer, { attachTo: document.body })
await nextTick()
shadowHost = wrapper.find('#shadow-root-container').element as HTMLElement
shadowRoot = (shadowHost as unknown as { shadowRoot: ShadowRoot }).shadowRoot!
// Open the dialog
const trigger = shadowRoot.querySelector('[data-testid="dialog-trigger"]') as HTMLElement
await fireEvent.click(trigger)
await sleep(1)
const dialogOverlay = shadowRoot.querySelector('[data-testid="dialog-overlay"]')
expect(dialogOverlay).toBeTruthy()
const dialogContent = shadowRoot.querySelector('[data-testid="dialog-content"]')
expect(dialogContent).toBeTruthy()
})

afterEach(async () => {
// Unmount the shadow-root container to avoid Vue patching into a cleared body
await wrapper.unmount()
await nextTick()
})

it('shadowRoot should be defined', () => {
expect(shadowRoot).toBeDefined()
})

it('should be able to dismiss Dialog on pressing Escape', async () => {
await fireEvent.keyDown(shadowHost, { key: 'Escape' })
await sleep(1)
expect(shadowRoot.querySelector('[data-testid="dialog-content"]')).toBeFalsy()
})

it('should dismiss Dialog when interacting outside via overlay', async () => {
await fireEvent.pointerDown(shadowRoot.querySelector('[data-testid="dialog-overlay"]')!)
await sleep(1)
expect(shadowRoot.querySelector('[data-testid="dialog-content"]')).toBeFalsy()
})

describe('nested Combobox (non-modal)', () => {
beforeEach(async () => {
const comboboxTrigger = shadowRoot.querySelector('[data-testid="combobox-trigger"]') as HTMLElement
await fireEvent.click(comboboxTrigger)
await sleep(1)
const comboboxContent = shadowRoot.querySelector('[data-testid="combobox-content"]') as HTMLElement
expect(comboboxContent).toBeTruthy()
})

it('interacting inside Combobox content should not close combobox content, nor the dialog', async () => {
const item = shadowRoot.querySelector('[data-testid="combobox-item"]') as HTMLElement
expect(item).toBeTruthy()
await fireEvent.pointerDown(item)
await sleep(1)
expect(shadowRoot.querySelector('[data-testid="combobox-content"]')).toBeTruthy()
expect(shadowRoot.querySelector('[data-testid="dialog-content"]')).toBeTruthy()
})

it('pressing Escape inside Combobox content should close combobox content, but not the dialog', async () => {
await fireEvent.keyDown(shadowHost, { key: 'Escape' })
await sleep(1)
expect(shadowRoot.querySelector('[data-testid="combobox-content"]')).toBeFalsy()
expect(shadowRoot.querySelector('[data-testid="dialog-content"]')).toBeTruthy()
})

it('interacting outside Combobox content should close combobox and close dialog', async () => {
await fireEvent.pointerDown(shadowRoot.querySelector('[data-testid="dialog-overlay"]')!)
await sleep(1)
expect(shadowRoot.querySelector('[data-testid="combobox-content"]')).toBeFalsy()
expect(shadowRoot.querySelector('[data-testid="dialog-content"]')).toBeFalsy()
})
})

describe('nested Popover', () => {
describe('modal popover', () => {
it('interacting inside popover content should not close the dialog', async () => {
const popoverTrigger = shadowRoot.querySelector('[data-testid="popover-trigger-modal"]') as HTMLElement
await fireEvent.click(popoverTrigger)
const popoverContent = shadowRoot.querySelector('[data-testid="popover-content-modal"]') as HTMLElement
expect(popoverContent).toBeTruthy()
const firstInput = shadowRoot.querySelector('[data-testid="popover-first-input"]') as HTMLElement
await fireEvent.pointerDown(firstInput)
await sleep(1)
expect(popoverContent).toBeTruthy()
expect(shadowRoot.querySelector('[data-testid="dialog-content"]')).toBeTruthy()
})

it('modal popover: pressing escape key should close popover, not dialog', async () => {
const popoverTrigger = shadowRoot.querySelector('[data-testid="popover-trigger-modal"]') as HTMLElement
await fireEvent.click(popoverTrigger)
const popoverContent = shadowRoot.querySelector('[data-testid="popover-content-modal"]') as HTMLElement
expect(popoverContent).toBeTruthy()
await fireEvent.keyDown(shadowHost, { key: 'Escape' })
await sleep(1)
expect(shadowRoot.querySelector('[data-testid="popover-content-modal"]')).toBeFalsy()
expect(shadowRoot.querySelector('[data-testid="dialog-content"]')).toBeTruthy()
})

// it('modal popover: interacting outside should close popover, not dialog', async () => {
// const popoverTrigger = shadowRoot.querySelector('[data-testid="popover-trigger-modal"]') as HTMLElement
// await fireEvent.click(popoverTrigger)
// const popoverContent = shadowRoot.querySelector('[data-testid="popover-content-modal"]') as HTMLElement
// expect(popoverContent).toBeTruthy()
// await fireEvent.pointerDown(shadowRoot.querySelector('[data-testid="dialog-overlay"]')!)
// await sleep(1)
// expect(shadowRoot.querySelector('[data-testid="popover-content-modal"]')).toBeFalsy()
// expect(shadowRoot.querySelector('[data-testid="dialog-content"]')).toBeTruthy()
// })
})

describe('non-modal popover', () => {
it('interacting inside popover content should not close the dialog', async () => {
const popoverTrigger = shadowRoot.querySelector('[data-testid="popover-trigger-nonmodal"]') as HTMLElement
await fireEvent.click(popoverTrigger)
const popoverContent = shadowRoot.querySelector('[data-testid="popover-content-nonmodal"]') as HTMLElement
expect(popoverContent).toBeTruthy()
const firstInput = shadowRoot.querySelector('[data-testid="popover-first-input"]') as HTMLElement
await fireEvent.pointerDown(firstInput)
await sleep(1)
expect(popoverContent).toBeTruthy()
expect(shadowRoot.querySelector('[data-testid="dialog-content"]')).toBeTruthy()
})

it('non-modal popover: pressing escape key should close popover, but not close the dialog', async () => {
const popoverTrigger = shadowRoot.querySelector('[data-testid="popover-trigger-nonmodal"]') as HTMLElement
await fireEvent.click(popoverTrigger)
await sleep(1)
const popoverContent = shadowRoot.querySelector('[data-testid="popover-content-nonmodal"]') as HTMLElement
expect(popoverContent).toBeTruthy()
await fireEvent.keyDown(shadowHost, { key: 'Escape' })
await sleep(1)
expect(shadowRoot.querySelector('[data-testid="popover-content-nonmodal"]')).toBeFalsy()
expect(shadowRoot.querySelector('[data-testid="dialog-content"]')).toBeTruthy()
})

it('non-modal popover: interacting outside should close popover and the dialog', async () => {
const popoverTrigger = shadowRoot.querySelector('[data-testid="popover-trigger-nonmodal"]') as HTMLElement
await fireEvent.click(popoverTrigger)
await sleep(1)
const popoverContent = shadowRoot.querySelector('[data-testid="popover-content-nonmodal"]') as HTMLElement
expect(popoverContent).toBeTruthy()
await fireEvent.pointerDown(shadowRoot.querySelector('[data-testid="dialog-overlay"]')!)
await sleep(1)
expect(shadowRoot.querySelector('[data-testid="popover-content-nonmodal"]')).toBeFalsy()
expect(shadowRoot.querySelector('[data-testid="dialog-content"]')).toBeFalsy()
})
})
})

describe('nested Dropdown Menu', () => {
describe('modal dropdown', () => {
it('interacting inside dropdown and sub menus should not close the dialog', async () => {
const trigger = shadowRoot.querySelector('[data-testid="dropdown-trigger-modal"]') as HTMLElement
await fireEvent.click(trigger)
await sleep(1)
const dropdownContent = shadowRoot.querySelector('[data-testid="dropdown-content-modal"]') as HTMLElement
expect(dropdownContent).toBeTruthy()
const moreTools = shadowRoot.querySelector('[data-testid="more-tools-subtrigger"]') as HTMLElement
await fireEvent.pointerDown(moreTools)
await sleep(1)
expect(dropdownContent).toBeTruthy()
expect(shadowRoot.querySelector('[data-testid="dialog-content"]')).toBeTruthy()
})

it('modal dropdown: pressing escape key should close dropdown, not the dialog', async () => {
const trigger = shadowRoot.querySelector('[data-testid="dropdown-trigger-modal"]') as HTMLElement
await fireEvent.click(trigger)
await sleep(1)
const dialogContent = shadowRoot.querySelector('[data-testid="dialog-content"]') as HTMLElement
expect(dialogContent).toBeTruthy()
await fireEvent.keyDown(shadowHost, { key: 'Escape' })
await sleep(1)
expect(shadowRoot.querySelector('[data-testid="dropdown-content-modal"]')).toBeFalsy()
expect(shadowRoot.querySelector('[data-testid="dialog-content"]')).toBeTruthy()
})

it('modal dropdown: interacting outside should close dropdown, not the dialog', async () => {
const trigger = shadowRoot.querySelector('[data-testid="dropdown-trigger-modal"]') as HTMLElement
await fireEvent.click(trigger)
await sleep(1)
const dialogContent = shadowRoot.querySelector('[data-testid="dialog-content"]') as HTMLElement
expect(dialogContent).toBeTruthy()
await fireEvent.pointerDown(shadowRoot.querySelector('[data-testid="dialog-overlay"]')!)
await sleep(1)
expect(shadowRoot.querySelector('[data-testid="dropdown-content-modal"]')).toBeFalsy()
expect(shadowRoot.querySelector('[data-testid="dialog-content"]')).toBeTruthy()
})
})

describe('non-modal dropdown', () => {
it('non-modal dropdown: pressing escape key should close dropdown, but not close the dialog', async () => {
const trigger = shadowRoot.querySelector('[data-testid="dropdown-trigger-nonmodal"]') as HTMLElement
await fireEvent.click(trigger)
await sleep(1)
const dialogContent = shadowRoot.querySelector('[data-testid="dialog-content"]') as HTMLElement
expect(dialogContent).toBeTruthy()
await fireEvent.keyDown(shadowHost, { key: 'Escape' })
await sleep(1)
expect(shadowRoot.querySelector('[data-testid="dropdown-content-nonmodal"]')).toBeFalsy()
expect(shadowRoot.querySelector('[data-testid="dialog-content"]')).toBeTruthy()
})

it('non-modal dropdown: interacting outside should close dropdown and close the dialog', async () => {
const trigger = shadowRoot.querySelector('[data-testid="dropdown-trigger-nonmodal"]') as HTMLElement
await fireEvent.click(trigger)
await sleep(1)
const dialogContent = shadowRoot.querySelector('[data-testid="dialog-content"]') as HTMLElement
expect(dialogContent).toBeTruthy()
await fireEvent.pointerDown(shadowRoot.querySelector('[data-testid="dialog-overlay"]')!)
await sleep(1)
expect(shadowRoot.querySelector('[data-testid="dropdown-content-nonmodal"]')).toBeFalsy()
expect(shadowRoot.querySelector('[data-testid="dialog-content"]')).toBeFalsy()
})
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<script setup lang="ts">
import ShadowRootContainer from './shadowRoot/ShadowRootContainer.vue'

function handleAlert() {
// eslint-disable-next-line no-alert
alert('Alert')
}
</script>

<template>
<Story
group="utilities"
title="DismissableLayer/DialogShadowRoot"
:layout="{ type: 'single', iframe: false }"
>
<Variant title="default">
<div class="h-[300vh]">
<strong>This example is to check the behaviour of multiple dismissible layers when used inside a shadow root</strong>
<br>
<strong>The shadow root teleported dialog should behave exactly like a dialog in the document's body</strong>

<ul class="list-disc ml-4">
<li>✅ focus should move inside `Dialog` when mounted</li>
<li>✅ focus should be trapped inside `Dialog`</li>
<li>✅ scrolling outside `Dialog` should be disabled</li>
<li>✅ should be able to dismiss `Dialog` on pressing escape</li>
<li class="ml-6">
✅ focus should return to the open button
</li>
<li>
✅ interacting outside `Dialog` should be disabled (clicking the
"alert me" button shouldn't do anything)
</li>
<li>➕</li>
<li>
✅ should be able to dismiss `Dialog` when interacting outside
</li>
<li class="ml-6">
✅ focus should return to the open button
</li>
</ul>
<hr class="my-2">
<strong>It should also pass these tests:</strong>
<ul class="list-disc ml-4">
<li>
<b>Combobox</b>
<ul>
<li>✅ interacting inside a nested combobox popper's content should not close the dialog</li>
<li>
✅ interacting outside a nested combobox popper's content should close the combobox's content,
but not close the dialog, as the combobox content is not in modal mode
</li>
</ul>
</li>
<li>
<b>Popover</b>
<ul>
<li>✅ interacting inside a nested popover's content should not close the dialog</li>
<li>
✅ interacting outside a nested popover's content (in modal mode) should close the popover's content,
but not close the dialog
</li>
<li>
✅ interacting outside a nested popover's content (in non-modal mode) should close the popover's content and the dialog
</li>
</ul>
</li>
<li>
<b>Dropdown Menu</b>
<ul>
<li>✅ interacting inside a dropdown menu or its sub menus should not close the dialog</li>
<li>
✅ interacting outside a dropdown menu or its sub menus (in modal mode) should close the dropdown menu, but not close the dialog
</li>
<li>
✅ interacting outside a dropdown menu or its sub menus (in non-modal mode) should close the dropdown menu and close the dialog
</li>
</ul>
</li>
</ul>

<div class="flex flex-col gap-4 mt-12">
<ShadowRootContainer />
<button @click="handleAlert">
Alert me
</button>
</div>
</div>
</Variant>
</Story>
</template>
Loading
Loading