diff --git a/ui/package-lock.json b/ui/package-lock.json index 30e827ba6..9bfaa100f 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -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", diff --git a/ui/package.json b/ui/package.json index d82d10b79..a4bee8243 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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", diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.js index 5b037d70d..0a25cf243 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.js @@ -1,4 +1,5 @@ import React from "react"; +import { act } from "react-dom/test-utils"; import { mount } from "enzyme"; @@ -270,7 +271,7 @@ describe("", () => { 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))); diff --git a/ui/src/Components/Grid/AlertGrid/index.test.js b/ui/src/Components/Grid/AlertGrid/index.test.js index 1320f5716..e6e532042 100644 --- a/ui/src/Components/Grid/AlertGrid/index.test.js +++ b/ui/src/Components/Grid/AlertGrid/index.test.js @@ -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("", () => { MountedGrid(); // skip a minute to trigger FontFaceObserver timeout handler advanceBy(60 * 1000); - jest.runOnlyPendingTimers(); + act(() => jest.runOnlyPendingTimers()); }); it("doesn't crash on unmount", () => { diff --git a/ui/src/Components/MainModal/index.test.js b/ui/src/Components/MainModal/index.test.js index d8af64b87..3f007c55e 100644 --- a/ui/src/Components/MainModal/index.test.js +++ b/ui/src/Components/MainModal/index.test.js @@ -1,4 +1,5 @@ import React from "react"; +import { act } from "react-dom/test-utils"; import { mount } from "enzyme"; @@ -77,12 +78,12 @@ describe("", () => { 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("", () => { 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); }); diff --git a/ui/src/Components/NavBar/index.js b/ui/src/Components/NavBar/index.js index f0fde2f6b..cac29fb64 100644 --- a/ui/src/Components/NavBar/index.js +++ b/ui/src/Components/NavBar/index.js @@ -198,4 +198,4 @@ const NavBar = observer( ); NavBar.contextType = ThemeContext; -export { NavBar }; +export { NavBar, MobileIdleTimeout, DesktopIdleTimeout }; diff --git a/ui/src/Components/NavBar/index.test.js b/ui/src/Components/NavBar/index.test.js index 13b79c5e1..121626bc1 100644 --- a/ui/src/Components/NavBar/index.test.js +++ b/ui/src/Components/NavBar/index.test.js @@ -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( - - ).find(".container"); -}; +afterEach(() => { + act(() => jest.runAllTimers()); +}); -const MountedNavbar = () => { +const MountedNavbar = (fixedTop) => { return mount( , { 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("", () => { }); it("navbar includes 'fixed-top' class with fixedTop=true", () => { - const tree = mount( - - ); + 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( - - ); + 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("", () => { }); describe("", () => { - 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("", () => { 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("", () => { 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("", () => { 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("", () => { 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("", () => { 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("", () => { const instance = tree.instance(); instance.onIdleTimerIdle(); - jest.runOnlyPendingTimers(); + act(() => jest.runOnlyPendingTimers()); tree.update(); expect( window @@ -255,7 +234,7 @@ describe("", () => { 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("", () => { 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); diff --git a/ui/src/Components/OverviewModal/index.test.js b/ui/src/Components/OverviewModal/index.test.js index 7fc6b8294..18c250f8f 100644 --- a/ui/src/Components/OverviewModal/index.test.js +++ b/ui/src/Components/OverviewModal/index.test.js @@ -1,4 +1,5 @@ import React from "react"; +import { act } from "react-dom/test-utils"; import { mount } from "enzyme"; @@ -47,12 +48,12 @@ describe("", () => { 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("", () => { 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); }); diff --git a/ui/src/Components/SilenceModal/Browser/index.test.js b/ui/src/Components/SilenceModal/Browser/index.test.js index 2a39f88d9..f58523e8a 100644 --- a/ui/src/Components/SilenceModal/Browser/index.test.js +++ b/ui/src/Components/SilenceModal/Browser/index.test.js @@ -357,8 +357,10 @@ describe("", () => { 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); }); diff --git a/ui/src/Components/SilenceModal/index.test.js b/ui/src/Components/SilenceModal/index.test.js index d1a003d86..38153aeb6 100644 --- a/ui/src/Components/SilenceModal/index.test.js +++ b/ui/src/Components/SilenceModal/index.test.js @@ -78,12 +78,12 @@ describe("", () => { 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("", () => { 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("", () => { // 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("", () => { silenceFormStore.toggle.visible = true; MountedSilenceModal(); - expect(callbacks).toHaveLength(2); + expect(callbacks).toHaveLength(4); act(() => { callbacks.forEach((f) => f()); }); diff --git a/ui/src/Components/TooltipWrapper/index.js b/ui/src/Components/TooltipWrapper/index.js index c7c97e33c..bab8a53ac 100644 --- a/ui/src/Components/TooltipWrapper/index.js +++ b/ui/src/Components/TooltipWrapper/index.js @@ -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 }) => ( - ( +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 ( +
- {tooltip} -
- )} - > - {({ getTriggerProps, triggerRef }) => ( -
{children}
- )} -
-); - -const TooltipWrapper = ({ title, children, className }) => ( - - {children} - -); + {isVisible + ? createPortal( +
+ {title} +
, + document.body + ) + : null} + + ); +}; TooltipWrapper.propTypes = { title: PropTypes.node.isRequired, children: PropTypes.node.isRequired, diff --git a/ui/src/Components/TooltipWrapper/index.test.js b/ui/src/Components/TooltipWrapper/index.test.js index c055b7cd2..ea3c9eef1 100644 --- a/ui/src/Components/TooltipWrapper/index.test.js +++ b/ui/src/Components/TooltipWrapper/index.test.js @@ -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( + + Hover me + + ); + 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( Hover me ); - 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( + + Hover me + + ); + + 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( + + Hover me + + ); + + 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()); + }); }); diff --git a/ui/src/Hooks/useSupportsTouch.js b/ui/src/Hooks/useSupportsTouch.js new file mode 100644 index 000000000..93a0496b2 --- /dev/null +++ b/ui/src/Hooks/useSupportsTouch.js @@ -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 }; diff --git a/ui/src/Hooks/useSupportsTouch.test.js b/ui/src/Hooks/useSupportsTouch.test.js new file mode 100644 index 000000000..282b482ff --- /dev/null +++ b/ui/src/Hooks/useSupportsTouch.test.js @@ -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); + }); +}); diff --git a/ui/src/Styles/DarkTheme.scss b/ui/src/Styles/DarkTheme.scss index 2a5f35541..c33e2351a 100644 --- a/ui/src/Styles/DarkTheme.scss +++ b/ui/src/Styles/DarkTheme.scss @@ -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"; diff --git a/ui/src/Styles/LightTheme.scss b/ui/src/Styles/LightTheme.scss index ce2497408..9333f0342 100644 --- a/ui/src/Styles/LightTheme.scss +++ b/ui/src/Styles/LightTheme.scss @@ -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"; diff --git a/ui/src/setupTests.js b/ui/src/setupTests.js index 5ac41c311..81e09b890 100644 --- a/ui/src/setupTests.js +++ b/ui/src/setupTests.js @@ -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;