mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Rewrite withTooltip to be more type correct
- Fixes mobx related "too many recursive actions" error - Change all the uses of withTooltips to be functional components Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
413fa8a815
commit
d42d2eb0c4
@ -5,15 +5,13 @@
|
||||
|
||||
import styles from "./badge.module.scss";
|
||||
|
||||
import React from "react";
|
||||
import { computed, makeObservable, observable } from "mobx";
|
||||
import React, { createRef, useState } from "react";
|
||||
import { action, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { cssNames } from "../../utils/cssNames";
|
||||
import type { TooltipDecoratorProps } from "../tooltip";
|
||||
import { withTooltip } from "../tooltip";
|
||||
import { autoBind } from "../../utils";
|
||||
|
||||
export interface BadgeProps extends React.HTMLAttributes<any>, TooltipDecoratorProps {
|
||||
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
small?: boolean;
|
||||
flat?: boolean;
|
||||
label?: React.ReactNode;
|
||||
@ -24,66 +22,56 @@ export interface BadgeProps extends React.HTMLAttributes<any>, TooltipDecoratorP
|
||||
|
||||
// Common handler for all Badge instances
|
||||
document.addEventListener("selectionchange", () => {
|
||||
Badge.badgeMeta.hasTextSelected ||= (window.getSelection()?.toString().trim().length ?? 0) > 0;
|
||||
badgeMeta.hasTextSelected ||= (window.getSelection()?.toString().trim().length ?? 0) > 0;
|
||||
});
|
||||
|
||||
@withTooltip
|
||||
@observer
|
||||
export class Badge extends React.Component<BadgeProps> {
|
||||
static defaultProps: Partial<BadgeProps> = {
|
||||
expandable: true,
|
||||
};
|
||||
const badgeMeta = observable({
|
||||
hasTextSelected: false,
|
||||
});
|
||||
|
||||
static badgeMeta = observable({
|
||||
hasTextSelected: false,
|
||||
export const Badge = withTooltip(observer(({
|
||||
small,
|
||||
flat,
|
||||
label,
|
||||
expandable = false,
|
||||
disabled,
|
||||
scrollable,
|
||||
className,
|
||||
children,
|
||||
...elemProps
|
||||
}: BadgeProps) => {
|
||||
const elem = createRef<HTMLDivElement>();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const isExpandable = expandable && elem.current
|
||||
? elem.current.clientWidth < elem.current.scrollWidth
|
||||
: false;
|
||||
|
||||
const onMouseUp = action(() => {
|
||||
if (!isExpandable || badgeMeta.hasTextSelected) {
|
||||
badgeMeta.hasTextSelected = false;
|
||||
} else {
|
||||
setIsExpanded(!isExpanded);
|
||||
}
|
||||
});
|
||||
|
||||
@observable.ref elem: HTMLDivElement | null = null;
|
||||
@observable isExpanded = false;
|
||||
|
||||
constructor(props: BadgeProps) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
@computed get isExpandable() {
|
||||
return this.props.expandable && this.elem
|
||||
? this.elem.clientWidth < this.elem.scrollWidth
|
||||
: false;
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
if (!this.isExpandable || Badge.badgeMeta.hasTextSelected) {
|
||||
Badge.badgeMeta.hasTextSelected = false;
|
||||
} else {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, label, disabled, scrollable, small, children, flat, expandable, ...elemProps } = this.props;
|
||||
const clickable = Boolean(this.props.onClick) || this.isExpandable;
|
||||
const classNames = cssNames(styles.badge, className, {
|
||||
[styles.small]: small,
|
||||
[styles.flat]: flat,
|
||||
[styles.clickable]: clickable,
|
||||
[styles.interactive]: this.isExpandable,
|
||||
[styles.isExpanded]: this.isExpanded,
|
||||
[styles.disabled]: disabled,
|
||||
[styles.scrollable]: scrollable,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
{...elemProps}
|
||||
className={classNames}
|
||||
onMouseUp={this.onMouseUp}
|
||||
ref={elem => this.elem = elem}
|
||||
>
|
||||
{label}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
{...elemProps}
|
||||
className={cssNames(styles.badge, className, {
|
||||
[styles.small]: small,
|
||||
[styles.flat]: flat,
|
||||
[styles.clickable]: Boolean(elemProps.onClick) || isExpandable,
|
||||
[styles.interactive]: isExpandable,
|
||||
[styles.isExpanded]: isExpanded,
|
||||
[styles.disabled]: disabled,
|
||||
[styles.scrollable]: scrollable,
|
||||
})}
|
||||
onMouseUp={onMouseUp}
|
||||
ref={elem}
|
||||
>
|
||||
{label}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}));
|
||||
|
||||
@ -7,10 +7,9 @@ import "./button.scss";
|
||||
import type { ButtonHTMLAttributes } from "react";
|
||||
import React from "react";
|
||||
import { cssNames } from "../../utils";
|
||||
import type { TooltipDecoratorProps } from "../tooltip";
|
||||
import { withTooltip } from "../tooltip";
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<any>, TooltipDecoratorProps {
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<any> {
|
||||
label?: React.ReactNode;
|
||||
waiting?: boolean;
|
||||
primary?: boolean;
|
||||
@ -26,36 +25,33 @@ export interface ButtonProps extends ButtonHTMLAttributes<any>, TooltipDecorator
|
||||
target?: "_blank"; // in case of using @href
|
||||
}
|
||||
|
||||
@withTooltip
|
||||
export class Button extends React.PureComponent<ButtonProps, {}> {
|
||||
render() {
|
||||
const {
|
||||
waiting, label, primary, accent, plain, hidden, active, big,
|
||||
round, outlined, tooltip, light, children, tooltipOverrideDisabled, ...btnProps
|
||||
} = this.props;
|
||||
export const Button = withTooltip((props: ButtonProps) => {
|
||||
const {
|
||||
waiting, label, primary, accent, plain, hidden, active, big,
|
||||
round, outlined, light, children, ...btnProps
|
||||
} = props;
|
||||
|
||||
if (hidden) return null;
|
||||
if (hidden) return null;
|
||||
|
||||
btnProps.className = cssNames("Button", btnProps.className, {
|
||||
waiting, primary, accent, plain, active, big, round, outlined, light,
|
||||
});
|
||||
btnProps.className = cssNames("Button", btnProps.className, {
|
||||
waiting, primary, accent, plain, active, big, round, outlined, light,
|
||||
});
|
||||
|
||||
// render as link
|
||||
if (this.props.href) {
|
||||
return (
|
||||
<a {...btnProps}>
|
||||
{label}
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
// render as button
|
||||
// render as link
|
||||
if (props.href) {
|
||||
return (
|
||||
<button type="button" {...btnProps}>
|
||||
<a {...btnProps}>
|
||||
{label}
|
||||
{children}
|
||||
</button>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// render as button
|
||||
return (
|
||||
<button type="button" {...btnProps}>
|
||||
{label}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
@ -18,7 +18,7 @@ import * as resourceApplierApi from "../../../../common/k8s-api/endpoints/resour
|
||||
import { Notifications } from "../../notifications";
|
||||
import logger from "../../../../common/logger";
|
||||
import type { ApiManager } from "../../../../common/k8s-api/api-manager";
|
||||
import { isString, prevDefault } from "../../../utils";
|
||||
import { isObject, prevDefault } from "../../../utils";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import createResourceTabStoreInjectable from "./store.injectable";
|
||||
import createResourceTemplatesInjectable from "./create-resource-templates.injectable";
|
||||
@ -77,7 +77,7 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
|
||||
}
|
||||
|
||||
// skip empty documents
|
||||
const resources = yaml.loadAll(this.data).filter(isString);
|
||||
const resources = yaml.loadAll(this.data).filter(isObject);
|
||||
|
||||
if (resources.length === 0) {
|
||||
return void logger.info("Nothing to create");
|
||||
|
||||
@ -5,11 +5,11 @@
|
||||
|
||||
import styles from "./editor-panel.module.scss";
|
||||
import throttle from "lodash/throttle";
|
||||
import React from "react";
|
||||
import { makeObservable, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import React, { createRef, useEffect } from "react";
|
||||
import { reaction } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import type { DockStore, TabId } from "./dock/store";
|
||||
import { cssNames } from "../../utils";
|
||||
import { cssNames, disposer } from "../../utils";
|
||||
import { MonacoEditor } from "../monaco-editor";
|
||||
import type { MonacoEditorProps, MonacoEditorRef } from "../monaco-editor";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
@ -28,51 +28,44 @@ interface Dependencies {
|
||||
dockStore: DockStore;
|
||||
}
|
||||
|
||||
const defaultProps: Partial<EditorPanelProps> = {
|
||||
autoFocus: true,
|
||||
};
|
||||
const NonInjectedEditorPanel = observer(({
|
||||
dockStore,
|
||||
onChange,
|
||||
tabId,
|
||||
value,
|
||||
autoFocus = true,
|
||||
className,
|
||||
onError,
|
||||
}: Dependencies & EditorPanelProps) => {
|
||||
const editor = createRef<MonacoEditorRef>();
|
||||
|
||||
@observer
|
||||
class NonInjectedEditorPanel extends React.Component<EditorPanelProps & Dependencies> {
|
||||
static defaultProps = defaultProps as object;
|
||||
|
||||
private readonly editor = React.createRef<MonacoEditorRef>();
|
||||
|
||||
constructor(props: EditorPanelProps & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
// keep focus on editor's area when <Dock/> just opened
|
||||
reaction(() => this.props.dockStore.isOpen, isOpen => isOpen && this.editor.current?.focus(), {
|
||||
useEffect(() => disposer(
|
||||
reaction(
|
||||
() => dockStore.isOpen,
|
||||
isOpen => isOpen && editor.current?.focus(),
|
||||
{
|
||||
fireImmediately: true,
|
||||
}),
|
||||
},
|
||||
),
|
||||
dockStore.onResize(throttle(() => editor.current?.focus(), 250)),
|
||||
));
|
||||
|
||||
// focus to editor on dock's resize or turning into fullscreen mode
|
||||
this.props.dockStore.onResize(throttle(() => this.editor.current?.focus(), 250)),
|
||||
]);
|
||||
if (!tabId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, autoFocus, tabId, value, onChange, onError } = this.props;
|
||||
|
||||
if (!tabId) return null;
|
||||
|
||||
return (
|
||||
<MonacoEditor
|
||||
autoFocus={autoFocus}
|
||||
id={tabId}
|
||||
value={value}
|
||||
className={cssNames(styles.EditorPanel, className)}
|
||||
onChange={onChange}
|
||||
onError={onError}
|
||||
ref={this.editor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<MonacoEditor
|
||||
autoFocus={autoFocus}
|
||||
id={tabId}
|
||||
value={value}
|
||||
className={cssNames(styles.EditorPanel, className)}
|
||||
onChange={onChange}
|
||||
onError={onError}
|
||||
ref={editor}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const EditorPanel = withInjectables<Dependencies, EditorPanelProps>(NonInjectedEditorPanel, {
|
||||
getProps: (di, props) => ({
|
||||
|
||||
@ -9,13 +9,12 @@ import type { ReactNode } from "react";
|
||||
import React, { createRef } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import type { LocationDescriptor } from "history";
|
||||
import { autoBind, cssNames } from "../../utils";
|
||||
import type { TooltipDecoratorProps } from "../tooltip";
|
||||
import { cssNames } from "../../utils";
|
||||
import { withTooltip } from "../tooltip";
|
||||
import isNumber from "lodash/isNumber";
|
||||
import { decode } from "../../../common/utils/base64";
|
||||
|
||||
export interface IconProps extends React.HTMLAttributes<any>, TooltipDecoratorProps {
|
||||
export interface IconProps extends React.HTMLAttributes<any> {
|
||||
material?: string; // material-icon, see available names at https://material.io/icons/
|
||||
svg?: string; // svg-filename without extension in current folder
|
||||
link?: LocationDescriptor; // render icon as NavLink from react-router-dom
|
||||
@ -31,67 +30,38 @@ export interface IconProps extends React.HTMLAttributes<any>, TooltipDecoratorPr
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@withTooltip
|
||||
export class Icon extends React.PureComponent<IconProps> {
|
||||
private readonly ref = createRef<HTMLAnchorElement>();
|
||||
export const Icon = Object.assign(
|
||||
withTooltip((props: IconProps) => {
|
||||
const ref = createRef<HTMLAnchorElement>();
|
||||
|
||||
static defaultProps: IconProps = {
|
||||
focusable: true,
|
||||
};
|
||||
|
||||
static isSvg(content: string) {
|
||||
return String(content).includes("svg+xml"); // data-url for raw svg-icon
|
||||
}
|
||||
|
||||
constructor(props: IconProps) {
|
||||
super(props);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
get isInteractive() {
|
||||
const { interactive, onClick, href, link } = this.props;
|
||||
|
||||
return interactive ?? !!(onClick || href || link);
|
||||
}
|
||||
|
||||
onClick(evt: React.MouseEvent) {
|
||||
if (this.props.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick(evt);
|
||||
}
|
||||
}
|
||||
|
||||
onKeyDown(evt: React.KeyboardEvent<any>) {
|
||||
switch (evt.nativeEvent.code) {
|
||||
case "Space":
|
||||
|
||||
// fallthrough
|
||||
case "Enter": {
|
||||
this.ref.current?.click();
|
||||
evt.preventDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.onKeyDown) {
|
||||
this.props.onKeyDown(evt);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isInteractive } = this;
|
||||
const {
|
||||
// skip passing props to icon's html element
|
||||
// skip passing props to icon's html element
|
||||
className, href, link, material, svg, size, smallest, small, big,
|
||||
disabled, sticker, active, focusable, children,
|
||||
interactive: _interactive,
|
||||
onClick: _onClick,
|
||||
onKeyDown: _onKeyDown,
|
||||
interactive, onClick, onKeyDown,
|
||||
...elemProps
|
||||
} = this.props;
|
||||
} = props;
|
||||
const isInteractive = interactive ?? !!(onClick || href || link);
|
||||
|
||||
const boundOnClick = (event: React.MouseEvent) => {
|
||||
if (!disabled) {
|
||||
onClick?.(event);
|
||||
}
|
||||
};
|
||||
const boundOnKeyDown = (event: React.KeyboardEvent<any>) => {
|
||||
switch (event.nativeEvent.code) {
|
||||
case "Space":
|
||||
|
||||
// fallthrough
|
||||
case "Enter": {
|
||||
ref.current?.click();
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onKeyDown?.(event);
|
||||
};
|
||||
|
||||
let iconContent: ReactNode;
|
||||
const iconProps: Partial<IconProps> = {
|
||||
@ -99,8 +69,8 @@ export class Icon extends React.PureComponent<IconProps> {
|
||||
{ svg, material, interactive: isInteractive, disabled, sticker, active, focusable },
|
||||
!size ? { smallest, small, big } : {},
|
||||
),
|
||||
onClick: isInteractive ? this.onClick : undefined,
|
||||
onKeyDown: isInteractive ? this.onKeyDown : undefined,
|
||||
onClick: isInteractive ? boundOnClick : undefined,
|
||||
onKeyDown: isInteractive ? boundOnKeyDown : undefined,
|
||||
tabIndex: isInteractive && focusable && !disabled ? 0 : undefined,
|
||||
style: size ? { "--size": size + (isNumber(size) ? "px" : "") } as React.CSSProperties : undefined,
|
||||
...elemProps,
|
||||
@ -137,7 +107,7 @@ export class Icon extends React.PureComponent<IconProps> {
|
||||
<NavLink
|
||||
className={className}
|
||||
to={link}
|
||||
ref={this.ref}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</NavLink>
|
||||
@ -149,11 +119,15 @@ export class Icon extends React.PureComponent<IconProps> {
|
||||
<a
|
||||
{...iconProps}
|
||||
href={href}
|
||||
ref={this.ref}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <i {...iconProps} ref={this.ref} />;
|
||||
}
|
||||
}
|
||||
return <i {...iconProps} ref={ref} />;
|
||||
}),
|
||||
{
|
||||
// data-url for raw svg-icon
|
||||
isSvg: (content: string) => String(content).includes("svg+xml"),
|
||||
},
|
||||
);
|
||||
|
||||
@ -6,10 +6,9 @@
|
||||
import "./line-progress.scss";
|
||||
import React from "react";
|
||||
import { cssNames } from "../../utils";
|
||||
import type { TooltipDecoratorProps } from "../tooltip";
|
||||
import { withTooltip } from "../tooltip";
|
||||
|
||||
export interface LineProgressProps extends React.HTMLProps<any>, TooltipDecoratorProps {
|
||||
export interface LineProgressProps extends React.HTMLProps<HTMLDivElement> {
|
||||
value: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
@ -17,6 +16,10 @@ export interface LineProgressProps extends React.HTMLProps<any>, TooltipDecorato
|
||||
precise?: number;
|
||||
}
|
||||
|
||||
function valuePercent({ value, min, max, precise }: Required<Pick<LineProgressProps, "value" | "min" | "max" | "precise">>) {
|
||||
return Math.min(100, value / (max - min) * 100).toFixed(precise);
|
||||
}
|
||||
|
||||
export const LineProgress = withTooltip(({
|
||||
className,
|
||||
min = 0,
|
||||
@ -25,13 +28,14 @@ export const LineProgress = withTooltip(({
|
||||
precise = 2,
|
||||
children,
|
||||
...props
|
||||
}: LineProgressProps) => {
|
||||
const valuePercents = Math.min(100, value / (max - min) * 100).toFixed(precise);
|
||||
|
||||
return (
|
||||
<div className={cssNames("LineProgress", className)} {...props}>
|
||||
<div className="line" style={{ width: `${valuePercents}%` }}></div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}: LineProgressProps) => (
|
||||
<div className={cssNames("LineProgress", className)} {...props}>
|
||||
<div
|
||||
className="line"
|
||||
style={{
|
||||
width: `${valuePercent({ min, max, value, precise })}%`,
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
));
|
||||
|
||||
@ -16,12 +16,13 @@ import type { MenuProps } from "./menu";
|
||||
import { Menu, MenuItem } from "./menu";
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
import isString from "lodash/isString";
|
||||
import type { TooltipDecoratorProps } from "../tooltip";
|
||||
|
||||
export interface MenuActionsProps extends Partial<MenuProps> {
|
||||
className?: string;
|
||||
toolbar?: boolean; // display menu as toolbar with icons
|
||||
autoCloseOnSelect?: boolean;
|
||||
triggerIcon?: string | IconProps | React.ReactNode;
|
||||
triggerIcon?: string | (IconProps & TooltipDecoratorProps) | React.ReactNode;
|
||||
removeConfirmationMessage?: React.ReactNode | (() => React.ReactNode);
|
||||
updateAction?: () => void | Promise<void>;
|
||||
removeAction?: () => void | Promise<void>;
|
||||
@ -75,7 +76,7 @@ export class MenuActions extends React.Component<MenuActionsProps> {
|
||||
|
||||
return React.cloneElement(triggerIcon, { id: this.id, className });
|
||||
}
|
||||
const iconProps: Partial<IconProps> = {
|
||||
const iconProps: IconProps & TooltipDecoratorProps = {
|
||||
id: this.id,
|
||||
interactive: true,
|
||||
material: isString(triggerIcon) ? triggerIcon : undefined,
|
||||
|
||||
@ -7,22 +7,13 @@ import "./status-brick.scss";
|
||||
|
||||
import React from "react";
|
||||
import { cssNames } from "../../utils";
|
||||
import type { TooltipDecoratorProps } from "../tooltip";
|
||||
import { withTooltip } from "../tooltip";
|
||||
|
||||
export interface StatusBrickProps extends React.HTMLAttributes<any>, TooltipDecoratorProps {
|
||||
export interface StatusBrickProps extends React.HTMLAttributes<any> {
|
||||
}
|
||||
|
||||
@withTooltip
|
||||
export class StatusBrick extends React.Component<StatusBrickProps> {
|
||||
render() {
|
||||
const { className, ...elemProps } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cssNames("StatusBrick", className)}
|
||||
{...elemProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
export const StatusBrick = withTooltip(({ className, ...elemProps }: StatusBrickProps) => (
|
||||
<div
|
||||
className={cssNames("StatusBrick", className)}
|
||||
{...elemProps} />
|
||||
));
|
||||
|
||||
@ -5,13 +5,14 @@
|
||||
|
||||
// Tooltip decorator for simple composition with other components
|
||||
|
||||
import type { HTMLAttributes, ReactNode } from "react";
|
||||
import React from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import React, { useState } from "react";
|
||||
import hoistNonReactStatics from "hoist-non-react-statics";
|
||||
import type { TooltipProps } from "./tooltip";
|
||||
import { Tooltip } from "./tooltip";
|
||||
import { isReactNode } from "../../utils/isReactNode";
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
import type { SingleOrMany } from "../../utils";
|
||||
|
||||
export interface TooltipDecoratorProps {
|
||||
tooltip?: ReactNode | Omit<TooltipProps, "targetId">;
|
||||
@ -20,41 +21,54 @@ export interface TooltipDecoratorProps {
|
||||
* useful for displaying tooltips even when the target is "disabled"
|
||||
*/
|
||||
tooltipOverrideDisabled?: boolean;
|
||||
id?: string | undefined;
|
||||
children?: SingleOrMany<React.ReactNode>;
|
||||
}
|
||||
|
||||
export function withTooltip<T extends React.ComponentType<any>>(Target: T): T {
|
||||
const DecoratedComponent = class extends React.Component<HTMLAttributes<any> & TooltipDecoratorProps> {
|
||||
static displayName = `withTooltip(${Target.displayName || Target.name})`;
|
||||
|
||||
export function withTooltip<TargetProps extends Pick<TooltipDecoratorProps, "id" | "children">>(Target: React.FunctionComponent<TargetProps>): React.FunctionComponent<TargetProps & TooltipDecoratorProps> {
|
||||
const DecoratedComponent = (props: TargetProps & TooltipDecoratorProps) => {
|
||||
// TODO: Remove side-effect to allow deterministic unit testing
|
||||
protected tooltipId = uniqueId("tooltip_target_");
|
||||
const [defaultTooltipId] = useState(uniqueId("tooltip_target_"));
|
||||
|
||||
render() {
|
||||
const { tooltip, tooltipOverrideDisabled, ...targetProps } = this.props;
|
||||
let {
|
||||
id: targetId,
|
||||
children: targetChildren,
|
||||
} = props;
|
||||
const {
|
||||
tooltip,
|
||||
tooltipOverrideDisabled,
|
||||
id: _unusedId,
|
||||
children: _unusedTargetChildren,
|
||||
...targetProps
|
||||
} = props;
|
||||
|
||||
if (tooltip) {
|
||||
const tooltipId = targetProps.id || this.tooltipId;
|
||||
const tooltipProps: TooltipProps = {
|
||||
targetId: tooltipId,
|
||||
tooltipOnParentHover: tooltipOverrideDisabled,
|
||||
formatters: { narrow: true },
|
||||
...(isReactNode(tooltip) ? { children: tooltip } : tooltip),
|
||||
};
|
||||
if (tooltip) {
|
||||
const tooltipProps: TooltipProps = {
|
||||
targetId: targetId || defaultTooltipId,
|
||||
tooltipOnParentHover: tooltipOverrideDisabled,
|
||||
formatters: { narrow: true },
|
||||
...(isReactNode(tooltip) ? { children: tooltip } : tooltip),
|
||||
};
|
||||
|
||||
targetProps.id = tooltipId;
|
||||
targetProps.children = (
|
||||
<>
|
||||
<div>
|
||||
{targetProps.children}
|
||||
</div>
|
||||
<Tooltip {...tooltipProps} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <Target {...targetProps as any} />;
|
||||
targetId = tooltipProps.targetId;
|
||||
targetChildren = (
|
||||
<>
|
||||
<div>
|
||||
{targetChildren}
|
||||
</div>
|
||||
<Tooltip {...tooltipProps} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Target id={targetId} {...targetProps as TargetProps}>
|
||||
{targetChildren}
|
||||
</Target>
|
||||
);
|
||||
};
|
||||
|
||||
return hoistNonReactStatics(DecoratedComponent, Target) as any;
|
||||
DecoratedComponent.displayName = `withTooltip(${Target.displayName || Target.name})`;
|
||||
|
||||
return hoistNonReactStatics(DecoratedComponent, Target);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user