From 071a8ec06dea9cafd8f454a81686d6fd5fc56dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Wed, 10 Jun 2020 17:24:35 +0100 Subject: [PATCH] fix(ui): rewrite NavBar with hooks --- ui/src/Components/NavBar/index.js | 276 ++++++++++--------------- ui/src/Components/NavBar/index.test.js | 24 +-- 2 files changed, 119 insertions(+), 181 deletions(-) diff --git a/ui/src/Components/NavBar/index.js b/ui/src/Components/NavBar/index.js index cac29fb64..753fc9c0b 100644 --- a/ui/src/Components/NavBar/index.js +++ b/ui/src/Components/NavBar/index.js @@ -1,8 +1,8 @@ -import React, { Component } from "react"; +import React, { useState, useRef, useEffect, useCallback } from "react"; import PropTypes from "prop-types"; -import { observable, action, reaction } from "mobx"; -import { observer } from "mobx-react"; +import { reaction } from "mobx"; +import { useObserver } from "mobx-react"; import ReactResizeDetector from "react-resize-detector"; @@ -24,178 +24,124 @@ import { FilterInput } from "./FilterInput"; const DesktopIdleTimeout = 1000 * 60 * 3; const MobileIdleTimeout = 1000 * 12; -const NavBar = observer( - class NavBar extends Component { - static propTypes = { - alertStore: PropTypes.instanceOf(AlertStore).isRequired, - settingsStore: PropTypes.instanceOf(Settings).isRequired, - silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired, - fixedTop: PropTypes.bool, - }; - static defaultProps = { - fixedTop: true, - }; +const NavBar = ({ alertStore, settingsStore, silenceFormStore, fixedTop }) => { + const idleTimer = useRef(null); + const [isIdle, setIsIdle] = useState(false); + const [containerClass, setContainerClass] = useState("visible"); + const [elementSize, setElementSize] = useState({ width: 0, height: 0 }); - constructor(props) { - super(props); + const context = React.useContext(ThemeContext); - this.idleTimer = null; - this.animationTimer = null; + const updateBodyPaddingTop = useCallback( + (idle) => { + const paddingTop = idle ? 0 : elementSize.height + 8; + document.body.style.paddingTop = `${paddingTop}px`; + setContainerClass(idle ? "invisible" : "visible"); + }, + [elementSize.height] + ); - this.activityStatus = observable( - { - idle: false, - className: "visible", - setIdle() { - this.idle = true; - }, - setActive() { - this.idle = false; - }, - hide() { - this.className = "invisible"; - }, - show() { - this.className = "visible"; - }, - }, - { - setIdle: action.bound, - setActive: action.bound, - hide: action.bound, - show: action.bound, - } + const onResize = useCallback((width, height) => { + setElementSize({ width: width, height: height }); + }, []); + + const onActive = useCallback(() => { + setIsIdle(false); + }, []); + + const onIdle = useCallback(() => { + setIsIdle(true); + }, []); + + useEffect(() => { + let timer; + if (isIdle) { + timer = setTimeout( + () => updateBodyPaddingTop(true), + context.animations.duration ); + } else { + updateBodyPaddingTop(false); + } + return () => clearTimeout(timer); + }, [ + elementSize.height, + updateBodyPaddingTop, + isIdle, + context.animations.duration, + ]); - this.activityStatusReaction = reaction( + useEffect( + () => + reaction( () => - props.alertStore.status.paused || - props.alertStore.filters.values.filter((f) => f.applied === false) - .length > 0, + !settingsStore.filterBarConfig.config.autohide || + alertStore.status.paused || + alertStore.filters.values.filter((f) => f.applied === false).length > + 0, (paused) => paused - ? this.idleTimer && this.idleTimer.pause() - : this.idleTimer && this.idleTimer.reset(), + ? idleTimer.current && idleTimer.current.pause() + : idleTimer.current && idleTimer.current.reset(), { fireImmediately: true } - ); - } + ), + [] // eslint-disable-line react-hooks/exhaustive-deps + ); - elementSize = observable( - { - width: 0, - height: 0, - setSize(width, height) { - this.width = width; - this.height = height; - }, - }, - { setSize: action } - ); - - updateBodyPaddingTop = () => { - const paddingTop = this.activityStatus.idle - ? 0 - : this.elementSize.height + 8; - document.body.style.paddingTop = `${paddingTop}px`; - }; - - onToggle = () => { - if (this.activityStatus.idle) { - this.activityStatus.hide(); - this.updateBodyPaddingTop(); - } else { - this.updateBodyPaddingTop(); - this.activityStatus.show(); - } - }; - - onIdleTimerActive = () => { - clearTimeout(this.animationTimer); - this.activityStatus.setActive(); - this.onToggle(); - }; - - onIdleTimerIdle = () => { - const { settingsStore } = this.props; - - if (settingsStore.filterBarConfig.config.autohide) { - this.activityStatus.setIdle(); - this.animationTimer = setTimeout( - this.onToggle, - this.context.animations.duration - ); - } - }; - - onResize = (width, height) => { - this.elementSize.setSize(width, height); - this.updateBodyPaddingTop(); - }; - - render() { - const { - alertStore, - settingsStore, - silenceFormStore, - fixedTop, - } = this.props; - - // if we have at least 1 filter then it's likely that filter input will - // use 2 lines, so set right side icons on small screeens to column mode - // for more compact layout - const flexClass = - alertStore.filters.values.length >= 1 - ? "flex-column flex-sm-row flex-md-row flex-lg-row flex-xl-row" - : "flex-row"; - - const isMobile = IsMobile(); - - return ( - { - this.idleTimer = ref; - }} - onActive={this.onIdleTimerActive} - onIdle={this.onIdleTimerIdle} - timeout={isMobile ? MobileIdleTimeout : DesktopIdleTimeout} - > -
( + +
+ +
-
- ); - } - } -); -NavBar.contextType = ThemeContext; + + + + + +
    = 1 + ? "flex-column flex-sm-row flex-md-row flex-lg-row flex-xl-row" + : "flex-row" + }`} + > + + +
+ + + +
+
+ )); +}; +NavBar.propTypes = { + alertStore: PropTypes.instanceOf(AlertStore).isRequired, + settingsStore: PropTypes.instanceOf(Settings).isRequired, + silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired, + fixedTop: PropTypes.bool, +}; +NavBar.defaultProps = { + fixedTop: true, +}; export { NavBar, MobileIdleTimeout, DesktopIdleTimeout }; diff --git a/ui/src/Components/NavBar/index.test.js b/ui/src/Components/NavBar/index.test.js index 121626bc1..c0061a31e 100644 --- a/ui/src/Components/NavBar/index.test.js +++ b/ui/src/Components/NavBar/index.test.js @@ -9,7 +9,6 @@ import { MockThemeContext } from "__mocks__/Theme"; import { AlertStore, NewUnappliedFilter } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; import { SilenceFormStore } from "Stores/SilenceFormStore"; -import { ThemeContext } from "Components/Theme"; import { NavBar, MobileIdleTimeout, DesktopIdleTimeout } from "."; let alertStore; @@ -42,11 +41,7 @@ const MountedNavbar = (fixedTop) => { settingsStore={settingsStore} silenceFormStore={silenceFormStore} fixedTop={fixedTop} - />, - { - wrappingComponent: ThemeContext.Provider, - wrappingComponentProps: { value: MockThemeContext }, - } + /> ); }; @@ -105,14 +100,14 @@ describe("", () => { it("body 'padding-top' style is updated after calling NavbarOnResize()", () => { const tree = MountedNavbar(); - tree.instance().onResize(0, 10); + act(() => tree.find("ResizeDetector").props().onResize(0, 10)); expect( window .getComputedStyle(document.body, null) .getPropertyValue("padding-top") ).toBe("18px"); - tree.instance().onResize(0, 36); + act(() => tree.find("ResizeDetector").props().onResize(0, 36)); expect( window .getComputedStyle(document.body, null) @@ -202,15 +197,15 @@ describe("", () => { it("hidden navbar shows up again after activity", () => { const tree = MountedNavbar(); - const instance = tree.instance(); - instance.onIdleTimerIdle(); - act(() => jest.runOnlyPendingTimers()); + act(() => jest.runTimersToTime(DesktopIdleTimeout + 1000)); tree.update(); expect(tree.find(".container").hasClass("visible")).toBe(false); expect(tree.find(".container").hasClass("invisible")).toBe(true); - instance.onIdleTimerActive(); + act(() => { + document.dispatchEvent(new MouseEvent("mousedown")); + }); act(() => jest.runOnlyPendingTimers()); tree.update(); expect(tree.find(".container").hasClass("visible")).toBe(true); @@ -219,10 +214,7 @@ describe("", () => { it("body padding-top is 0px when navbar is hidden", () => { const tree = MountedNavbar(); - const instance = tree.instance(); - - instance.onIdleTimerIdle(); - act(() => jest.runOnlyPendingTimers()); + act(() => jest.runTimersToTime(DesktopIdleTimeout + 1000)); tree.update(); expect( window