fix(ui): migrate to react-hotkeys-hook

This commit is contained in:
Łukasz Mierzwa
2020-06-15 19:47:21 +01:00
committed by Łukasz Mierzwa
parent 9267a2cd20
commit 83e802dd9b
19 changed files with 157 additions and 113 deletions
+14
View File
@@ -11830,6 +11830,11 @@
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
},
"hotkeys-js": {
"version": "3.7.6",
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.7.6.tgz",
"integrity": "sha512-X5d16trjp79o+OaCn7syXu0cs+TkLYlK/teE5FhpD1Cj9ROcEIhfIQ7Mhrk761ynF3NQLbLn5xRojP2UuSqDAw=="
},
"hpack.js": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
@@ -19071,10 +19076,19 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0.tgz",
"integrity": "sha512-3n3OU8vLX/pfcJrR3xJ1zlww6KS1kEJt0Whxc4FiGV+MJrQ1mYSYI3qS/11d2MJDFm8IhOXMTFQirfu6AVOF6Q==",
"dev": true,
"requires": {
"prop-types": "^15.6.1"
}
},
"react-hotkeys-hook": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-2.1.3.tgz",
"integrity": "sha512-p4cH9n4c8Ac3kkNIf6a/97sLEJq+nnhP7fpT0bHhTwByZhBDU3j5hluu00FomxE3hMzJWk4knYG5ZuUM/qVJPw==",
"requires": {
"hotkeys-js": "3.7.6"
}
},
"react-idle-timer": {
"version": "4.2.12",
"resolved": "https://registry.npmjs.org/react-idle-timer/-/react-idle-timer-4.2.12.tgz",
+1 -1
View File
@@ -41,7 +41,7 @@
"react-day-picker": "7.4.8",
"react-dom": "16.13.1",
"react-highlighter": "0.4.3",
"react-hotkeys": "2.0.0",
"react-hotkeys-hook": "2.1.3",
"react-idle-timer": "4.2.12",
"react-input-range": "1.3.0",
"react-js-pagination": "3.0.3",
@@ -50,9 +50,7 @@ exports[`<LabelSetList /> matches snapshot with populated list 1`] = `
</li>
</ul>
</div>
<div tabindex=\\"-1\\"
class=\\"components-pagination\\"
>
<div class=\\"components-pagination\\">
</div>
</div>
"
@@ -127,7 +127,7 @@ describe("<AlertGroupSortConfiguration />", () => {
});
it("label select handles fetch errors", () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: null,
error: "fake error",
isLoading: false,
@@ -64,7 +64,7 @@ describe("<MultiGridConfiguration />", () => {
});
it("label select handles fetch errors", () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: null,
error: "fake error",
isLoading: false,
@@ -9,6 +9,7 @@ import moment from "moment";
import { EmptyAPIResponse } from "__mocks__/Fetch";
import { MockSilence } from "__mocks__/Alerts";
import { PressKey } from "__mocks__/PressKey";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { useFetchGet } from "Hooks/useFetchGet";
@@ -108,9 +109,7 @@ describe("<DeleteSilence />", () => {
tree.find("button.btn-danger").simulate("click");
expect(tree.find(".modal-body")).toHaveLength(1);
tree
.find("div.modal")
.simulate("keyDown", { key: "Escape", keyCode: 27, which: 27 });
PressKey("Escape", 27);
act(() => jest.runOnlyPendingTimers());
tree.update();
expect(tree.find(".modal-body")).toHaveLength(0);
@@ -141,7 +140,7 @@ describe("<DeleteSilenceModalContent />", () => {
});
it("renders ProgressMessage while loading preview", () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: null,
error: false,
isLoading: true,
@@ -158,7 +157,7 @@ describe("<DeleteSilenceModalContent />", () => {
});
it("renders empty LabelSetList with empty response", () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: EmptyAPIResponse(),
error: false,
isLoading: false,
@@ -181,7 +180,7 @@ describe("<DeleteSilenceModalContent />", () => {
});
it("handles empty grid response correctly", () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: EmptyAPIResponse(),
error: false,
isLoading: false,
@@ -192,7 +191,7 @@ describe("<DeleteSilenceModalContent />", () => {
});
it("renders ErrorMessage on failed preview fetch", () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: null,
error: "fake error",
isLoading: false,
+5 -13
View File
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
import { disableBodyScroll, enableBodyScroll } from "body-scroll-lock";
import { HotKeys } from "react-hotkeys";
import { useHotkeys } from "react-hotkeys-hook";
import {
MountModal,
@@ -13,11 +13,6 @@ import {
const ModalInner = ({ size, isUpper, toggleOpen, children }) => {
const ref = useRef(null);
const hotKeysRef = useRef(null);
useEffect(() => {
hotKeysRef.current && hotKeysRef.current.focus();
}, []);
useEffect(() => {
document.body.classList.add("modal-open");
@@ -30,13 +25,10 @@ const ModalInner = ({ size, isUpper, toggleOpen, children }) => {
};
}, [isUpper]);
useHotkeys("esc", toggleOpen);
return (
<HotKeys
innerRef={hotKeysRef}
keyMap={{ CLOSE: "Escape" }}
handlers={{ CLOSE: toggleOpen }}
className="modal-open"
>
<div className="modal-open">
<div ref={ref} className="modal d-block" role="dialog">
<div
className={`modal-dialog modal-${size} ${
@@ -47,7 +39,7 @@ const ModalInner = ({ size, isUpper, toggleOpen, children }) => {
<div className="modal-content">{children}</div>
</div>
</div>
</HotKeys>
</div>
);
};
+3 -5
View File
@@ -2,6 +2,7 @@ import React from "react";
import { mount } from "enzyme";
import { PressKey } from "__mocks__/PressKey";
import { Modal } from ".";
const fakeToggle = jest.fn();
@@ -90,11 +91,8 @@ describe("<Modal />", () => {
});
it("toggleOpen is called after pressing 'esc'", () => {
const tree = MountedModal(true);
tree
.find("div")
.at(0)
.simulate("keyDown", { key: "Escape", keyCode: 27, which: 27 });
MountedModal(true);
PressKey("Escape", 27);
expect(fakeToggle).toHaveBeenCalled();
});
});
@@ -155,7 +155,7 @@ describe("<FilterInput Autosuggest />", () => {
});
it("handles failed suggestion fetches", async () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: null,
error: "fake error",
isLoading: false,
+24 -34
View File
@@ -1,7 +1,7 @@
import React, { useState, useCallback, useEffect, useRef } from "react";
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { HotKeys } from "react-hotkeys";
import { useHotkeys } from "react-hotkeys-hook";
import Pagination from "react-js-pagination";
@@ -21,11 +21,11 @@ const PageSelect = ({
setPageCallback,
}) => {
const [activePage, setActivePage] = useState(initialPage);
const ref = useRef(null);
useEffect(() => {
ref.current && ref.current.focus();
}, []);
const onChange = (page) => {
setActivePage(page);
setPageCallback(page);
};
useEffect(() => {
if (activePage > totalPages) {
@@ -35,40 +35,30 @@ const PageSelect = ({
}
}, [activePage, maxPerPage, totalPages, setPageCallback]);
const onChange = useCallback(
(page) => {
useHotkeys(
"left",
() => {
const page = Math.max(activePage - 1, 1);
setActivePage(page);
setPageCallback(page);
},
[setPageCallback]
{},
[activePage, setActivePage, setPageCallback]
);
const onPageUp = useCallback(() => {
const page = Math.min(activePage + 1, totalPages);
setActivePage(page);
setPageCallback(page);
}, [activePage, setPageCallback, totalPages]);
const onPageDown = useCallback(() => {
const page = Math.max(activePage - 1, 1);
setActivePage(page);
setPageCallback(page);
}, [activePage, setPageCallback]);
useHotkeys(
"right",
() => {
const page = Math.min(activePage + 1, totalPages);
setActivePage(page);
setPageCallback(page);
},
{},
[activePage, totalPages, setActivePage, setPageCallback]
);
return (
<HotKeys
className="components-pagination"
innerRef={ref}
keyMap={{
onArrowLeft: "ArrowLeft",
onArrowRight: "ArrowRight",
}}
handlers={{
onArrowLeft: onPageDown,
onArrowRight: onPageUp,
}}
allowChanges
>
<div className="components-pagination">
{totalItemsCount > maxPerPage ? (
<div className="mt-3">
<Pagination
@@ -90,7 +80,7 @@ const PageSelect = ({
/>
</div>
) : null}
</HotKeys>
</div>
);
};
PageSelect.propTypes = {
+9 -9
View File
@@ -2,7 +2,7 @@ import React from "react";
import { mount } from "enzyme";
import { PressKey } from "__mocks__/KeyPress";
import { PressKey } from "__mocks__/PressKey";
import { PageSelect } from ".";
let originalInnerWidth;
@@ -33,28 +33,28 @@ describe("<PageSelect />", () => {
);
tree.simulate("focus");
PressKey(tree, "ArrowRight", 39);
PressKey("ArrowRight", 39);
expect(setPageCallback).toHaveBeenLastCalledWith(2);
PressKey(tree, "ArrowRight", 39);
PressKey("ArrowRight", 39);
expect(setPageCallback).toHaveBeenLastCalledWith(3);
PressKey(tree, "ArrowRight", 39);
PressKey("ArrowRight", 39);
expect(setPageCallback).toHaveBeenLastCalledWith(4);
PressKey(tree, "ArrowRight", 39);
PressKey("ArrowRight", 39);
expect(setPageCallback).toHaveBeenLastCalledWith(4);
PressKey(tree, "ArrowLeft", 37);
PressKey("ArrowLeft", 37);
expect(setPageCallback).toHaveBeenLastCalledWith(3);
PressKey(tree, "ArrowLeft", 37);
PressKey("ArrowLeft", 37);
expect(setPageCallback).toHaveBeenLastCalledWith(2);
PressKey(tree, "ArrowLeft", 37);
PressKey("ArrowLeft", 37);
expect(setPageCallback).toHaveBeenLastCalledWith(1);
PressKey(tree, "ArrowLeft", 37);
PressKey("ArrowLeft", 37);
expect(setPageCallback).toHaveBeenLastCalledWith(1);
});
@@ -10,7 +10,7 @@ import { advanceTo, clear } from "jest-date-mock";
import { MockSilence } from "__mocks__/Alerts";
import { MockThemeContext } from "__mocks__/Theme";
import { PressKey } from "__mocks__/KeyPress";
import { PressKey } from "__mocks__/PressKey";
import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
@@ -163,7 +163,7 @@ describe("<Browser />", () => {
});
it("renders loading placeholder before fetch finishes", () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: null,
error: false,
isLoading: true,
@@ -175,7 +175,7 @@ describe("<Browser />", () => {
});
it("renders loading placeholder before fetch finishes", () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: null,
error: false,
isLoading: true,
@@ -187,7 +187,7 @@ describe("<Browser />", () => {
});
it("renders empty placeholder after fetch with zero results", () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: [],
error: false,
isLoading: false,
@@ -199,7 +199,7 @@ describe("<Browser />", () => {
});
it("renders silences after successful fetch", () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: [
{
cluster: cluster,
@@ -217,7 +217,7 @@ describe("<Browser />", () => {
it("renders only first 6 silences on desktop", () => {
global.window.innerWidth = 1024;
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: MockSilenceList(7),
error: false,
isLoading: false,
@@ -229,7 +229,7 @@ describe("<Browser />", () => {
it("renders only first 6 silences on mobile", () => {
global.window.innerWidth = 500;
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: MockSilenceList(7),
error: false,
isLoading: false,
@@ -240,7 +240,7 @@ describe("<Browser />", () => {
});
it("renders last silence after page change", () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: MockSilenceList(7),
error: false,
isLoading: false,
@@ -260,7 +260,7 @@ describe("<Browser />", () => {
});
it("renders next/previous page after arrow key press", () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: MockSilenceList(13),
error: false,
isLoading: false,
@@ -274,33 +274,39 @@ describe("<Browser />", () => {
const paginator = tree.find(".components-pagination").at(0);
paginator.simulate("focus");
PressKey(paginator, "ArrowRight", 39);
PressKey("ArrowRight", 39);
tree.update();
expect(tree.find("li.page-item").at(2).hasClass("active")).toBe(true);
expect(tree.find("ManagedSilence")).toHaveLength(6);
PressKey(paginator, "ArrowRight", 39);
PressKey("ArrowRight", 39);
tree.update();
expect(tree.find("li.page-item").at(3).hasClass("active")).toBe(true);
expect(tree.find("ManagedSilence")).toHaveLength(1);
PressKey(paginator, "ArrowRight", 39);
PressKey("ArrowRight", 39);
tree.update();
expect(tree.find("li.page-item").at(3).hasClass("active")).toBe(true);
expect(tree.find("ManagedSilence")).toHaveLength(1);
PressKey(paginator, "ArrowLeft", 37);
PressKey("ArrowLeft", 37);
tree.update();
expect(tree.find("li.page-item").at(2).hasClass("active")).toBe(true);
expect(tree.find("ManagedSilence")).toHaveLength(6);
PressKey(paginator, "ArrowLeft", 37);
PressKey("ArrowLeft", 37);
tree.update();
expect(tree.find("li.page-item").at(1).hasClass("active")).toBe(true);
expect(tree.find("ManagedSilence")).toHaveLength(6);
PressKey(paginator, "ArrowLeft", 37);
PressKey("ArrowLeft", 37);
tree.update();
expect(tree.find("li.page-item").at(1).hasClass("active")).toBe(true);
expect(tree.find("ManagedSilence")).toHaveLength(6);
});
it("resets pagination to last page on truncation", () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: MockSilenceList(13),
error: false,
isLoading: false,
@@ -315,7 +321,7 @@ describe("<Browser />", () => {
expect(tree.find("ManagedSilence")).toHaveLength(1);
expect(tree.find("li.page-item").at(3).hasClass("active")).toBe(true);
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: MockSilenceList(8),
error: false,
isLoading: false,
@@ -326,7 +332,7 @@ describe("<Browser />", () => {
expect(tree.find("ManagedSilence")).toHaveLength(2);
expect(tree.find("li.page-item").at(2).hasClass("active")).toBe(true);
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: [],
error: false,
isLoading: false,
@@ -339,7 +345,7 @@ describe("<Browser />", () => {
});
it("renders error after failed fetch", () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: null,
error: "fake failure",
isLoading: false,
@@ -74,7 +74,7 @@ describe("<LabelNameInput />", () => {
});
it("handles fetch errors when populating suggestions", () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: null,
error: "fake error",
isLoading: false,
@@ -40,7 +40,7 @@ describe("<MatchCounter />", () => {
});
it("renders spinner icon while fetching", () => {
useFetchGet.mockReturnValueOnce({
useFetchGet.fetch.setMockedData({
response: null,
error: false,
isLoading: true,
@@ -53,7 +53,7 @@ describe("<MatchCounter />", () => {
});
it("renders spinner icon with text-danger while retrying fetching", () => {
useFetchGet.mockReturnValueOnce({
useFetchGet.fetch.setMockedData({
response: null,
error: false,
isLoading: true,
@@ -65,7 +65,7 @@ describe("<MatchCounter />", () => {
});
it("renders error icon on failed fetch", () => {
useFetchGet.mockReturnValueOnce({
useFetchGet.fetch.setMockedData({
response: null,
error: "failed",
isLoading: false,
@@ -77,7 +77,7 @@ describe("<MatchCounter />", () => {
});
it("totalAlerts is 0 after mount", () => {
useFetchGet.mockReturnValueOnce({
useFetchGet.fetch.setMockedData({
response: { totalAlerts: 0 },
error: false,
isLoading: false,
@@ -89,9 +89,7 @@ exports[`<SilencePreview /> matches snapshot 1`] = `
</li>
</ul>
</div>
<div tabindex=\\"-1\\"
class=\\"components-pagination\\"
>
<div class=\\"components-pagination\\">
</div>
</div>
</div>
@@ -107,7 +107,7 @@ describe("<SilencePreview />", () => {
});
it("matches snapshot", () => {
useFetchGet.mockReturnValueOnce({
useFetchGet.fetch.setMockedData({
response: MockAPIResponse(),
error: false,
isLoading: false,
@@ -119,7 +119,7 @@ describe("<SilencePreview />", () => {
});
it("renders Placeholder while loading preview", () => {
useFetchGet.mockReturnValue({
useFetchGet.fetch.setMockedData({
response: null,
error: false,
isLoading: true,
@@ -136,7 +136,7 @@ describe("<SilencePreview />", () => {
});
it("handles empty grid response correctly", () => {
useFetchGet.mockReturnValueOnce({
useFetchGet.fetch.setMockedData({
response: EmptyAPIResponse(),
error: false,
isLoading: false,
@@ -148,7 +148,7 @@ describe("<SilencePreview />", () => {
});
it("renders FetchError on failed fetch", () => {
useFetchGet.mockReturnValueOnce({
useFetchGet.fetch.setMockedData({
response: null,
error: "Fetch error",
isLoading: false,
+34 -1
View File
@@ -12,6 +12,21 @@ const MockFetchStats = {
},
reset() {
this.getCalls = [];
this.mockedData = {
response: undefined,
error: undefined,
isLoading: undefined,
isRetrying: undefined,
};
},
mockedData: {
response: undefined,
error: undefined,
isLoading: undefined,
isRetrying: undefined,
},
setMockedData(data) {
this.mockedData = data;
},
};
@@ -82,7 +97,25 @@ const Mock = (uri, { autorun = true, deps = [] } = {}) => {
// eslint-disable-next-line
}, [uri, get, autorun, ...deps]);
return { response, error, isLoading, isRetrying, get };
return {
response:
MockFetchStats.mockedData.response !== undefined
? MockFetchStats.mockedData.response
: response,
error:
MockFetchStats.mockedData.error !== undefined
? MockFetchStats.mockedData.error
: error,
isLoading:
MockFetchStats.mockedData.isLoading !== undefined
? MockFetchStats.mockedData.isLoading
: isLoading,
isRetrying:
MockFetchStats.mockedData.isRetrying !== undefined
? MockFetchStats.mockedData.isRetrying
: isRetrying,
get,
};
};
const useFetchGet = jest.fn(Mock);
-6
View File
@@ -1,6 +0,0 @@
const PressKey = (tree, key, code) => {
tree.simulate("keyDown", { key: key, keyCode: code, which: code });
tree.simulate("keyUp", { key: key, keyCode: code, which: code });
};
export { PressKey };
+22
View File
@@ -0,0 +1,22 @@
import { act } from "react-dom/test-utils";
function PressKey(key, code) {
act(() => {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: key,
keyCode: code,
which: code,
})
);
document.dispatchEvent(
new KeyboardEvent("keyup", {
key: key,
keyCode: code,
which: code,
})
);
});
}
export { PressKey };