diff --git a/cypress/integration/dialog.test.ts b/cypress/integration/dialog.test.ts new file mode 100644 index 0000000000..0d1934f69d --- /dev/null +++ b/cypress/integration/dialog.test.ts @@ -0,0 +1,65 @@ +/* eslint-disable jest/expect-expect */ +describe("dialog", () => { + beforeEach(() => { + cy.visit("/dialog") + cy.injectAxe() + cy.findByTestId("trigger-1").realClick() + }) + + it("should have no accessibility violation", () => { + cy.checkA11y(".dialog__content") + }) + + it("should focus on close button when dialog is open", () => { + cy.findByTestId("close-1").should("have.focus") + }) + + it("should trap focus within dialog", () => { + cy.findByTestId("close-1").should("have.focus") + cy.findByTestId("close-1").tab().tab().tab().tab() + cy.findByTestId("close-1").should("have.focus") + }) + + it("should close modal on escape", () => { + cy.focused().realType("{esc}") + cy.findByTestId("trigger-1").should("have.focus") + }) + + // potentially flaky test + it("should close modal on overlay click", () => { + cy.findByTestId("overlay-1").click(400, 400, { force: true }) + cy.findByTestId("trigger-1").should("have.focus") + }) + + describe("Nested dialogs", () => { + beforeEach(() => { + cy.findByTestId("trigger-2").click() + }) + it("should focus close button", () => { + cy.findByTestId("close-2").should("have.focus") + }) + + it("should trap focus", () => { + cy.findByTestId("close-2").tab().tab() + cy.findByTestId("close-2").should("have.focus") + }) + + it("should focus on nested buttton on escape", () => { + cy.findByTestId("close-2").realType("{esc}") + cy.findByTestId("trigger-2").should("have.focus") + }) + + it("should close modal on overlay click", () => { + cy.findByTestId("overlay-2").click(400, 400, { force: true }) + cy.findByTestId("trigger-2").should("have.focus") + }) + + it("should close parent modal from child", () => { + cy.findByTestId("special-close").realClick() + cy.findByTestId("overlay-2").should("not.exist") + cy.findByTestId("overlay-1").should("not.exist") + // This works in browsers but not in cypress for some reason + // cy.findByTestId("trigger-1").should("have.focus") + }) + }) +}) diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 2b1b35fe46..3ac4a23f30 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -2,7 +2,7 @@ import "cypress-axe" const COMMAND_DELAY = 500 -for (const cmd of ["click", "trigger", "type"]) { +for (const cmd of ["click", "trigger", "type", "tab"]) { Cypress.Commands.overwrite(cmd, (fn, ...args) => { return fn(...args).then((val) => { return Cypress.Promise.resolve(val).delay(COMMAND_DELAY) diff --git a/cypress/support/index.ts b/cypress/support/index.ts index c22d573fd3..afb9cf883d 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -16,6 +16,7 @@ // Import commands.js using ES2015 syntax: import "@testing-library/cypress/add-commands" import "cypress-real-events/support" +import "cypress-plugin-tab" import "./commands" // Import commands.js using ES2015 syntax: diff --git a/examples/next-ts/pages/dialog.tsx b/examples/next-ts/pages/dialog.tsx index 4c9c58fe16..8e6fc7c6e4 100644 --- a/examples/next-ts/pages/dialog.tsx +++ b/examples/next-ts/pages/dialog.tsx @@ -10,7 +10,7 @@ export default function Page() { // Dialog 1 const [state, send] = useMachine( dialog.machine.withContext({ - initialFocusEl: () => inputRef.current, + // initialFocusEl: () => inputRef.current, }), ) const ref = useSetup({ send, id: "123" }) @@ -25,13 +25,13 @@ export default function Page() { <>
-
{d1.isOpen && ( -
+
)} {d1.isOpen && ( @@ -41,19 +41,19 @@ export default function Page() { Edit profile

Make changes to your profile here. Click save when you are done.

- - - + + - {d2.isOpen && ( -
+
)} {d2.isOpen && ( @@ -62,10 +62,12 @@ export default function Page() {

Nested

- - +
)} @@ -120,6 +122,11 @@ export default function Page() { right: 10px; border-radius: 100%; } + + .dialog__close-button:focus { + outline: 2px blue solid; + outline-offset: 2px; + } `} ) diff --git a/package.json b/package.json index 7a7482700c..6b3781ebbb 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "commitlint": "^12.1.4", "cypress": "^8.5.0", "cypress-axe": "^0.13.0", + "cypress-plugin-tab": "^1.0.5", "cypress-real-events": "^1.5.1", "eslint": "^7.26.0", "eslint-config-prettier": "^8.3.0", diff --git a/packages/machines/src/dialog/dialog.connect.ts b/packages/machines/src/dialog/dialog.connect.ts index 34419bcda3..97e02ca4c4 100644 --- a/packages/machines/src/dialog/dialog.connect.ts +++ b/packages/machines/src/dialog/dialog.connect.ts @@ -25,7 +25,7 @@ export function dialogConnect( id: dom.getTriggerId(ctx), "aria-haspopup": "dialog", type: "button", - "aria-expanded": ariaAttr(isOpen), + "aria-expanded": isOpen, "aria-controls": dom.getContentId(ctx), onClick() { send(isOpen ? "CLOSE" : "OPEN") diff --git a/packages/machines/src/dialog/dialog.machine.ts b/packages/machines/src/dialog/dialog.machine.ts index 275db444d2..ee1239a2fc 100644 --- a/packages/machines/src/dialog/dialog.machine.ts +++ b/packages/machines/src/dialog/dialog.machine.ts @@ -132,7 +132,9 @@ export const dialogMachine = createMachine trap?.deactivate() }, @@ -152,7 +154,9 @@ export const dialogMachine = createMachine { const el = dom.getContentEl(ctx) - unhide = hideOthers(el) + try { + unhide = hideOthers(el) + } catch {} }) return () => unhide?.() }, diff --git a/yarn.lock b/yarn.lock index 0efe0fe411..3b7f87f152 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2678,6 +2678,14 @@ ajv@^8.0.1: require-from-string "^2.0.2" uri-js "^4.2.2" +ally.js@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/ally.js/-/ally.js-1.4.1.tgz#9fb7e6ba58efac4ee9131cb29aa9ee3b540bcf1e" + integrity sha1-n7fmuljvrE7pExyymqnuO1QLzx4= + dependencies: + css.escape "^1.5.0" + platform "1.3.3" + anser@1.4.9: version "1.4.9" resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.9.tgz#1f85423a5dcf8da4631a341665ff675b96845760" @@ -4027,7 +4035,7 @@ crypto-browserify@3.12.0, crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" -css.escape@1.5.1, css.escape@^1.5.1: +css.escape@1.5.1, css.escape@^1.5.0, css.escape@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= @@ -4117,6 +4125,13 @@ cypress-axe@^0.13.0: resolved "https://registry.yarnpkg.com/cypress-axe/-/cypress-axe-0.13.0.tgz#3234e1a79a27701f2451fcf2f333eb74204c7966" integrity sha512-fCIy7RiDCm7t30U3C99gGwQrUO307EYE1QqXNaf9ToK4DVqW8y5on+0a/kUHMrHdlls2rENF6TN9ZPpPpwLrnw== +cypress-plugin-tab@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/cypress-plugin-tab/-/cypress-plugin-tab-1.0.5.tgz#a40714148104004bb05ed62b1bf46bb544f8eb4a" + integrity sha512-QtTJcifOVwwbeMP3hsOzQOKf3EqKsLyjtg9ZAGlYDntrCRXrsQhe4ZQGIthRMRLKpnP6/tTk6G0gJ2sZUfRliQ== + dependencies: + ally.js "^1.4.1" + cypress-real-events@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.5.1.tgz#5eeb86d2a7aad9aa6d5271e288a23e46373915cd" @@ -8680,6 +8695,11 @@ pkg-up@^2.0.0: dependencies: find-up "^2.1.0" +platform@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.3.tgz#646c77011899870b6a0903e75e997e8e51da7461" + integrity sha1-ZGx3ARiZhwtqCQPnXpl+jlHadGE= + platform@1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7"