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