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