From da3d46d9b6ce244fff0273ffa918ae77c0c60790 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Feb 2023 10:14:45 +0000 Subject: [PATCH 1/3] build(deps): bump @sideway/formula from 3.0.0 to 3.0.1 Bumps [@sideway/formula](https://github.com/sideway/formula) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/sideway/formula/releases) - [Commits](https://github.com/sideway/formula/compare/v3.0.0...v3.0.1) --- updated-dependencies: - dependency-name: "@sideway/formula" dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 61d91d3b0..5db3a59fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5985,9 +5985,9 @@ } }, "node_modules/@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" }, "node_modules/@sideway/pinpoint": { "version": "2.0.0", @@ -33427,9 +33427,9 @@ } }, "@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" }, "@sideway/pinpoint": { "version": "2.0.0", From 63ce03aec075f0ea99dc7f21ecf06284b853c065 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Feb 2023 10:15:01 +0000 Subject: [PATCH 2/3] build(deps): bump http-cache-semantics from 4.1.0 to 4.1.1 Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/kornelski/http-cache-semantics/releases) - [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1) --- updated-dependencies: - dependency-name: http-cache-semantics dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 61d91d3b0..0ff00bfd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15807,9 +15807,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, "node_modules/http-errors": { "version": "2.0.0", @@ -40900,9 +40900,9 @@ } }, "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, "http-errors": { "version": "2.0.0", From e86b1a2ca82912d89b3e0b87228618f6439f1bcd Mon Sep 17 00:00:00 2001 From: "Ewe Seong, Yeoh" Date: Wed, 29 Mar 2023 18:07:53 +0800 Subject: [PATCH 3/3] fix(Modal): manage tab navigation - prevent events from leaking into parent - store and return focus of trigger element --- docs/src/pages/docs/modal.tsx | 6 +- lib/src/Modal/Modal.test.tsx | 214 ++++++++++++++------------------ lib/src/Modal/Modal.tsx | 26 ++-- lib/src/utils/keyboardHelper.ts | 1 + 4 files changed, 113 insertions(+), 134 deletions(-) diff --git a/docs/src/pages/docs/modal.tsx b/docs/src/pages/docs/modal.tsx index d68fec282..2207d138b 100644 --- a/docs/src/pages/docs/modal.tsx +++ b/docs/src/pages/docs/modal.tsx @@ -26,7 +26,6 @@ const positions: Array> = [ const NotificationPage: React.FC = () => { const [toggle, setToggle] = React.useState(false); - const toggleButtonRef: React.MutableRefObject = React.useRef(); const { renderForm, @@ -55,7 +54,6 @@ const NotificationPage: React.FC = () => { const dismiss = React.useCallback(() => { setToggle(false); - toggleButtonRef.current?.focus(); }, []); const { position, size, fullscreen, centered, trapfocus, autoFocus } = controls as { [k: string]: any }; @@ -65,9 +63,7 @@ const NotificationPage: React.FC = () => { mainFile={importString} example={ <> - + { - let container: HTMLDivElement = null; const MOCK_MODAL_BODY: React.ReactNode = (
- -
); - function renderModal(props: ModalProps = {}): void { - act(() => { - render(, container); - }); - - act(() => Simulate.animationEnd(document.querySelector(props.fullscreen ? ".modal" : ".modal-dialog"))); - } - - function pressKey(data: Partial, element: Element = document.activeElement): void { - act(() => Simulate.keyDown(element, data)); + function renderModal(props: ModalProps = {}, sibling?: React.ReactNode): RenderResult { + const result = render( + <> + + {sibling} + + ); + fireEvent.animationEnd(screen.getByRole(props.fullscreen ? "dialog" : "document")); + return result; } - beforeEach(() => { - container = document.createElement("div"); - document.body.appendChild(container); - }); - - afterEach(() => { - unmountComponentAtNode(container); - container.remove(); - container = null; + it("Should render correctly", () => { + renderModal(); + expect(screen.getByRole("dialog")).toBeInTheDocument(); + expect(screen.getByRole("document")).toBeInTheDocument(); }); - it("Should render correctly", () => { - renderModal({ toggle: false }); - const modal: HTMLDivElement = document.body.querySelector("div.rc.modal"); - expect(modal).not.toBeNull(); - expect(modal.classList.contains("show")).toBeFalsy(); - expect(modal.classList.contains("hide")).toBeFalsy(); - expect(modal.firstElementChild.classList.contains("modal-dialog")).toBeTruthy(); - expect(modal.firstElementChild.firstElementChild.classList.contains("modal-content")).toBeTruthy(); + it("Should toggle visibility correctly", () => { + const { rerender } = renderModal(); + expect(screen.getByRole("dialog")).not.toHaveClass("hide"); + expect(screen.getByRole("dialog")).toHaveClass("show"); + rerender(); + expect(screen.getByRole("dialog")).not.toHaveClass("show"); + expect(screen.getByRole("dialog")).toHaveClass("hide"); }); it("Should be centered when configured", () => { renderModal({ centered: true }); - expect(document.querySelector(".modal").classList.contains("modal-centered")).toBeTruthy(); + expect(screen.getByRole("dialog")).toHaveClass("modal-centered"); }); it("Should be fullscreen when configured", () => { renderModal({ fullscreen: true }); - expect(document.querySelector(".modal").classList.contains("modal-fullscreen")).toBeTruthy(); + expect(screen.getByRole("dialog")).toHaveClass("modal-fullscreen"); }); it("Should fire animation handler when animation has ended", () => { @@ -64,57 +57,54 @@ describe("Component: Modal", () => { expect(onAnimationEnd).toHaveBeenCalled(); }); - it("Should fire backdrop handler when backdrop is dismissed", () => { + it("Should fire backdrop handler when backdrop is dismissed", async () => { + const user: UserEvent = userEvent.setup(); const onBackdropDismiss: jest.Mock = jest.fn(); const onClick: jest.Mock = jest.fn(); - renderModal({ onBackdropDismiss, onClick }); expect(onClick).not.toBeCalled(); - act(() => Simulate.click(document.querySelector(".modal-dialog"))); + await user.click(screen.getByRole("document")); expect(onClick).toBeCalled(); expect(onBackdropDismiss).not.toBeCalled(); - act(() => Simulate.click(document.querySelector(".modal"))); + await user.click(screen.getByRole("dialog")); expect(onBackdropDismiss).toBeCalled(); }); - it("Should fire escape handler when escape key is pressed", () => { + it("Should fire escape handler when escape key is pressed", async () => { + const user: UserEvent = userEvent.setup(); const onEscape: jest.Mock = jest.fn(); - renderModal({ onEscape }); expect(onEscape).not.toBeCalled(); - pressKey({ key: Key.Escape }, document.querySelector(".rc.modal")); + await user.click(screen.getByRole("document")); + await user.keyboard(`[${Key.Escape}]`); expect(onEscape).toBeCalled(); }); describe("Sizes", () => { - const sizes: ModalSize[] = ["sm", "md", "lg"]; - sizes.forEach((size: ModalSize) => { + ["sm", "md", "lg"].forEach((size: ModalSize) => { it(`Should render size (${size}) when configured`, () => { renderModal({ size }); - expect(document.querySelector(".modal-dialog").classList.contains(`modal-${size}`)).toBeTruthy(); + expect(screen.getByRole("document")).toHaveClass(`modal-${size}`); }); }); }); describe("Positions", () => { - const positions: ModalPosition[] = ["left", "right"]; - positions.forEach((position: ModalPosition) => + ["left", "right"].forEach((position: ModalPosition) => it(`Should render position (${position}) when configured`, () => { renderModal({ position }); - expect(document.querySelector(".modal").classList.contains("modal-aside")).toBeTruthy(); - expect(document.querySelector(".modal").classList.contains(`modal-aside-${position}`)).toBeTruthy(); + expect(screen.getByRole("dialog")).toHaveClass("modal-aside", `modal-aside-${position}`); }) ); it("Should render position (default) when configured", () => { renderModal({ position: "default" }); - expect(document.querySelector(".modal").classList.contains("modal-aside")).toBeFalsy(); + expect(screen.getByRole("dialog")).not.toHaveClass("modal-aside"); }); it("Should be overridden when fullscreen is configured", () => { renderModal({ fullscreen: true, position: "left" }); - expect(document.querySelector(".modal").classList.contains("modal-aside")).toBeFalsy(); - expect(document.querySelector(".modal").classList.contains("modal-aside-left")).toBeFalsy(); + expect(screen.getByRole("dialog")).not.toHaveClass("modal-aside", "modal-aside-left"); }); }); @@ -122,118 +112,98 @@ describe("Component: Modal", () => { describe("Should focus on first input when modal is visible", () => { it("default mode", () => { renderModal({ children: MOCK_MODAL_BODY }); - expect(document.activeElement).toBe(document.getElementById("test-input")); + expect(screen.getByTestId("test-input")).toHaveFocus(); }); it("fullscreen mode", () => { - renderModal({ - children: MOCK_MODAL_BODY, - fullscreen: true, - }); - expect(document.activeElement).toBe(document.getElementById("test-input")); + renderModal({ children: MOCK_MODAL_BODY, fullscreen: true }); + expect(screen.getByTestId("test-input")).toHaveFocus(); }); }); describe("Should not focus on first input when modal not visible", () => { it("default mode", () => { - renderModal({ - children: MOCK_MODAL_BODY, - toggle: false, - }); - expect(document.activeElement).toBe(document.body); + renderModal({ children: MOCK_MODAL_BODY, toggle: false }); + expect(document.body).toHaveFocus(); }); it("fullscreen mode", () => { - renderModal({ - children: MOCK_MODAL_BODY, - fullscreen: true, - toggle: false, - }); - expect(document.activeElement).toBe(document.body); + renderModal({ children: MOCK_MODAL_BODY, fullscreen: true, toggle: false }); + expect(document.body).toHaveFocus(); }); }); describe("Should retain focus body when input is not available in modal", () => { it("default mode", () => { renderModal({ children:
}); - expect(document.activeElement).toBe(document.body); + expect(document.body).toHaveFocus(); }); it("fullscreen mode", () => { renderModal({ children:
, fullscreen: true }); - expect(document.activeElement).toBe(document.body); + expect(document.body).toHaveFocus(); }); }); }); describe("Should trap focus", () => { - function tabForward() { - act(() => pressKey({ key: Key.Tab }, document.querySelector(".rc.modal"))); - } + const extraButton =