fix(ui): migrate from popper to floating-ui

This commit is contained in:
Łukasz Mierzwa
2022-03-19 20:16:53 +00:00
committed by Łukasz Mierzwa
parent 53a79f2e6b
commit bdfa962731
17 changed files with 326 additions and 339 deletions

View File

@@ -6,7 +6,6 @@ ignores:
- sass
- typeface-open-sans
- prettier
- "@popperjs/core"
- react-scripts
- typescript
# devDeps

122
ui/package-lock.json generated
View File

@@ -9,12 +9,12 @@
"version": "0.0.1",
"license": "Apache-2.0",
"dependencies": {
"@floating-ui/react-dom": "0.6.0",
"@fortawesome/fontawesome-svg-core": "6.1.0",
"@fortawesome/free-regular-svg-icons": "6.1.0",
"@fortawesome/free-solid-svg-icons": "6.1.0",
"@fortawesome/react-fontawesome": "0.1.18",
"@juggle/resize-observer": "3.3.1",
"@popperjs/core": "2.11.4",
"body-scroll-lock": "3.1.5",
"bootstrap": "5.1.3",
"bootswatch": "5.1.3",
@@ -46,7 +46,6 @@
"react-json-pretty": "2.2.0",
"react-linkify": "0.2.2",
"react-media-hook": "0.4.9",
"react-popper": "2.2.5",
"react-range": "1.8.12",
"react-select": "5.2.2",
"react-transition-group": "4.4.2",
@@ -2372,6 +2371,32 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"node_modules/@floating-ui/core": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.6.1.tgz",
"integrity": "sha512-Y30eVMcZva8o84c0HcXAtDO4BEzPJMvF6+B7x7urL2xbAqVsGJhojOyHLaoQHQYjb6OkqRq5kO+zeySycQwKqg=="
},
"node_modules/@floating-ui/dom": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.4.1.tgz",
"integrity": "sha512-VAdIkn+zc4VB3pSemBk2JwjXlbVEBUbQft4V5jGpUeA9Vxd1fQD5eL3dMIMLVW003NYCOoW42JjjyBPNTpr8uQ==",
"dependencies": {
"@floating-ui/core": "^0.6.1"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.6.0.tgz",
"integrity": "sha512-/dF+jpAUtoonjs2lO0F+miqEQlzA9XJGOxWBiJsq/COhK2kz7XzryyJdxfaNEH+RN63vRs9osDolOJXlst50hg==",
"dependencies": {
"@floating-ui/dom": "^0.4.0",
"use-isomorphic-layout-effect": "^1.1.1"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.0.tgz",
@@ -3658,6 +3683,7 @@
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz",
"integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@@ -26226,11 +26252,6 @@
"integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==",
"dev": true
},
"node_modules/react-fast-compare": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
"node_modules/react-hotkeys-hook": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-3.4.4.tgz",
@@ -26296,19 +26317,6 @@
"react": ">=16.8.0"
}
},
"node_modules/react-popper": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.5.tgz",
"integrity": "sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw==",
"dependencies": {
"react-fast-compare": "^3.0.1",
"warning": "^4.0.2"
},
"peerDependencies": {
"@popperjs/core": "^2.0.0",
"react": "^16.8.0 || ^17"
}
},
"node_modules/react-range": {
"version": "1.8.12",
"resolved": "https://registry.npmjs.org/react-range/-/react-range-1.8.12.tgz",
@@ -31495,6 +31503,19 @@
"node": ">=0.10.0"
}
},
"node_modules/use-isomorphic-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz",
"integrity": "sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/util": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
@@ -31678,14 +31699,6 @@
"makeerror": "1.0.12"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/watchpack": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
@@ -34604,6 +34617,28 @@
}
}
},
"@floating-ui/core": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.6.1.tgz",
"integrity": "sha512-Y30eVMcZva8o84c0HcXAtDO4BEzPJMvF6+B7x7urL2xbAqVsGJhojOyHLaoQHQYjb6OkqRq5kO+zeySycQwKqg=="
},
"@floating-ui/dom": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.4.1.tgz",
"integrity": "sha512-VAdIkn+zc4VB3pSemBk2JwjXlbVEBUbQft4V5jGpUeA9Vxd1fQD5eL3dMIMLVW003NYCOoW42JjjyBPNTpr8uQ==",
"requires": {
"@floating-ui/core": "^0.6.1"
}
},
"@floating-ui/react-dom": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.6.0.tgz",
"integrity": "sha512-/dF+jpAUtoonjs2lO0F+miqEQlzA9XJGOxWBiJsq/COhK2kz7XzryyJdxfaNEH+RN63vRs9osDolOJXlst50hg==",
"requires": {
"@floating-ui/dom": "^0.4.0",
"use-isomorphic-layout-effect": "^1.1.1"
}
},
"@fortawesome/fontawesome-common-types": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.0.tgz",
@@ -35571,7 +35606,8 @@
"@popperjs/core": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz",
"integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg=="
"integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==",
"peer": true
},
"@rollup/plugin-babel": {
"version": "5.3.1",
@@ -53120,11 +53156,6 @@
"integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==",
"dev": true
},
"react-fast-compare": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
"react-hotkeys-hook": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-3.4.4.tgz",
@@ -53174,15 +53205,6 @@
"integrity": "sha512-FZr/2xA1+23vDJ1IZ794yLqMRRkBoCNOiJATdtTfB5GyVc5djf8FL2qEB/68pSkiNgHdHsmKknMSDr0sC4zBKQ==",
"requires": {}
},
"react-popper": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.5.tgz",
"integrity": "sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw==",
"requires": {
"react-fast-compare": "^3.0.1",
"warning": "^4.0.2"
}
},
"react-range": {
"version": "1.8.12",
"resolved": "https://registry.npmjs.org/react-range/-/react-range-1.8.12.tgz",
@@ -57124,6 +57146,12 @@
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
"dev": true
},
"use-isomorphic-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz",
"integrity": "sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ==",
"requires": {}
},
"util": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
@@ -57278,14 +57306,6 @@
"makeerror": "1.0.12"
}
},
"warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"requires": {
"loose-envify": "^1.0.0"
}
},
"watchpack": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",

View File

@@ -8,12 +8,12 @@
"node": "17.7.2"
},
"dependencies": {
"@floating-ui/react-dom": "0.6.0",
"@fortawesome/fontawesome-svg-core": "6.1.0",
"@fortawesome/free-regular-svg-icons": "6.1.0",
"@fortawesome/free-solid-svg-icons": "6.1.0",
"@fortawesome/react-fontawesome": "0.1.18",
"@juggle/resize-observer": "3.3.1",
"@popperjs/core": "2.11.4",
"body-scroll-lock": "3.1.5",
"bootstrap": "5.1.3",
"bootswatch": "5.1.3",
@@ -45,7 +45,6 @@
"react-json-pretty": "2.2.0",
"react-linkify": "0.2.2",
"react-media-hook": "0.4.9",
"react-popper": "2.2.5",
"react-range": "1.8.12",
"react-select": "5.2.2",
"react-transition-group": "4.4.2",

View File

@@ -1,29 +0,0 @@
export const CommonPopperModifiers = [
{ name: "arrow", enabled: false },
{
name: "computeStyles",
options: {
gpuAcceleration: false,
},
},
{
name: "offset",
options: {
offset: [0, 5],
},
},
{
name: "computeStyles",
options: {
gpuAcceleration: false,
adaptive: false,
roundOffsets: true,
},
},
{
name: "flip",
options: {
fallbackPlacements: [],
},
},
];

View File

@@ -172,9 +172,10 @@ describe("<AlertMenu />", () => {
const MountedMenuContent = (group: APIAlertGroupT) => {
return mount(
<MenuContent
popperPlacement="top"
popperRef={jest.fn()}
popperStyle={{}}
x={0}
y={0}
floating={null}
strategy={"absolute"}
group={group}
alert={alert}
afterClick={MockAfterClick}

View File

@@ -2,7 +2,7 @@ import { FC, Ref, CSSProperties, useRef, useState, useCallback } from "react";
import { observer } from "mobx-react-lite";
import { Manager, Reference, Popper } from "react-popper";
import { useFloating, shift, flip, offset } from "@floating-ui/react-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCaretDown } from "@fortawesome/free-solid-svg-icons/faCaretDown";
@@ -21,18 +21,12 @@ import {
SilenceFormStore,
AlertmanagerClustersToOption,
} from "Stores/SilenceFormStore";
import { CommonPopperModifiers } from "Common/Popper";
import { FetchPauser } from "Components/FetchPauser";
import { DropdownSlide } from "Components/Animations/DropdownSlide";
import { DateFromNow } from "Components/DateFromNow";
import { useOnClickOutside } from "Hooks/useOnClickOutside";
import { MenuLink } from "Components/Grid/AlertGrid/AlertGroup/MenuLink";
const PopperModifiers = [
...CommonPopperModifiers,
{ name: "offset", options: { offset: "-5px, 0px" } },
];
const onSilenceClick = (
alertStore: AlertStore,
silenceFormStore: SilenceFormStore,
@@ -60,18 +54,20 @@ const onSilenceClick = (
};
const MenuContent: FC<{
popperPlacement?: string;
popperRef?: Ref<HTMLDivElement>;
popperStyle?: CSSProperties;
x: number | null;
y: number | null;
floating: Ref<HTMLDivElement> | null;
strategy: CSSProperties["position"];
group: APIAlertGroupT;
alert: APIAlertT;
afterClick: () => void;
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
}> = ({
popperPlacement,
popperRef,
popperStyle,
x,
y,
floating,
strategy,
group,
alert,
afterClick,
@@ -91,9 +87,12 @@ const MenuContent: FC<{
<FetchPauser alertStore={alertStore}>
<div
className="dropdown-menu d-block shadow m-0"
ref={popperRef}
style={popperStyle}
data-placement={popperPlacement}
ref={floating}
style={{
position: strategy,
top: y ?? "",
left: x ?? "",
}}
>
<h6 className="dropdown-header">Alert source links:</h6>
{alert.alertmanager.map((am) => (
@@ -166,43 +165,39 @@ const AlertMenu: FC<{
const rootRef = useRef<HTMLSpanElement | null>(null);
useOnClickOutside(rootRef, hide, !isHidden);
const { x, y, reference, floating, strategy } = useFloating({
placement: "bottom-start",
middleware: [shift(), flip(), offset(5)],
});
return (
<span ref={rootRef}>
<Manager>
<Reference>
{({ ref }) => (
<span
className="components-label components-label-with-hover px-1 me-1 badge bg-secondary cursor-pointer"
ref={ref}
onClick={toggle}
data-toggle="dropdown"
>
<FontAwesomeIcon
className="pe-1"
style={{ width: "0.8rem" }}
icon={faCaretDown}
/>
<DateFromNow timestamp={alert.startsAt} />
</span>
)}
</Reference>
<DropdownSlide in={!isHidden} unmountOnExit>
<Popper placement="bottom" modifiers={PopperModifiers}>
{({ placement, ref, style }) => (
<MenuContent
popperPlacement={placement}
popperRef={ref}
popperStyle={style}
group={group}
alert={alert}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
afterClick={hide}
/>
)}
</Popper>
</DropdownSlide>
</Manager>
<span
className="components-label components-label-with-hover px-1 me-1 badge bg-secondary cursor-pointer"
ref={reference}
onClick={toggle}
data-toggle="dropdown"
>
<FontAwesomeIcon
className="pe-1"
style={{ width: "0.8rem" }}
icon={faCaretDown}
/>
<DateFromNow timestamp={alert.startsAt} />
</span>
<DropdownSlide in={!isHidden} unmountOnExit>
<MenuContent
group={group}
alert={alert}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
afterClick={hide}
x={x}
y={y}
floating={floating}
strategy={strategy}
/>
</DropdownSlide>
</span>
);
}

View File

@@ -191,9 +191,10 @@ describe("<GroupMenu />", () => {
const MountedMenuContent = (group: APIAlertGroupT) => {
return mount(
<MenuContent
popperPlacement="top"
popperRef={jest.fn()}
popperStyle={{}}
x={0}
y={0}
floating={null}
strategy={"absolute"}
group={group}
afterClick={MockAfterClick}
alertStore={alertStore}

View File

@@ -2,7 +2,7 @@ import { FC, useRef, useState, useCallback, Ref, CSSProperties } from "react";
import copy from "copy-to-clipboard";
import { Manager, Reference, Popper } from "react-popper";
import { useFloating, shift, flip, offset } from "@floating-ui/react-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBars } from "@fortawesome/free-solid-svg-icons/faBars";
@@ -18,17 +18,11 @@ import {
AlertmanagerClustersToOption,
} from "Stores/SilenceFormStore";
import { QueryOperators, StaticLabels, FormatQuery } from "Common/Query";
import { CommonPopperModifiers } from "Common/Popper";
import { DropdownSlide } from "Components/Animations/DropdownSlide";
import { FetchPauser } from "Components/FetchPauser";
import { useOnClickOutside } from "Hooks/useOnClickOutside";
import { MenuLink } from "Components/Grid/AlertGrid/AlertGroup/MenuLink";
const PopperModifiers = [
...CommonPopperModifiers,
{ name: "offset", options: { offset: "-5px, 0px" } },
];
const onSilenceClick = (
alertStore: AlertStore,
silenceFormStore: SilenceFormStore,
@@ -56,17 +50,19 @@ const onSilenceClick = (
};
const MenuContent: FC<{
popperPlacement?: string;
popperRef?: Ref<HTMLDivElement>;
popperStyle?: CSSProperties;
x: number | null;
y: number | null;
floating: Ref<HTMLDivElement> | null;
strategy: CSSProperties["position"];
group: APIAlertGroupT;
afterClick: () => void;
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
}> = ({
popperPlacement,
popperRef,
popperStyle,
x,
y,
floating,
strategy,
group,
afterClick,
alertStore,
@@ -94,9 +90,12 @@ const MenuContent: FC<{
<FetchPauser alertStore={alertStore}>
<div
className="dropdown-menu d-block shadow m-0"
ref={popperRef}
style={popperStyle}
data-placement={popperPlacement}
ref={floating}
style={{
position: strategy,
top: y ?? "",
left: x ?? "",
}}
>
{actions.length ? (
<>
@@ -165,39 +164,35 @@ const GroupMenu: FC<{
const rootRef = useRef<HTMLSpanElement | null>(null);
useOnClickOutside(rootRef, hide, !isHidden);
const { x, y, reference, floating, strategy } = useFloating({
placement: "bottom-start",
middleware: [shift(), flip(), offset(5)],
});
return (
<span ref={rootRef}>
<Manager>
<Reference>
{({ ref }) => (
<span
ref={ref}
onClick={toggle}
className={`${
themed ? "text-white with-click-light" : "text-muted"
} cursor-pointer badge components-label components-label-with-hover with-click me-1`}
data-toggle="dropdown"
>
<FontAwesomeIcon icon={faBars} />
</span>
)}
</Reference>
<DropdownSlide in={!isHidden} unmountOnExit>
<Popper placement="bottom" modifiers={PopperModifiers}>
{({ placement, ref, style }) => (
<MenuContent
popperPlacement={placement}
popperRef={ref}
popperStyle={style}
group={group}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
afterClick={hide}
/>
)}
</Popper>
</DropdownSlide>
</Manager>
<span
ref={reference}
onClick={toggle}
className={`${
themed ? "text-white with-click-light" : "text-muted"
} cursor-pointer badge components-label components-label-with-hover with-click me-1`}
data-toggle="dropdown"
>
<FontAwesomeIcon icon={faBars} />
</span>
<DropdownSlide in={!isHidden} unmountOnExit>
<MenuContent
group={group}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
afterClick={hide}
x={x}
y={y}
floating={floating}
strategy={strategy}
/>
</DropdownSlide>
</span>
);
};

View File

@@ -9,7 +9,7 @@ import React, {
import { observer } from "mobx-react-lite";
import { Manager, Reference, Popper } from "react-popper";
import { useFloating, shift, flip, offset } from "@floating-ui/react-dom";
import type { OnChangeValue } from "react-select";
import AsyncSelect from "react-select/async";
@@ -20,7 +20,6 @@ import { faCaretDown } from "@fortawesome/free-solid-svg-icons/faCaretDown";
import type { AlertStore } from "Stores/AlertStore";
import type { Settings } from "Stores/Settings";
import type { APIGridT } from "Models/APITypes";
import { CommonPopperModifiers } from "Common/Popper";
import { StringToOption, OptionT } from "Common/Select";
import { DropdownSlide } from "Components/Animations/DropdownSlide";
import { ThemeContext } from "Components/Theme";
@@ -90,17 +89,19 @@ const GridLabelNameSelect: FC<{
};
const Dropdown: FC<{
popperPlacement?: string;
popperRef?: Ref<HTMLDivElement>;
popperStyle?: CSSProperties;
x: number | null;
y: number | null;
floating: Ref<HTMLDivElement> | null;
strategy: CSSProperties["position"];
alertStore: AlertStore;
settingsStore: Settings;
grid: APIGridT;
onClose: () => void;
}> = ({
popperPlacement,
popperRef,
popperStyle,
x,
y,
floating,
strategy,
alertStore,
settingsStore,
grid,
@@ -109,13 +110,14 @@ const Dropdown: FC<{
return (
<div
className="dropdown-menu d-block shadow components-grid-label-select-menu border-0 p-0 m-0"
ref={popperRef}
ref={floating}
style={{
fontSize: "1rem",
fontWeight: "normal",
...popperStyle,
position: strategy,
top: y ?? "",
left: x ?? "",
}}
data-placement={popperPlacement}
>
<GridLabelNameSelect
alertStore={alertStore}
@@ -140,37 +142,33 @@ const GridLabelSelect: FC<{
const ref = useRef<HTMLDivElement | null>(null);
useOnClickOutside(ref, hide, isVisible);
const { x, y, reference, floating, strategy } = useFloating({
placement: "bottom",
middleware: [shift(), flip(), offset(5)],
});
return (
<div ref={ref} className="components-label badge ps-1 pe-2">
<Manager>
<Reference>
{({ ref }) => (
<span
ref={ref}
onClick={toggle}
className="border-0 rounded-0 bg-inherit cursor-pointer px-1 py-0 components-grid-label-select-dropdown"
data-toggle="dropdown"
>
<FontAwesomeIcon className="text-muted" icon={faCaretDown} />
</span>
)}
</Reference>
<DropdownSlide in={isVisible} unmountOnExit>
<Popper placement="bottom" modifiers={CommonPopperModifiers}>
{({ placement, ref, style }) => (
<Dropdown
popperPlacement={placement}
popperRef={ref}
popperStyle={style}
alertStore={alertStore}
settingsStore={settingsStore}
grid={grid}
onClose={toggle}
/>
)}
</Popper>
</DropdownSlide>
</Manager>
<span
ref={reference}
onClick={toggle}
className="border-0 rounded-0 bg-inherit cursor-pointer px-1 py-0 components-grid-label-select-dropdown"
data-toggle="dropdown"
>
<FontAwesomeIcon className="text-muted" icon={faCaretDown} />
</span>
<DropdownSlide in={isVisible} unmountOnExit>
<Dropdown
alertStore={alertStore}
settingsStore={settingsStore}
grid={grid}
onClose={toggle}
x={x}
y={y}
floating={floating}
strategy={strategy}
/>
</DropdownSlide>
</div>
);
});

View File

@@ -234,7 +234,8 @@ describe("<HistoryMenu />", () => {
await act(() => promise);
});
it("clicking on 'Save filters' saves current filter set to Settings", () => {
it("clicking on 'Save filters' saves current filter set to Settings", async () => {
const promise = Promise.resolve();
alertStore.filters.setFilterValues([
AppliedFilter("foo", "=", "bar"),
AppliedFilter("bar", "=~", "baz"),
@@ -252,9 +253,11 @@ describe("<HistoryMenu />", () => {
expect(settingsStore.savedFilters.config.filters).toHaveLength(2);
expect(settingsStore.savedFilters.config.filters).toContain("foo=bar");
expect(settingsStore.savedFilters.config.filters).toContain("bar=~baz");
await act(() => promise);
});
it("clicking on 'Reset filters' clears current filter set in Settings", () => {
it("clicking on 'Reset filters' clears current filter set in Settings", async () => {
const promise = Promise.resolve();
settingsStore.savedFilters.save(["foo=bar"]);
const tree = MountedHistory();
tree.find("button.cursor-pointer").simulate("click");
@@ -266,6 +269,7 @@ describe("<HistoryMenu />", () => {
jest.runOnlyPendingTimers();
});
expect(settingsStore.savedFilters.config.filters).toHaveLength(0);
await act(() => promise);
});
it("clicking on 'Clear history' clears the history", async () => {

View File

@@ -1,19 +1,19 @@
import {
FC,
Ref,
CSSProperties,
useEffect,
useRef,
useState,
useCallback,
ReactNode,
CSSProperties,
} from "react";
import { action } from "mobx";
import { observer } from "mobx-react-lite";
import { localStored } from "mobx-stored";
import { Manager, Reference, Popper } from "react-popper";
import { useFloating, shift, flip, offset, size } from "@floating-ui/react-dom";
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -26,7 +26,6 @@ import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
import type { AlertStore, FilterT } from "Stores/AlertStore";
import type { Settings } from "Stores/Settings";
import { IsMobile } from "Common/Device";
import { CommonPopperModifiers } from "Common/Popper";
import { DropdownSlide } from "Components/Animations/DropdownSlide";
import HistoryLabel from "Components/Labels/HistoryLabel";
import { useOnClickOutside } from "Hooks/useOnClickOutside";
@@ -69,18 +68,24 @@ const ActionButton: FC<{
);
const HistoryMenu: FC<{
popperPlacement?: string;
popperRef?: Ref<HTMLDivElement>;
popperStyle?: CSSProperties;
x: number | null;
y: number | null;
floating: Ref<HTMLDivElement> | null;
strategy: CSSProperties["position"];
maxWidth: number | null;
maxHeight: number | null;
filters: ReduceFilterT[][];
alertStore: AlertStore;
settingsStore: Settings;
afterClick: () => void;
onClear: () => void;
}> = ({
popperPlacement,
popperRef,
popperStyle,
x,
y,
floating,
strategy,
maxWidth,
maxHeight,
filters,
alertStore,
settingsStore,
@@ -92,9 +97,14 @@ const HistoryMenu: FC<{
return (
<div
className="dropdown-menu d-block shadow components-navbar-historymenu m-0"
ref={popperRef}
style={popperStyle}
data-placement={popperPlacement}
ref={floating}
style={{
position: strategy,
top: y ?? "",
left: x ?? "",
maxWidth: maxWidth ?? "",
maxHeight: maxHeight ?? "",
}}
>
<h6 className="dropdown-header text-center">
<FontAwesomeIcon icon={faHistory} className="me-1" />
@@ -185,41 +195,52 @@ const History: FC<{
// this will be dumped to local storage via mobx-stored
const [history] = useState<HistoryStorage>(new HistoryStorage());
const [isVisible, setIsVisible] = useState<boolean>(false);
const [maxWidth, setMaxWidth] = useState<number | null>(null);
const [maxHeight, setMaxHeight] = useState<number | null>(null);
const hide = useCallback(() => setIsVisible(false), []);
const toggle = useCallback(() => setIsVisible(!isVisible), [isVisible]);
const mountRef = useRef<boolean>(false);
const { x, y, reference, floating, strategy } = useFloating({
placement: "bottom-end",
middleware: [
shift(),
flip(),
offset(5),
size({
apply({ width, height }) {
setMaxWidth(width);
setMaxHeight(height);
},
}),
],
});
// every time this component updates we will rewrite history
// (if there are changes)
useEffect(() => {
if (mountRef.current) {
// we don't store unapplied (we only have raw text for those, we need
// name & value for coloring) or invalid filters
// also check for value, name might be missing for fuzzy filters, but
// the value should always be set
const validAppliedFilters = alertStore.filters.values
.filter((f) => f.applied && f.isValid && f.value)
.map((f) => ReduceFilter(f));
// we don't store unapplied (we only have raw text for those, we need
// name & value for coloring) or invalid filters
// also check for value, name might be missing for fuzzy filters, but
// the value should always be set
const validAppliedFilters = alertStore.filters.values
.filter((f) => f.applied && f.isValid && f.value)
.map((f) => ReduceFilter(f));
// don't store empty filters in history
if (validAppliedFilters.length === 0) return;
// make a JSON dump for comparing later with what's already stored
const filtersJSON = JSON.stringify(validAppliedFilters);
// don't store empty filters in history
if (validAppliedFilters.length === 0) return;
// make a JSON dump for comparing later with what's already stored
const filtersJSON = JSON.stringify(validAppliedFilters);
// rewrite history putting current filter set on top, this will move
// it up if user selects a filter set that was already in history
const newHistory = [
...[validAppliedFilters],
...history.config.filters.filter(
(f) => JSON.stringify(f) !== filtersJSON
),
].slice(0, 8);
history.setFilters(newHistory);
} else {
mountRef.current = true;
}
});
// rewrite history putting current filter set on top, this will move
// it up if user selects a filter set that was already in history
const newHistory = [
...[validAppliedFilters],
...history.config.filters.filter(
(f) => JSON.stringify(f) !== filtersJSON
),
].slice(0, 8);
history.setFilters(newHistory);
}, [history, alertStore.filters.values]);
const ref = useRef<HTMLSpanElement | null>(null);
useOnClickOutside(ref, hide, isVisible);
@@ -234,45 +255,34 @@ const History: FC<{
ref={ref}
className="input-group-text border-0 rounded-0 bg-inherit px-0"
>
<Manager
data-filters={alertStore.filters.values
.map((f) => ReduceFilter(f))
.join(" ")}
<button
ref={reference}
onClick={toggle}
className="btn border-0 rounded-0 bg-inherit cursor-pointer components-navbar-history px-2 py-0 components-navbar-icon"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="true"
>
<Reference>
{({ ref }) => (
<button
ref={ref}
onClick={toggle}
className="btn border-0 rounded-0 bg-inherit cursor-pointer components-navbar-history px-2 py-0 components-navbar-icon"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="true"
>
<FontAwesomeIcon icon={faCaretDown} />
</button>
)}
</Reference>
<DropdownSlide in={isVisible} unmountOnExit>
<Popper placement="bottom" modifiers={CommonPopperModifiers}>
{({ placement, ref, style }) => (
<HistoryMenu
popperPlacement={placement}
popperRef={ref}
popperStyle={style}
filters={history.config.filters}
onClear={() => {
history.setFilters([]);
}}
alertStore={alertStore}
settingsStore={settingsStore}
afterClick={hide}
/>
)}
</Popper>
</DropdownSlide>
</Manager>
<FontAwesomeIcon icon={faCaretDown} />
</button>
<DropdownSlide in={isVisible} unmountOnExit>
<HistoryMenu
filters={history.config.filters}
onClear={() => {
history.setFilters([]);
}}
alertStore={alertStore}
settingsStore={settingsStore}
afterClick={hide}
x={x}
y={y}
floating={floating}
strategy={strategy}
maxWidth={maxWidth}
maxHeight={maxHeight}
/>
</DropdownSlide>
</span>
);
});

View File

@@ -172,9 +172,12 @@ storiesOf("NavBar", module).add("NavBar", () => {
fixedTop={false}
/>
<HistoryMenu
popperPlacement="top"
popperRef={() => {}}
popperStyle={{}}
x={0}
y={0}
floating={null}
strategy={"absolute"}
maxWidth={null}
maxHeight={null}
filters={history}
onClear={() => {}}
alertStore={alertStore}

View File

@@ -29,7 +29,7 @@ describe("TooltipWrapper", () => {
expect(tree.find("div.foo").text()).toBe("Hover me");
});
it("on non-touch devices it renders tooltip on mouseOver and hides on mouseLeave", () => {
it("on non-touch devices it renders tooltip on mouseOver and hides on mouseLeave", async () => {
const tree = mount(
<TooltipWrapper title="my title">
<span>Hover me</span>
@@ -49,9 +49,13 @@ describe("TooltipWrapper", () => {
});
tree.update();
expect(tree.find("div.tooltip")).toHaveLength(0);
await act(async () => {
jest.runAllTimers();
});
});
it("on touch devices it renders tooltip on touchStart and hides on touchEnd", () => {
it("on touch devices it renders tooltip on touchStart and hides on touchEnd", async () => {
const tree = mount(
<TooltipWrapper title="my title">
<span>Hover me</span>
@@ -77,6 +81,10 @@ describe("TooltipWrapper", () => {
});
tree.update();
expect(tree.find("div.tooltip")).toHaveLength(0);
await act(async () => {
jest.runAllTimers();
});
});
it("hides the tooltip after click and show again on mouseOver", () => {

View File

@@ -3,7 +3,7 @@ import { createPortal } from "react-dom";
import { CSSTransition } from "react-transition-group";
import { usePopper } from "react-popper";
import { useFloating, shift, flip } from "@floating-ui/react-dom";
import { useSupportsTouch } from "Hooks/useSupportsTouch";
@@ -12,20 +12,9 @@ const TooltipWrapper: FC<{
children: ReactNode;
className?: string;
}> = ({ title, children, className }) => {
const [referenceElement, setReferenceElement] = useState<HTMLElement | null>(
null
);
const [popperElement, setPopperElement] = useState<HTMLElement | null>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
const { x, y, reference, floating, strategy } = useFloating({
placement: "top",
modifiers: [
{
name: "preventOverflow",
options: {
rootBoundary: "viewport",
},
},
],
middleware: [shift(), flip()],
});
const supportsTouch = useSupportsTouch();
@@ -69,7 +58,7 @@ const TooltipWrapper: FC<{
onTouchStart={supportsTouch ? showTooltip : undefined}
onTouchCancel={supportsTouch ? hideTooltip : undefined}
onTouchEnd={supportsTouch ? hideTooltip : undefined}
ref={setReferenceElement}
ref={reference}
className={`${className ? className : ""} tooltip-trigger`}
>
{children}
@@ -86,9 +75,12 @@ const TooltipWrapper: FC<{
>
<div
className="tooltip tooltip-inner"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
ref={floating}
style={{
position: strategy,
top: y ?? "",
left: x ?? "",
}}
>
{title}
</div>

View File

@@ -1,5 +1,6 @@
.dropdown-menu.components-navbar-historymenu {
white-space: nowrap;
overflow: scroll;
}
.dropdown-menu.components-navbar-historymenu > .dropdown-item {

View File

@@ -1,10 +0,0 @@
// https://github.com/FezVrasta/popper.js/issues/478#issuecomment-341506071
export default class Popper {
constructor() {
return {
destroy: () => {},
scheduleUpdate: () => {},
};
}
}

View File

@@ -50,7 +50,7 @@ global.console.trace = consoleHandler;
FetchRetryConfig.minTimeout = 2;
FetchRetryConfig.maxTimeout = 10;
// usePopper uses useLayoutEffect but that fails in enzyme
// floating-ui uses useLayoutEffect but that fails in enzyme
React.useLayoutEffect = React.useEffect;
beforeEach(() => {