mirror of
https://github.com/prymitive/karma
synced 2026-05-09 03:36:44 +00:00
fix(ui): rewrite Modal with hooks
This commit is contained in:
committed by
Łukasz Mierzwa
parent
1e86e78142
commit
9dfe40c837
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ afterEach(() => {
|
||||
useFetchGet.mockReset();
|
||||
useFetchDelete.mockReset();
|
||||
clear();
|
||||
document.body.className = "";
|
||||
});
|
||||
|
||||
const MockOnHide = jest.fn();
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user