diff --git a/src/renderer/components/+cluster-settings/cluster-icon.scss b/src/renderer/components/+cluster-settings/cluster-icon.scss index 296cd1f8f7..7d118c2cf1 100644 --- a/src/renderer/components/+cluster-settings/cluster-icon.scss +++ b/src/renderer/components/+cluster-settings/cluster-icon.scss @@ -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); } diff --git a/src/renderer/components/+cluster-settings/cluster-icon.tsx b/src/renderer/components/+cluster-settings/cluster-icon.tsx index 8a6146770f..1d236dc556 100644 --- a/src/renderer/components/+cluster-settings/cluster-icon.tsx +++ b/src/renderer/components/+cluster-settings/cluster-icon.tsx @@ -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 { cluster: Cluster; @@ -44,9 +44,7 @@ export class ClusterIcon extends React.Component { return (
{showTooltip && ( - - {clusterName} - + {clusterName} )} {icon && {clusterName}/} {!icon && } diff --git a/src/renderer/components/+events/events.tsx b/src/renderer/components/+events/events.tsx index 834fbacf40..a8f6d8f61b 100644 --- a/src/renderer/components/+events/events.tsx +++ b/src/renderer/components/+events/events.tsx @@ -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 { const detailsUrl = getDetailsUrl(lookupApiLink(involvedObject, event)); return [ { - className: { - warning: isWarning - }, + className: { warning: isWarning }, title: ( - <> + {message} - - - {message} - + + {message} - + ) }, event.getNs(), diff --git a/src/renderer/components/+events/kube-event-icon.tsx b/src/renderer/components/+events/kube-event-icon.tsx index bad4c055ca..204dfd3a36 100644 --- a/src/renderer/components/+events/kube-event-icon.tsx +++ b/src/renderer/components/+events/kube-event-icon.tsx @@ -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 { - {event.message} -
- - {event.getAge(undefined, undefined, true)} + tooltip={{ + children: ( +
+
{event.message}
+
+ + {event.getAge(undefined, undefined, true)} +
- - )} + ) + }} /> ) } diff --git a/src/renderer/components/+nodes/node-details.tsx b/src/renderer/components/+nodes/node-details.tsx index 259c5c2c1a..73a497063e 100644 --- a/src/renderer/components/+nodes/node-details.tsx +++ b/src/renderer/components/+nodes/node-details.tsx @@ -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 { key={type} label={type} className={kebabCase(type)} - tooltip={ - - {Object.entries(condition).map(([key, value]) => -
-
{upperFirst(key)}
-
{value}
-
- )} -
- } + tooltip={{ + formatters: { + tableView: true, + }, + children: Object.entries(condition).map(([key, value]) => +
+
{upperFirst(key)}
+
{value}
+
+ ) + }} /> ) }) diff --git a/src/renderer/components/+nodes/nodes.tsx b/src/renderer/components/+nodes/nodes.tsx index 2b9658fbf3..2320219d34 100644 --- a/src/renderer/components/+nodes/nodes.tsx +++ b/src/renderer/components/+nodes/nodes.tsx @@ -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 { return (
{type} - - - {Object.entries(condition).map(([key, value]) => -
-
{upperFirst(key)}
-
{value}
-
- )} -
+ + {Object.entries(condition).map(([key, value]) => +
+
{upperFirst(key)}
+
{value}
+
+ )}
) }) @@ -162,7 +160,7 @@ export class Nodes extends React.Component { this.renderDiskUsage(node), <> {node.getTaints().length} - + {node.getTaints().map(({ key, effect }) => `${key}: ${effect}`).join("\n")} , diff --git a/src/renderer/components/+workloads-pods/pods.tsx b/src/renderer/components/+workloads-pods/pods.tsx index 890a13fe41..5f5ec19fbd 100644 --- a/src/renderer/components/+workloads-pods/pods.tsx +++ b/src/renderer/components/+workloads-pods/pods.tsx @@ -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 { renderContainersStatus(pod: Pod) { return pod.getContainerStatuses().map(containerStatus => { const { name, state, ready } = containerStatus; - const tooltip = ( - - {Object.keys(state).map(status => ( - -
- {name} ({status}{ready ? ", ready" : ""}) -
- {toPairs(state[status]).map(([name, value]) => ( -
-
{startCase(name)}
-
{value}
-
- ))} -
- ))} -
- ); return ( - + ( + +
+ {name} ({status}{ready ? ", ready" : ""}) +
+ {toPairs(state[status]).map(([name, value]) => ( +
+
{startCase(name)}
+
{value}
+
+ ))} +
+ )) + }} + />
) }); diff --git a/src/renderer/components/animate/animate.tsx b/src/renderer/components/animate/animate.tsx index f2b7258b8b..36c30bc8e4 100644 --- a/src/renderer/components/animate/animate.tsx +++ b/src/renderer/components/animate/animate.tsx @@ -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 { static defaultProps: AnimateProps = { name: "opacity", enter: true, - enabled: true, onEnter: noop, onLeave: noop, }; @@ -80,10 +78,7 @@ export class Animate extends React.Component { } 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), diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 164ecb7973..d794440995 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -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 { })}
- - - Add Cluster - + + Add Cluster {newContexts.size > 0 && ( diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index 0b742a21b3..2b43829d8b 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -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({ pinned: false }); type SidebarContextValue = { @@ -83,17 +82,9 @@ export class Sidebar extends React.Component { Compact view} material={isPinned ? "keyboard_arrow_left" : "keyboard_arrow_right"} onClick={toggle} - tooltip={{ - following: false, - position: { right: true }, - children: ( - - Compact view - - ) - }} focusable={false} />
diff --git a/src/renderer/components/tooltip/tooltip.scss b/src/renderer/components/tooltip/tooltip.scss index 72b4bd86b3..67662d20c8 100644 --- a/src/renderer/components/tooltip/tooltip.scss +++ b/src/renderer/components/tooltip/tooltip.scss @@ -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; + } } } } \ No newline at end of file diff --git a/src/renderer/components/tooltip/tooltip.tsx b/src/renderer/components/tooltip/tooltip.tsx index 8c7e1d15ef..2ece85f38a 100644 --- a/src/renderer/components/tooltip/tooltip.tsx +++ b/src/renderer/components/tooltip/tooltip.tsx @@ -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 = { - useAnimation: true, - position: { - bottom: true, - } -}; + offset: 10, +} @observer export class Tooltip extends React.Component { 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 = { - 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 { } render() { - const { isVisible } = this; - const { useAnimation, position, following, style, children } = this.props; - let { className } = this.props; - className = cssNames('Tooltip', position, { following }, className); - const tooltip = ( - -
- {children} -
-
- ); - 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 { - 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 ( -
+
{children}
- ) + ); } } diff --git a/src/renderer/components/tooltip/withTooltip.tsx b/src/renderer/components/tooltip/withTooltip.tsx index e74cb68f59..f9b9a65f69 100644 --- a/src/renderer/components/tooltip/withTooltip.tsx +++ b/src/renderer/components/tooltip/withTooltip.tsx @@ -7,7 +7,7 @@ import { isReactNode } from "../../utils/isReactNode"; import uniqueId from "lodash/uniqueId" export interface TooltipDecoratorProps { - tooltip?: ReactNode | Omit; + tooltip?: ReactNode | Omit; } export function withTooltip>(Target: T): T { @@ -18,20 +18,12 @@ export function withTooltip>(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 = ( <>