From 2020328c123aa1f40a5c92d8f3f07f312b6c771b Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 21 Apr 2023 20:10:43 +0200 Subject: [PATCH] re-hydration Signed-off-by: Gabriel --- .../ui-components/error-boundary/README.md | 20 + .../ui-components/error-boundary/index.ts | 2 + .../jest.config.js | 0 .../ui-components/error-boundary/package.json | 48 ++ .../error-boundary/src/error-boundary.scss | 34 ++ .../error-boundary/src/error-boundary.tsx | 103 +++++ .../{tooltip => error-boundary}/src/index.ts | 3 +- .../error-boundary/tailwind.config.js | 30 ++ .../{tooltip => error-boundary}/tsconfig.json | 0 .../webpack.config.js | 0 .../ui-components/resizing-anchor/README.md | 15 + .../ui-components/resizing-anchor/index.ts | 6 + .../resizing-anchor/jest.config.js | 1 + .../resizing-anchor/package.json | 49 +++ .../resizing-anchor/src/index.ts | 6 + .../resizing-anchor/src/resizing-anchor.scss | 77 ++++ .../resizing-anchor/src/resizing-anchor.tsx | 310 +++++++++++++ .../resizing-anchor/tailwind.config.js | 30 ++ .../resizing-anchor/tsconfig.json | 4 + .../resizing-anchor/webpack.config.js | 1 + packages/ui-components/tooltip/.eslintrc.json | 6 - packages/ui-components/tooltip/.prettierrc | 1 - packages/ui-components/tooltip/.swcrc | 19 - packages/ui-components/tooltip/README.md | 20 - .../src/__snapshots__/tooltip.test.tsx.snap | 245 ----------- packages/ui-components/tooltip/src/helpers.ts | 141 ------ .../ui-components/tooltip/src/tooltip.scss | 92 ---- .../tooltip/src/tooltip.test.tsx | 414 ------------------ .../ui-components/tooltip/src/tooltip.tsx | 171 -------- .../ui-components/tooltip/src/withTooltip.tsx | 70 --- 30 files changed, 737 insertions(+), 1181 deletions(-) create mode 100644 packages/ui-components/error-boundary/README.md create mode 100644 packages/ui-components/error-boundary/index.ts rename packages/ui-components/{tooltip => error-boundary}/jest.config.js (100%) create mode 100644 packages/ui-components/error-boundary/package.json create mode 100644 packages/ui-components/error-boundary/src/error-boundary.scss create mode 100644 packages/ui-components/error-boundary/src/error-boundary.tsx rename packages/ui-components/{tooltip => error-boundary}/src/index.ts (71%) create mode 100644 packages/ui-components/error-boundary/tailwind.config.js rename packages/ui-components/{tooltip => error-boundary}/tsconfig.json (100%) rename packages/ui-components/{tooltip => error-boundary}/webpack.config.js (100%) create mode 100644 packages/ui-components/resizing-anchor/README.md create mode 100644 packages/ui-components/resizing-anchor/index.ts create mode 100644 packages/ui-components/resizing-anchor/jest.config.js create mode 100644 packages/ui-components/resizing-anchor/package.json create mode 100644 packages/ui-components/resizing-anchor/src/index.ts create mode 100644 packages/ui-components/resizing-anchor/src/resizing-anchor.scss create mode 100644 packages/ui-components/resizing-anchor/src/resizing-anchor.tsx create mode 100644 packages/ui-components/resizing-anchor/tailwind.config.js create mode 100644 packages/ui-components/resizing-anchor/tsconfig.json create mode 100644 packages/ui-components/resizing-anchor/webpack.config.js delete mode 100644 packages/ui-components/tooltip/.eslintrc.json delete mode 100644 packages/ui-components/tooltip/.prettierrc delete mode 100644 packages/ui-components/tooltip/.swcrc delete mode 100644 packages/ui-components/tooltip/README.md delete mode 100644 packages/ui-components/tooltip/src/__snapshots__/tooltip.test.tsx.snap delete mode 100644 packages/ui-components/tooltip/src/helpers.ts delete mode 100644 packages/ui-components/tooltip/src/tooltip.scss delete mode 100644 packages/ui-components/tooltip/src/tooltip.test.tsx delete mode 100644 packages/ui-components/tooltip/src/tooltip.tsx delete mode 100644 packages/ui-components/tooltip/src/withTooltip.tsx diff --git a/packages/ui-components/error-boundary/README.md b/packages/ui-components/error-boundary/README.md new file mode 100644 index 0000000000..187facd888 --- /dev/null +++ b/packages/ui-components/error-boundary/README.md @@ -0,0 +1,20 @@ +# @k8slens/error-boundary + +This package contains stuff related to creating Lens-applications. + +# Usage + +```bash +$ npm install @k8slens/error-boundary +``` + +```typescript +import { Tooltip, TooltipPosition } from "@k8slens/error-boundary"; +import { withTooltip } from "@k8slens/error-boundary"; + +import type { TooltipProps } from "@k8slens/error-boundary"; +import type { TooltipDecoratorProps } from "@k8slens/error-boundary"; + +``` + +## Extendability diff --git a/packages/ui-components/error-boundary/index.ts b/packages/ui-components/error-boundary/index.ts new file mode 100644 index 0000000000..597cccdd1e --- /dev/null +++ b/packages/ui-components/error-boundary/index.ts @@ -0,0 +1,2 @@ +export * from "./src/error-boundary"; +export * from "./src/withTooltip"; \ No newline at end of file diff --git a/packages/ui-components/tooltip/jest.config.js b/packages/ui-components/error-boundary/jest.config.js similarity index 100% rename from packages/ui-components/tooltip/jest.config.js rename to packages/ui-components/error-boundary/jest.config.js diff --git a/packages/ui-components/error-boundary/package.json b/packages/ui-components/error-boundary/package.json new file mode 100644 index 0000000000..bb40344a7c --- /dev/null +++ b/packages/ui-components/error-boundary/package.json @@ -0,0 +1,48 @@ +{ + "name": "@k8slens/error-boundary", + "private": false, + "version": "1.0.0-alpha.0", + "description": "Highly extendable error-boundary in the Lens.", + "type": "commonjs", + "files": [ + "dist" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/lensapp/lens.git" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "author": { + "name": "OpenLens Authors", + "email": "info@k8slens.dev" + }, + "license": "MIT", + "homepage": "https://github.com/lensapp/lens", + "scripts": { + "build": "webpack", + "test:unit": "jest --coverage --runInBand", + "lint": "lens-lint", + "lint:fix": "lens-lint --fix" + }, + "peerDependencies": { + "@k8slens/utilities": "^1.0.0-alpha.1", + "auto-bind": "^4.0.0", + "lodash": "^4.17.21", + "mobx": "^6.8.0", + "mobx-react": "^7.6.0", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "devDependencies": { + "@async-fn/jest": "^1.6.4", + "@k8slens/eslint-config": "6.5.0-alpha.1", + "@k8slens/react-testing-library-discovery": "^1.0.0-alpha.0", + "@testing-library/react": "^12.1.5", + "@testing-library/user-event": "^12.8.3" + } +} diff --git a/packages/ui-components/error-boundary/src/error-boundary.scss b/packages/ui-components/error-boundary/src/error-boundary.scss new file mode 100644 index 0000000000..b68ab5878f --- /dev/null +++ b/packages/ui-components/error-boundary/src/error-boundary.scss @@ -0,0 +1,34 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +.ErrorBoundary { + --flex-gap: #{$padding * 2}; + + padding: var(--flex-gap); + word-break: break-all; + + .wrapper { + display: grid; + grid-template-columns: minmax(300px, 1fr) minmax(300px, 1fr); + column-gap: 12px; + row-gap: 12px; + + @media screen and (max-width: 900px) { + grid-template-columns: auto; + } + } + + code { + max-height: none; + border: 5px solid var(--borderFaintColor); + white-space: pre-wrap; + background: var(--contentColor); + color: var(--textColorSecondary); + } + + a { + color: var(--colorInfo); + } +} diff --git a/packages/ui-components/error-boundary/src/error-boundary.tsx b/packages/ui-components/error-boundary/src/error-boundary.tsx new file mode 100644 index 0000000000..4d732a3ded --- /dev/null +++ b/packages/ui-components/error-boundary/src/error-boundary.tsx @@ -0,0 +1,103 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./error-boundary.scss"; + +import type { ErrorInfo } from "react"; +import React from "react"; +import { observer } from "mobx-react"; +import { Button } from "../button"; +import { issuesTrackerUrl, forumsUrl } from "../../../common/vars"; +import type { SingleOrMany } from "@k8slens/utilities"; +import type { ObservableHistory } from "mobx-observable-history"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import observableHistoryInjectable from "../../navigation/observable-history.injectable"; + +export interface ErrorBoundaryProps { + children?: SingleOrMany; +} + +interface State { + error?: Error; + errorInfo?: ErrorInfo; +} + +interface Dependencies { + observableHistory: ObservableHistory; +} + +@observer +class NonInjectedErrorBoundary extends React.Component { + public state: State = {}; + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + this.setState({ error, errorInfo }); + } + + back = () => { + this.setState({ error: undefined, errorInfo: undefined }); + this.props.observableHistory.goBack(); + }; + + render() { + const { error, errorInfo } = this.state; + + if (error) { + return ( +
+
+ {"App crash at "} + {location.pathname} +
+

+ + {"To help us improve the product please report bugs on"} + + Lens Forums + + {" or on our"} + + Github + + {" issues tracker."} +

+
+ +

Component stack:

+ {errorInfo?.componentStack} +
+ +

Error stack:

+ {error.stack} +
+
+
+ ); + } + + return this.props.children; + } +} + +export const ErrorBoundary = withInjectables(NonInjectedErrorBoundary, { + getProps: (di, props) => ({ + ...props, + observableHistory: di.inject(observableHistoryInjectable), + }), +}); diff --git a/packages/ui-components/tooltip/src/index.ts b/packages/ui-components/error-boundary/src/index.ts similarity index 71% rename from packages/ui-components/tooltip/src/index.ts rename to packages/ui-components/error-boundary/src/index.ts index b507e8fc97..49746a7b25 100644 --- a/packages/ui-components/tooltip/src/index.ts +++ b/packages/ui-components/error-boundary/src/index.ts @@ -3,5 +3,4 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -export * from "./tooltip"; -export * from "./withTooltip"; +export * from "./error-boundary"; diff --git a/packages/ui-components/error-boundary/tailwind.config.js b/packages/ui-components/error-boundary/tailwind.config.js new file mode 100644 index 0000000000..59cf6201b3 --- /dev/null +++ b/packages/ui-components/error-boundary/tailwind.config.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +const path = require('path'); + +module.exports = { + content: [ + path.join(__dirname, "src/**/*.tsx") + ], + darkMode: "class", + theme: { + fontFamily: { + sans: ["Roboto", "Helvetica", "Arial", "sans-serif"], + }, + extend: { + colors: { + textAccent: "var(--textColorAccent)", + textPrimary: "var(--textColorPrimary)", + textTertiary: "var(--textColorTertiary)", + textDimmed: "var(--textColorDimmed)", + }, + }, + }, + variants: { + extend: {}, + }, + plugins: [], +}; diff --git a/packages/ui-components/tooltip/tsconfig.json b/packages/ui-components/error-boundary/tsconfig.json similarity index 100% rename from packages/ui-components/tooltip/tsconfig.json rename to packages/ui-components/error-boundary/tsconfig.json diff --git a/packages/ui-components/tooltip/webpack.config.js b/packages/ui-components/error-boundary/webpack.config.js similarity index 100% rename from packages/ui-components/tooltip/webpack.config.js rename to packages/ui-components/error-boundary/webpack.config.js diff --git a/packages/ui-components/resizing-anchor/README.md b/packages/ui-components/resizing-anchor/README.md new file mode 100644 index 0000000000..bcf7e3003e --- /dev/null +++ b/packages/ui-components/resizing-anchor/README.md @@ -0,0 +1,15 @@ +# @k8slens/resizing-anchor + +This package contains stuff related to creating Lens-applications. + +# Usage + +```bash +$ npm install @k8slens/resizing-anchor +``` + +```typescript +import { ResizeDirection, ResizeGrowthDirection, ResizeSide, ResizingAnchor } from "@k8slens/resizing-anchor"; +``` + +## Extendability diff --git a/packages/ui-components/resizing-anchor/index.ts b/packages/ui-components/resizing-anchor/index.ts new file mode 100644 index 0000000000..f76358c781 --- /dev/null +++ b/packages/ui-components/resizing-anchor/index.ts @@ -0,0 +1,6 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +export * from "./src/resizing-anchor"; diff --git a/packages/ui-components/resizing-anchor/jest.config.js b/packages/ui-components/resizing-anchor/jest.config.js new file mode 100644 index 0000000000..38d54ab7b6 --- /dev/null +++ b/packages/ui-components/resizing-anchor/jest.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact; diff --git a/packages/ui-components/resizing-anchor/package.json b/packages/ui-components/resizing-anchor/package.json new file mode 100644 index 0000000000..368885050e --- /dev/null +++ b/packages/ui-components/resizing-anchor/package.json @@ -0,0 +1,49 @@ +{ + "name": "@k8slens/resizing-anchor", + "private": false, + "version": "1.0.0-alpha.0", + "description": "Highly extendable resizing-anchor in the Lens.", + "type": "commonjs", + "files": [ + "dist" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/lensapp/lens.git" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "author": { + "name": "OpenLens Authors", + "email": "info@k8slens.dev" + }, + "license": "MIT", + "homepage": "https://github.com/lensapp/lens", + "scripts": { + "build": "webpack", + "lint": "lens-lint", + "lint:fix": "lens-lint --fix" + }, + "peerDependencies": { + "@k8slens/feature-core": "^6.5.0-alpha.0", + "@k8slens/utilities": "^1.0.0-alpha.1", + "@ogre-tools/injectable": "^15.1.2", + "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", + "@ogre-tools/fp": "^15.1.2", + "auto-bind": "^4.0.0", + "lodash": "^4.17.21", + "mobx": "^6.8.0", + "mobx-react": "^7.6.0", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "devDependencies": { + "@async-fn/jest": "^1.6.4", + "@k8slens/eslint-config": "6.5.0-alpha.1", + "@k8slens/react-testing-library-discovery": "^1.0.0-alpha.0" + } +} diff --git a/packages/ui-components/resizing-anchor/src/index.ts b/packages/ui-components/resizing-anchor/src/index.ts new file mode 100644 index 0000000000..7973d52012 --- /dev/null +++ b/packages/ui-components/resizing-anchor/src/index.ts @@ -0,0 +1,6 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +export * from "./resizing-anchor"; diff --git a/packages/ui-components/resizing-anchor/src/resizing-anchor.scss b/packages/ui-components/resizing-anchor/src/resizing-anchor.scss new file mode 100644 index 0000000000..bcdb159a43 --- /dev/null +++ b/packages/ui-components/resizing-anchor/src/resizing-anchor.scss @@ -0,0 +1,77 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +body.resizing { + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +.ResizingAnchor { + $dimension: 12px; + + position: absolute; + z-index: 10; + + &::after { + content: " "; + display: block; + width: 3px; + height: 100%; + margin-left: 50%; + background: transparent; + transition: background 0.2s 0s; + } + + &:hover, &.resizing { + &::after { + background: var(--blue); + transition: background 0.2s 0.5s; + } + } + + &:hover.wasDragging { + &::after { + background: transparent; + transition: background 0.2s 0s; + } + } + + &.disabled { + display: none; + } + + &.vertical { + left: 0; + right: 0; + cursor: row-resize; + height: $dimension; + + &::after { + height: 3px; + width: 100%; + margin-left: 0; + } + + &.trailing { + bottom: -$dimension * 0.5; + } + } + + &.horizontal { + top: 0; + bottom: 0; + cursor: col-resize; + width: $dimension; + + &.leading { + left: -$dimension * 0.5; + } + + &.trailing { + right: -$dimension * 0.5; + } + } +} diff --git a/packages/ui-components/resizing-anchor/src/resizing-anchor.tsx b/packages/ui-components/resizing-anchor/src/resizing-anchor.tsx new file mode 100644 index 0000000000..0a1aeb02fd --- /dev/null +++ b/packages/ui-components/resizing-anchor/src/resizing-anchor.tsx @@ -0,0 +1,310 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./resizing-anchor.scss"; +import React from "react"; +import { action, observable, makeObservable } from "mobx"; +import _ from "lodash"; +import { cssNames, noop } from "@k8slens/utilities"; +import { observer } from "mobx-react"; + +export enum ResizeDirection { + HORIZONTAL = "horizontal", + VERTICAL = "vertical", +} + +/** + * ResizeSide is for customizing where the area should be rendered. + * That location is determined in conjunction with the `ResizeDirection` using the following table: + * + * +----------+------------+----------+ + * | | HORIZONTAL | VERTICAL | + * +----------+------------+----------+ + * | LEADING | left | top | + * +----------+------------+----------+ + * | TRAILING | right | bottom | + * +----------+------------+----------+ + */ +export enum ResizeSide { + LEADING = "leading", + TRAILING = "trailing", +} + +/** + * ResizeGrowthDirection determines how the anchor interprets the drag. + * + * Because the origin of the screen is top left a drag from bottom to top + * results in a negative directional delta. However, if the component being + * dragged grows in the opposite direction, this needs to be compensated for. + */ +export enum ResizeGrowthDirection { + TOP_TO_BOTTOM = 1, + BOTTOM_TO_TOP = -1, + LEFT_TO_RIGHT = 1, + RIGHT_TO_LEFT = -1, +} + +export interface ResizingAnchorProps { + direction: ResizeDirection; + + /** + * getCurrentExtent should return the current prominent dimension in the + * given resizing direction. Width for HORIZONTAL and height for VERTICAL + */ + getCurrentExtent: () => number; + + disabled?: boolean; + placement: ResizeSide; + growthDirection: ResizeGrowthDirection; + + // Ability to restrict which mouse buttons are allowed to resize this component + // Reference: https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons + onlyButtons?: number; + + // onStart is called when the ResizeAnchor is first clicked (mouse down) + onStart: () => void; + + // onEnd is called when the ResizeAnchor is released (mouse up) + onEnd: () => void; + + /** + * onDrag is called whenever there is a mousemove event. All calls will be + * bounded by matching `onStart` and `onEnd` calls. + */ + onDrag: (newExtent: number) => void; + + // onDoubleClick is called when the the ResizeAnchor is double clicked + onDoubleClick?: () => void; + + /** + * The following two extents represent the max and min values set to `onDrag` + */ + maxExtent: number; + minExtent: number; + + /** + * The following events are triggered with respect to the above values. + * - The "__Exceed" call will be made when the unbounded extent goes from + * < the above to >= the above + * - The "__Subceed" call is similar but is triggered when the unbounded + * extent goes from >= the above to < the above. + */ + onMaxExtentExceed?: () => void; + onMaxExtentSubceed?: () => void; + onMinExtentSucceed?: () => void; + onMinExtentExceed?: () => void; +} + +interface Position { + readonly pageX: number; + readonly pageY: number; +} + +/** + * Return the direction delta, but ignore drags leading up to a moved item + * 1. `->|` => return `false` + * 2. `<-|` => return `directed length (M, P2)` (negative) + * 3. `-|>` => return `directed length (M, P2)` (positive) + * 4. `<|-` => return `directed length (M, P2)` (negative) + * 5. `|->` => return `directed length (M, P2)` (positive) + * 6. `|<-` => return `false` + * @param P1 the starting position on the number line + * @param P2 the ending position on the number line + * @param M a third point that determines if the delta is meaningful + * @returns the directional difference between including appropriate sign. + */ +function directionDelta(P1: number, P2: number, M: number): number | false { + const delta = Math.abs(M - P2); + + if (P1 < M) { + if (P2 >= M) { + // case 3 + return delta; + } + + if (P2 < P1) { + // case 2 + return -delta; + } + + // case 1 + return false; + } + + if (P2 < M) { + // case 4 + return -delta; + } + + if (P1 < P2) { + // case 5 + return delta; + } + + // case 6 + return false; +} + +@observer +export class ResizingAnchor extends React.PureComponent { + @observable lastMouseEvent?: MouseEvent; + ref = React.createRef(); + @observable isDragging = false; + @observable wasDragging = false; + + static defaultProps = { + onStart: noop, + onDrag: noop, + onEnd: noop, + onMaxExtentExceed: noop, + onMinExtentExceed: noop, + onMinExtentSubceed: noop, + onMaxExtentSubceed: noop, + onDoubleClick: noop, + disabled: false, + growthDirection: ResizeGrowthDirection.BOTTOM_TO_TOP, + maxExtent: Number.POSITIVE_INFINITY, + minExtent: 0, + placement: ResizeSide.LEADING, + }; + static IS_RESIZING = "resizing"; + + constructor(props: ResizingAnchorProps) { + super(props); + + makeObservable(this); + + if (props.maxExtent < props.minExtent) { + throw new Error("maxExtent must be >= minExtent"); + } + + const cur = props.getCurrentExtent(); + + if (cur > props.maxExtent) { + props.onDrag(props.maxExtent); + } else if (cur < props.minExtent) { + props.onDrag(props.minExtent); + } + } + + componentWillUnmount() { + document.removeEventListener("mousemove", this.onDrag); + document.removeEventListener("mouseup", this.onDragEnd); + } + + onDragInit = action((event: React.MouseEvent) => { + const { onStart, onlyButtons } = this.props; + + if (typeof onlyButtons === "number" && onlyButtons !== event.buttons) { + return; + } + + document.addEventListener("mousemove", this.onDrag); + document.addEventListener("mouseup", this.onDragEnd); + document.body.classList.add(ResizingAnchor.IS_RESIZING); + this.isDragging = true; + + this.lastMouseEvent = undefined; + onStart(); + }); + + calculateDelta(from: Position, to: Position): number | false { + const node = this.ref.current; + + if (!node) { + return false; + } + + const boundingBox = node.getBoundingClientRect(); + + if (this.props.direction === ResizeDirection.HORIZONTAL) { + const barX = Math.round(boundingBox.x + (boundingBox.width / 2)); + + return directionDelta(from.pageX, to.pageX, barX); + } else { // direction === ResizeDirection.VERTICAL + const barY = Math.round(boundingBox.y + (boundingBox.height / 2)); + + return directionDelta(from.pageY, to.pageY, barY); + } + } + + onDrag = _.throttle((event: MouseEvent) => { + /** + * Some notes to help understand the following: + * - A browser's origin point is in the top left of the screen + * - X increases going from left to right + * - Y increases going from top to bottom + * - Since the resize bar should always be a rectangle, use its centre + * line (in the resizing direction) as the line for determining if + * the bar has "jumped around" + * + * Desire: + * - Always ignore movement in the non-resizing direction + * - Figure out how much the user has "dragged" the resize bar + * - If the resize bar has jumped around, compensate by ignoring movement + * in the resizing direction if it is moving "towards" the resize bar's + * new location. + */ + + if (!this.lastMouseEvent) { + this.lastMouseEvent = event; + + return; + } + + const { maxExtent, minExtent, getCurrentExtent, growthDirection } = this.props; + const { onDrag, onMaxExtentExceed, onMinExtentSucceed: onMinExtentSubceed, onMaxExtentSubceed, onMinExtentExceed } = this.props; + const delta = this.calculateDelta(this.lastMouseEvent, event); + + // always update the last mouse event + this.lastMouseEvent = event; + + if (delta === false) { + return; + } + + const previousExtent = getCurrentExtent(); + const unboundedExtent = previousExtent + (delta * growthDirection); + const boundedExtent = Math.round(Math.max(minExtent, Math.min(maxExtent, unboundedExtent))); + + onDrag(boundedExtent); + + if (previousExtent <= minExtent && minExtent <= unboundedExtent) { + onMinExtentExceed?.(); + } else if (previousExtent >= minExtent && minExtent >= unboundedExtent) { + onMinExtentSubceed?.(); + } + + if (previousExtent <= maxExtent && maxExtent <= unboundedExtent) { + onMaxExtentExceed?.(); + } else if (previousExtent >= maxExtent && maxExtent >= unboundedExtent) { + onMaxExtentSubceed?.(); + } + }, 100); + + onDragEnd = action(() => { + this.props.onEnd(); + document.removeEventListener("mousemove", this.onDrag); + document.removeEventListener("mouseup", this.onDragEnd); + document.body.classList.remove(ResizingAnchor.IS_RESIZING); + this.isDragging = false; + this.wasDragging = true; + + setTimeout(() => this.wasDragging = false, 200); + }); + + render() { + const { disabled, direction, placement, onDoubleClick } = this.props; + + return ( +
+ ); + } +} diff --git a/packages/ui-components/resizing-anchor/tailwind.config.js b/packages/ui-components/resizing-anchor/tailwind.config.js new file mode 100644 index 0000000000..59cf6201b3 --- /dev/null +++ b/packages/ui-components/resizing-anchor/tailwind.config.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +const path = require('path'); + +module.exports = { + content: [ + path.join(__dirname, "src/**/*.tsx") + ], + darkMode: "class", + theme: { + fontFamily: { + sans: ["Roboto", "Helvetica", "Arial", "sans-serif"], + }, + extend: { + colors: { + textAccent: "var(--textColorAccent)", + textPrimary: "var(--textColorPrimary)", + textTertiary: "var(--textColorTertiary)", + textDimmed: "var(--textColorDimmed)", + }, + }, + }, + variants: { + extend: {}, + }, + plugins: [], +}; diff --git a/packages/ui-components/resizing-anchor/tsconfig.json b/packages/ui-components/resizing-anchor/tsconfig.json new file mode 100644 index 0000000000..9e140d79da --- /dev/null +++ b/packages/ui-components/resizing-anchor/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@k8slens/typescript/config/base.json", + "include": ["**/*.ts", "**/*.tsx"], +} diff --git a/packages/ui-components/resizing-anchor/webpack.config.js b/packages/ui-components/resizing-anchor/webpack.config.js new file mode 100644 index 0000000000..1cda407f5a --- /dev/null +++ b/packages/ui-components/resizing-anchor/webpack.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/webpack").configForReact; diff --git a/packages/ui-components/tooltip/.eslintrc.json b/packages/ui-components/tooltip/.eslintrc.json deleted file mode 100644 index b15115cb69..0000000000 --- a/packages/ui-components/tooltip/.eslintrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "@k8slens/eslint-config/eslint", - "parserOptions": { - "project": "./tsconfig.json" - } -} diff --git a/packages/ui-components/tooltip/.prettierrc b/packages/ui-components/tooltip/.prettierrc deleted file mode 100644 index edd47b479e..0000000000 --- a/packages/ui-components/tooltip/.prettierrc +++ /dev/null @@ -1 +0,0 @@ -"@k8slens/eslint-config/prettier" diff --git a/packages/ui-components/tooltip/.swcrc b/packages/ui-components/tooltip/.swcrc deleted file mode 100644 index 4dd5c11a89..0000000000 --- a/packages/ui-components/tooltip/.swcrc +++ /dev/null @@ -1,19 +0,0 @@ -{ - "module": { - "type": "commonjs" - }, - "jsc": { - "parser": { - "syntax": "typescript", - "tsx": true, - "decorators": true, - "dynamicImport": false - }, - "transform": { - "legacyDecorator": true, - "decoratorMetadata": true - }, - "target": "es2019" - } -} - diff --git a/packages/ui-components/tooltip/README.md b/packages/ui-components/tooltip/README.md deleted file mode 100644 index 3dc391655e..0000000000 --- a/packages/ui-components/tooltip/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# @k8slens/tooltip - -This package contains stuff related to creating Lens-applications. - -# Usage - -```bash -$ npm install @k8slens/tooltip -``` - -```typescript -import { Tooltip, TooltipPosition } from "@k8slens/tooltip"; -import { withTooltip } from "@k8slens/tooltip"; - -import type { TooltipProps } from "@k8slens/tooltip"; -import type { TooltipDecoratorProps } from "@k8slens/tooltip"; - -``` - -## Extendability diff --git a/packages/ui-components/tooltip/src/__snapshots__/tooltip.test.tsx.snap b/packages/ui-components/tooltip/src/__snapshots__/tooltip.test.tsx.snap deleted file mode 100644 index babbf9d0cf..0000000000 --- a/packages/ui-components/tooltip/src/__snapshots__/tooltip.test.tsx.snap +++ /dev/null @@ -1,245 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` does not render to DOM if not visible 1`] = ` - -
-
- Target Text -
-
- -`; - -exports[` renders to DOM when forced to by visible prop 1`] = ` - -
- -
- Target Text -
-
- -`; - -exports[` renders to DOM when hovering over target 1`] = ` - -
- -
- Target Text -
-
- -`; - -exports[` uses a portal if usePortal is specified 1`] = ` - -
-
-
- Target Text -
-
-
- - -`; - -exports[` when specifying a tooltip for a component renders 1`] = ` - -
-
-
- Target Text -
-
-
- -`; - -exports[` when specifying a tooltip for a component that doesn't exist with show on parent hover renders 1`] = ` - -
- -`; - -exports[` when specifying a tooltip for a component when hovering over the target element renders 1`] = ` - -
-
-
- Target Text -
-
-
- - -`; - -exports[` when specifying a tooltip for a component when hovering over the target element when no longer hovering the target renders 1`] = ` - -
-
-
- Target Text -
-
-
- -`; - -exports[` when specifying a tooltip for a component with show on parent hover renders 1`] = ` - -
-
-
-
- Target Text -
-
-
-
- -`; - -exports[` when specifying a tooltip for a component with show on parent hover when hovering over the target element renders 1`] = ` - -
-
-
-
- Target Text -
-
-
-
- - -`; - -exports[` when specifying a tooltip for a component with show on parent hover when hovering over the target element when no longer hovering the target renders 1`] = ` - -
-
-
-
- Target Text -
-
-
-
- -`; - -exports[` when specifying a tooltip for a component with show on parent hover when hovering over the target's parent element renders 1`] = ` - -
-
-
-
- Target Text -
-
-
-
- - -`; - -exports[` when specifying a tooltip for a component with show on parent hover when hovering over the target's parent element when no longer hovering the target's parent renders 1`] = ` - -
-
-
-
- Target Text -
-
-
-
- -`; diff --git a/packages/ui-components/tooltip/src/helpers.ts b/packages/ui-components/tooltip/src/helpers.ts deleted file mode 100644 index ab346f1142..0000000000 --- a/packages/ui-components/tooltip/src/helpers.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { iter } from "@k8slens/utilities"; -import { TooltipPosition } from "./tooltip"; - -export type RectangleDimensions = { - left: number; - width: number; - top: number; - height: number; - bottom: number; - right: number; -}; - -export type FloatingRectangleDimensions = { - width: number; - height: number; -}; - -export type DomElementWithRectangle = { - getBoundingClientRect: () => RectangleDimensions; -}; - -export type DomElementWithFloatingRectangle = { - getBoundingClientRect: () => FloatingRectangleDimensions; -}; - -export type ComputeNextPositionArgs = { - tooltip: DomElementWithFloatingRectangle; - target: DomElementWithRectangle; - offset: number; - preferredPositions?: TooltipPosition | TooltipPosition[]; -}; - -export type NextPosition = { - position: TooltipPosition; - top: number; - left: number; -}; - -type GetPositionArgs = { - offset: number; - position: TooltipPosition; - tooltipBounds: FloatingRectangleDimensions; - targetBounds: RectangleDimensions; -}; - -const getPosition = ({ offset, position, tooltipBounds, targetBounds }: GetPositionArgs) => { - const horizontalCenter = targetBounds.left + (targetBounds.width - tooltipBounds.width) / 2; - const verticalCenter = targetBounds.top + (targetBounds.height - tooltipBounds.height) / 2; - const topCenter = targetBounds.top - tooltipBounds.height - offset; - const bottomCenter = targetBounds.bottom + offset; - const [left, top] = (() => { - switch (position) { - case TooltipPosition.TOP: - return [horizontalCenter, topCenter]; - case TooltipPosition.BOTTOM: - return [horizontalCenter, bottomCenter]; - case TooltipPosition.LEFT: - return [targetBounds.left - tooltipBounds.width - offset, verticalCenter]; - case TooltipPosition.RIGHT: - return [targetBounds.right + offset, verticalCenter]; - case TooltipPosition.TOP_LEFT: - return [targetBounds.left, topCenter]; - case TooltipPosition.TOP_RIGHT: - return [targetBounds.right - tooltipBounds.width, topCenter]; - case TooltipPosition.BOTTOM_LEFT: - return [targetBounds.left, bottomCenter]; - case TooltipPosition.BOTTOM_RIGHT: - return [targetBounds.right - tooltipBounds.width, bottomCenter]; - } - })(); - - return { - left, - top, - right: left + tooltipBounds.width, - bottom: top + tooltipBounds.height, - }; -}; - -const isTooltipPosition = (value: unknown): value is TooltipPosition => - Object.values(TooltipPosition).includes(value as TooltipPosition); - -export const computeNextPosition = ({ - offset, - preferredPositions, - target, - tooltip, -}: ComputeNextPositionArgs): NextPosition => { - const positions = new Set([ - ...[preferredPositions ?? []].filter(isTooltipPosition).flat(), - TooltipPosition.RIGHT, - TooltipPosition.BOTTOM, - TooltipPosition.TOP, - TooltipPosition.LEFT, - TooltipPosition.TOP_RIGHT, - TooltipPosition.TOP_LEFT, - TooltipPosition.BOTTOM_RIGHT, - TooltipPosition.BOTTOM_LEFT, - ]); - - const tooltipBounds = tooltip.getBoundingClientRect(); - const targetBounds = target.getBoundingClientRect(); - const { innerWidth: viewportWidth, innerHeight: viewportHeight } = window; - - // find proper position - for (const position of positions) { - const { left, top, right, bottom } = getPosition({ - offset, - position, - tooltipBounds, - targetBounds, - }); - const fitsToWindow = - left >= 0 && top >= 0 && right <= viewportWidth && bottom <= viewportHeight; - - if (fitsToWindow) { - return { - left, - top, - position, - }; - } - } - - const position = iter.first(positions) as TooltipPosition; - - return { - position, - ...getPosition({ - offset, - position, - tooltipBounds, - targetBounds, - }), - }; -}; diff --git a/packages/ui-components/tooltip/src/tooltip.scss b/packages/ui-components/tooltip/src/tooltip.scss deleted file mode 100644 index 28db216a4e..0000000000 --- a/packages/ui-components/tooltip/src/tooltip.scss +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - - -.Tooltip { - - position: fixed; - margin: 0 !important; - background: var(--mainBackground); - font-size: small; - font-weight: normal; - border-radius: 3px; - color: var(--textColorAccent); - white-space: normal; - padding: .5em; - text-align: center; - pointer-events: none; - transition: opacity 150ms 150ms ease-in-out; - z-index: 100000; - box-shadow: 0 8px 16px rgba(0,0,0,0.24); - left: 0; - top: 0; - opacity: 0; - visibility: hidden; - - &.visible { - opacity: 1; - visibility: visible; - } - - &:empty { - display: none; - } - - &.formatter { - &.nowrap { - &, * { - white-space: nowrap; - } - } - - &.narrow { - max-width: 300px; - text-overflow: ellipsis; - word-wrap: break-word; - text-align: left; - } - - &.small { - font-size: 12px; - } - - &.warning { - color: var(--colorError); - } - - &.tableView { - display: grid; - gap: var(--padding); - grid-template-columns: max-content 1fr; - grid-template-rows: repeat(2, 1fr); - - > .flex { - display: contents; - } - - > * { - white-space: pre-wrap; - word-break: break-word; - } - - .title { - grid-column: 1 / 3; - color: var(--textColorAccent); - text-align: center; - font-weight: bold; - } - - .name { - text-align: right; - color: var(--textColorAccent); - } - - .value { - text-align: left; - color: var(--textColorSecondary); - } - } - } -} diff --git a/packages/ui-components/tooltip/src/tooltip.test.tsx b/packages/ui-components/tooltip/src/tooltip.test.tsx deleted file mode 100644 index b47c337552..0000000000 --- a/packages/ui-components/tooltip/src/tooltip.test.tsx +++ /dev/null @@ -1,414 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { render } from "@testing-library/react"; -import type { RenderResult } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import assert from "assert"; -import React from "react"; -import { computeNextPosition, RectangleDimensions } from "./helpers"; -import { Tooltip, TooltipPosition } from "./tooltip"; - -const getRectangle = ( - parts: Omit, -): RectangleDimensions => { - assert(parts.right >= parts.left); - assert(parts.bottom >= parts.top); - - return { - ...parts, - width: parts.right - parts.left, - height: parts.bottom - parts.top, - }; -}; - -describe("", () => { - let requestAnimationFrameSpy: jest.SpyInstance; - - beforeEach(() => { - requestAnimationFrameSpy = jest.spyOn(window, "requestAnimationFrame"); - - requestAnimationFrameSpy.mockImplementation((cb) => { - cb(0); - - return 0; - }); - }); - - afterEach(() => { - requestAnimationFrameSpy.mockRestore(); - }); - - it("does not render to DOM if not visible", () => { - const result = render( - <> - - I am a tooltip - -
Target Text
- , - ); - - expect(result.baseElement).toMatchSnapshot(); - }); - - it("renders to DOM when hovering over target", () => { - const result = render( - <> - - I am a tooltip - -
Target Text
- , - ); - - const target = result.baseElement.querySelector("#my-target"); - - assert(target); - - userEvent.hover(target); - expect(result.baseElement).toMatchSnapshot(); - }); - - it("renders to DOM when forced to by visible prop", () => { - const result = render( - <> - - I am a tooltip - -
Target Text
- , - ); - - expect(result.baseElement).toMatchSnapshot(); - }); - - it("uses a portal if usePortal is specified", () => { - const result = render( -
- - I am a tooltip - -
Target Text
-
, - ); - - expect(result.baseElement).toMatchSnapshot(); - }); - - describe("when specifying a tooltip for a component", () => { - let result: RenderResult; - - beforeEach(() => { - result = render( -
- - I am a tooltip - -
- Target Text -
-
, - ); - }); - - it("renders", () => { - expect(result.baseElement).toMatchSnapshot(); - }); - - it("doesn't render the tooltip", () => { - expect(result.queryByTestId("tooltip")).not.toBeInTheDocument(); - }); - - describe("when hovering over the target element", () => { - beforeEach(() => { - userEvent.hover(result.getByTestId("target")); - }); - - it("renders", () => { - expect(result.baseElement).toMatchSnapshot(); - }); - - it("renders the tooltip", () => { - expect(result.getByTestId("tooltip")).toBeInTheDocument(); - }); - - describe("when no longer hovering the target", () => { - beforeEach(() => { - userEvent.unhover(result.getByTestId("target")); - }); - - it("renders", () => { - expect(result.baseElement).toMatchSnapshot(); - }); - - it("doesn't render the tooltip", () => { - expect(result.queryByTestId("tooltip")).not.toBeInTheDocument(); - }); - }); - }); - }); - - describe("when specifying a tooltip for a component with show on parent hover", () => { - let result: RenderResult; - - beforeEach(() => { - result = render( -
- - I am a tooltip - -
-
- Target Text -
-
-
, - ); - }); - - it("renders", () => { - expect(result.baseElement).toMatchSnapshot(); - }); - - it("doesn't render the tooltip", () => { - expect(result.queryByTestId("tooltip")).not.toBeInTheDocument(); - }); - - describe("when hovering over the target element", () => { - beforeEach(() => { - userEvent.hover(result.getByTestId("target")); - }); - - it("renders", () => { - expect(result.baseElement).toMatchSnapshot(); - }); - - it("renders the tooltip", () => { - expect(result.getByTestId("tooltip")).toBeInTheDocument(); - }); - - describe("when no longer hovering the target", () => { - beforeEach(() => { - userEvent.unhover(result.getByTestId("target")); - }); - - it("renders", () => { - expect(result.baseElement).toMatchSnapshot(); - }); - - it("doesn't render the tooltip", () => { - expect(result.queryByTestId("tooltip")).not.toBeInTheDocument(); - }); - }); - }); - - describe("when hovering over the target's parent element", () => { - beforeEach(() => { - userEvent.hover(result.getByTestId("target-parent")); - }); - - it("renders", () => { - expect(result.baseElement).toMatchSnapshot(); - }); - - it("renders the tooltip", () => { - expect(result.getByTestId("tooltip")).toBeInTheDocument(); - }); - - describe("when no longer hovering the target's parent", () => { - beforeEach(() => { - userEvent.unhover(result.getByTestId("target-parent")); - }); - - it("renders", () => { - expect(result.baseElement).toMatchSnapshot(); - }); - - it("doesn't render the tooltip", () => { - expect(result.queryByTestId("tooltip")).not.toBeInTheDocument(); - }); - }); - }); - }); - - describe("when specifying a tooltip for a component that doesn't exist with show on parent hover", () => { - let result: RenderResult; - - beforeEach(() => { - result = render( - <> - - I am a tooltip - - , - ); - }); - - it("renders", () => { - expect(result.baseElement).toMatchSnapshot(); - }); - - it("doesn't render the tooltip", () => { - expect(result.queryByTestId("tooltip")).not.toBeInTheDocument(); - }); - }); -}); - -describe("computeNextPosition technical tests", () => { - describe("given a 1280x720 window", () => { - beforeEach(() => { - window.innerHeight = 720; - window.innerWidth = 1280; - }); - - [ - { - position: "right", - targetRect: { - top: 700, - left: 700, - bottom: 720, - right: 720, - }, - }, - { - position: "bottom", - targetRect: { - top: 500, - left: 1200, - bottom: 520, - right: 1280, - }, - }, - { - position: "top", - targetRect: { - top: 700, - left: 1200, - bottom: 720, - right: 1280, - }, - }, - { - position: "left", - targetRect: { - top: 0, - left: 1200, - bottom: 720, - right: 1280, - }, - }, - ].forEach(({ position, targetRect }) => { - it(`renders to the "${position}" by default if there is enough room`, () => { - expect( - computeNextPosition({ - offset: 10, - target: { - getBoundingClientRect: () => getRectangle(targetRect), - }, - tooltip: { - getBoundingClientRect: () => ({ - height: 20, - width: 20, - }), - }, - }), - ).toMatchObject({ - position, - }); - }); - }); - - it("doesn't throw if the preferredPosition is invalid", () => { - expect( - computeNextPosition({ - preferredPositions: "some-invalid-data" as any, - offset: 10, - target: { - getBoundingClientRect: () => - getRectangle({ - top: 50, - left: 50, - bottom: 100, - right: 100, - }), - }, - tooltip: { - getBoundingClientRect: () => ({ - height: 20, - width: 20, - }), - }, - }), - ).toMatchObject({ - position: "right", - }); - }); - - it("defaults to right if no other location works", () => { - expect( - computeNextPosition({ - offset: 10, - target: { - getBoundingClientRect: () => - getRectangle({ - top: 0, - left: 0, - bottom: 720, - right: 1280, - }), - }, - tooltip: { - getBoundingClientRect: () => ({ - height: 20, - width: 20, - }), - }, - }), - ).toMatchObject({ - position: "right", - }); - }); - - it.each([ - TooltipPosition.RIGHT, - TooltipPosition.BOTTOM, - TooltipPosition.TOP, - TooltipPosition.LEFT, - TooltipPosition.TOP_RIGHT, - TooltipPosition.TOP_LEFT, - TooltipPosition.BOTTOM_RIGHT, - TooltipPosition.BOTTOM_LEFT, - ])( - "computes to the %p if there is space and it is specified as a preferred position", - (position) => { - expect( - computeNextPosition({ - preferredPositions: position, - offset: 10, - target: { - getBoundingClientRect: () => - getRectangle({ - top: 50, - left: 50, - bottom: 100, - right: 100, - }), - }, - tooltip: { - getBoundingClientRect: () => ({ - height: 20, - width: 20, - }), - }, - }), - ).toMatchObject({ - position, - }); - }, - ); - }); -}); diff --git a/packages/ui-components/tooltip/src/tooltip.tsx b/packages/ui-components/tooltip/src/tooltip.tsx deleted file mode 100644 index c2f80b4d05..0000000000 --- a/packages/ui-components/tooltip/src/tooltip.tsx +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import "./tooltip.scss"; - -import React from "react"; -import { createPortal } from "react-dom"; -import { observer } from "mobx-react"; -import type { IClassName } from "@k8slens/utilities"; -import { cssNames } from "@k8slens/utilities"; -import { observable, makeObservable, action, runInAction } from "mobx"; -import autoBindReact from "auto-bind/react"; -import { computeNextPosition } from "./helpers"; - -export enum TooltipPosition { - TOP = "top", - BOTTOM = "bottom", - LEFT = "left", - RIGHT = "right", - TOP_LEFT = "top_left", - TOP_RIGHT = "top_right", - BOTTOM_LEFT = "bottom_left", - BOTTOM_RIGHT = "bottom_right", -} - -export interface TooltipProps { - targetId: string; // html-id of target element to bind for - tooltipOnParentHover?: boolean; // detect hover on parent of target - 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 - preferredPositions?: TooltipPosition | TooltipPosition[]; - className?: IClassName; - formatters?: TooltipContentFormatters; - style?: React.CSSProperties; - children?: React.ReactNode; - "data-testid"?: string; -} - -export interface TooltipContentFormatters { - narrow?: boolean; // max-width - warning?: boolean; // color - small?: boolean; // font-size - nowrap?: boolean; // white-space - tableView?: boolean; -} - -const defaultProps = { - usePortal: true, - offset: 10, -}; - -@observer -class DefaultedTooltip extends React.Component { - static defaultProps = defaultProps as object; - - @observable.ref elem: HTMLDivElement | null = null; - - @observable activePosition?: TooltipPosition; - - @observable isVisible = false; - - @observable isContentVisible = false; // animation manager - - constructor(props: TooltipProps & typeof defaultProps) { - super(props); - makeObservable(this); - autoBindReact(this); - } - - get targetElem(): HTMLElement | null { - return document.getElementById(this.props.targetId); - } - - get hoverTarget(): HTMLElement | null { - if (this.props.tooltipOnParentHover) { - return this.targetElem?.parentElement ?? null; - } - - return this.targetElem; - } - - componentDidMount() { - this.hoverTarget?.addEventListener("mouseenter", this.onEnterTarget); - this.hoverTarget?.addEventListener("mouseleave", this.onLeaveTarget); - this.refreshPosition(); - } - - componentDidUpdate() { - this.refreshPosition(); - } - - componentWillUnmount() { - this.hoverTarget?.removeEventListener("mouseenter", this.onEnterTarget); - this.hoverTarget?.removeEventListener("mouseleave", this.onLeaveTarget); - } - - @action - protected onEnterTarget() { - this.isVisible = true; - requestAnimationFrame(action(() => (this.isContentVisible = true))); - } - - @action - protected onLeaveTarget() { - this.isVisible = false; - this.isContentVisible = false; - } - - refreshPosition() { - const { preferredPositions, offset } = this.props; - const { elem, targetElem } = this; - - if (!elem || !targetElem) { - return; - } - - this.setPosition(elem, { left: 0, top: 0 }); - - const { position, ...location } = computeNextPosition({ - offset, - preferredPositions, - target: targetElem, - tooltip: elem, - }); - - runInAction(() => { - this.activePosition = position; - this.setPosition(elem, location); - }); - } - - protected setPosition(elem: HTMLDivElement, pos: { left: number; top: number }) { - elem.style.left = `${pos.left}px`; - elem.style.top = `${pos.top}px`; - } - - render() { - const { style, formatters, usePortal, children, visible = this.isVisible } = this.props; - - if (!visible) { - return null; - } - - const className = cssNames("Tooltip", this.props.className, formatters, this.activePosition, { - visible: this.isContentVisible || this.props.visible, - formatter: !!formatters, - }); - const tooltip = ( -
(this.elem = elem)} - role="tooltip" - data-testid={this.props["data-testid"]} - > - {children} -
- ); - - if (usePortal) { - return createPortal(tooltip, document.body); - } - - return tooltip; - } -} - -export const Tooltip = DefaultedTooltip as React.ComponentClass; diff --git a/packages/ui-components/tooltip/src/withTooltip.tsx b/packages/ui-components/tooltip/src/withTooltip.tsx deleted file mode 100644 index 819b4b1bd4..0000000000 --- a/packages/ui-components/tooltip/src/withTooltip.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { ReactNode } from "react"; -import React, { useState } from "react"; -import type { TooltipProps } from "./tooltip"; -import { Tooltip } from "./tooltip"; -import { isReactNode } from "@k8slens/utilities"; -import uniqueId from "lodash/uniqueId"; -import type { SingleOrMany } from "@k8slens/utilities"; - -export interface TooltipDecoratorProps { - tooltip?: ReactNode | Omit; - /** - * forces tooltip to detect the target's parent for mouse events. This is - * useful for displaying tooltips even when the target is "disabled" - */ - tooltipOverrideDisabled?: boolean; - id?: string; - children?: SingleOrMany; -} - -export function withTooltip( - Target: TargetProps extends Pick - ? React.FunctionComponent - : never, -): React.FunctionComponent { - const DecoratedComponent = (props: TargetProps & TooltipDecoratorProps) => { - // TODO: Remove side-effect to allow deterministic unit testing - const [defaultTooltipId] = useState(uniqueId("tooltip_target_")); - - let { id: targetId, children: targetChildren } = props; - const { - tooltip, - tooltipOverrideDisabled, - id: _unusedId, - children: _unusedTargetChildren, - ...targetProps - } = props; - - if (tooltip) { - const tooltipProps: TooltipProps = { - targetId: targetId || defaultTooltipId, - tooltipOnParentHover: tooltipOverrideDisabled, - formatters: { narrow: true }, - ...(isReactNode(tooltip) ? { children: tooltip } : tooltip), - }; - - targetId = tooltipProps.targetId; - targetChildren = ( - <> -
{targetChildren}
- - - ); - } - - return ( - - {targetChildren} - - ); - }; - - DecoratedComponent.displayName = `withTooltip(${Target.displayName || Target.name})`; - - return DecoratedComponent; -}