mirror of
https://github.com/prymitive/karma
synced 2026-05-07 03:26:52 +00:00
fix(ui): rewrite NavBar with hooks
This commit is contained in:
committed by
Łukasz Mierzwa
parent
1c4e8fc161
commit
071a8ec06d
@@ -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 (
|
||||
<IdleTimer
|
||||
ref={(ref) => {
|
||||
this.idleTimer = ref;
|
||||
}}
|
||||
onActive={this.onIdleTimerActive}
|
||||
onIdle={this.onIdleTimerIdle}
|
||||
timeout={isMobile ? MobileIdleTimeout : DesktopIdleTimeout}
|
||||
>
|
||||
<div
|
||||
className={`container p-0 m-0 mw-100 ${this.activityStatus.className}`}
|
||||
return useObserver(() => (
|
||||
<IdleTimer
|
||||
ref={idleTimer}
|
||||
onActive={onActive}
|
||||
onIdle={onIdle}
|
||||
timeout={IsMobile() ? MobileIdleTimeout : DesktopIdleTimeout}
|
||||
>
|
||||
<div className={`container p-0 m-0 mw-100 ${containerClass}`}>
|
||||
<Fade top when={!isIdle}>
|
||||
<nav
|
||||
className={`navbar navbar-expand navbar-dark p-1 bg-primary-transparent d-inline-block ${
|
||||
fixedTop ? "fixed-top" : "w-100"
|
||||
}`}
|
||||
>
|
||||
<Fade top when={!this.activityStatus.idle}>
|
||||
<nav
|
||||
className={`navbar navbar-expand navbar-dark p-1 bg-primary-transparent d-inline-block ${
|
||||
fixedTop ? "fixed-top" : "w-100"
|
||||
}`}
|
||||
>
|
||||
<ReactResizeDetector handleHeight onResize={this.onResize} />
|
||||
<span className="navbar-brand p-0 my-0 mx-2 h1 d-none d-sm-block float-left">
|
||||
<OverviewModal alertStore={alertStore} />
|
||||
<FetchIndicator alertStore={alertStore} />
|
||||
</span>
|
||||
<ul className={`navbar-nav float-right d-flex ${flexClass}`}>
|
||||
<SilenceModal
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
settingsStore={settingsStore}
|
||||
/>
|
||||
<MainModal
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
/>
|
||||
</ul>
|
||||
<FilterInput
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
/>
|
||||
</nav>
|
||||
</Fade>
|
||||
</div>
|
||||
</IdleTimer>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
NavBar.contextType = ThemeContext;
|
||||
<ReactResizeDetector handleHeight onResize={onResize} />
|
||||
<span className="navbar-brand p-0 my-0 mx-2 h1 d-none d-sm-block float-left">
|
||||
<OverviewModal alertStore={alertStore} />
|
||||
<FetchIndicator alertStore={alertStore} />
|
||||
</span>
|
||||
<ul
|
||||
className={`navbar-nav float-right d-flex ${
|
||||
alertStore.filters.values.length >= 1
|
||||
? "flex-column flex-sm-row flex-md-row flex-lg-row flex-xl-row"
|
||||
: "flex-row"
|
||||
}`}
|
||||
>
|
||||
<SilenceModal
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
settingsStore={settingsStore}
|
||||
/>
|
||||
<MainModal
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
/>
|
||||
</ul>
|
||||
<FilterInput
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
/>
|
||||
</nav>
|
||||
</Fade>
|
||||
</div>
|
||||
</IdleTimer>
|
||||
));
|
||||
};
|
||||
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 };
|
||||
|
||||
@@ -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("<NavBar />", () => {
|
||||
|
||||
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("<IdleTimer />", () => {
|
||||
|
||||
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("<IdleTimer />", () => {
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user