Skip to content

Commit

Permalink
fix(modal, modal-manager): modal and manager now set a refocus flag w…
Browse files Browse the repository at this point in the history
…hen stacked modals are closed

When there are at least two  `Modal`s stacked and one closes the `ModalManager` will trigger a
callback to set a flag which is passed to the `FocusTrap` via `ModalContext` and triggers
refocussing of an element within the top most `Modal`

fix #4502
  • Loading branch information
edleeks87 committed Nov 18, 2021
1 parent 5ae3922 commit 5b9c5f7
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 14 deletions.
39 changes: 34 additions & 5 deletions src/components/modal/__internal__/modal-manager.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,55 @@
class ModalManagerInstance {
#modalList = [];

addModal = (modal) => {
this.#modalList.push(modal);
#getTopModal() {
if (!this.#modalList.length) {
return {};
}

return this.#modalList[this.#modalList.length - 1];
}

addModal = (modal, setTriggerRefocusFlag) => {
const {
modal: topModal,
setTriggerRefocusFlag: setTrapFlag,
} = this.#getTopModal();

if (topModal && setTrapFlag) {
setTrapFlag(false);
}

this.#modalList.push({ modal, setTriggerRefocusFlag });
};

isTopmost(modal) {
if (!modal || !this.#modalList.length) {
const { modal: topModal } = this.#getTopModal();

if (!modal || !topModal) {
return false;
}

return this.#modalList.indexOf(modal) === this.#modalList.length - 1;
return modal === topModal;
}

removeModal(modal) {
const modalIndex = this.#modalList.indexOf(modal);
const modalIndex = this.#modalList.findIndex(({ modal: m }) => m === modal);

if (modalIndex === -1) {
return;
}

this.#modalList.splice(modalIndex, 1);

if (!this.#modalList.length) {
return;
}

const { setTriggerRefocusFlag } = this.#getTopModal();

if (setTriggerRefocusFlag) {
setTriggerRefocusFlag(true);
}
}

clearList() {
Expand Down
44 changes: 37 additions & 7 deletions src/components/modal/__internal__/modal-manager.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,21 @@ import ModalManager from "./modal-manager";
describe("ModalManager", () => {
describe("when the addModal method has been called", () => {
it("then the element passed in an attribute should be the topmost element", () => {
const mockModal = { foo: "bar" };
const cb1 = jest.fn();
const cb2 = jest.fn();

ModalManager.addModal(mockModal);
expect(ModalManager.isTopmost(mockModal)).toBe(true);
const mockModal1 = { foo: "foo" };
const mockModal2 = { bar: "bar" };

ModalManager.addModal(mockModal1, cb1);
expect(ModalManager.isTopmost(mockModal1)).toBe(true);
expect(cb1).not.toHaveBeenCalled();

ModalManager.addModal(mockModal2, cb2);
expect(ModalManager.isTopmost(mockModal1)).toBe(false);
expect(ModalManager.isTopmost(mockModal2)).toBe(true);
expect(cb1).toHaveBeenCalledWith(false);
expect(cb2).not.toHaveBeenCalled();
});
});

Expand All @@ -23,13 +34,22 @@ describe("ModalManager", () => {

describe("when the removeModal method has been called", () => {
it("then the element passed in an attribute should not be the topmost element", () => {
const mockModal = { foo: "bar" };
const cb1 = jest.fn();
const cb2 = jest.fn();

const mockModal1 = { foo: "foo" };
const mockModal2 = { bar: "bar" };

ModalManager.clearList();
ModalManager.addModal(mockModal);
ModalManager.removeModal(mockModal);
ModalManager.addModal(mockModal1, cb1);
ModalManager.addModal(mockModal2, cb2);
ModalManager.removeModal(mockModal2);
expect(ModalManager.isTopmost(mockModal2)).toBe(false);
expect(cb1).toHaveBeenCalledWith(true);

expect(ModalManager.isTopmost(mockModal)).toBe(false);
ModalManager.removeModal(mockModal1);

expect(ModalManager.isTopmost(mockModal1)).toBe(false);
});

it("then nothing happens if removed modal is not found", () => {
Expand All @@ -39,5 +59,15 @@ describe("ModalManager", () => {
ModalManager.addModal(mockModal);
ModalManager.removeModal({ some: "value" });
});

it("does not trigger refocus if no callback is found for passed modal", () => {
const mockModal1 = { foo: "foo" };
const mockModal2 = { bar: "bar" };

ModalManager.clearList();
ModalManager.addModal(mockModal1);
ModalManager.addModal(mockModal2);
ModalManager.removeModal(mockModal2);
});
});
});
10 changes: 8 additions & 2 deletions src/components/modal/modal.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const Modal = ({
const modalRegistered = useRef(false);
const originalOverflow = useRef(undefined);
const [isAnimationComplete, setAnimationComplete] = useState(false);
const [triggerRefocusFlag, setTriggerRefocusFlag] = useState(false);

const setOverflow = useCallback(() => {
if (
Expand Down Expand Up @@ -113,7 +114,7 @@ const Modal = ({
const registerModal = useCallback(() => {
/* istanbul ignore else */
if (!modalRegistered.current) {
ModalManager.addModal(ref.current);
ModalManager.addModal(ref.current, setTriggerRefocusFlag);

modalRegistered.current = true;
}
Expand Down Expand Up @@ -181,7 +182,12 @@ const Modal = ({
<TransitionGroup>
{content && (
<CSSTransition appear classNames="modal" timeout={timeout}>
<ModalContext.Provider value={{ isAnimationComplete }}>
<ModalContext.Provider
value={{
isAnimationComplete,
triggerRefocusFlag,
}}
>
{content}
</ModalContext.Provider>
</CSSTransition>
Expand Down

0 comments on commit 5b9c5f7

Please sign in to comment.