mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
re-hydration
Signed-off-by: Gabriel <gaccettola@mirantis.com>
This commit is contained in:
parent
8330dd257d
commit
2020328c12
20
packages/ui-components/error-boundary/README.md
Normal file
20
packages/ui-components/error-boundary/README.md
Normal file
@ -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
|
||||||
2
packages/ui-components/error-boundary/index.ts
Normal file
2
packages/ui-components/error-boundary/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./src/error-boundary";
|
||||||
|
export * from "./src/withTooltip";
|
||||||
48
packages/ui-components/error-boundary/package.json
Normal file
48
packages/ui-components/error-boundary/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
103
packages/ui-components/error-boundary/src/error-boundary.tsx
Normal file
103
packages/ui-components/error-boundary/src/error-boundary.tsx
Normal file
@ -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<React.ReactNode>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
error?: Error;
|
||||||
|
errorInfo?: ErrorInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
observableHistory: ObservableHistory<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class NonInjectedErrorBoundary extends React.Component<ErrorBoundaryProps & Dependencies, State> {
|
||||||
|
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 (
|
||||||
|
<div className="ErrorBoundary flex column gaps">
|
||||||
|
<h5>
|
||||||
|
{"App crash at "}
|
||||||
|
<span className="contrast">{location.pathname}</span>
|
||||||
|
</h5>
|
||||||
|
<p>
|
||||||
|
|
||||||
|
{"To help us improve the product please report bugs on"}
|
||||||
|
<a
|
||||||
|
href={forumsUrl}
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Lens Forums
|
||||||
|
</a>
|
||||||
|
{" or on our"}
|
||||||
|
<a
|
||||||
|
href={issuesTrackerUrl}
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Github
|
||||||
|
</a>
|
||||||
|
{" issues tracker."}
|
||||||
|
</p>
|
||||||
|
<div className="wrapper">
|
||||||
|
<code className="block">
|
||||||
|
<p className="contrast">Component stack:</p>
|
||||||
|
{errorInfo?.componentStack}
|
||||||
|
</code>
|
||||||
|
<code className="block">
|
||||||
|
<p className="contrast">Error stack:</p>
|
||||||
|
{error.stack}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="box self-flex-start"
|
||||||
|
primary
|
||||||
|
label="Back"
|
||||||
|
onClick={this.back}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorBoundary = withInjectables<Dependencies, ErrorBoundaryProps>(NonInjectedErrorBoundary, {
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
...props,
|
||||||
|
observableHistory: di.inject(observableHistoryInjectable),
|
||||||
|
}),
|
||||||
|
});
|
||||||
@ -3,5 +3,4 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./tooltip";
|
export * from "./error-boundary";
|
||||||
export * from "./withTooltip";
|
|
||||||
30
packages/ui-components/error-boundary/tailwind.config.js
Normal file
30
packages/ui-components/error-boundary/tailwind.config.js
Normal file
@ -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: [],
|
||||||
|
};
|
||||||
15
packages/ui-components/resizing-anchor/README.md
Normal file
15
packages/ui-components/resizing-anchor/README.md
Normal file
@ -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
|
||||||
6
packages/ui-components/resizing-anchor/index.ts
Normal file
6
packages/ui-components/resizing-anchor/index.ts
Normal file
@ -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";
|
||||||
1
packages/ui-components/resizing-anchor/jest.config.js
Normal file
1
packages/ui-components/resizing-anchor/jest.config.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact;
|
||||||
49
packages/ui-components/resizing-anchor/package.json
Normal file
49
packages/ui-components/resizing-anchor/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/ui-components/resizing-anchor/src/index.ts
Normal file
6
packages/ui-components/resizing-anchor/src/index.ts
Normal file
@ -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";
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
310
packages/ui-components/resizing-anchor/src/resizing-anchor.tsx
Normal file
310
packages/ui-components/resizing-anchor/src/resizing-anchor.tsx
Normal file
@ -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<ResizingAnchorProps> {
|
||||||
|
@observable lastMouseEvent?: MouseEvent;
|
||||||
|
ref = React.createRef<HTMLDivElement>();
|
||||||
|
@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 (
|
||||||
|
<div
|
||||||
|
ref={this.ref}
|
||||||
|
className={cssNames("ResizingAnchor", direction, placement, { disabled, resizing: this.isDragging, wasDragging: this.wasDragging })}
|
||||||
|
onMouseDown={this.onDragInit}
|
||||||
|
onDoubleClick={onDoubleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
packages/ui-components/resizing-anchor/tailwind.config.js
Normal file
30
packages/ui-components/resizing-anchor/tailwind.config.js
Normal file
@ -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: [],
|
||||||
|
};
|
||||||
4
packages/ui-components/resizing-anchor/tsconfig.json
Normal file
4
packages/ui-components/resizing-anchor/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "@k8slens/typescript/config/base.json",
|
||||||
|
"include": ["**/*.ts", "**/*.tsx"],
|
||||||
|
}
|
||||||
1
packages/ui-components/resizing-anchor/webpack.config.js
Normal file
1
packages/ui-components/resizing-anchor/webpack.config.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require("@k8slens/webpack").configForReact;
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@k8slens/eslint-config/eslint",
|
|
||||||
"parserOptions": {
|
|
||||||
"project": "./tsconfig.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
"@k8slens/eslint-config/prettier"
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"module": {
|
|
||||||
"type": "commonjs"
|
|
||||||
},
|
|
||||||
"jsc": {
|
|
||||||
"parser": {
|
|
||||||
"syntax": "typescript",
|
|
||||||
"tsx": true,
|
|
||||||
"decorators": true,
|
|
||||||
"dynamicImport": false
|
|
||||||
},
|
|
||||||
"transform": {
|
|
||||||
"legacyDecorator": true,
|
|
||||||
"decoratorMetadata": true
|
|
||||||
},
|
|
||||||
"target": "es2019"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -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
|
|
||||||
@ -1,245 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`<Tooltip /> does not render to DOM if not visible 1`] = `
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
id="my-target"
|
|
||||||
>
|
|
||||||
Target Text
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<Tooltip /> renders to DOM when forced to by visible prop 1`] = `
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="Tooltip right visible"
|
|
||||||
data-testid="tooltip"
|
|
||||||
role="tooltip"
|
|
||||||
style="left: 10px; top: 0px;"
|
|
||||||
>
|
|
||||||
I am a tooltip
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
id="my-target"
|
|
||||||
>
|
|
||||||
Target Text
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<Tooltip /> renders to DOM when hovering over target 1`] = `
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="Tooltip right visible"
|
|
||||||
data-testid="tooltip"
|
|
||||||
role="tooltip"
|
|
||||||
style="left: 10px; top: 0px;"
|
|
||||||
>
|
|
||||||
I am a tooltip
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
id="my-target"
|
|
||||||
>
|
|
||||||
Target Text
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<Tooltip /> uses a portal if usePortal is specified 1`] = `
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
id="my-target"
|
|
||||||
>
|
|
||||||
Target Text
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="Tooltip right visible"
|
|
||||||
data-testid="tooltip"
|
|
||||||
role="tooltip"
|
|
||||||
style="left: 10px; top: 0px;"
|
|
||||||
>
|
|
||||||
I am a tooltip
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<Tooltip /> when specifying a tooltip for a component renders 1`] = `
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
data-testid="target"
|
|
||||||
id="my-target"
|
|
||||||
>
|
|
||||||
Target Text
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<Tooltip /> when specifying a tooltip for a component that doesn't exist with show on parent hover renders 1`] = `
|
|
||||||
<body>
|
|
||||||
<div />
|
|
||||||
</body>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<Tooltip /> when specifying a tooltip for a component when hovering over the target element renders 1`] = `
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
data-testid="target"
|
|
||||||
id="my-target"
|
|
||||||
>
|
|
||||||
Target Text
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="Tooltip right visible"
|
|
||||||
data-testid="tooltip"
|
|
||||||
role="tooltip"
|
|
||||||
style="left: 10px; top: 0px;"
|
|
||||||
>
|
|
||||||
I am a tooltip
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<Tooltip /> when specifying a tooltip for a component when hovering over the target element when no longer hovering the target renders 1`] = `
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
data-testid="target"
|
|
||||||
id="my-target"
|
|
||||||
>
|
|
||||||
Target Text
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<Tooltip /> when specifying a tooltip for a component with show on parent hover renders 1`] = `
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
data-testid="target-parent"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-testid="target"
|
|
||||||
id="my-target"
|
|
||||||
>
|
|
||||||
Target Text
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<Tooltip /> when specifying a tooltip for a component with show on parent hover when hovering over the target element renders 1`] = `
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
data-testid="target-parent"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-testid="target"
|
|
||||||
id="my-target"
|
|
||||||
>
|
|
||||||
Target Text
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="Tooltip right visible"
|
|
||||||
data-testid="tooltip"
|
|
||||||
role="tooltip"
|
|
||||||
style="left: 10px; top: 0px;"
|
|
||||||
>
|
|
||||||
I am a tooltip
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<Tooltip /> 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`] = `
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
data-testid="target-parent"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-testid="target"
|
|
||||||
id="my-target"
|
|
||||||
>
|
|
||||||
Target Text
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<Tooltip /> when specifying a tooltip for a component with show on parent hover when hovering over the target's parent element renders 1`] = `
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
data-testid="target-parent"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-testid="target"
|
|
||||||
id="my-target"
|
|
||||||
>
|
|
||||||
Target Text
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="Tooltip right visible"
|
|
||||||
data-testid="tooltip"
|
|
||||||
role="tooltip"
|
|
||||||
style="left: 10px; top: 0px;"
|
|
||||||
>
|
|
||||||
I am a tooltip
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<Tooltip /> 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`] = `
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
data-testid="target-parent"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-testid="target"
|
|
||||||
id="my-target"
|
|
||||||
>
|
|
||||||
Target Text
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`;
|
|
||||||
@ -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<TooltipPosition>([
|
|
||||||
...[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,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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, "width" | "height">,
|
|
||||||
): RectangleDimensions => {
|
|
||||||
assert(parts.right >= parts.left);
|
|
||||||
assert(parts.bottom >= parts.top);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...parts,
|
|
||||||
width: parts.right - parts.left,
|
|
||||||
height: parts.bottom - parts.top,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("<Tooltip />", () => {
|
|
||||||
let requestAnimationFrameSpy: jest.SpyInstance<number, [callback: FrameRequestCallback]>;
|
|
||||||
|
|
||||||
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(
|
|
||||||
<>
|
|
||||||
<Tooltip targetId="my-target" usePortal={false}>
|
|
||||||
I am a tooltip
|
|
||||||
</Tooltip>
|
|
||||||
<div id="my-target">Target Text</div>
|
|
||||||
</>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.baseElement).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders to DOM when hovering over target", () => {
|
|
||||||
const result = render(
|
|
||||||
<>
|
|
||||||
<Tooltip targetId="my-target" data-testid="tooltip" usePortal={false}>
|
|
||||||
I am a tooltip
|
|
||||||
</Tooltip>
|
|
||||||
<div id="my-target">Target Text</div>
|
|
||||||
</>,
|
|
||||||
);
|
|
||||||
|
|
||||||
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(
|
|
||||||
<>
|
|
||||||
<Tooltip targetId="my-target" data-testid="tooltip" visible={true} usePortal={false}>
|
|
||||||
I am a tooltip
|
|
||||||
</Tooltip>
|
|
||||||
<div id="my-target">Target Text</div>
|
|
||||||
</>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.baseElement).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses a portal if usePortal is specified", () => {
|
|
||||||
const result = render(
|
|
||||||
<div>
|
|
||||||
<Tooltip targetId="my-target" data-testid="tooltip" visible={true} usePortal>
|
|
||||||
I am a tooltip
|
|
||||||
</Tooltip>
|
|
||||||
<div id="my-target">Target Text</div>
|
|
||||||
</div>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.baseElement).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when specifying a tooltip for a component", () => {
|
|
||||||
let result: RenderResult;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
result = render(
|
|
||||||
<div>
|
|
||||||
<Tooltip targetId="my-target" data-testid="tooltip">
|
|
||||||
I am a tooltip
|
|
||||||
</Tooltip>
|
|
||||||
<div id="my-target" data-testid="target">
|
|
||||||
Target Text
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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(
|
|
||||||
<div>
|
|
||||||
<Tooltip targetId="my-target" data-testid="tooltip" tooltipOnParentHover>
|
|
||||||
I am a tooltip
|
|
||||||
</Tooltip>
|
|
||||||
<div data-testid="target-parent">
|
|
||||||
<div id="my-target" data-testid="target">
|
|
||||||
Target Text
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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(
|
|
||||||
<>
|
|
||||||
<Tooltip targetId="my-target" data-testid="tooltip" tooltipOnParentHover>
|
|
||||||
I am a tooltip
|
|
||||||
</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,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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<TooltipProps & typeof defaultProps> {
|
|
||||||
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 = (
|
|
||||||
<div
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
ref={(elem) => (this.elem = elem)}
|
|
||||||
role="tooltip"
|
|
||||||
data-testid={this.props["data-testid"]}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (usePortal) {
|
|
||||||
return createPortal(tooltip, document.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tooltip;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Tooltip = DefaultedTooltip as React.ComponentClass<TooltipProps>;
|
|
||||||
@ -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<TooltipProps, "targetId">;
|
|
||||||
/**
|
|
||||||
* 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<React.ReactNode>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withTooltip<TargetProps>(
|
|
||||||
Target: TargetProps extends Pick<TooltipDecoratorProps, "id" | "children">
|
|
||||||
? React.FunctionComponent<TargetProps>
|
|
||||||
: never,
|
|
||||||
): React.FunctionComponent<TargetProps & TooltipDecoratorProps> {
|
|
||||||
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 = (
|
|
||||||
<>
|
|
||||||
<div>{targetChildren}</div>
|
|
||||||
<Tooltip {...tooltipProps} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Target id={targetId} {...(targetProps as any)}>
|
|
||||||
{targetChildren}
|
|
||||||
</Target>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
DecoratedComponent.displayName = `withTooltip(${Target.displayName || Target.name})`;
|
|
||||||
|
|
||||||
return DecoratedComponent;
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user