mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Migrating Vue components to React and stores refactoring (#585)
Signed-off-by: Roman <ixrock@gmail.com> Co-authored-by: Sebastian Malton <sebastian@malton.name> Co-authored-by: Sebastian Malton <smalton@mirantis.com> Co-authored-by: Lauri Nevala <lauri.nevala@gmail.com> Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
297 files changed
+8186
-9103
No files matched your search
@@ -1,104 +1,78 @@
|
||||
|
||||
.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: $mainBackground;
|
||||
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;
|
||||
z-index: 100000;
|
||||
|
||||
* {
|
||||
white-space: normal;
|
||||
&.hidden {
|
||||
left: 0;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.following {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
&:not(.following) {
|
||||
&.left {
|
||||
left: 0;
|
||||
&.formatter {
|
||||
&.nowrap {
|
||||
&, * {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&.right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.top {
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
&.center {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.TooltipContent {
|
||||
&.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.narrow {
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: normal;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,109 +1,151 @@
|
||||
import './tooltip.scss'
|
||||
|
||||
import React from "react"
|
||||
import { observer } from "mobx-react";
|
||||
import { observable } from "mobx";
|
||||
import { createPortal } from "react-dom"
|
||||
import { autobind, cssNames } from "../../utils";
|
||||
import { Animate } from "../animate";
|
||||
import { observer } from "mobx-react";
|
||||
import { autobind, cssNames, IClassName } from "../../utils";
|
||||
import { observable } from "mobx";
|
||||
|
||||
export enum TooltipPosition {
|
||||
TOP = "top",
|
||||
BOTTOM = "bottom",
|
||||
LEFT = "left",
|
||||
RIGHT = "right",
|
||||
}
|
||||
|
||||
export interface TooltipProps {
|
||||
htmlFor: string;
|
||||
className?: string;
|
||||
position?: Position;
|
||||
useAnimation?: boolean;
|
||||
following?: boolean; // tooltip is following mouse position
|
||||
targetId: string; // html-id of target element to bind for
|
||||
visible?: boolean; // initial visibility
|
||||
offset?: number; // offset from target element in pixels (all sides)
|
||||
usePortal?: boolean; // renders element outside of parent (in body), disable for "easy-styling", default: true
|
||||
position?: TooltipPosition;
|
||||
className?: IClassName;
|
||||
formatters?: TooltipContentFormatters;
|
||||
style?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface Position {
|
||||
left?: boolean;
|
||||
right?: boolean;
|
||||
top?: boolean;
|
||||
bottom?: boolean;
|
||||
center?: 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: {
|
||||
center: true,
|
||||
bottom: true,
|
||||
}
|
||||
};
|
||||
usePortal: 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;
|
||||
|
||||
const positionPreference = new Set<TooltipPosition>();
|
||||
if (typeof position !== "undefined") {
|
||||
positionPreference.add(position);
|
||||
}
|
||||
positionPreference.add(TooltipPosition.RIGHT)
|
||||
.add(TooltipPosition.BOTTOM)
|
||||
.add(TooltipPosition.TOP)
|
||||
.add(TooltipPosition.LEFT)
|
||||
|
||||
// reset position first and get all possible client-rect area for tooltip element
|
||||
this.setPosition({ left: 0, top: 0 });
|
||||
|
||||
const selfBounds = elem.getBoundingClientRect();
|
||||
const targetBounds = targetElem.getBoundingClientRect();
|
||||
const { innerWidth: viewportWidth, innerHeight: viewportHeight } = window;
|
||||
|
||||
// find proper position
|
||||
for (const pos of positionPreference) {
|
||||
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 });
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const offsetX = -10;
|
||||
const offsetY = 10;
|
||||
const { clientX, clientY } = evt;
|
||||
const { innerWidth, innerHeight } = window;
|
||||
const preferedPosition = Array.from(positionPreference)[0];
|
||||
const { left, top } = this.getPosition(preferedPosition, selfBounds, targetBounds)
|
||||
this.activePosition = preferedPosition;
|
||||
this.setPosition({ left, top });
|
||||
}
|
||||
|
||||
const initialPos: Partial<CSSStyleDeclaration> = {
|
||||
top: "auto",
|
||||
left: "auto",
|
||||
right: (innerWidth - clientX + offsetX) + "px",
|
||||
bottom: (innerHeight - clientY + offsetY) + "px",
|
||||
}
|
||||
protected setPosition(pos: { left: number, top: number }) {
|
||||
const elemStyle = this.elem.style;
|
||||
elemStyle.left = pos.left + "px"
|
||||
elemStyle.top = pos.top + "px"
|
||||
}
|
||||
|
||||
Object.assign(this.elem.style, initialPos);
|
||||
|
||||
// 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 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: left,
|
||||
top: top,
|
||||
right: left + selfBounds.width,
|
||||
bottom: top + selfBounds.height,
|
||||
};
|
||||
}
|
||||
|
||||
@autobind()
|
||||
@@ -112,40 +154,19 @@ 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 { style, formatters, usePortal, children } = this.props;
|
||||
const className = cssNames("Tooltip", this.props.className, formatters, this.activePosition, {
|
||||
hidden: !this.isVisible,
|
||||
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);
|
||||
<div className={className} style={style} ref={this.bindRef}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
if (usePortal) {
|
||||
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 (
|
||||
<div className={cssNames("TooltipContent", className, modifiers)}>
|
||||
{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 = (
|
||||
<>
|
||||
|
||||
Reference in new issue
Block a user