mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
chore: Fix unit tests by increasing coverage of tooltip to 100%
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
1eeddbb377
commit
9656b8c577
24
package-lock.json
generated
24
package-lock.json
generated
@ -36730,14 +36730,12 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@async-fn/jest": "^1.6.4",
|
"@async-fn/jest": "^1.6.4",
|
||||||
"@k8slens/eslint-config": "6.5.0-alpha.1",
|
"@k8slens/eslint-config": "6.5.0-alpha.1",
|
||||||
"@k8slens/react-testing-library-discovery": "^1.0.0-alpha.0"
|
"@k8slens/react-testing-library-discovery": "^1.0.0-alpha.0",
|
||||||
|
"@testing-library/react": "^12.1.5",
|
||||||
|
"@testing-library/user-event": "^12.8.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@k8slens/feature-core": "^6.5.0-alpha.0",
|
|
||||||
"@k8slens/utilities": "^1.0.0-alpha.1",
|
"@k8slens/utilities": "^1.0.0-alpha.1",
|
||||||
"@ogre-tools/fp": "^15.1.2",
|
|
||||||
"@ogre-tools/injectable": "^15.1.2",
|
|
||||||
"@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2",
|
|
||||||
"auto-bind": "^4.0.0",
|
"auto-bind": "^4.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mobx": "^6.8.0",
|
"mobx": "^6.8.0",
|
||||||
@ -36774,6 +36772,22 @@
|
|||||||
"prettier": ">= 2"
|
"prettier": ">= 2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/ui-components/tooltip/node_modules/@testing-library/user-event": {
|
||||||
|
"version": "12.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-12.8.3.tgz",
|
||||||
|
"integrity": "sha512-IR0iWbFkgd56Bu5ZI/ej8yQwrkCv8Qydx6RzwbKz9faXazR/+5tvYKsZQgyXJiwgpcva127YO6JcWy7YlCfofQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10",
|
||||||
|
"npm": ">=6"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@testing-library/dom": ">=7.21.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/utility-features/react-testing-library-discovery": {
|
"packages/utility-features/react-testing-library-discovery": {
|
||||||
"name": "@k8slens/react-testing-library-discovery",
|
"name": "@k8slens/react-testing-library-discovery",
|
||||||
"version": "1.0.0-alpha.3",
|
"version": "1.0.0-alpha.3",
|
||||||
|
|||||||
@ -1,13 +1,3 @@
|
|||||||
const { configForReact } = require("@k8slens/jest").monorepoPackageConfig(__dirname);
|
const { configForReact } = require("@k8slens/jest").monorepoPackageConfig(__dirname);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = configForReact;
|
||||||
...configForReact,
|
|
||||||
coverageThreshold: {
|
|
||||||
global: {
|
|
||||||
statements: 84,
|
|
||||||
branches: 67,
|
|
||||||
lines: 84,
|
|
||||||
functions: 92,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@ -30,11 +30,7 @@
|
|||||||
"lint:fix": "lens-lint --fix"
|
"lint:fix": "lens-lint --fix"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@k8slens/feature-core": "^6.5.0-alpha.0",
|
|
||||||
"@k8slens/utilities": "^1.0.0-alpha.1",
|
"@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",
|
"auto-bind": "^4.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mobx": "^6.8.0",
|
"mobx": "^6.8.0",
|
||||||
@ -45,6 +41,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@async-fn/jest": "^1.6.4",
|
"@async-fn/jest": "^1.6.4",
|
||||||
"@k8slens/eslint-config": "6.5.0-alpha.1",
|
"@k8slens/eslint-config": "6.5.0-alpha.1",
|
||||||
"@k8slens/react-testing-library-discovery": "^1.0.0-alpha.0"
|
"@k8slens/react-testing-library-discovery": "^1.0.0-alpha.0",
|
||||||
|
"@testing-library/react": "^12.1.5",
|
||||||
|
"@testing-library/user-event": "^12.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ exports[`<Tooltip /> renders to DOM when forced to by visible prop 1`] = `
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="Tooltip right visible"
|
class="Tooltip right visible"
|
||||||
|
data-testid="tooltip"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
style="left: 10px; top: 0px;"
|
style="left: 10px; top: 0px;"
|
||||||
>
|
>
|
||||||
@ -36,6 +37,7 @@ exports[`<Tooltip /> renders to DOM when hovering over target 1`] = `
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="Tooltip right visible"
|
class="Tooltip right visible"
|
||||||
|
data-testid="tooltip"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
style="left: 10px; top: 0px;"
|
style="left: 10px; top: 0px;"
|
||||||
>
|
>
|
||||||
@ -49,3 +51,195 @@ exports[`<Tooltip /> renders to DOM when hovering over target 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</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>
|
||||||
|
`;
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`withTooltip tests does not render a tooltip when not specified 1`] = `
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
foobar
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`withTooltip tests renders a tooltip when specified 1`] = `
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
foobar
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`withTooltip tests renders a tooltip when specified via tooltip props 1`] = `
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
foobar
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`withTooltip tests renders a tooltip when specified without id 1`] = `
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
foobar
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
`;
|
||||||
141
packages/ui-components/tooltip/src/helpers.ts
Normal file
141
packages/ui-components/tooltip/src/helpers.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -4,10 +4,25 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
|
import type { RenderResult } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Tooltip } from "./tooltip";
|
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 />", () => {
|
describe("<Tooltip />", () => {
|
||||||
let requestAnimationFrameSpy: jest.SpyInstance<number, [callback: FrameRequestCallback]>;
|
let requestAnimationFrameSpy: jest.SpyInstance<number, [callback: FrameRequestCallback]>;
|
||||||
@ -69,4 +84,331 @@ describe("<Tooltip />", () => {
|
|||||||
|
|
||||||
expect(result.baseElement).toMatchSnapshot();
|
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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import type { IClassName } from "@k8slens/utilities";
|
|||||||
import { cssNames } from "@k8slens/utilities";
|
import { cssNames } from "@k8slens/utilities";
|
||||||
import { observable, makeObservable, action, runInAction } from "mobx";
|
import { observable, makeObservable, action, runInAction } from "mobx";
|
||||||
import autoBindReact from "auto-bind/react";
|
import autoBindReact from "auto-bind/react";
|
||||||
|
import { computeNextPosition } from "./helpers";
|
||||||
|
|
||||||
export enum TooltipPosition {
|
export enum TooltipPosition {
|
||||||
TOP = "top",
|
TOP = "top",
|
||||||
@ -35,6 +36,7 @@ export interface TooltipProps {
|
|||||||
formatters?: TooltipContentFormatters;
|
formatters?: TooltipContentFormatters;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
"data-testid"?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TooltipContentFormatters {
|
export interface TooltipContentFormatters {
|
||||||
@ -108,54 +110,26 @@ class DefaultedTooltip extends React.Component<TooltipProps & typeof defaultProp
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshPosition() {
|
refreshPosition() {
|
||||||
const { preferredPositions } = this.props;
|
const { preferredPositions, offset } = this.props;
|
||||||
const { elem, targetElem } = this;
|
const { elem, targetElem } = this;
|
||||||
|
|
||||||
if (!elem || !targetElem) {
|
if (!elem || !targetElem) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const positions = new Set<TooltipPosition>([
|
|
||||||
...[preferredPositions ?? []].flat(),
|
|
||||||
TooltipPosition.RIGHT,
|
|
||||||
TooltipPosition.BOTTOM,
|
|
||||||
TooltipPosition.TOP,
|
|
||||||
TooltipPosition.LEFT,
|
|
||||||
TooltipPosition.TOP_RIGHT,
|
|
||||||
TooltipPosition.TOP_LEFT,
|
|
||||||
TooltipPosition.BOTTOM_RIGHT,
|
|
||||||
TooltipPosition.BOTTOM_LEFT,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// reset position first and get all possible client-rect area for tooltip element
|
|
||||||
this.setPosition(elem, { left: 0, top: 0 });
|
this.setPosition(elem, { left: 0, top: 0 });
|
||||||
|
|
||||||
const selfBounds = elem.getBoundingClientRect();
|
const { position, ...location } = computeNextPosition({
|
||||||
const targetBounds = targetElem.getBoundingClientRect();
|
offset,
|
||||||
const { innerWidth: viewportWidth, innerHeight: viewportHeight } = window;
|
preferredPositions,
|
||||||
|
target: targetElem,
|
||||||
// find proper position
|
tooltip: elem,
|
||||||
for (const pos of positions) {
|
|
||||||
const { left, top, right, bottom } = this.getPosition(pos, selfBounds, targetBounds);
|
|
||||||
const fitsToWindow =
|
|
||||||
left >= 0 && top >= 0 && right <= viewportWidth && bottom <= viewportHeight;
|
|
||||||
|
|
||||||
if (fitsToWindow) {
|
|
||||||
runInAction(() => {
|
|
||||||
this.activePosition = pos;
|
|
||||||
this.setPosition(elem, { top, left });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
runInAction(() => {
|
||||||
}
|
this.activePosition = position;
|
||||||
}
|
this.setPosition(elem, location);
|
||||||
|
});
|
||||||
// apply fallback position if nothing helped from above
|
|
||||||
const fallbackPosition = Array.from(positions)[0];
|
|
||||||
const { left, top } = this.getPosition(fallbackPosition, selfBounds, targetBounds);
|
|
||||||
|
|
||||||
this.activePosition = fallbackPosition;
|
|
||||||
this.setPosition(elem, { left, top });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setPosition(elem: HTMLDivElement, pos: { left: number; top: number }) {
|
protected setPosition(elem: HTMLDivElement, pos: { left: number; top: number }) {
|
||||||
@ -163,60 +137,6 @@ class DefaultedTooltip extends React.Component<TooltipProps & typeof defaultProp
|
|||||||
elem.style.top = `${pos.top}px`;
|
elem.style.top = `${pos.top}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getPosition(position: TooltipPosition, tooltipBounds: DOMRect, targetBounds: DOMRect) {
|
|
||||||
let left: number;
|
|
||||||
let top: number;
|
|
||||||
const offset = this.props.offset;
|
|
||||||
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;
|
|
||||||
|
|
||||||
switch (position) {
|
|
||||||
case "top":
|
|
||||||
left = horizontalCenter;
|
|
||||||
top = topCenter;
|
|
||||||
break;
|
|
||||||
case "bottom":
|
|
||||||
left = horizontalCenter;
|
|
||||||
top = bottomCenter;
|
|
||||||
break;
|
|
||||||
case "left":
|
|
||||||
top = verticalCenter;
|
|
||||||
left = targetBounds.left - tooltipBounds.width - offset;
|
|
||||||
break;
|
|
||||||
case "right":
|
|
||||||
top = verticalCenter;
|
|
||||||
left = targetBounds.right + offset;
|
|
||||||
break;
|
|
||||||
case "top_left":
|
|
||||||
left = targetBounds.left;
|
|
||||||
top = topCenter;
|
|
||||||
break;
|
|
||||||
case "top_right":
|
|
||||||
left = targetBounds.right - tooltipBounds.width;
|
|
||||||
top = topCenter;
|
|
||||||
break;
|
|
||||||
case "bottom_left":
|
|
||||||
top = bottomCenter;
|
|
||||||
left = targetBounds.left;
|
|
||||||
break;
|
|
||||||
case "bottom_right":
|
|
||||||
top = bottomCenter;
|
|
||||||
left = targetBounds.right - tooltipBounds.width;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new TypeError("Invalid props.position value");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
right: left + tooltipBounds.width,
|
|
||||||
bottom: top + tooltipBounds.height,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { style, formatters, usePortal, children, visible = this.isVisible } = this.props;
|
const { style, formatters, usePortal, children, visible = this.isVisible } = this.props;
|
||||||
|
|
||||||
@ -229,7 +149,13 @@ class DefaultedTooltip extends React.Component<TooltipProps & typeof defaultProp
|
|||||||
formatter: !!formatters,
|
formatter: !!formatters,
|
||||||
});
|
});
|
||||||
const tooltip = (
|
const tooltip = (
|
||||||
<div className={className} style={style} ref={(elem) => (this.elem = elem)} role="tooltip">
|
<div
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
ref={(elem) => (this.elem = elem)}
|
||||||
|
role="tooltip"
|
||||||
|
data-testid={this.props["data-testid"]}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
45
packages/ui-components/tooltip/src/withTooltip.test.tsx
Normal file
45
packages/ui-components/tooltip/src/withTooltip.test.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { SingleOrMany } from "@k8slens/utilities";
|
||||||
|
import { render } from "@testing-library/react";
|
||||||
|
import React from "react";
|
||||||
|
import { withTooltip } from "./withTooltip";
|
||||||
|
|
||||||
|
type MyComponentProps = {
|
||||||
|
text: string;
|
||||||
|
id?: string;
|
||||||
|
children?: SingleOrMany<React.ReactNode>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MyComponent = withTooltip(({ text }: MyComponentProps) => <div>{text}</div>);
|
||||||
|
|
||||||
|
describe("withTooltip tests", () => {
|
||||||
|
it("does not render a tooltip when not specified", () => {
|
||||||
|
const result = render(<MyComponent text="foobar" />);
|
||||||
|
|
||||||
|
expect(result.baseElement).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a tooltip when specified", () => {
|
||||||
|
const result = render(<MyComponent text="foobar" tooltip="my-tooltip" id="bat" />);
|
||||||
|
|
||||||
|
expect(result.baseElement).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a tooltip when specified via tooltip props", () => {
|
||||||
|
const result = render(
|
||||||
|
<MyComponent text="foobar" tooltip={{ children: "my-tooltip" }} id="bat" />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.baseElement).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a tooltip when specified without id", () => {
|
||||||
|
const result = render(<MyComponent text="foobar" tooltip="my-tooltip" />);
|
||||||
|
|
||||||
|
expect(result.baseElement).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -3,8 +3,6 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Tooltip decorator for simple composition with other components
|
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import type { TooltipProps } from "./tooltip";
|
import type { TooltipProps } from "./tooltip";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user