diff --git a/cypress/e2e/state.cy.ts b/cypress/e2e/state.cy.ts index 4347bd66..04facc04 100644 --- a/cypress/e2e/state.cy.ts +++ b/cypress/e2e/state.cy.ts @@ -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) }) @@ -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') @@ -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) }) @@ -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') diff --git a/cypress/e2e/utils.ts b/cypress/e2e/utils.ts index e6be41a8..239a718f 100644 --- a/cypress/e2e/utils.ts +++ b/cypress/e2e/utils.ts @@ -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') } @@ -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') } diff --git a/cypress/e2e/webComponents.cy.ts b/cypress/e2e/webComponents.cy.ts new file mode 100644 index 00000000..a07fb7ee --- /dev/null +++ b/cypress/e2e/webComponents.cy.ts @@ -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) + }) +}) diff --git a/cypress/fixtures/base.html b/cypress/fixtures/base.html index 6701f532..d4efb823 100644 --- a/cypress/fixtures/base.html +++ b/cypress/fixtures/base.html @@ -12,6 +12,7 @@

Tests — Base

+ Open the dialog window
@@ -42,6 +43,7 @@

Dialog title

+ - + \ No newline at end of file diff --git a/cypress/fixtures/web-components.html b/cypress/fixtures/web-components.html new file mode 100644 index 00000000..029b625d --- /dev/null +++ b/cypress/fixtures/web-components.html @@ -0,0 +1,57 @@ + + + + + + Tests — Web Components + + + + +
+

Tests — Web Components

+ + + + +
+ + + + + + + \ No newline at end of file diff --git a/src/a11y-dialog.ts b/src/a11y-dialog.ts index 4dde431e..2e21e68c 100644 --- a/src/a11y-dialog.ts +++ b/src/a11y-dialog.ts @@ -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. + // ``), 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