mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
chore: extract tooltip into package
- chore: add sanity check unit test - chore: added tailwind.config.js to fix build - chore: .swcrc added for tests to pass - chore(deps): sass is not a dependency - fix: manually import the tooltip css to fix tooltips not showing up - chore: Run lint:fix against code - chore: Fix use of mock in @k8slens/core Signed-off-by: Gabriel <gaccettola@mirantis.com> Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
1 parent
c7df928acc
commit
01059d8cb1
42 files changed
+268
-101
No files matched your search
@@ -64,7 +64,7 @@ export const ConfirmDialog = Object.assign(_ConfirmDialog, {
|
||||
});
|
||||
|
||||
export * from "../../renderer/components/icon";
|
||||
export * from "../../renderer/components/tooltip";
|
||||
export * from "@k8slens/tooltip";
|
||||
export * from "../../renderer/components/tabs";
|
||||
export * from "../../renderer/components/table";
|
||||
export * from "../../renderer/components/badge";
|
||||
|
||||
@@ -9,6 +9,8 @@ import { TextEncoder, TextDecoder as TextDecoderNode } from "util";
|
||||
import glob from "glob";
|
||||
import path from "path";
|
||||
import { enableMapSet, setAutoFreeze } from "immer";
|
||||
import type * as K8slensTooltip from "@k8slens/tooltip";
|
||||
import React from "react";
|
||||
|
||||
declare global {
|
||||
interface InjectablePaths {
|
||||
@@ -53,8 +55,25 @@ global.ResizeObserver = class {
|
||||
};
|
||||
|
||||
jest.mock("./renderer/components/monaco-editor/monaco-editor");
|
||||
jest.mock("./renderer/components/tooltip/withTooltip");
|
||||
jest.mock("@k8slens/tooltip", () => ({
|
||||
...jest.requireActual("@k8slens/tooltip"),
|
||||
withTooltip: (Target => ({ tooltip, tooltipOverrideDisabled, ...props }: any) => {
|
||||
if (tooltip) {
|
||||
const testId = props["data-testid"];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Target {...props} />
|
||||
<div data-testid={testId && `tooltip-content-for-${testId}`}>
|
||||
{tooltip.children || tooltip}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <Target {...props} />;
|
||||
}) as typeof K8slensTooltip.withTooltip,
|
||||
}));
|
||||
jest.mock("monaco-editor");
|
||||
|
||||
const getInjectables = (environment: "renderer" | "main", filePathGlob: string) => [
|
||||
@@ -16,7 +16,7 @@ import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
import type { KubeEvent, KubeEventApi, KubeEventData } from "../../../common/k8s-api/endpoints/events.api";
|
||||
import type { TableSortCallbacks, TableSortParams } from "../table";
|
||||
import type { HeaderCustomizer } from "../item-object-list";
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { Tooltip } from "@k8slens/tooltip";
|
||||
import { Link } from "react-router-dom";
|
||||
import type { IClassName } from "@k8slens/utilities";
|
||||
import { cssNames, stopPropagation } from "@k8slens/utilities";
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Icon } from "../icon";
|
||||
import { observer } from "mobx-react";
|
||||
import { Input, InputValidators } from "../input";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { TooltipPosition } from "../tooltip";
|
||||
import { TooltipPosition } from "@k8slens/tooltip";
|
||||
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import styles from "./subnamespace-badge.module.scss";
|
||||
|
||||
import React from "react";
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { Tooltip } from "@k8slens/tooltip";
|
||||
import { cssNames } from "@k8slens/utilities";
|
||||
|
||||
interface SubnamespaceBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
import type { Node } from "../../../common/k8s-api/endpoints/node.api";
|
||||
import { formatNodeTaint } from "../../../common/k8s-api/endpoints/node.api";
|
||||
import { LineProgress } from "../line-progress";
|
||||
import { Tooltip, TooltipPosition } from "../tooltip";
|
||||
import { Tooltip, TooltipPosition } from "@k8slens/tooltip";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import upperFirst from "lodash/upperFirst";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import { onMultiSelectFor, Select } from "../../../select";
|
||||
import { Wizard, WizardStep } from "../../../wizard";
|
||||
import { ObservableHashSet, iter } from "@k8slens/utilities";
|
||||
import { Input } from "../../../input";
|
||||
import { TooltipPosition } from "../../../tooltip";
|
||||
import { TooltipPosition } from "@k8slens/tooltip";
|
||||
import type { Subject } from "../../../../../common/k8s-api/endpoints/types/subject";
|
||||
import type { ClusterRoleBindingDialogState } from "./state.injectable";
|
||||
import type { ClusterRoleStore } from "../../+cluster-roles/store";
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { IComputedValue } from "mobx";
|
||||
import { makeObservable, observable, reaction } from "mobx";
|
||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
||||
import { Icon } from "../icon";
|
||||
import { TooltipPosition } from "../tooltip";
|
||||
import { TooltipPosition } from "@k8slens/tooltip";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import clusterFrameContextForNamespacedResourcesInjectable from "../../cluster-frame-context/for-namespaced-resources.injectable";
|
||||
import type { SubscribeStores } from "../../kube-watch-api/kube-watch-api";
|
||||
|
||||
@@ -29,7 +29,7 @@ import nodeApiInjectable from "../../../common/k8s-api/endpoints/node.api.inject
|
||||
import eventStoreInjectable from "../+events/store.injectable";
|
||||
import podStoreInjectable from "./store.injectable";
|
||||
import { NamespaceSelectBadge } from "../+namespaces/namespace-select-badge";
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { Tooltip } from "@k8slens/tooltip";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
|
||||
@@ -9,7 +9,7 @@ import React, { useEffect, useRef, useState } from "react";
|
||||
import { action, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { cssNames } from "@k8slens/utilities";
|
||||
import { withTooltip } from "../tooltip";
|
||||
import { withTooltip } from "@k8slens/tooltip";
|
||||
|
||||
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
small?: boolean;
|
||||
|
||||
@@ -7,7 +7,7 @@ import "./button.scss";
|
||||
import type { ButtonHTMLAttributes } from "react";
|
||||
import React from "react";
|
||||
import { cssNames } from "@k8slens/utilities";
|
||||
import { withTooltip } from "../tooltip";
|
||||
import { withTooltip } from "@k8slens/tooltip";
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<any> {
|
||||
label?: React.ReactNode;
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Menu, MenuItem } from "../menu";
|
||||
import { observable } from "mobx";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import dockStoreInjectable from "./dock/store.injectable";
|
||||
import { Tooltip, TooltipPosition } from "../tooltip";
|
||||
import { Tooltip, TooltipPosition } from "@k8slens/tooltip";
|
||||
import isMacInjectable from "../../../common/vars/is-mac.injectable";
|
||||
import autoBindReact from "auto-bind/react";
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { observer } from "mobx-react";
|
||||
import type { AvatarProps } from "../avatar";
|
||||
import { Avatar } from "../avatar";
|
||||
import { Icon } from "../icon";
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { Tooltip } from "@k8slens/tooltip";
|
||||
import type { NormalizeCatalogEntityContextMenu } from "../../catalog/normalize-menu-item.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import normalizeCatalogEntityContextMenuInjectable from "../../catalog/normalize-menu-item.injectable";
|
||||
|
||||
@@ -8,7 +8,7 @@ import React, { useRef, useState } from "react";
|
||||
import { Icon } from "../icon";
|
||||
import { Badge } from "../badge";
|
||||
import { HotbarSwitchCommand } from "./hotbar-switch-command";
|
||||
import { Tooltip, TooltipPosition } from "../tooltip";
|
||||
import { Tooltip, TooltipPosition } from "@k8slens/tooltip";
|
||||
import { observer } from "mobx-react";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||
|
||||
@@ -10,7 +10,7 @@ import React, { createRef } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import type { LocationDescriptor } from "history";
|
||||
import { cssNames } from "@k8slens/utilities";
|
||||
import { withTooltip } from "../tooltip";
|
||||
import { withTooltip } from "@k8slens/tooltip";
|
||||
import isNumber from "lodash/isNumber";
|
||||
import Configuration from "./configuration.svg";
|
||||
import Crane from "./crane.svg";
|
||||
|
||||
@@ -10,8 +10,8 @@ import React from "react";
|
||||
import type { SingleOrMany } from "@k8slens/utilities";
|
||||
import { debouncePromise, isPromiseSettledFulfilled, cssNames } from "@k8slens/utilities";
|
||||
import { Icon } from "../icon";
|
||||
import type { TooltipProps } from "../tooltip";
|
||||
import { Tooltip } from "../tooltip";
|
||||
import type { TooltipProps } from "@k8slens/tooltip";
|
||||
import { Tooltip } from "@k8slens/tooltip";
|
||||
import * as Validators from "./input_validators";
|
||||
import type { InputValidator, InputValidation, InputValidationResult, SyncValidationMessage } from "./input_validators";
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import { KubeObjectMenu } from "../kube-object-menu";
|
||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
||||
import { ResourceKindMap, ResourceNames } from "../../utils/rbac";
|
||||
import { Icon } from "../icon";
|
||||
import { TooltipPosition } from "../tooltip";
|
||||
import { TooltipPosition } from "@k8slens/tooltip";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import clusterFrameContextForNamespacedResourcesInjectable from "../../cluster-frame-context/for-namespaced-resources.injectable";
|
||||
import type { SubscribableStore, SubscribeStores } from "../../kube-watch-api/kube-watch-api";
|
||||
|
||||
@@ -12,7 +12,7 @@ import { IpcRendererNavigationEvents } from "../../../common/ipc/navigation-even
|
||||
import { Avatar } from "../avatar";
|
||||
import { Icon } from "../icon";
|
||||
import { Menu, MenuItem } from "../menu";
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { Tooltip } from "@k8slens/tooltip";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { VisitEntityContextMenu } from "../../../common/catalog/visit-entity-context-menu.injectable";
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import "./line-progress.scss";
|
||||
import React from "react";
|
||||
import { cssNames } from "@k8slens/utilities";
|
||||
import { withTooltip } from "../tooltip";
|
||||
import { withTooltip } from "@k8slens/tooltip";
|
||||
|
||||
export interface LineProgressProps extends React.HTMLProps<HTMLDivElement> {
|
||||
value: number;
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Icon } from "../icon";
|
||||
import type { MenuProps } from "./menu";
|
||||
import { Menu, MenuItem } from "./menu";
|
||||
import isString from "lodash/isString";
|
||||
import type { TooltipDecoratorProps } from "../tooltip";
|
||||
import type { TooltipDecoratorProps } from "@k8slens/tooltip";
|
||||
import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import openConfirmDialogInjectable from "../confirm-dialog/open.injectable";
|
||||
|
||||
@@ -7,7 +7,7 @@ import "./status-brick.scss";
|
||||
|
||||
import React from "react";
|
||||
import { cssNames } from "@k8slens/utilities";
|
||||
import { withTooltip } from "../tooltip";
|
||||
import { withTooltip } from "@k8slens/tooltip";
|
||||
|
||||
export interface StatusBrickProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import React from "react";
|
||||
|
||||
export const withTooltip =
|
||||
(Target: any) =>
|
||||
({ tooltip, tooltipOverrideDisabled, ...props }: any) => {
|
||||
if (tooltip) {
|
||||
const testId = props["data-testid"];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Target {...props} />
|
||||
<div data-testid={testId && `tooltip-content-for-${testId}`}>
|
||||
{tooltip.children || tooltip}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <Target {...props} />;
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Tooltip /> does not render to DOM if not visibile 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
id="my-target"
|
||||
>
|
||||
Target Text
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`<Tooltip /> renders to DOM when forced to by visibile prop 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="Tooltip right visible"
|
||||
role="tooltip"
|
||||
style="left: 10px; top: 0px;"
|
||||
>
|
||||
I am a tooltip
|
||||
</div>
|
||||
<div
|
||||
id="my-target"
|
||||
>
|
||||
Target Text
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`<Tooltip /> renders to DOM when hovering over target 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="Tooltip right visible"
|
||||
role="tooltip"
|
||||
style="left: 10px; top: 0px;"
|
||||
>
|
||||
I am a tooltip
|
||||
</div>
|
||||
<div
|
||||
id="my-target"
|
||||
>
|
||||
Target Text
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export * from "./tooltip";
|
||||
export * from "./withTooltip";
|
||||
@@ -1,99 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
|
||||
.Tooltip {
|
||||
--bgc: var(--mainBackground);
|
||||
--radius: #{$radius};
|
||||
--color: var(--textColorAccent);
|
||||
--border: 1px solid var(--borderColor);
|
||||
|
||||
// use positioning relative to viewport (window)
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/position
|
||||
position: fixed;
|
||||
margin: 0 !important;
|
||||
background: var(--bgc);
|
||||
font-size: small;
|
||||
font-weight: normal;
|
||||
border-radius: var(--radius);
|
||||
color: var(--color);
|
||||
white-space: normal;
|
||||
padding: .5em;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
transition: opacity 150ms 150ms ease-in-out;
|
||||
z-index: 100000;
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.24);
|
||||
left: 0;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.formatter {
|
||||
&.nowrap {
|
||||
&, * {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&.narrow {
|
||||
max-width: 300px;
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: break-word;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.small {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: var(--colorError);
|
||||
}
|
||||
|
||||
&.tableView {
|
||||
display: grid;
|
||||
gap: var(--padding);
|
||||
grid-template-columns: max-content 1fr;
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
|
||||
// backward compatibility: skips element in DOM to consider only children in grid-flow
|
||||
> .flex {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
> * {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.title {
|
||||
grid-column: 1 / 3; // merge
|
||||
color: var(--textColorAccent);
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: right;
|
||||
color: var(--textColorAccent);
|
||||
}
|
||||
|
||||
.value {
|
||||
text-align: left;
|
||||
color: var(--textColorSecondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import assert from "assert";
|
||||
import React from "react";
|
||||
import { Tooltip } from "./tooltip";
|
||||
|
||||
describe("<Tooltip />", () => {
|
||||
let requestAnimationFrameSpy: jest.SpyInstance<number, [callback: FrameRequestCallback]>;
|
||||
|
||||
beforeEach(() => {
|
||||
requestAnimationFrameSpy = jest.spyOn(window, "requestAnimationFrame");
|
||||
|
||||
requestAnimationFrameSpy.mockImplementation(cb => {
|
||||
cb(0);
|
||||
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
requestAnimationFrameSpy.mockRestore();
|
||||
});
|
||||
|
||||
|
||||
it("does not render to DOM if not visibile", () => {
|
||||
const result = render((
|
||||
<>
|
||||
<Tooltip targetId="my-target" usePortal={false}>I am a tooltip</Tooltip>
|
||||
<div id="my-target">Target Text</div>
|
||||
</>
|
||||
));
|
||||
|
||||
expect(result.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders to DOM when hovering over target", () => {
|
||||
const result = render((
|
||||
<>
|
||||
<Tooltip
|
||||
targetId="my-target"
|
||||
data-testid="tooltip"
|
||||
usePortal={false}
|
||||
>
|
||||
I am a tooltip
|
||||
</Tooltip>
|
||||
<div id="my-target">Target Text</div>
|
||||
</>
|
||||
));
|
||||
|
||||
const target = result.baseElement.querySelector("#my-target");
|
||||
|
||||
assert(target);
|
||||
|
||||
userEvent.hover(target);
|
||||
expect(result.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders to DOM when forced to by visibile prop", () => {
|
||||
const result = render((
|
||||
<>
|
||||
<Tooltip
|
||||
targetId="my-target"
|
||||
data-testid="tooltip"
|
||||
visible={true}
|
||||
usePortal={false}
|
||||
>
|
||||
I am a tooltip
|
||||
</Tooltip>
|
||||
<div id="my-target">Target Text</div>
|
||||
</>
|
||||
));
|
||||
|
||||
expect(result.baseElement).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,243 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "./tooltip.scss";
|
||||
|
||||
import React from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { observer } from "mobx-react";
|
||||
import type { IClassName } from "@k8slens/utilities";
|
||||
import { cssNames } from "@k8slens/utilities";
|
||||
import { observable, makeObservable, action } from "mobx";
|
||||
import autoBindReact from "auto-bind/react";
|
||||
|
||||
export enum TooltipPosition {
|
||||
TOP = "top",
|
||||
BOTTOM = "bottom",
|
||||
LEFT = "left",
|
||||
RIGHT = "right",
|
||||
TOP_LEFT = "top_left",
|
||||
TOP_RIGHT = "top_right",
|
||||
BOTTOM_LEFT = "bottom_left",
|
||||
BOTTOM_RIGHT = "bottom_right",
|
||||
}
|
||||
|
||||
export interface TooltipProps {
|
||||
targetId: string; // html-id of target element to bind for
|
||||
tooltipOnParentHover?: boolean; // detect hover on parent of target
|
||||
visible?: boolean; // initial visibility
|
||||
offset?: number; // offset from target element in pixels (all sides)
|
||||
usePortal?: boolean; // renders element outside of parent (in body), disable for "easy-styling", default: true
|
||||
preferredPositions?: TooltipPosition | TooltipPosition[];
|
||||
className?: IClassName;
|
||||
formatters?: TooltipContentFormatters;
|
||||
style?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface TooltipContentFormatters {
|
||||
narrow?: boolean; // max-width
|
||||
warning?: boolean; // color
|
||||
small?: boolean; // font-size
|
||||
nowrap?: boolean; // white-space
|
||||
tableView?: boolean;
|
||||
}
|
||||
|
||||
const defaultProps: Partial<TooltipProps> = {
|
||||
usePortal: true,
|
||||
offset: 10,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class Tooltip extends React.Component<TooltipProps> {
|
||||
static defaultProps = defaultProps as object;
|
||||
|
||||
@observable.ref elem: HTMLDivElement | null = null;
|
||||
@observable activePosition?: TooltipPosition;
|
||||
@observable isVisible = false;
|
||||
@observable isContentVisible = false; // animation manager
|
||||
|
||||
constructor(props: TooltipProps) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
autoBindReact(this);
|
||||
}
|
||||
|
||||
get targetElem(): HTMLElement | null {
|
||||
return document.getElementById(this.props.targetId);
|
||||
}
|
||||
|
||||
get hoverTarget(): HTMLElement | null {
|
||||
if (this.props.tooltipOnParentHover) {
|
||||
return this.targetElem?.parentElement ?? null;
|
||||
}
|
||||
|
||||
return this.targetElem;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.hoverTarget?.addEventListener("mouseenter", this.onEnterTarget);
|
||||
this.hoverTarget?.addEventListener("mouseleave", this.onLeaveTarget);
|
||||
this.refreshPosition();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.refreshPosition();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.hoverTarget?.removeEventListener("mouseenter", this.onEnterTarget);
|
||||
this.hoverTarget?.removeEventListener("mouseleave", this.onLeaveTarget);
|
||||
}
|
||||
|
||||
@action
|
||||
protected onEnterTarget() {
|
||||
this.isVisible = true;
|
||||
requestAnimationFrame(action(() => this.isContentVisible = true));
|
||||
}
|
||||
|
||||
@action
|
||||
protected onLeaveTarget() {
|
||||
this.isVisible = false;
|
||||
this.isContentVisible = false;
|
||||
}
|
||||
|
||||
refreshPosition() {
|
||||
const { preferredPositions } = this.props;
|
||||
const { elem, targetElem } = this;
|
||||
|
||||
if (!elem || !targetElem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const positions = new Set<TooltipPosition>([
|
||||
...[preferredPositions ?? []].flat(),
|
||||
TooltipPosition.RIGHT,
|
||||
TooltipPosition.BOTTOM,
|
||||
TooltipPosition.TOP,
|
||||
TooltipPosition.LEFT,
|
||||
TooltipPosition.TOP_RIGHT,
|
||||
TooltipPosition.TOP_LEFT,
|
||||
TooltipPosition.BOTTOM_RIGHT,
|
||||
TooltipPosition.BOTTOM_LEFT,
|
||||
]);
|
||||
|
||||
// reset position first and get all possible client-rect area for tooltip element
|
||||
this.setPosition(elem, { left: 0, top: 0 });
|
||||
|
||||
const selfBounds = elem.getBoundingClientRect();
|
||||
const targetBounds = targetElem.getBoundingClientRect();
|
||||
const { innerWidth: viewportWidth, innerHeight: viewportHeight } = window;
|
||||
|
||||
// find proper position
|
||||
for (const pos of positions) {
|
||||
const { left, top, right, bottom } = this.getPosition(pos, selfBounds, targetBounds);
|
||||
const fitsToWindow = left >= 0 && top >= 0 && right <= viewportWidth && bottom <= viewportHeight;
|
||||
|
||||
if (fitsToWindow) {
|
||||
this.activePosition = pos;
|
||||
this.setPosition(elem, { top, left });
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// apply fallback position if nothing helped from above
|
||||
const fallbackPosition = Array.from(positions)[0];
|
||||
const { left, top } = this.getPosition(fallbackPosition, selfBounds, targetBounds);
|
||||
|
||||
this.activePosition = fallbackPosition;
|
||||
this.setPosition(elem, { left, top });
|
||||
}
|
||||
|
||||
protected setPosition(elem: HTMLDivElement, pos: { left: number; top: number }) {
|
||||
elem.style.left = `${pos.left}px`;
|
||||
elem.style.top = `${pos.top}px`;
|
||||
}
|
||||
|
||||
protected getPosition(position: TooltipPosition, tooltipBounds: DOMRect, targetBounds: DOMRect) {
|
||||
let left: number;
|
||||
let top: number;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const offset = this.props.offset!;
|
||||
const horizontalCenter = targetBounds.left + (targetBounds.width - tooltipBounds.width) / 2;
|
||||
const verticalCenter = targetBounds.top + (targetBounds.height - tooltipBounds.height) / 2;
|
||||
const topCenter = targetBounds.top - tooltipBounds.height - offset;
|
||||
const bottomCenter = targetBounds.bottom + offset;
|
||||
|
||||
switch (position) {
|
||||
case "top":
|
||||
left = horizontalCenter;
|
||||
top = topCenter;
|
||||
break;
|
||||
case "bottom":
|
||||
left = horizontalCenter;
|
||||
top = bottomCenter;
|
||||
break;
|
||||
case "left":
|
||||
top = verticalCenter;
|
||||
left = targetBounds.left - tooltipBounds.width - offset;
|
||||
break;
|
||||
case "right":
|
||||
top = verticalCenter;
|
||||
left = targetBounds.right + offset;
|
||||
break;
|
||||
case "top_left":
|
||||
left = targetBounds.left;
|
||||
top = topCenter;
|
||||
break;
|
||||
case "top_right":
|
||||
left = targetBounds.right - tooltipBounds.width;
|
||||
top = topCenter;
|
||||
break;
|
||||
case "bottom_left":
|
||||
top = bottomCenter;
|
||||
left = targetBounds.left;
|
||||
break;
|
||||
case "bottom_right":
|
||||
top = bottomCenter;
|
||||
left = targetBounds.right - tooltipBounds.width;
|
||||
break;
|
||||
default:
|
||||
throw new TypeError("Invalid props.postition value");
|
||||
}
|
||||
|
||||
return {
|
||||
left,
|
||||
top,
|
||||
right: left + tooltipBounds.width,
|
||||
bottom: top + tooltipBounds.height,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { style, formatters, usePortal, children, visible = this.isVisible } = this.props;
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const className = cssNames("Tooltip", this.props.className, formatters, this.activePosition, {
|
||||
visible: this.isContentVisible || this.props.visible,
|
||||
formatter: !!formatters,
|
||||
});
|
||||
const tooltip = (
|
||||
<div
|
||||
className={className}
|
||||
style={style}
|
||||
ref={elem => this.elem = elem}
|
||||
role="tooltip"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (usePortal) {
|
||||
return createPortal(tooltip, document.body);
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Tooltip decorator for simple composition with other components
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import React, { useState } from "react";
|
||||
import type { TooltipProps } from "./tooltip";
|
||||
import { Tooltip } from "./tooltip";
|
||||
import { isReactNode } from "@k8slens/utilities";
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
import type { SingleOrMany } from "@k8slens/utilities";
|
||||
|
||||
export interface TooltipDecoratorProps {
|
||||
tooltip?: ReactNode | Omit<TooltipProps, "targetId">;
|
||||
/**
|
||||
* forces tooltip to detect the target's parent for mouse events. This is
|
||||
* useful for displaying tooltips even when the target is "disabled"
|
||||
*/
|
||||
tooltipOverrideDisabled?: boolean;
|
||||
id?: string;
|
||||
children?: SingleOrMany<React.ReactNode>;
|
||||
}
|
||||
|
||||
export function withTooltip<TargetProps>(
|
||||
Target: TargetProps extends Pick<TooltipDecoratorProps, "id" | "children">
|
||||
? React.FunctionComponent<TargetProps>
|
||||
: never,
|
||||
): React.FunctionComponent<TargetProps & TooltipDecoratorProps> {
|
||||
const DecoratedComponent = (props: TargetProps & TooltipDecoratorProps) => {
|
||||
// TODO: Remove side-effect to allow deterministic unit testing
|
||||
const [defaultTooltipId] = useState(uniqueId("tooltip_target_"));
|
||||
|
||||
let {
|
||||
id: targetId,
|
||||
children: targetChildren,
|
||||
} = props;
|
||||
const {
|
||||
tooltip,
|
||||
tooltipOverrideDisabled,
|
||||
id: _unusedId,
|
||||
children: _unusedTargetChildren,
|
||||
...targetProps
|
||||
} = props;
|
||||
|
||||
if (tooltip) {
|
||||
const tooltipProps: TooltipProps = {
|
||||
targetId: targetId || defaultTooltipId,
|
||||
tooltipOnParentHover: tooltipOverrideDisabled,
|
||||
formatters: { narrow: true },
|
||||
...(isReactNode(tooltip) ? { children: tooltip } : tooltip),
|
||||
};
|
||||
|
||||
targetId = tooltipProps.targetId;
|
||||
targetChildren = (
|
||||
<>
|
||||
<div>
|
||||
{targetChildren}
|
||||
</div>
|
||||
<Tooltip {...tooltipProps} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Target id={targetId} {...targetProps as any}>
|
||||
{targetChildren}
|
||||
</Target>
|
||||
);
|
||||
};
|
||||
|
||||
DecoratedComponent.displayName = `withTooltip(${Target.displayName || Target.name})`;
|
||||
|
||||
return DecoratedComponent;
|
||||
}
|
||||
Reference in new issue
Block a user