fix(ui): rewrite Modal with hooks

This commit is contained in:
Łukasz Mierzwa
2020-06-09 17:58:18 +01:00
committed by Łukasz Mierzwa
parent 1e86e78142
commit 9dfe40c837
7 changed files with 107 additions and 112 deletions

View File

@@ -34,6 +34,7 @@ beforeEach(() => {
afterEach(() => {
jest.restoreAllMocks();
fetchMock.reset();
document.body.className = "";
});
const MountedMainModal = () => {
@@ -111,8 +112,12 @@ describe("<MainModal />", () => {
it("'modal-open' class is removed from body node after modal is hidden", () => {
const tree = MountedMainModal();
const toggle = tree.find(".nav-link");
toggle.simulate("click");
expect(document.body.className.split(" ")).toContain("modal-open");
toggle.simulate("click");
act(() => jest.runOnlyPendingTimers());
expect(document.body.className.split(" ")).not.toContain("modal-open");
});

View File

@@ -52,6 +52,7 @@ afterEach(() => {
useFetchGet.mockReset();
useFetchDelete.mockReset();
clear();
document.body.className = "";
});
const MockOnHide = jest.fn();

View File

@@ -1,10 +1,7 @@
import React, { Component } from "react";
import React, { useRef, useEffect, useCallback } from "react";
import ReactDOM from "react-dom";
import PropTypes from "prop-types";
import { observer } from "mobx-react";
import { observable } from "mobx";
import { disableBodyScroll, clearAllBodyScrollLocks } from "body-scroll-lock";
import { HotKeys } from "react-hotkeys";
@@ -14,105 +11,76 @@ import {
MountModalBackdrop,
} from "Components/Animations/MountModal";
const Modal = observer(
class Modal extends Component {
static propTypes = {
size: PropTypes.oneOf(["lg", "xl"]),
isOpen: PropTypes.bool.isRequired,
isUpper: PropTypes.bool,
toggleOpen: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
const ModalInner = ({ size, isUpper, toggleOpen, children }) => {
const ref = useRef(null);
useEffect(() => {
document.body.classList.add("modal-open");
disableBodyScroll(ref.current);
return () => {
document.body.classList.remove("modal-open");
clearAllBodyScrollLocks();
};
static defaultProps = {
size: "lg",
isUpper: false,
}, []);
const onRemount = useCallback(() => {
document.body.classList.add("modal-open");
disableBodyScroll(ref.current);
}, []);
useEffect(() => {
window.addEventListener("remountModal", onRemount);
return () => {
window.removeEventListener("remountModal", onRemount);
};
}, [onRemount]);
constructor(props) {
super(props);
this.modalRef = React.createRef();
this.HotKeysRef = React.createRef();
this.lastIsOpen = observable.box(false);
}
return (
<HotKeys
innerRef={(r) => r && r.focus()}
keyMap={{ CLOSE: "Escape" }}
handlers={{ CLOSE: toggleOpen }}
className="modal-open"
>
<div ref={ref} className="modal d-block" role="dialog">
<div
className={`modal-dialog modal-${size} ${
isUpper ? "modal-upper shadow" : ""
}`}
role="document"
>
<div className="modal-content">{children}</div>
</div>
</div>
</HotKeys>
);
};
toggleBodyClass = (isOpen) => {
document.body.classList.toggle("modal-open", isOpen);
if (isOpen) {
this.HotKeysRef.current.focus();
disableBodyScroll(this.modalRef.current);
} else {
clearAllBodyScrollLocks();
}
this.lastIsOpen.set(isOpen);
};
componentDidMount() {
const { isOpen } = this.props;
if (isOpen) {
this.toggleBodyClass(isOpen);
}
}
componentDidUpdate() {
const { isOpen } = this.props;
// we shouldn't update if modal is hidden and was hidden previously
// which can happen when the button gets re-rendered
if (this.lastIsOpen.get() === true || isOpen === true) {
this.toggleBodyClass(isOpen);
}
}
componentWillUnmount() {
const { isOpen } = this.props;
if (isOpen) {
this.toggleBodyClass(false);
}
}
render() {
const {
size,
isOpen,
isUpper,
toggleOpen,
children,
...props
} = this.props;
return ReactDOM.createPortal(
<React.Fragment>
<MountModal
in={isOpen}
unmountOnExit
className="modal-open"
{...props}
>
<HotKeys
innerRef={this.HotKeysRef}
keyMap={{ CLOSE: "Escape" }}
handlers={{ CLOSE: toggleOpen }}
>
<div ref={this.modalRef} className="modal d-block" role="dialog">
<div
className={`modal-dialog modal-${size} ${
isUpper && "modal-upper shadow"
}`}
role="document"
>
<div className="modal-content">{children}</div>
</div>
</div>
</HotKeys>
</MountModal>
<MountModalBackdrop in={isOpen} unmountOnExit>
<div className="modal-backdrop d-block" />
</MountModalBackdrop>
</React.Fragment>,
document.body
);
}
}
);
const Modal = ({ size, isOpen, isUpper, toggleOpen, children, ...props }) => {
return ReactDOM.createPortal(
<React.Fragment>
<MountModal in={isOpen} unmountOnExit {...props}>
<ModalInner size={size} isUpper={isUpper} toggleOpen={toggleOpen}>
{children}
</ModalInner>
</MountModal>
<MountModalBackdrop in={isOpen} unmountOnExit>
<div className="modal-backdrop d-block" />
</MountModalBackdrop>
</React.Fragment>,
document.body
);
};
Modal.propTypes = {
size: PropTypes.oneOf(["lg", "xl"]),
isOpen: PropTypes.bool.isRequired,
isUpper: PropTypes.bool,
toggleOpen: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
};
Modal.defaultProps = {
size: "lg",
isUpper: false,
};
export { Modal };

View File

@@ -16,15 +16,22 @@ const MountedModal = (isOpen) => {
afterEach(() => {
jest.resetAllMocks();
document.body.className = "";
});
describe("<Modal />", () => {
it("'modal-open' class is appended to MountModal container", () => {
const tree = MountedModal(true);
expect(tree.find("div").at(0).hasClass("modal-open")).toBe(true);
});
it("'modal-open' class is appended to body node when modal is visible", () => {
MountedModal(true);
expect(document.body.className.split(" ")).toContain("modal-open");
});
it("'modal-open' class is not removed from body node after hidden modal is unmounted", () => {
document.body.classList.add("modal-open");
const tree = MountedModal(false);
tree.unmount();
expect(document.body.className.split(" ")).toContain("modal-open");
@@ -51,9 +58,7 @@ describe("<Modal />", () => {
const tree = MountedModal(isOpen);
expect(document.body.className.split(" ")).toContain("modal-open");
isOpen = false;
// force update
tree.setProps({ style: {} });
tree.setProps({ isOpen: false });
expect(document.body.className.split(" ")).toContain("modal-open");
});
@@ -70,7 +75,10 @@ describe("<Modal />", () => {
it("toggleOpen is called after pressing 'esc'", () => {
const tree = MountedModal(true);
tree.simulate("keyDown", { key: "Escape", keyCode: 27, which: 27 });
tree
.find("div")
.at(0)
.simulate("keyDown", { key: "Escape", keyCode: 27, which: 27 });
expect(fakeToggle).toHaveBeenCalled();
});
});

View File

@@ -16,6 +16,10 @@ beforeEach(() => {
alertStore = new AlertStore([]);
});
afterEach(() => {
document.body.className = "";
});
const MountedOverviewModal = () => {
return mount(<OverviewModal alertStore={alertStore} />);
};
@@ -80,9 +84,12 @@ describe("<OverviewModal />", () => {
it("'modal-open' class is removed from body node after modal is hidden", () => {
const tree = MountedOverviewModal();
const toggle = tree.find("div.navbar-brand");
toggle.simulate("click");
toggle.simulate("click");
tree.find("div.navbar-brand").simulate("click");
expect(document.body.className.split(" ")).toContain("modal-open");
tree.find("div.navbar-brand").simulate("click");
act(() => jest.runOnlyPendingTimers());
expect(document.body.className.split(" ")).not.toContain("modal-open");
});

View File

@@ -1,4 +1,4 @@
import React, { useRef } from "react";
import React from "react";
import PropTypes from "prop-types";
import { observer } from "mobx-react";
@@ -22,10 +22,9 @@ const SilenceModalContent = React.lazy(() =>
const SilenceModal = observer(
({ alertStore, silenceFormStore, settingsStore }) => {
const modalRef = useRef();
const onDeleteModalClose = React.useCallback(() => {
modalRef.current.toggleBodyClass(true);
const event = new CustomEvent("remountModal");
window.dispatchEvent(event);
}, []);
return (
@@ -46,7 +45,6 @@ const SilenceModal = observer(
</TooltipWrapper>
</li>
<Modal
ref={modalRef}
isOpen={silenceFormStore.toggle.visible}
toggleOpen={silenceFormStore.toggle.toggle}
onExited={silenceFormStore.data.resetProgress}

View File

@@ -32,6 +32,10 @@ beforeEach(() => {
silenceFormStore = new SilenceFormStore();
});
afterEach(() => {
document.body.className = "";
});
const MountedSilenceModal = () => {
return mount(
<ThemeContext.Provider
@@ -131,8 +135,12 @@ describe("<SilenceModal />", () => {
it("'modal-open' class is removed from body node after modal is hidden", () => {
const tree = MountedSilenceModal();
const toggle = tree.find(".nav-link");
toggle.simulate("click");
expect(document.body.className.split(" ")).toContain("modal-open");
toggle.simulate("click");
act(() => jest.runOnlyPendingTimers());
expect(document.body.className.split(" ")).not.toContain("modal-open");
});