mirror of
https://github.com/prymitive/karma
synced 2026-05-05 03:16:51 +00:00
fix(ui): use custom component for tooltips
This commit is contained in:
committed by
Łukasz Mierzwa
parent
a65f9dad8e
commit
40edb80314
2
ui/package-lock.json
generated
2
ui/package-lock.json
generated
@@ -19169,6 +19169,7 @@
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/react-popper-tooltip/-/react-popper-tooltip-2.11.1.tgz",
|
||||
"integrity": "sha512-04A2f24GhyyMicKvg/koIOQ5BzlrRbKiAgP6L+Pdj1MVX3yJ1NeZ8+EidndQsbejFT55oW1b++wg2Z8KlAyhfQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"react-popper": "^1.3.7"
|
||||
@@ -19178,6 +19179,7 @@
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz",
|
||||
"integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"create-react-context": "^0.3.0",
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
"react-media": "1.10.0",
|
||||
"react-moment": "0.9.7",
|
||||
"react-popper": "2.2.3",
|
||||
"react-popper-tooltip": "2.11.1",
|
||||
"react-resize-detector": "4.2.3",
|
||||
"react-reveal": "1.2.2",
|
||||
"react-scripts": "3.4.1",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
|
||||
import { mount } from "enzyme";
|
||||
|
||||
@@ -270,7 +271,7 @@ describe("<Alert />", () => {
|
||||
expect(tree.find("time").text()).toBe("a few seconds ago");
|
||||
|
||||
advanceTo(new Date(Date.UTC(2018, 7, 14, 17, 37, 41)));
|
||||
jest.advanceTimersByTime(61 * 1000);
|
||||
act(() => jest.advanceTimersByTime(61 * 1000));
|
||||
expect(tree.find("time").text()).toBe("a minute ago");
|
||||
|
||||
advanceTo(new Date(Date.UTC(2018, 7, 14, 18, 36, 41)));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
|
||||
import { shallow, mount } from "enzyme";
|
||||
|
||||
@@ -371,7 +372,7 @@ describe("<Grid />", () => {
|
||||
MountedGrid();
|
||||
// skip a minute to trigger FontFaceObserver timeout handler
|
||||
advanceBy(60 * 1000);
|
||||
jest.runOnlyPendingTimers();
|
||||
act(() => jest.runOnlyPendingTimers());
|
||||
});
|
||||
|
||||
it("doesn't crash on unmount", () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
|
||||
import { mount } from "enzyme";
|
||||
|
||||
@@ -77,12 +78,12 @@ describe("<MainModal />", () => {
|
||||
const toggle = tree.find(".nav-link");
|
||||
|
||||
toggle.simulate("click");
|
||||
jest.runOnlyPendingTimers();
|
||||
act(() => jest.runOnlyPendingTimers());
|
||||
tree.update();
|
||||
expect(tree.find("MainModalContent")).toHaveLength(1);
|
||||
|
||||
toggle.simulate("click");
|
||||
jest.runOnlyPendingTimers();
|
||||
act(() => jest.runOnlyPendingTimers());
|
||||
tree.update();
|
||||
expect(tree.find("MainModalContent")).toHaveLength(0);
|
||||
});
|
||||
@@ -95,7 +96,7 @@ describe("<MainModal />", () => {
|
||||
expect(tree.find("MainModalContent")).toHaveLength(1);
|
||||
|
||||
tree.find("button.close").simulate("click");
|
||||
jest.runOnlyPendingTimers();
|
||||
act(() => jest.runOnlyPendingTimers());
|
||||
tree.update();
|
||||
expect(tree.find("MainModalContent")).toHaveLength(0);
|
||||
});
|
||||
|
||||
@@ -198,4 +198,4 @@ const NavBar = observer(
|
||||
);
|
||||
NavBar.contextType = ThemeContext;
|
||||
|
||||
export { NavBar };
|
||||
export { NavBar, MobileIdleTimeout, DesktopIdleTimeout };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
|
||||
import { shallow, mount } from "enzyme";
|
||||
import { mount } from "enzyme";
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
@@ -10,13 +10,17 @@ import { AlertStore, NewUnappliedFilter } from "Stores/AlertStore";
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { ThemeContext } from "Components/Theme";
|
||||
|
||||
import { NavBar } from ".";
|
||||
import { NavBar, MobileIdleTimeout, DesktopIdleTimeout } from ".";
|
||||
|
||||
let alertStore;
|
||||
let settingsStore;
|
||||
let silenceFormStore;
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.spyOn(React, "useContext").mockImplementation(() => MockThemeContext);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
alertStore = new AlertStore([]);
|
||||
settingsStore = new Settings();
|
||||
@@ -25,26 +29,19 @@ beforeEach(() => {
|
||||
// fix startsAt & endsAt dates so they don't change between tests
|
||||
silenceFormStore.data.startsAt = moment([2018, 1, 30, 10, 25, 50]).utc();
|
||||
silenceFormStore.data.endsAt = moment([2018, 1, 30, 11, 25, 50]).utc();
|
||||
|
||||
jest.spyOn(React, "useContext").mockImplementation(() => MockThemeContext);
|
||||
});
|
||||
|
||||
const RenderNavbar = () => {
|
||||
return shallow(
|
||||
<NavBar
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
).find(".container");
|
||||
};
|
||||
afterEach(() => {
|
||||
act(() => jest.runAllTimers());
|
||||
});
|
||||
|
||||
const MountedNavbar = () => {
|
||||
const MountedNavbar = (fixedTop) => {
|
||||
return mount(
|
||||
<NavBar
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
fixedTop={fixedTop}
|
||||
/>,
|
||||
{
|
||||
wrappingComponent: ThemeContext.Provider,
|
||||
@@ -57,7 +54,7 @@ const ValidateNavClass = (totalFilters, expectedClass) => {
|
||||
for (let i = 0; i < totalFilters; i++) {
|
||||
alertStore.filters.values.push(NewUnappliedFilter(`foo=${i}`));
|
||||
}
|
||||
const tree = RenderNavbar();
|
||||
const tree = MountedNavbar();
|
||||
const nav = tree.find(".navbar-nav");
|
||||
expect(nav.props().className.split(" ")).toContain(expectedClass);
|
||||
};
|
||||
@@ -93,28 +90,14 @@ describe("<NavBar />", () => {
|
||||
});
|
||||
|
||||
it("navbar includes 'fixed-top' class with fixedTop=true", () => {
|
||||
const tree = mount(
|
||||
<NavBar
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
fixedTop={true}
|
||||
/>
|
||||
);
|
||||
const tree = MountedNavbar(true);
|
||||
const nav = tree.find(".navbar");
|
||||
expect(nav.props().className.split(" ")).toContain("fixed-top");
|
||||
expect(nav.props().className.split(" ")).not.toContain("w-100");
|
||||
});
|
||||
|
||||
it("navbar doesn't 'fixed-top' class with fixedTop=false", () => {
|
||||
const tree = mount(
|
||||
<NavBar
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
fixedTop={false}
|
||||
/>
|
||||
);
|
||||
const tree = MountedNavbar(false);
|
||||
const nav = tree.find(".navbar");
|
||||
expect(nav.props().className.split(" ")).not.toContain("fixed-top");
|
||||
expect(nav.props().className.split(" ")).toContain("w-100");
|
||||
@@ -139,23 +122,19 @@ describe("<NavBar />", () => {
|
||||
});
|
||||
|
||||
describe("<IdleTimer />", () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
it("hides navbar after 12 seconds on mobile", () => {
|
||||
it("hides navbar after MobileIdleTimeout on mobile", () => {
|
||||
global.window.innerWidth = 500;
|
||||
const tree = MountedNavbar();
|
||||
jest.runTimersToTime(1000 * 13);
|
||||
act(() => jest.runTimersToTime(MobileIdleTimeout + 1000));
|
||||
tree.update();
|
||||
expect(tree.find(".container").hasClass("visible")).toBe(false);
|
||||
expect(tree.find(".container").hasClass("invisible")).toBe(true);
|
||||
});
|
||||
|
||||
it("hides navbar after 3 minutes on desktop", () => {
|
||||
it("hides navbar after DesktopIdleTimeout on desktop", () => {
|
||||
global.window.innerWidth = 769;
|
||||
const tree = MountedNavbar();
|
||||
jest.runTimersToTime(1000 * 60 * 3 + 1000);
|
||||
act(() => jest.runTimersToTime(DesktopIdleTimeout + 1000));
|
||||
tree.update();
|
||||
expect(tree.find(".container").hasClass("visible")).toBe(false);
|
||||
expect(tree.find(".container").hasClass("invisible")).toBe(true);
|
||||
@@ -166,7 +145,7 @@ describe("<IdleTimer />", () => {
|
||||
const tree = MountedNavbar();
|
||||
act(() => {
|
||||
alertStore.filters.values.push(NewUnappliedFilter("cluster=dev"));
|
||||
jest.runTimersToTime(1000 * 13);
|
||||
jest.runTimersToTime(MobileIdleTimeout + 1000);
|
||||
});
|
||||
tree.update();
|
||||
expect(tree.find(".container").hasClass("visible")).toBe(true);
|
||||
@@ -178,7 +157,7 @@ describe("<IdleTimer />", () => {
|
||||
const tree = MountedNavbar();
|
||||
act(() => {
|
||||
alertStore.filters.values.push(NewUnappliedFilter("cluster=dev"));
|
||||
jest.runTimersToTime(1000 * 60 * 3 + 1000);
|
||||
jest.runTimersToTime(DesktopIdleTimeout + 1000);
|
||||
});
|
||||
tree.update();
|
||||
expect(tree.find(".container").hasClass("visible")).toBe(true);
|
||||
@@ -190,14 +169,14 @@ describe("<IdleTimer />", () => {
|
||||
const tree = MountedNavbar();
|
||||
act(() => {
|
||||
alertStore.filters.values.push(NewUnappliedFilter("cluster=dev"));
|
||||
jest.runTimersToTime(1000 * 13);
|
||||
jest.runTimersToTime(MobileIdleTimeout + 1000);
|
||||
});
|
||||
tree.update();
|
||||
expect(tree.find(".container").hasClass("visible")).toBe(true);
|
||||
expect(tree.find(".container").hasClass("invisible")).toBe(false);
|
||||
|
||||
alertStore.filters.applyAllFilters();
|
||||
jest.runTimersToTime(1000 * 13);
|
||||
act(() => jest.runTimersToTime(MobileIdleTimeout + 1000));
|
||||
tree.update();
|
||||
expect(tree.find(".container").hasClass("visible")).toBe(false);
|
||||
expect(tree.find(".container").hasClass("invisible")).toBe(true);
|
||||
@@ -208,14 +187,14 @@ describe("<IdleTimer />", () => {
|
||||
const tree = MountedNavbar();
|
||||
act(() => {
|
||||
alertStore.filters.values.push(NewUnappliedFilter("cluster=dev"));
|
||||
jest.runTimersToTime(1000 * 60 * 3 + 1000);
|
||||
jest.runTimersToTime(DesktopIdleTimeout + 1000);
|
||||
});
|
||||
tree.update();
|
||||
expect(tree.find(".container").hasClass("visible")).toBe(true);
|
||||
expect(tree.find(".container").hasClass("invisible")).toBe(false);
|
||||
|
||||
alertStore.filters.applyAllFilters();
|
||||
jest.runTimersToTime(1000 * 60 * 3 + 1000);
|
||||
act(() => jest.runTimersToTime(DesktopIdleTimeout + 1000));
|
||||
tree.update();
|
||||
expect(tree.find(".container").hasClass("visible")).toBe(false);
|
||||
expect(tree.find(".container").hasClass("invisible")).toBe(true);
|
||||
@@ -226,13 +205,13 @@ describe("<IdleTimer />", () => {
|
||||
const instance = tree.instance();
|
||||
|
||||
instance.onIdleTimerIdle();
|
||||
jest.runOnlyPendingTimers();
|
||||
act(() => jest.runOnlyPendingTimers());
|
||||
tree.update();
|
||||
expect(tree.find(".container").hasClass("visible")).toBe(false);
|
||||
expect(tree.find(".container").hasClass("invisible")).toBe(true);
|
||||
|
||||
instance.onIdleTimerActive();
|
||||
jest.runOnlyPendingTimers();
|
||||
act(() => jest.runOnlyPendingTimers());
|
||||
tree.update();
|
||||
expect(tree.find(".container").hasClass("visible")).toBe(true);
|
||||
expect(tree.find(".container").hasClass("invisible")).toBe(false);
|
||||
@@ -243,7 +222,7 @@ describe("<IdleTimer />", () => {
|
||||
const instance = tree.instance();
|
||||
|
||||
instance.onIdleTimerIdle();
|
||||
jest.runOnlyPendingTimers();
|
||||
act(() => jest.runOnlyPendingTimers());
|
||||
tree.update();
|
||||
expect(
|
||||
window
|
||||
@@ -255,7 +234,7 @@ describe("<IdleTimer />", () => {
|
||||
it("doesn't hide when autohide is disabled in settingsStore", () => {
|
||||
settingsStore.filterBarConfig.config.autohide = false;
|
||||
const tree = MountedNavbar();
|
||||
jest.runTimersToTime(1000 * 3600);
|
||||
act(() => jest.runTimersToTime(1000 * 3600));
|
||||
tree.update();
|
||||
expect(tree.find(".container").hasClass("visible")).toBe(true);
|
||||
expect(tree.find(".container").hasClass("invisible")).toBe(false);
|
||||
@@ -265,7 +244,7 @@ describe("<IdleTimer />", () => {
|
||||
settingsStore.filterBarConfig.config.autohide = true;
|
||||
const tree = MountedNavbar();
|
||||
alertStore.status.pause();
|
||||
jest.runTimersToTime(1000 * 3600);
|
||||
act(() => jest.runTimersToTime(1000 * 3600));
|
||||
tree.update();
|
||||
expect(tree.find(".container").hasClass("visible")).toBe(true);
|
||||
expect(tree.find(".container").hasClass("invisible")).toBe(false);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
|
||||
import { mount } from "enzyme";
|
||||
|
||||
@@ -47,12 +48,12 @@ describe("<OverviewModal />", () => {
|
||||
const toggle = tree.find("div.navbar-brand");
|
||||
|
||||
toggle.simulate("click");
|
||||
jest.runOnlyPendingTimers();
|
||||
act(() => jest.runOnlyPendingTimers());
|
||||
tree.update();
|
||||
expect(tree.find(".modal-title").text()).toBe("Overview");
|
||||
|
||||
toggle.simulate("click");
|
||||
jest.runOnlyPendingTimers();
|
||||
act(() => jest.runOnlyPendingTimers());
|
||||
tree.update();
|
||||
expect(tree.find(".modal-title")).toHaveLength(0);
|
||||
});
|
||||
@@ -65,7 +66,7 @@ describe("<OverviewModal />", () => {
|
||||
expect(tree.find(".modal-title").text()).toBe("Overview");
|
||||
|
||||
tree.find("button.close").simulate("click");
|
||||
jest.runOnlyPendingTimers();
|
||||
act(() => jest.runOnlyPendingTimers());
|
||||
tree.update();
|
||||
expect(tree.find("OverviewModalContent")).toHaveLength(0);
|
||||
});
|
||||
|
||||
@@ -357,8 +357,10 @@ describe("<Browser />", () => {
|
||||
|
||||
tree.unmount();
|
||||
|
||||
advanceTo(moment.utc([2000, 0, 1, 0, 30, 59]));
|
||||
act(() => jest.runOnlyPendingTimers());
|
||||
act(() => {
|
||||
advanceTo(moment.utc([2000, 0, 1, 0, 30, 59]));
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
expect(useFetchGet.fetch.calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -78,12 +78,12 @@ describe("<SilenceModal />", () => {
|
||||
const toggle = tree.find(".nav-link");
|
||||
|
||||
toggle.simulate("click");
|
||||
jest.runOnlyPendingTimers();
|
||||
act(() => jest.runOnlyPendingTimers());
|
||||
tree.update();
|
||||
expect(tree.find("SilenceModalContent")).toHaveLength(1);
|
||||
|
||||
toggle.simulate("click");
|
||||
jest.runOnlyPendingTimers();
|
||||
act(() => jest.runOnlyPendingTimers());
|
||||
tree.update();
|
||||
expect(tree.find("SilenceModalContent")).toHaveLength(0);
|
||||
});
|
||||
@@ -93,12 +93,12 @@ describe("<SilenceModal />", () => {
|
||||
const toggle = tree.find(".nav-link");
|
||||
|
||||
toggle.simulate("click");
|
||||
jest.runOnlyPendingTimers();
|
||||
act(() => jest.runOnlyPendingTimers());
|
||||
tree.update();
|
||||
expect(tree.find("SilenceModalContent")).toHaveLength(1);
|
||||
|
||||
silenceFormStore.toggle.hide();
|
||||
jest.runOnlyPendingTimers();
|
||||
act(() => jest.runOnlyPendingTimers());
|
||||
tree.update();
|
||||
expect(tree.find("SilenceModalContent")).toHaveLength(0);
|
||||
});
|
||||
@@ -114,7 +114,7 @@ describe("<SilenceModal />", () => {
|
||||
// click to hide
|
||||
toggle.simulate("click");
|
||||
// wait for animation to finish
|
||||
jest.runOnlyPendingTimers();
|
||||
act(() => jest.runOnlyPendingTimers());
|
||||
tree.update();
|
||||
// form should be reset
|
||||
expect(silenceFormStore.data.currentStage).toBe(SilenceFormStage.UserInput);
|
||||
@@ -154,7 +154,7 @@ describe("<SilenceModal />", () => {
|
||||
silenceFormStore.toggle.visible = true;
|
||||
MountedSilenceModal();
|
||||
|
||||
expect(callbacks).toHaveLength(2);
|
||||
expect(callbacks).toHaveLength(4);
|
||||
act(() => {
|
||||
callbacks.forEach((f) => f());
|
||||
});
|
||||
|
||||
@@ -1,51 +1,94 @@
|
||||
import React from "react";
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import TooltipTrigger from "react-popper-tooltip";
|
||||
import { usePopper } from "react-popper";
|
||||
|
||||
const Tooltip = ({ html, tooltip, children, className }) => (
|
||||
<TooltipTrigger
|
||||
trigger="hover"
|
||||
placement="top"
|
||||
delayShow={1000}
|
||||
delayHide={100}
|
||||
tooltip={({
|
||||
arrowRef,
|
||||
tooltipRef,
|
||||
getArrowProps,
|
||||
getTooltipProps,
|
||||
placement,
|
||||
}) => (
|
||||
import { useSupportsTouch } from "Hooks/useSupportsTouch";
|
||||
|
||||
const TooltipWrapper = ({ title, children, className }) => {
|
||||
const [referenceElement, setReferenceElement] = useState(null);
|
||||
const [popperElement, setPopperElement] = useState(null);
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: "top",
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
boundariesElement: "viewport",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const supportsTouch = useSupportsTouch();
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [wasClicked, setWasClicked] = useState(false);
|
||||
|
||||
const showTooltip = useCallback(() => setIsHovering(true), []);
|
||||
const hideTooltip = useCallback(() => setIsHovering(false), []);
|
||||
const handleClick = useCallback(() => setWasClicked(true), []);
|
||||
|
||||
useEffect(() => {
|
||||
let timerShow;
|
||||
let timerHide;
|
||||
|
||||
if (!isHovering) {
|
||||
if (isVisible) {
|
||||
clearTimeout(timerShow);
|
||||
timerHide = setTimeout(() => setIsVisible(false), 100);
|
||||
}
|
||||
setWasClicked(false);
|
||||
} else if (wasClicked) {
|
||||
clearTimeout(timerShow);
|
||||
clearTimeout(timerHide);
|
||||
setIsVisible(false);
|
||||
} else if (!isVisible && isHovering) {
|
||||
clearTimeout(timerHide);
|
||||
timerShow = setTimeout(() => setIsVisible(true), 1000);
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(timerShow);
|
||||
clearTimeout(timerHide);
|
||||
};
|
||||
}, [isHovering, isVisible, wasClicked]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div
|
||||
{...getTooltipProps({
|
||||
ref: tooltipRef,
|
||||
className: "tooltip show tooltip-inner",
|
||||
style: { transition: "opacity 0.2s" },
|
||||
})}
|
||||
>
|
||||
{tooltip}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{({ getTriggerProps, triggerRef }) => (
|
||||
<div
|
||||
{...getTriggerProps({
|
||||
ref: triggerRef,
|
||||
})}
|
||||
onClick={handleClick}
|
||||
onMouseOver={supportsTouch ? null : showTooltip}
|
||||
onMouseLeave={supportsTouch ? null : hideTooltip}
|
||||
onTouchStart={supportsTouch ? showTooltip : null}
|
||||
onTouchCancel={supportsTouch ? hideTooltip : null}
|
||||
onTouchEnd={supportsTouch ? hideTooltip : null}
|
||||
ref={setReferenceElement}
|
||||
style={{ display: "inline-block", maxWidth: "100%" }}
|
||||
className={`${className ? className : ""} tooltip-trigger`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
);
|
||||
|
||||
const TooltipWrapper = ({ title, children, className }) => (
|
||||
<Tooltip tooltip={title} className={className}>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
{isVisible
|
||||
? createPortal(
|
||||
<div
|
||||
className="tooltip show tooltip-inner"
|
||||
ref={setPopperElement}
|
||||
style={{
|
||||
willChange: "opacity",
|
||||
transition: "opacity 0.2s",
|
||||
...styles.popper,
|
||||
}}
|
||||
{...attributes.popper}
|
||||
>
|
||||
{title}
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
: null}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
TooltipWrapper.propTypes = {
|
||||
title: PropTypes.node.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
|
||||
import { mount } from "enzyme";
|
||||
|
||||
@@ -7,17 +8,6 @@ import { TooltipWrapper } from ".";
|
||||
describe("TooltipWrapper", () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
// https://stackoverflow.com/a/60974039/1154047
|
||||
const mutationObserverMock = jest.fn(function MutationObserver(callback) {
|
||||
this.observe = jest.fn();
|
||||
this.disconnect = jest.fn();
|
||||
// Optionally add a trigger() method to manually trigger a change
|
||||
this.trigger = (mockedMutationsList) => {
|
||||
callback(mockedMutationsList, this);
|
||||
};
|
||||
});
|
||||
global.MutationObserver = mutationObserverMock;
|
||||
});
|
||||
|
||||
it("renders only children", () => {
|
||||
@@ -30,21 +20,86 @@ describe("TooltipWrapper", () => {
|
||||
expect(tree.find("div.tooltip")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("renders tooltip on hover and hides on blur", () => {
|
||||
it("uses passed className", () => {
|
||||
const tree = mount(
|
||||
<TooltipWrapper title="my title" className="foo">
|
||||
<span>Hover me</span>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
expect(tree.find("div.foo")).toHaveLength(1);
|
||||
expect(tree.find("div.foo").text()).toBe("Hover me");
|
||||
});
|
||||
|
||||
it("on non-touch devices it renders tooltip on mouseOver and hides on mouseLeave", () => {
|
||||
const tree = mount(
|
||||
<TooltipWrapper title="my title">
|
||||
<span>Hover me</span>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
|
||||
tree.simulate("mouseEnter");
|
||||
jest.runAllTimers();
|
||||
tree.simulate("mouseOver");
|
||||
act(() => jest.runAllTimers());
|
||||
tree.update();
|
||||
expect(tree.find("div.tooltip")).toHaveLength(1);
|
||||
|
||||
tree.simulate("mouseLeave");
|
||||
jest.runAllTimers();
|
||||
act(() => jest.runAllTimers());
|
||||
tree.update();
|
||||
expect(tree.find("div.tooltip")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("on touch devices it renders tooltip on touchStart and hides on touchEnd", () => {
|
||||
const tree = mount(
|
||||
<TooltipWrapper title="my title">
|
||||
<span>Hover me</span>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
|
||||
act(() => {
|
||||
const event = new Event("touchstart");
|
||||
global.window.dispatchEvent(event);
|
||||
});
|
||||
tree.update();
|
||||
|
||||
tree.simulate("touchStart");
|
||||
act(() => jest.runAllTimers());
|
||||
tree.update();
|
||||
expect(tree.find("div.tooltip")).toHaveLength(1);
|
||||
|
||||
tree.simulate("touchEnd");
|
||||
act(() => jest.runAllTimers());
|
||||
tree.update();
|
||||
expect(tree.find("div.tooltip")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("hides the tooltip after click and show again on mouseOver", () => {
|
||||
const tree = mount(
|
||||
<TooltipWrapper title="my title">
|
||||
<span>Hover me</span>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
|
||||
tree.simulate("mouseOver");
|
||||
act(() => jest.runAllTimers());
|
||||
tree.update();
|
||||
expect(tree.find("div.tooltip")).toHaveLength(1);
|
||||
|
||||
tree.simulate("click");
|
||||
act(() => jest.runAllTimers());
|
||||
tree.update();
|
||||
expect(tree.find("div.tooltip")).toHaveLength(0);
|
||||
|
||||
tree.simulate("mouseLeave");
|
||||
act(() => jest.runAllTimers());
|
||||
tree.update();
|
||||
expect(tree.find("div.tooltip")).toHaveLength(0);
|
||||
|
||||
tree.simulate("mouseOver");
|
||||
act(() => jest.runAllTimers());
|
||||
tree.update();
|
||||
expect(tree.find("div.tooltip")).toHaveLength(1);
|
||||
|
||||
tree.unmount();
|
||||
act(() => jest.runAllTimers());
|
||||
});
|
||||
});
|
||||
|
||||
18
ui/src/Hooks/useSupportsTouch.js
Normal file
18
ui/src/Hooks/useSupportsTouch.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
function useSupportsTouch() {
|
||||
const [supportsTouch, setSupportsTouch] = useState(false);
|
||||
|
||||
const handler = useCallback(() => setSupportsTouch(true), []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("touchstart", handler);
|
||||
return () => {
|
||||
window.removeEventListener("touchstart", handler);
|
||||
};
|
||||
}, [handler]);
|
||||
|
||||
return supportsTouch;
|
||||
}
|
||||
|
||||
export { useSupportsTouch };
|
||||
24
ui/src/Hooks/useSupportsTouch.test.js
Normal file
24
ui/src/Hooks/useSupportsTouch.test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { renderHook, act } from "@testing-library/react-hooks";
|
||||
|
||||
import { useSupportsTouch } from "./useSupportsTouch";
|
||||
|
||||
describe("useSupportsTouch", () => {
|
||||
it("returns false by default", () => {
|
||||
const { result } = renderHook(() => useSupportsTouch());
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true after touchStart event", () => {
|
||||
const { waitForNextUpdate, result } = renderHook(() => useSupportsTouch());
|
||||
expect(result.current).toBe(false);
|
||||
|
||||
act(() => {
|
||||
const event = new Event("touchstart");
|
||||
global.window.dispatchEvent(event);
|
||||
});
|
||||
|
||||
waitForNextUpdate();
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,8 @@ $btn-box-shadow: 0;
|
||||
$btn-focus-box-shadow: 0;
|
||||
$btn-active-box-shadow: 0;
|
||||
|
||||
$tooltip-max-width: 400px;
|
||||
|
||||
@import "Styles/Fonts";
|
||||
@import "~bootswatch/dist/darkly/variables";
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ $btn-box-shadow: 0;
|
||||
$btn-focus-box-shadow: 0;
|
||||
$btn-active-box-shadow: 0;
|
||||
|
||||
$tooltip-max-width: 400px;
|
||||
|
||||
@import "Styles/Fonts";
|
||||
@import "~bootswatch/dist/flatly/variables";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import Enzyme from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
|
||||
@@ -30,3 +31,6 @@ for (const level of ["error", "warn", "info", "log", "trace"]) {
|
||||
|
||||
FetchRetryConfig.minTimeout = 2;
|
||||
FetchRetryConfig.maxTimeout = 10;
|
||||
|
||||
// usePopper uses useLayoutEffect but that fails in enzyme
|
||||
React.useLayoutEffect = React.useEffect;
|
||||
|
||||
Reference in New Issue
Block a user