Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix integration with web components not working properly #589

Merged
merged 5 commits into from
Aug 18, 2024
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
14 changes: 9 additions & 5 deletions cypress/e2e/state.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('State', { testIsolation: false }, () => {
cy.get('[data-a11y-dialog-show="something-else"]').click()
cy.get('.dialog').then(shouldBeHidden)

cy.get('[data-a11y-dialog-show="my-dialog"]').click()
cy.get('[data-a11y-dialog-show="my-dialog"]').first().click()
cy.get('.dialog').then(shouldBeVisible)
})

Expand All @@ -28,14 +28,18 @@ describe('State', { testIsolation: false }, () => {
cy.get('.dialog').then(shouldBeHidden)
})

it('should open the dialog when clicking a custom element opener', () => {
cy.get('fancy-button').click()
cy.get('.dialog').then(shouldBeVisible)
})

it('should close when pressing ESC', () => {
cy.get('[data-a11y-dialog-show="my-dialog"]').click()
cy.realPress('Escape')
cy.get('.dialog').then(shouldBeHidden)
})

it('should not close when pressing ESC if it contains an open popover', () => {
cy.get('[data-a11y-dialog-show="my-dialog"]').click()
cy.get('[data-a11y-dialog-show="my-dialog"]').first().click()
cy.get('[popovertarget]').click()
cy.get('[popover]').should('be.visible')
cy.realPress('Escape')
Expand All @@ -46,7 +50,7 @@ describe('State', { testIsolation: false }, () => {
})

it('should close when clicking the backdrop', () => {
cy.get('[data-a11y-dialog-show="my-dialog"]').click()
cy.get('[data-a11y-dialog-show="my-dialog"]').first().click()
cy.get('.dialog-overlay').click({ force: true })
cy.get('.dialog').then(shouldBeHidden)
})
Expand All @@ -56,7 +60,7 @@ describe('State', { testIsolation: false }, () => {
$node[0].addEventListener('show', cy.stub().as('shown'))
$node[0].addEventListener('hide', cy.stub().as('hidden'))
})
cy.get('[data-a11y-dialog-show="my-dialog"]').click()
cy.get('[data-a11y-dialog-show="my-dialog"]').first().click()
cy.get('@shown').should('have.been.calledOnce')
cy.get('.dialog-overlay').click({ force: true })
cy.get('@hidden').should('have.been.calledOnce')
Expand Down
22 changes: 17 additions & 5 deletions cypress/e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ export function shouldBeVisible($subject: Parameters<(typeof cy)['wrap']>[0]) {
consoleProps: () => ({ $el: $subject }),
})

cy.wrap($subject, { log: false }).should('not.have.attr', 'aria-hidden')
cy.wrap($subject, { log: false })
if ($subject[0].shadowRoot) {
cy.wrap($subject, { log: false }).shadow().find('.dialog').as('subject')
} else {
cy.wrap($subject, { log: false }).as('subject')
}

cy.get('@subject').should('not.have.attr', 'aria-hidden')
cy.get('@subject')
.find('.dialog-content', { log: false })
.should('be.visible')
}
Expand All @@ -20,11 +26,17 @@ export function shouldBeHidden($subject: Parameters<(typeof cy)['wrap']>[0]) {
consoleProps: () => ({ $el: $subject }),
})

cy.wrap($subject, { log: false })
.should('have.attr', 'aria-hidden', 'true')
if ($subject[0].shadowRoot) {
cy.wrap($subject, { log: false }).shadow().find('.dialog').as('subject')
} else {
cy.wrap($subject, { log: false }).as('subject')
}

cy.get('@subject').should('have.attr', 'aria-hidden', 'true')
cy.get('@subject')
.find('.dialog-overlay', { log: false })
.should('not.be.visible')
cy.wrap($subject, { log: false })
cy.get('@subject')
.find('.dialog-content', { log: false })
.should('not.be.visible')
}
61 changes: 61 additions & 0 deletions cypress/e2e/webComponents.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { shouldBeHidden, shouldBeVisible } from './utils.ts'

describe('Web Components', () => {
beforeEach(() => cy.visit('/web-components'))

it('should focus the dialog container on open', () => {
cy.get('my-dialog').shadow().find('[data-show]').first().click()
cy.get('my-dialog').then(shouldBeVisible)
cy.get('my-dialog').should('have.focus')
})

it('should close with ESC', () => {
cy.get('my-dialog').shadow().find('[data-show]').first().click()
cy.realPress('Escape')
cy.get('my-dialog').then(shouldBeHidden)
})

it('should close with close button', () => {
cy.get('my-dialog').shadow().find('[data-show]').first().click()
cy.get('my-dialog').shadow().find('.dialog-close').click()
cy.get('my-dialog').then(shouldBeHidden)
})

it('should restore focus to the previously focused element', () => {
cy.get('my-dialog').shadow().find('[data-show]').first().click()
cy.get('my-dialog').shadow().find('.dialog-close').click()
cy.get('my-dialog')
.shadow()
.find('[data-show]')
.first()
.should('have.focus')
})

it('should handle opening and closing with a custom element', () => {
// In a custom element, the event target ends up being the shadow root which
// is the custom dialog element in this instance
const handlers = {
show: event => {
expect(event.detail.target.tagName).to.eq('MY-DIALOG')
expect(event.detail.composedPath()[0].tagName).to.eq('FANCY-BUTTON')
},
hide: event => {
expect(event.detail.target.tagName).to.eq('MY-DIALOG')
expect(event.detail.composedPath()[0].tagName).to.eq('FANCY-BUTTON')
},
}

cy.spy(handlers, 'show').as('show')
cy.spy(handlers, 'hide').as('hide')
cy.window().its('instance').invoke('on', 'show', handlers.show)
cy.window().its('instance').invoke('on', 'hide', handlers.hide)
cy.get('my-dialog').shadow().find('fancy-button').first().click()
cy.get('@show').should('have.been.called')
cy.get('my-dialog').then(shouldBeVisible)
cy.get('my-dialog').shadow().find('fancy-button').last().click()
cy.get('@hide').should('have.been.called')
cy.get('my-dialog').then(shouldBeHidden)
cy.window().its('instance').invoke('off', 'show', handlers.show)
cy.window().its('instance').invoke('off', 'hide', handlers.hide)
})
})
4 changes: 3 additions & 1 deletion cypress/fixtures/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<h1>Tests — Base</h1>
<button class="link-like" data-a11y-dialog-show="my-dialog">Open the dialog window</button>
<button class="link-like" data-a11y-dialog-show="something-else">Open the dialog window</button>
<fancy-button class="link-like" data-a11y-dialog-show="my-dialog">Open the dialog window</fancy-button>
</main>

<div class="dialog" data-a11y-dialog="my-dialog" aria-labelledby="my-dialog-title">
Expand Down Expand Up @@ -42,11 +43,12 @@ <h1 id="my-dialog-title">Dialog title</h1>
</div>

<script src="./a11y-dialog.js"></script>
<script src="./shadow-dom-fixture.js"></script>
<script>
document.querySelector('#move-focus-outside').addEventListener('click', () => {
document.querySelector('#focus-me').focus()
})
</script>
</body>

</html>
</html>
57 changes: 57 additions & 0 deletions cypress/fixtures/web-components.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!doctype html>
<html lang="en">

<head>
<meta charset="utf-8">
<title>Tests — Web Components</title>
<link rel="stylesheet" href="../styles.css" />
</head>

<body>
<main>
<h1>Tests — Web Components</h1>

<my-dialog>
<template>
<button type="button" data-show>Open the dialog</button>
<fancy-button data-a11y-dialog-show="my-dialog">
Open the dialog
</fancy-button>
<div class="dialog" id="my-dialog" aria-labelledby="my-dialog-title" aria-hidden="true">
<div class="dialog-overlay" data-a11y-dialog-hide></div>
<div class="dialog-content" role="document">
<button data-a11y-dialog-hide class="dialog-close" aria-label="Close this dialog window">&times;</button>
<fancy-button data-a11y-dialog-hide>
Close the dialog
</fancy-button>
<h1 id="my-dialog-title">Your dialog title</h1>
</div>
</div>
<link rel="stylesheet" href="../styles.css" />
</template>
</my-dialog>
</main>

<script src="./a11y-dialog.js"></script>
<script src="./shadow-dom-fixture.js"></script>
<script>
class MyDialog extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
const template = this.querySelector('template');
shadow.appendChild(template.content.cloneNode(true));

const container = this.shadowRoot.querySelector("#my-dialog");
const dialog = new A11yDialog(container);
window.instance = dialog

const triggers = this.shadowRoot.querySelectorAll('[data-show]');
triggers.forEach(trigger => trigger.addEventListener('click', (event) => dialog.show(event)))
}
}

customElements.define("my-dialog", MyDialog);
</script>
</body>

</html>
7 changes: 6 additions & 1 deletion src/a11y-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,12 @@ export default class A11yDialog {
* dialog are clicked, and call `show` or `hide`, respectively
*/
private handleTriggerClicks(event: Event) {
const target = event.target as HTMLElement
// We need to retrieve the click target while accounting for Shadow DOM.
// When within a web component, `event.target` is the shadow root (e.g.
// `<my-dialog>`), so we need to use `event.composedPath()` to get the click
// target
// See: https://github.com/KittyGiraudel/a11y-dialog/issues/582
const target = event.composedPath()[0] as HTMLElement

// We use `.closest(..)` and not `.matches(..)` here so that clicking
// an element nested within a dialog opener does cause the dialog to open
Expand Down
Loading