1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

ui-components/tooltip

Signed-off-by: Gabriel <gaccettola@mirantis.com>
This commit is contained in:
Gabriel 2023-04-12 13:43:06 +02:00
parent 67fa57c15c
commit 7aaabc9526
35 changed files with 684 additions and 22 deletions

View File

@ -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 "../../utils";
import { cssNames, stopPropagation } from "../../utils";

View File

@ -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";

View File

@ -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 "../../utils";
interface SubnamespaceBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {

View File

@ -13,7 +13,7 @@ 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 { bytesToUnits } from "../../../common/utils/convertMemory";
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";

View File

@ -20,7 +20,7 @@ import { onMultiSelectFor, Select } from "../../../select";
import { Wizard, WizardStep } from "../../../wizard";
import { ObservableHashSet, nFircate } from "../../../../utils";
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";

View File

@ -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";

View File

@ -9,7 +9,7 @@ import React, { useEffect, useRef, useState } from "react";
import { action, observable } from "mobx";
import { observer } from "mobx-react";
import { cssNames } from "../../utils/cssNames";
import { withTooltip } from "../tooltip";
import { withTooltip } from "@k8slens/tooltip";
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
small?: boolean;

View File

@ -7,7 +7,7 @@ import "./button.scss";
import type { ButtonHTMLAttributes } from "react";
import React from "react";
import { cssNames } from "../../utils";
import { withTooltip } from "../tooltip";
import { withTooltip } from "@k8slens/tooltip";
export interface ButtonProps extends ButtonHTMLAttributes<any> {
label?: React.ReactNode;

View File

@ -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";
export interface DockTabProps extends TabProps<DockTabModel> {

View File

@ -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";

View File

@ -9,7 +9,7 @@ import { Icon } from "../icon";
import { Badge } from "../badge";
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
import { HotbarSwitchCommand } from "./hotbar-switch-command";
import { Tooltip, TooltipPosition } from "../tooltip";
import { Tooltip, TooltipPosition } from "@k8slens/tooltip";
import { observer } from "mobx-react";
import type { Hotbar } from "../../../common/hotbars/types";
import { withInjectables } from "@ogre-tools/injectable-react";

View File

@ -10,7 +10,7 @@ import React, { createRef } from "react";
import { NavLink } from "react-router-dom";
import type { LocationDescriptor } from "history";
import { cssNames } from "../../utils";
import { withTooltip } from "../tooltip";
import { withTooltip } from "@k8slens/tooltip";
import isNumber from "lodash/isNumber";
import Configuration from "./configuration.svg";
import Crane from "./crane.svg";

View File

@ -10,8 +10,8 @@ import React from "react";
import type { SingleOrMany } from "../../utils";
import { autoBind, cssNames, debouncePromise, getRandId, isPromiseSettledFulfilled } from "../../utils";
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";

View File

@ -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";

View File

@ -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 hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
import type { HotbarStore } from "../../../common/hotbars/store";

View File

@ -6,7 +6,7 @@
import "./line-progress.scss";
import React from "react";
import { cssNames } from "../../utils";
import { withTooltip } from "../tooltip";
import { withTooltip } from "@k8slens/tooltip";
export interface LineProgressProps extends React.HTMLProps<HTMLDivElement> {
value: number;

View File

@ -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";

View File

@ -7,7 +7,7 @@ import "./status-brick.scss";
import React from "react";
import { cssNames } from "../../utils";
import { withTooltip } from "../tooltip";
import { withTooltip } from "@k8slens/tooltip";
export interface StatusBrickProps extends React.HTMLAttributes<HTMLDivElement> {
}

View File

@ -7,7 +7,7 @@ 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";
import { Tooltip } from "@k8slens/tooltip";
describe("<Tooltip />", () => {
let requestAnimationFrameSpy: jest.SpyInstance<number, [callback: FrameRequestCallback]>;

View File

@ -7,8 +7,8 @@
import type { ReactNode } from "react";
import React, { useState } from "react";
import type { TooltipProps } from "./tooltip";
import { Tooltip } from "./tooltip";
import type { TooltipProps } from "@k8slens/tooltip";
import { Tooltip } from "@k8slens/tooltip";
import { isReactNode } from "../../utils/isReactNode";
import uniqueId from "lodash/uniqueId";
import type { SingleOrMany } from "../../utils";

View File

@ -0,0 +1,6 @@
{
"extends": "@k8slens/eslint-config/eslint",
"parserOptions": {
"project": "./tsconfig.json"
}
}

View File

@ -0,0 +1 @@
"@k8slens/eslint-config/prettier"

View File

@ -0,0 +1,21 @@
# @k8slens/tooltip
This package contains stuff related to creating Lens-applications.
# Usage
```bash
$ npm install @k8slens/tooltip
```
```typescript
import { tooltipFeature } from "@k8slens/tooltip";
import { registerFeature } from "@k8slens/feature-core";
import { createContainer } from "@ogre-tools/injectable";
const di = createContainer("some-container");
registerFeature(di, tooltipFeature);
```
## Extendability

View File

@ -0,0 +1,2 @@
export * from "./src/tooltip";
export * from "./src/withTooltip";

View File

@ -0,0 +1 @@
module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact;

View File

@ -0,0 +1,45 @@
{
"name": "@k8slens/tooltip",
"private": false,
"version": "1.0.0-alpha.0",
"description": "Highly extendable tooltip in the Lens.",
"type": "commonjs",
"files": [
"dist"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"type": "git",
"url": "git+https://github.com/lensapp/lens.git"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": {
"name": "OpenLens Authors",
"email": "info@k8slens.dev"
},
"license": "MIT",
"homepage": "https://github.com/lensapp/lens",
"scripts": {
"buildasd": "webpack",
"devasdasd": "webpack --mode=development --watch",
"test:unit": "jest --coverage --runInBand",
"lint": "lens-lint",
"lint:fix": "lens-lint --fix"
},
"peerDependencies": {
"@k8slens/feature-core": "^6.5.0-alpha.0",
"@ogre-tools/injectable": "^15.1.2",
"@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2",
"@ogre-tools/fp": "^15.1.2",
"lodash": "^4.17.21"
},
"devDependencies": {
"@async-fn/jest": "^1.6.4",
"@k8slens/eslint-config": "6.5.0-alpha.1",
"@k8slens/react-testing-library-discovery": "^1.0.0-alpha.0"
}
}

View File

@ -0,0 +1,24 @@
/**
* 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} />;
};

View File

@ -0,0 +1,51 @@
// 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>
`;

View File

@ -0,0 +1,7 @@
/**
* 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";

View File

@ -0,0 +1,99 @@
/**
* 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);
}
}
}
}

View File

@ -0,0 +1,80 @@
/**
* 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 "@k8slens/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();
});
});

View File

@ -0,0 +1,243 @@
/**
* 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;
}
}

View File

@ -0,0 +1,77 @@
/**
* 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 "@k8slens/tooltip";
import { Tooltip } from "@k8slens/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;
}

View File

@ -0,0 +1,4 @@
{
"extends": "@k8slens/typescript/config/base.json",
"include": ["**/*.ts", "**/*.tsx"],
}

View File

@ -0,0 +1 @@
module.exports = require("@k8slens/webpack").configForReact;