mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
tooltip refactoring -- part 1
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
4087cf025e
commit
f2803abcb6
@ -2,18 +2,21 @@
|
|||||||
--size: 37px;
|
--size: 37px;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
opacity: .55;
|
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
padding: $radius;
|
padding: $radius;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.active, &.interactive:hover {
|
&.active, &.interactive:hover {
|
||||||
opacity: 1;
|
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
|
||||||
|
img {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> img {
|
img {
|
||||||
|
opacity: .55;
|
||||||
width: var(--size);
|
width: var(--size);
|
||||||
height: var(--size);
|
height: var(--size);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { Hashicon } from "@emeraldpay/hashicon-react";
|
|||||||
import { Cluster } from "../../../main/cluster";
|
import { Cluster } from "../../../main/cluster";
|
||||||
import { cssNames, IClassName } from "../../utils";
|
import { cssNames, IClassName } from "../../utils";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { Tooltip, TooltipContent } from "../tooltip";
|
import { Tooltip } from "../tooltip";
|
||||||
|
|
||||||
interface Props extends DOMAttributes<HTMLElement> {
|
interface Props extends DOMAttributes<HTMLElement> {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
@ -44,9 +44,7 @@ export class ClusterIcon extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<div {...elemProps} className={className} id={clusterIconId}>
|
<div {...elemProps} className={className} id={clusterIconId}>
|
||||||
{showTooltip && (
|
{showTooltip && (
|
||||||
<Tooltip htmlFor={clusterIconId} position={{ right: true }}>
|
<Tooltip targetId={clusterIconId}>{clusterName}</Tooltip>
|
||||||
<TooltipContent nowrap>{clusterName}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
{icon && <img src={icon} alt={clusterName}/>}
|
{icon && <img src={icon} alt={clusterName}/>}
|
||||||
{!icon && <Hashicon value={clusterName} options={options}/>}
|
{!icon && <Hashicon value={clusterName} options={options}/>}
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import "./events.scss";
|
import "./events.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React, { Fragment } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { MainLayout } from "../layout/main-layout";
|
import { MainLayout } from "../layout/main-layout";
|
||||||
import { eventStore } from "./event.store";
|
import { eventStore } from "./event.store";
|
||||||
import { KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
|
import { KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { KubeEvent } from "../../api/endpoints/events.api";
|
import { KubeEvent } from "../../api/endpoints/events.api";
|
||||||
import { Tooltip, TooltipContent } from "../tooltip";
|
import { Tooltip } from "../tooltip";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { cssNames, IClassName, stopPropagation } from "../../utils";
|
import { cssNames, IClassName, stopPropagation } from "../../utils";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
@ -90,18 +90,14 @@ export class Events extends React.Component<Props> {
|
|||||||
const detailsUrl = getDetailsUrl(lookupApiLink(involvedObject, event));
|
const detailsUrl = getDetailsUrl(lookupApiLink(involvedObject, event));
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
className: {
|
className: { warning: isWarning },
|
||||||
warning: isWarning
|
|
||||||
},
|
|
||||||
title: (
|
title: (
|
||||||
<>
|
<Fragment>
|
||||||
<span id={tooltipId}>{message}</span>
|
<span id={tooltipId}>{message}</span>
|
||||||
<Tooltip htmlFor={tooltipId} following>
|
<Tooltip targetId={tooltipId} formatters={{ narrow: true, warning: isWarning }}>
|
||||||
<TooltipContent narrow warning={isWarning}>
|
{message}
|
||||||
{message}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</Fragment>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
event.getNs(),
|
event.getNs(),
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import "./kube-event-icon.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { TooltipContent } from "../tooltip";
|
|
||||||
import { KubeObject } from "../../api/kube-object";
|
import { KubeObject } from "../../api/kube-object";
|
||||||
import { eventStore } from "./event.store";
|
import { eventStore } from "./event.store";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
@ -34,15 +33,17 @@ export class KubeEventIcon extends React.Component<Props> {
|
|||||||
<Icon
|
<Icon
|
||||||
material="warning"
|
material="warning"
|
||||||
className={cssNames("KubeEventIcon", { warning: event.isWarning() })}
|
className={cssNames("KubeEventIcon", { warning: event.isWarning() })}
|
||||||
tooltip={(
|
tooltip={{
|
||||||
<TooltipContent className="KubeEventTooltip">
|
children: (
|
||||||
{event.message}
|
<div className="KubeEventTooltip">
|
||||||
<div className="age">
|
<div className="msg">{event.message}</div>
|
||||||
<Icon material="access_time"/>
|
<div className="age">
|
||||||
{event.getAge(undefined, undefined, true)}
|
<Icon material="access_time"/>
|
||||||
|
{event.getAge(undefined, undefined, true)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TooltipContent>
|
)
|
||||||
)}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { disposeOnUnmount, observer } from "mobx-react";
|
|||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { DrawerItem, DrawerItemLabels } from "../drawer";
|
import { DrawerItem, DrawerItemLabels } from "../drawer";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { TooltipContent } from "../tooltip";
|
|
||||||
import { nodesStore } from "./nodes.store";
|
import { nodesStore } from "./nodes.store";
|
||||||
import { ResourceMetrics } from "../resource-metrics";
|
import { ResourceMetrics } from "../resource-metrics";
|
||||||
import { podsStore } from "../+workloads-pods/pods.store";
|
import { podsStore } from "../+workloads-pods/pods.store";
|
||||||
@ -127,16 +126,17 @@ export class NodeDetails extends React.Component<Props> {
|
|||||||
key={type}
|
key={type}
|
||||||
label={type}
|
label={type}
|
||||||
className={kebabCase(type)}
|
className={kebabCase(type)}
|
||||||
tooltip={
|
tooltip={{
|
||||||
<TooltipContent tableView>
|
formatters: {
|
||||||
{Object.entries(condition).map(([key, value]) =>
|
tableView: true,
|
||||||
<div key={key} className="flex gaps align-center">
|
},
|
||||||
<div className="name">{upperFirst(key)}</div>
|
children: Object.entries(condition).map(([key, value]) =>
|
||||||
<div className="value">{value}</div>
|
<div key={key} className="flex gaps align-center">
|
||||||
</div>
|
<div className="name">{upperFirst(key)}</div>
|
||||||
)}
|
<div className="value">{value}</div>
|
||||||
</TooltipContent>
|
</div>
|
||||||
}
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { NodeMenu } from "./node-menu";
|
|||||||
import { LineProgress } from "../line-progress";
|
import { LineProgress } from "../line-progress";
|
||||||
import { _i18n } from "../../i18n";
|
import { _i18n } from "../../i18n";
|
||||||
import { bytesToUnits } from "../../utils/convertMemory";
|
import { bytesToUnits } from "../../utils/convertMemory";
|
||||||
import { Tooltip, TooltipContent } from "../tooltip";
|
import { Tooltip } from "../tooltip";
|
||||||
import kebabCase from "lodash/kebabCase";
|
import kebabCase from "lodash/kebabCase";
|
||||||
import upperFirst from "lodash/upperFirst";
|
import upperFirst from "lodash/upperFirst";
|
||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
@ -101,15 +101,13 @@ export class Nodes extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<div key={type} id={tooltipId} className={cssNames("condition", kebabCase(type))}>
|
<div key={type} id={tooltipId} className={cssNames("condition", kebabCase(type))}>
|
||||||
{type}
|
{type}
|
||||||
<Tooltip htmlFor={tooltipId} following>
|
<Tooltip targetId={tooltipId} formatters={{ tableView: true }}>
|
||||||
<TooltipContent tableView>
|
{Object.entries(condition).map(([key, value]) =>
|
||||||
{Object.entries(condition).map(([key, value]) =>
|
<div key={key} className="flex gaps align-center">
|
||||||
<div key={key} className="flex gaps align-center">
|
<div className="name">{upperFirst(key)}</div>
|
||||||
<div className="name">{upperFirst(key)}</div>
|
<div className="value">{value}</div>
|
||||||
<div className="value">{value}</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>)
|
</div>)
|
||||||
})
|
})
|
||||||
@ -162,7 +160,7 @@ export class Nodes extends React.Component<Props> {
|
|||||||
this.renderDiskUsage(node),
|
this.renderDiskUsage(node),
|
||||||
<>
|
<>
|
||||||
<span id={tooltipId}>{node.getTaints().length}</span>
|
<span id={tooltipId}>{node.getTaints().length}</span>
|
||||||
<Tooltip htmlFor={tooltipId} style={{ whiteSpace: "pre-line" }}>
|
<Tooltip targetId={tooltipId} style={{ whiteSpace: "pre-line" }}>
|
||||||
{node.getTaints().map(({ key, effect }) => `${key}: ${effect}`).join("\n")}
|
{node.getTaints().map(({ key, effect }) => `${key}: ${effect}`).join("\n")}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>,
|
</>,
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import { Pod, podsApi } from "../../api/endpoints";
|
|||||||
import { PodMenu } from "./pod-menu";
|
import { PodMenu } from "./pod-menu";
|
||||||
import { StatusBrick } from "../status-brick";
|
import { StatusBrick } from "../status-brick";
|
||||||
import { cssNames, stopPropagation } from "../../utils";
|
import { cssNames, stopPropagation } from "../../utils";
|
||||||
import { TooltipContent } from "../tooltip";
|
|
||||||
import { KubeEventIcon } from "../+events/kube-event-icon";
|
import { KubeEventIcon } from "../+events/kube-event-icon";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
import { getDetailsUrl } from "../../navigation";
|
||||||
import toPairs from "lodash/toPairs";
|
import toPairs from "lodash/toPairs";
|
||||||
@ -42,26 +41,29 @@ export class Pods extends React.Component<Props> {
|
|||||||
renderContainersStatus(pod: Pod) {
|
renderContainersStatus(pod: Pod) {
|
||||||
return pod.getContainerStatuses().map(containerStatus => {
|
return pod.getContainerStatuses().map(containerStatus => {
|
||||||
const { name, state, ready } = containerStatus;
|
const { name, state, ready } = containerStatus;
|
||||||
const tooltip = (
|
|
||||||
<TooltipContent tableView>
|
|
||||||
{Object.keys(state).map(status => (
|
|
||||||
<Fragment key={status}>
|
|
||||||
<div className="title">
|
|
||||||
{name} <span className="text-secondary">({status}{ready ? ", ready" : ""})</span>
|
|
||||||
</div>
|
|
||||||
{toPairs(state[status]).map(([name, value]) => (
|
|
||||||
<div key={name} className="flex gaps align-center">
|
|
||||||
<div className="name">{startCase(name)}</div>
|
|
||||||
<div className="value">{value}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</TooltipContent>
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={name}>
|
<Fragment key={name}>
|
||||||
<StatusBrick className={cssNames(state, { ready })} tooltip={tooltip}/>
|
<StatusBrick
|
||||||
|
className={cssNames(state, { ready })}
|
||||||
|
tooltip={{
|
||||||
|
formatters: {
|
||||||
|
tableView: true
|
||||||
|
},
|
||||||
|
children: Object.keys(state).map(status => (
|
||||||
|
<Fragment key={status}>
|
||||||
|
<div className="title">
|
||||||
|
{name} <span className="text-secondary">({status}{ready ? ", ready" : ""})</span>
|
||||||
|
</div>
|
||||||
|
{toPairs(state[status]).map(([name, value]) => (
|
||||||
|
<div key={name} className="flex gaps align-center">
|
||||||
|
<div className="name">{startCase(name)}</div>
|
||||||
|
<div className="value">{value}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Fragment>
|
||||||
|
))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|||||||
@ -9,7 +9,6 @@ export type AnimateName = "opacity" | "slide-right" | "opacity-scale" | string;
|
|||||||
export interface AnimateProps {
|
export interface AnimateProps {
|
||||||
name?: AnimateName; // predefined names in css
|
name?: AnimateName; // predefined names in css
|
||||||
enter?: boolean;
|
enter?: boolean;
|
||||||
enabled?: boolean;
|
|
||||||
onEnter?: () => void;
|
onEnter?: () => void;
|
||||||
onLeave?: () => void;
|
onLeave?: () => void;
|
||||||
}
|
}
|
||||||
@ -21,7 +20,6 @@ export class Animate extends React.Component<AnimateProps> {
|
|||||||
static defaultProps: AnimateProps = {
|
static defaultProps: AnimateProps = {
|
||||||
name: "opacity",
|
name: "opacity",
|
||||||
enter: true,
|
enter: true,
|
||||||
enabled: true,
|
|
||||||
onEnter: noop,
|
onEnter: noop,
|
||||||
onLeave: noop,
|
onLeave: noop,
|
||||||
};
|
};
|
||||||
@ -80,10 +78,7 @@ export class Animate extends React.Component<AnimateProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { name, enabled, children } = this.props;
|
const { name } = this.props;
|
||||||
if (!enabled) {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
const contentElem = this.contentElem;
|
const contentElem = this.contentElem;
|
||||||
return React.cloneElement(contentElem, {
|
return React.cloneElement(contentElem, {
|
||||||
className: cssNames("Animate", name, contentElem.props.className, this.statusClassName),
|
className: cssNames("Animate", name, contentElem.props.className, this.statusClassName),
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import { navigate, navigation } from "../../navigation";
|
|||||||
import { addClusterURL } from "../+add-cluster";
|
import { addClusterURL } from "../+add-cluster";
|
||||||
import { clusterSettingsURL } from "../+cluster-settings";
|
import { clusterSettingsURL } from "../+cluster-settings";
|
||||||
import { landingURL } from "../+landing-page";
|
import { landingURL } from "../+landing-page";
|
||||||
import { Tooltip, TooltipContent } from "../tooltip";
|
import { Tooltip } from "../tooltip";
|
||||||
import { ConfirmDialog } from "../confirm-dialog";
|
import { ConfirmDialog } from "../confirm-dialog";
|
||||||
import { clusterIpc } from "../../../common/cluster-ipc";
|
import { clusterIpc } from "../../../common/cluster-ipc";
|
||||||
import { clusterStatusURL } from "./cluster-status.route";
|
import { clusterStatusURL } from "./cluster-status.route";
|
||||||
@ -118,10 +118,8 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="add-cluster" onClick={this.addCluster}>
|
<div className="add-cluster" onClick={this.addCluster}>
|
||||||
<Tooltip htmlFor="add-cluster-icon" position={{ right: true }}>
|
<Tooltip targetId="add-cluster-icon">
|
||||||
<TooltipContent nowrap>
|
<Trans>Add Cluster</Trans>
|
||||||
<Trans>Add Cluster</Trans>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Icon big material="add" id="add-cluster-icon"/>
|
<Icon big material="add" id="add-cluster-icon"/>
|
||||||
{newContexts.size > 0 && (
|
{newContexts.size > 0 && (
|
||||||
|
|||||||
@ -28,7 +28,6 @@ import { CrdList, crdResourcesRoute, crdRoute, crdURL } from "../+custom-resourc
|
|||||||
import { CustomResources } from "../+custom-resources/custom-resources";
|
import { CustomResources } from "../+custom-resources/custom-resources";
|
||||||
import { navigation } from "../../navigation";
|
import { navigation } from "../../navigation";
|
||||||
import { isAllowedResource } from "../../api/rbac"
|
import { isAllowedResource } from "../../api/rbac"
|
||||||
import { TooltipContent } from "../tooltip";
|
|
||||||
|
|
||||||
const SidebarContext = React.createContext<SidebarContextValue>({ pinned: false });
|
const SidebarContext = React.createContext<SidebarContextValue>({ pinned: false });
|
||||||
type SidebarContextValue = {
|
type SidebarContextValue = {
|
||||||
@ -83,17 +82,9 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
<Icon
|
<Icon
|
||||||
className="pin-icon"
|
className="pin-icon"
|
||||||
|
tooltip={<Trans>Compact view</Trans>}
|
||||||
material={isPinned ? "keyboard_arrow_left" : "keyboard_arrow_right"}
|
material={isPinned ? "keyboard_arrow_left" : "keyboard_arrow_right"}
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
tooltip={{
|
|
||||||
following: false,
|
|
||||||
position: { right: true },
|
|
||||||
children: (
|
|
||||||
<TooltipContent nowrap>
|
|
||||||
<Trans>Compact view</Trans>
|
|
||||||
</TooltipContent>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
focusable={false}
|
focusable={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,101 +1,76 @@
|
|||||||
|
|
||||||
.Tooltip {
|
.Tooltip {
|
||||||
--tooltip-bgc: #{$contentColor};
|
// use positioning relative to viewport (window)
|
||||||
--tooltip-color: #{$textColorSecondary};
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/position
|
||||||
--tooltip-margin: #{$padding / 2 $padding / 3};
|
position: fixed;
|
||||||
|
margin: 0 !important;
|
||||||
position: absolute;
|
background: $contentColor;
|
||||||
background: var(--tooltip-bgc);
|
|
||||||
font-size: small;
|
font-size: small;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
border: 1px solid $borderColor;
|
border: 1px solid $borderColor;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
color: var(--tooltip-color);
|
color: $textColorSecondary;
|
||||||
margin: var(--tooltip-margin);
|
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition-delay: 100ms; // delay for .Animate
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
transition: opacity 150ms 25ms ease-in-out;
|
||||||
|
|
||||||
* {
|
&.hidden {
|
||||||
white-space: normal;
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.following {
|
&.formatter {
|
||||||
position: fixed;
|
&.nowrap {
|
||||||
}
|
&, * {
|
||||||
|
white-space: nowrap;
|
||||||
&:not(.following) {
|
}
|
||||||
margin: #{$margin};
|
|
||||||
|
|
||||||
&.left {
|
|
||||||
right: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.right {
|
&.narrow {
|
||||||
left: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.top {
|
|
||||||
bottom: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.bottom {
|
|
||||||
top: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.TooltipContent {
|
|
||||||
&.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: $colorError;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.tableView {
|
|
||||||
min-width: 200px;
|
|
||||||
|
|
||||||
> :not(:last-child) {
|
|
||||||
margin-bottom: var(--flex-gap);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
color: $textColorAccent;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
color: $textColorAccent;
|
|
||||||
text-align: right;
|
|
||||||
flex: 0 0 35%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
text-align: left;
|
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
word-break: break-word;
|
text-overflow: ellipsis;
|
||||||
|
word-wrap: break-word;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
font-size: $font-size-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
color: $colorError;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tableView {
|
||||||
|
min-width: 200px;
|
||||||
|
|
||||||
|
> :not(:last-child) {
|
||||||
|
margin-bottom: var(--flex-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: $textColorAccent;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
color: $textColorAccent;
|
||||||
|
text-align: right;
|
||||||
|
flex: 0 0 35%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
text-align: left;
|
||||||
|
max-width: 300px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,109 +2,141 @@ import './tooltip.scss'
|
|||||||
|
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { autobind, cssNames, IClassName } from "../../utils";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { createPortal } from "react-dom"
|
|
||||||
import { autobind, cssNames } from "../../utils";
|
|
||||||
import { Animate } from "../animate";
|
|
||||||
|
|
||||||
// fixme: refactor -- better positioning + remove "flying effect"
|
export type TooltipPosition = "top" | "left" | "right" | "bottom";
|
||||||
// todo: always render outside of parent element ("overflow: auto" should not affect tooltip)
|
|
||||||
|
|
||||||
export interface TooltipProps {
|
export interface TooltipProps {
|
||||||
htmlFor: string;
|
targetId: string; // "id" of target html-element to bind
|
||||||
className?: string;
|
visible?: boolean;
|
||||||
position?: Position;
|
offset?: number; // px
|
||||||
useAnimation?: boolean;
|
position?: TooltipPosition;
|
||||||
following?: boolean; // tooltip is following mouse position
|
className?: IClassName;
|
||||||
|
formatters?: TooltipContentFormatters;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Position {
|
export interface TooltipContentFormatters {
|
||||||
left?: boolean;
|
narrow?: boolean; // max-width
|
||||||
right?: boolean;
|
warning?: boolean; // color
|
||||||
top?: boolean;
|
small?: boolean; // font-size
|
||||||
bottom?: boolean;
|
nowrap?: boolean; // white-space
|
||||||
|
tableView?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultProps: Partial<TooltipProps> = {
|
const defaultProps: Partial<TooltipProps> = {
|
||||||
useAnimation: true,
|
offset: 10,
|
||||||
position: {
|
}
|
||||||
bottom: true,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class Tooltip extends React.Component<TooltipProps> {
|
export class Tooltip extends React.Component<TooltipProps> {
|
||||||
static defaultProps = defaultProps as object;
|
static defaultProps = defaultProps as object;
|
||||||
|
|
||||||
public anchor: HTMLElement;
|
@observable.ref elem: HTMLElement;
|
||||||
public elem: HTMLElement;
|
@observable activePosition: TooltipPosition;
|
||||||
|
@observable isVisible = !!this.props.visible;
|
||||||
|
|
||||||
@observable isVisible = false;
|
get targetElem(): HTMLElement {
|
||||||
|
return document.getElementById(this.props.targetId)
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { htmlFor } = this.props;
|
this.targetElem.addEventListener("mouseenter", this.onEnterTarget)
|
||||||
this.anchor = htmlFor ? document.getElementById(htmlFor) : this.elem.parentElement;
|
this.targetElem.addEventListener("mouseleave", this.onLeaveTarget)
|
||||||
if (this.anchor) {
|
|
||||||
if (window.getComputedStyle(this.anchor).position === "static") {
|
|
||||||
this.anchor.style.position = "relative"
|
|
||||||
}
|
|
||||||
this.anchor.addEventListener("mouseenter", this.onMouseEnter);
|
|
||||||
this.anchor.addEventListener("mouseleave", this.onMouseLeave);
|
|
||||||
this.anchor.addEventListener("mousemove", this.onMouseMove);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
if (this.anchor) {
|
this.targetElem.removeEventListener("mouseenter", this.onEnterTarget)
|
||||||
this.anchor.removeEventListener("mouseenter", this.onMouseEnter);
|
this.targetElem.removeEventListener("mouseleave", this.onLeaveTarget)
|
||||||
this.anchor.removeEventListener("mouseleave", this.onMouseLeave);
|
|
||||||
this.anchor.removeEventListener("mousemove", this.onMouseMove);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
onMouseEnter(evt: MouseEvent) {
|
protected onEnterTarget(evt: MouseEvent) {
|
||||||
this.isVisible = true;
|
this.isVisible = true;
|
||||||
this.onMouseMove(evt);
|
this.refreshPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
onMouseLeave(evt: MouseEvent) {
|
protected onLeaveTarget(evt: MouseEvent) {
|
||||||
this.isVisible = false;
|
this.isVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
onMouseMove(evt: MouseEvent) {
|
refreshPosition() {
|
||||||
if (!this.props.following) {
|
const { position } = this.props;
|
||||||
return;
|
const { elem, targetElem } = this;
|
||||||
|
|
||||||
|
let allPositions: TooltipPosition[] = ["right", "bottom", "top", "left"];
|
||||||
|
if (allPositions.includes(position)) {
|
||||||
|
allPositions = [
|
||||||
|
position, // put first as priority side for positioning
|
||||||
|
...allPositions.filter(pos => pos !== position),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const offsetX = -10;
|
// reset position first and get all possible client-rect area for tooltip element
|
||||||
const offsetY = 10;
|
this.setPosition({ left: 0, top: 0 });
|
||||||
const { clientX, clientY } = evt;
|
|
||||||
const { innerWidth, innerHeight } = window;
|
|
||||||
|
|
||||||
const initialPos: Partial<CSSStyleDeclaration> = {
|
const selfBounds = elem.getBoundingClientRect();
|
||||||
top: "auto",
|
const targetBounds = targetElem.getBoundingClientRect();
|
||||||
left: "auto",
|
const { innerWidth: viewportWidth, innerHeight: viewportHeight } = window;
|
||||||
right: (innerWidth - clientX + offsetX) + "px",
|
|
||||||
bottom: (innerHeight - clientY + offsetY) + "px",
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(this.elem.style, initialPos);
|
// find proper position
|
||||||
|
this.activePosition = null;
|
||||||
|
for (const pos of allPositions) {
|
||||||
|
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({ top, left });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.activePosition) {
|
||||||
|
const { left, top } = this.getPosition(allPositions[0], selfBounds, targetBounds)
|
||||||
|
this.activePosition = allPositions[0];
|
||||||
|
this.setPosition({ left, top });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// correct position if not fits to viewport
|
protected setPosition(pos: { left: number, top: number }) {
|
||||||
const { left, top } = this.elem.getBoundingClientRect();
|
const elemStyle = this.elem.style;
|
||||||
if (left < 0) {
|
elemStyle.left = pos.left + "px"
|
||||||
this.elem.style.left = clientX + offsetX + "px";
|
elemStyle.top = pos.top + "px"
|
||||||
this.elem.style.right = "auto"
|
}
|
||||||
}
|
|
||||||
if (top < 0) {
|
protected getPosition(position: TooltipPosition, selfBounds: DOMRect, targetBounds: DOMRect) {
|
||||||
this.elem.style.top = clientY + offsetY + "px";
|
let left: number
|
||||||
this.elem.style.bottom = "auto"
|
let top: number
|
||||||
|
const offset = this.props.offset;
|
||||||
|
const horizontalCenter = targetBounds.left + (targetBounds.width - selfBounds.width) / 2;
|
||||||
|
const verticalCenter = targetBounds.top + (targetBounds.height - selfBounds.height) / 2;
|
||||||
|
switch (position) {
|
||||||
|
case "top":
|
||||||
|
left = horizontalCenter;
|
||||||
|
top = targetBounds.top - selfBounds.height - offset;
|
||||||
|
break;
|
||||||
|
case "bottom":
|
||||||
|
left = horizontalCenter;
|
||||||
|
top = targetBounds.bottom + offset;
|
||||||
|
break;
|
||||||
|
case "left":
|
||||||
|
top = verticalCenter;
|
||||||
|
left = targetBounds.left - selfBounds.width - offset;
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
top = verticalCenter;
|
||||||
|
left = targetBounds.right + offset;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
right: left + selfBounds.width,
|
||||||
|
bottom: top + selfBounds.height,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
@ -113,40 +145,15 @@ export class Tooltip extends React.Component<TooltipProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isVisible } = this;
|
const { style, formatters, position, children } = this.props;
|
||||||
const { useAnimation, position, following, style, children } = this.props;
|
const className = cssNames("Tooltip", this.props.className, formatters, this.activePosition, {
|
||||||
let { className } = this.props;
|
hidden: !this.isVisible,
|
||||||
className = cssNames('Tooltip', position, { following }, className);
|
formatter: !!formatters,
|
||||||
const tooltip = (
|
});
|
||||||
<Animate enter={isVisible} enabled={useAnimation}>
|
|
||||||
<div className={className} ref={this.bindRef} style={style}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</Animate>
|
|
||||||
);
|
|
||||||
if (following) {
|
|
||||||
return createPortal(tooltip, document.body);
|
|
||||||
}
|
|
||||||
return tooltip;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TooltipContentProps {
|
|
||||||
className?: string;
|
|
||||||
narrow?: boolean; // max-width
|
|
||||||
warning?: boolean; // color
|
|
||||||
small?: boolean; // font-size
|
|
||||||
nowrap?: boolean; // white-space
|
|
||||||
tableView?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TooltipContent extends React.Component<TooltipContentProps> {
|
|
||||||
render() {
|
|
||||||
const { className, children, ...modifiers } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("TooltipContent", className, modifiers)}>
|
<div className={className} style={style} ref={this.bindRef}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { isReactNode } from "../../utils/isReactNode";
|
|||||||
import uniqueId from "lodash/uniqueId"
|
import uniqueId from "lodash/uniqueId"
|
||||||
|
|
||||||
export interface TooltipDecoratorProps {
|
export interface TooltipDecoratorProps {
|
||||||
tooltip?: ReactNode | Omit<TooltipProps, "htmlFor">;
|
tooltip?: ReactNode | Omit<TooltipProps, "targetId">;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withTooltip<T extends React.ComponentType<any>>(Target: T): T {
|
export function withTooltip<T extends React.ComponentType<any>>(Target: T): T {
|
||||||
@ -18,20 +18,12 @@ export function withTooltip<T extends React.ComponentType<any>>(Target: T): T {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { tooltip, ...targetProps } = this.props;
|
const { tooltip, ...targetProps } = this.props;
|
||||||
|
|
||||||
if (tooltip) {
|
if (tooltip) {
|
||||||
const tooltipId = targetProps.id || this.tooltipId;
|
const tooltipId = targetProps.id || this.tooltipId;
|
||||||
const tooltipProps: TooltipProps = {
|
const tooltipProps: TooltipProps = {
|
||||||
htmlFor: tooltipId,
|
targetId: tooltipId,
|
||||||
following: true,
|
|
||||||
...(isReactNode(tooltip) ? { children: tooltip } : tooltip),
|
...(isReactNode(tooltip) ? { children: tooltip } : tooltip),
|
||||||
};
|
};
|
||||||
if (!tooltipProps.following) {
|
|
||||||
targetProps.style = {
|
|
||||||
position: "relative",
|
|
||||||
...(targetProps.style || {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
targetProps.id = tooltipId;
|
targetProps.id = tooltipId;
|
||||||
targetProps.children = (
|
targetProps.children = (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user