diff --git a/package-lock.json b/package-lock.json index af02cb63f..c56447b65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3544,6 +3544,15 @@ "cosmiconfig": "^5.2.0", "lodash": "4.17.15", "resolve-from": "^5.0.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true, + "optional": true + } } }, "@commitlint/message": { @@ -3584,6 +3593,15 @@ "lodash": "4.17.15", "resolve-from": "^5.0.0", "resolve-global": "^1.0.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true, + "optional": true + } } }, "@commitlint/rules": { @@ -7935,6 +7953,12 @@ "once": "^1.3.0", "path-is-absolute": "^1.0.0" } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true } } }, @@ -16687,9 +16711,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "lodash._baseisequal": { "version": "3.0.7", diff --git a/src/Dropdown/Dropdown.tsx b/src/Dropdown/Dropdown.tsx index 7e53d1054..06eb1021d 100644 --- a/src/Dropdown/Dropdown.tsx +++ b/src/Dropdown/Dropdown.tsx @@ -330,10 +330,10 @@ const Dropdown: React.FunctionComponent = (props: DropdownProps): return props.placeholders?.emptyText || "Empty"; } if (selectedList && selectedList.length > 0) { - if (allSelected) { - return props.placeholders?.selectAllText || `All selected (${selectedList.length})`; - } if (props.multi) { + if (allSelected) { + return props.placeholders?.selectAllText || `All selected (${selectedList.length})`; + } if (selectedList.length === 1) { return selectedList[0].label; } diff --git a/src/Tooltip/Tooltip.test.tsx b/src/Tooltip/Tooltip.test.tsx index 8179c5ad6..336ce8769 100644 --- a/src/Tooltip/Tooltip.test.tsx +++ b/src/Tooltip/Tooltip.test.tsx @@ -34,67 +34,67 @@ describe("Component: Tooltip", () => { document.body.innerHTML = ""; }); - it("Should render", () => { - act(() => { + it("Should render", async () => { + await act(async () => { render(, container); }); expect(container.querySelector(".tooltip-container")).toBeDefined(); }); - it("Should pass custom class and id", () => { + it("Should pass custom class and id", async () => { const className: string = "myTooltipClass"; const id: string = "myTooltipId"; - act(() => { + await act(async () => { render(, container); }); expect(container.querySelector(".tooltip-container").classList.contains(className)).toBeTruthy(); expect(container.querySelector(".tooltip-container").id).toBe(id); }); - it("Should render reference with custom svg if svg is passed", () => { + it("Should render reference with custom svg if svg is passed", async () => { const content: string = "test"; const svg: JSX.Element = {content}; - act(() => { + await act(async () => { render(, container); }); expect(document.body.querySelectorAll(".default-content").length).toBe(1); expect(document.body.querySelector(".default-content>svg").innerHTML).toEqual(content); }); - it("Should render content", () => { + it("Should render content", async () => { const content: string = "my tooltip"; - act(() => { + await act(async () => { render(, container); }); expect(document.body.querySelectorAll(".tooltip").length).toBe(1); expect(document.body.querySelector(".tooltip-inner").innerHTML).toEqual(content); }); - it("Should render message and default message if no message is passed", () => { + it("Should render message and default message if no message is passed", async () => { const message: string = "my tooltip"; - act(() => { + await act(async () => { render(, container); }); expect(document.body.querySelectorAll(".tooltip").length).toBe(1); expect(document.body.querySelector(".message-container>.message").innerHTML).toEqual("Tooltip is empty. Please pass a message."); - act(() => { + await act(async () => { render(, container); }); expect(document.body.querySelectorAll(".tooltip").length).toBe(1); expect(document.body.querySelector(".message-container>.message").innerHTML).toEqual(message); }); - it("Should render message with title", () => { + it("Should render message with title", async () => { const message: string = "my tooltip"; const title: string = "title"; - act(() => { + await act(async () => { render(, container); }); expect(document.body.querySelectorAll(".tooltip").length).toBe(1); expect(document.body.querySelector(".message-container>.title").innerHTML).toEqual(title); }); - it("Should render message group", () => { + it("Should render message group", async () => { const messageGroup: Array = [ { title: "Tooltip title", @@ -104,16 +104,16 @@ describe("Component: Tooltip", () => { message: "tooltip 2", }, ]; - act(() => { + await act(async () => { render(, container); }); expect(document.body.querySelectorAll(".tooltip").length).toBe(1); expect(document.body.querySelectorAll(".message-list-item").length).toEqual(messageGroup.length); }); - it("Should render node as message", () => { + it("Should render node as message", async () => { const tooltipStr: string = "this is tooltip"; - act(() => { + await act(async () => { render(, container); }); expect(document.body.querySelector(".tooltip-inner").textContent).toBe(tooltipStr); @@ -124,37 +124,37 @@ describe("Component: Tooltip", () => {
tooltip body
); - act(() => { + await act(async () => { render(, container); }); expect(document.body.querySelectorAll(".tooltip").length).toBe(1); expect(document.body.querySelectorAll(`.${customClassName}`).length).toBe(1); }); - it("Should render tooltip with one of the supported themes", () => { + it("Should render tooltip with one of the supported themes", async () => { const theme: TooltipTheme = "danger"; - act(() => { + await act(async () => { render(, container); }); expect(document.body.querySelector(".tooltip").classList.contains("default")).toBeTruthy(); - act(() => { + await act(async () => { render(, container); }); expect(document.body.querySelector(".tooltip").classList.contains(theme)).toBeTruthy(); }); - it("Should call callback on visibility change if callback is set", () => { + it("Should call callback on visibility change if callback is set", async () => { const callback: jest.Mock = jest.fn(); - act(() => { + await act(async () => { render(, container); }); - act(() => { + await act(async () => { container.querySelector(".default-content").dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(callback).toBeCalledTimes(1); }); - it("Should be able to force show and force hide the tooltip from another component", () => { + it("Should be able to force show and force hide the tooltip from another component", async () => { const content: string = "my tooltip"; let myTooltip: Tooltip; let forceShowSpy: jest.SpyInstance; @@ -167,7 +167,7 @@ describe("Component: Tooltip", () => { myTooltip.forceShow(); } }; - act(() => { + await act(async () => { render(
@@ -183,11 +183,11 @@ describe("Component: Tooltip", () => { forceShowSpy = jest.spyOn(myTooltip, "forceShow"); forceDismissSpy = jest.spyOn(myTooltip, "forceDismiss"); }); - act(() => { + await act(async () => { container.querySelector("button").dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(forceShowSpy).toBeCalledTimes(1); - act(() => { + await act(async () => { container.querySelector("button").dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(forceDismissSpy).toBeCalledTimes(1); @@ -225,15 +225,15 @@ describe("Component: Tooltip", () => { }, ]; triggerTestCases.map((testCase: TriggerTestCase) => { - it(`Should enable tooltip on ${testCase.trigger} when trigger mode is set to ${testCase.trigger}`, () => { - act(() => { + it(`Should enable tooltip on ${testCase.trigger} when trigger mode is set to ${testCase.trigger}`, async () => { + await act(async () => { render(, container); }); - act(() => { + await act(async () => { container.querySelector(".default-content").dispatchEvent(testCase.toggleEvent); }); expect(document.body.querySelector(".overlay-container.show")).toBeDefined(); - act(() => { + await act(async () => { document.body.querySelector(testCase.untoggleEventElementClass).dispatchEvent(testCase.untoggleEvent); }); expect(document.body.querySelector(".overlay-container.show")).toBeNull(); @@ -241,14 +241,14 @@ describe("Component: Tooltip", () => { }); }); - it("Should not set auto position if disableAutoPosition is true", () => { - act(() => { + it("Should not set auto position if disableAutoPosition is true", async () => { + await act(async () => { render(, container); }); - act(() => { + await act(async () => { render(, container); }); - act(() => { + await act(async () => { container.querySelector(".default-content").dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(document.body.querySelector(".overlay-container").classList.contains("asd")).toBeTruthy(); @@ -318,9 +318,9 @@ describe("Component: Tooltip", () => { }, ]; for (const testCase of positionTestCases) { - test(`able to render on ${testCase.position}`, () => { + test(`able to render on ${testCase.position}`, async () => { container.style.position = "relative"; - act(() => { + await act(async () => { render(, container); }); (container.querySelector(".tooltip-reference").getBoundingClientRect as any) = jest.fn(() => { @@ -329,7 +329,7 @@ describe("Component: Tooltip", () => { (document.body.querySelector(".tooltip").getBoundingClientRect as any) = jest.fn(() => { return testCase.mockTooltipBoundingClientRect; }); - act(() => { + await act(async () => { container.querySelector(".default-content").dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(document.body.querySelector(".overlay-container").classList.contains(testCase.position)).toBeTruthy(); @@ -413,9 +413,9 @@ describe("Component: Tooltip", () => { }, ]; for (const testCase of positionTestCases) { - test(`able to render on ${testCase.relativePosition} when tooltip is overflow on ${testCase.position}`, () => { + test(`able to render on ${testCase.relativePosition} when tooltip is overflow on ${testCase.position}`, async () => { container.style.position = "relative"; - act(() => { + await act(async () => { render(, container); }); expect(document.body.querySelector(".overlay-container").classList.contains(testCase.position)).toBeTruthy(); @@ -425,7 +425,7 @@ describe("Component: Tooltip", () => { (document.body.querySelector(".tooltip").getBoundingClientRect as any) = jest.fn(() => { return testCase.mockTooltipBoundingClientRect; }); - act(() => { + await act(async () => { container.querySelector(".default-content").dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(document.body.querySelector(".overlay-container").classList.contains(testCase.relativePosition)).toBeTruthy(); @@ -433,16 +433,16 @@ describe("Component: Tooltip", () => { } }); - it("Should retain tooltip if user click on tooltip reference on focus mode", () => { + it("Should retain tooltip if user click on tooltip reference on focus mode", async () => { let tooltip: Tooltip; - act(() => { + await act(async () => { render( (tooltip = e)} content="this is tooltip" trigger="focus" />, container); }); - act(() => { + await act(async () => { container.querySelector(".default-content").dispatchEvent(new Event("focus", { bubbles: true })); }); expect(document.body.querySelector(".overlay-container.show")).toBeDefined(); - act(() => { + await act(async () => { tooltip.onTooltipContentBlur({ relatedTarget: container.querySelector(".default-content") } as any); }); expect(document.body.querySelector(".overlay-container.show")).toBeDefined(); diff --git a/src/__utils/Overlay/Overlay.test.tsx b/src/__utils/Overlay/Overlay.test.tsx index 02d07dd91..2cd114e83 100644 --- a/src/__utils/Overlay/Overlay.test.tsx +++ b/src/__utils/Overlay/Overlay.test.tsx @@ -44,12 +44,12 @@ describe("Component: Overlay", () => { expect(overlayContainer.innerHTML).toEqual(content); }); - it("Should be set to focus on overlay show", () => { + it("Should be set to focus on overlay show", async () => { const newProps: OverlayProps = { ...overlayProps, overlayReference: () => container.querySelector(".ref") }; - act(() => { render(
ref
overlay
, container); }); + await act(async () => { render(
ref
overlay
, container); }); expect(document.body.querySelector(".overlay-container:focus")).toBeFalsy(); const updatedProps: OverlayProps = { ...newProps, show: true }; - act(() => { render(
ref
overlay
, container); }); + await act(async () => { render(
ref
overlay
, container); }); expect(document.body.querySelector(".overlay-container:focus")).toBeTruthy(); }); diff --git a/src/__utils/Overlay/Overlay.tsx b/src/__utils/Overlay/Overlay.tsx index 360ea2467..4a364f7e5 100644 --- a/src/__utils/Overlay/Overlay.tsx +++ b/src/__utils/Overlay/Overlay.tsx @@ -59,22 +59,25 @@ const Overlay: React.FC = React.forwardRef((props: OverlayProps, r * onScroll handler * @param {Event} ev The window scroll event */ - function onScroll(ev: Event): void { + async function onScroll(ev: Event) { const target: HTMLElement = ev.target as HTMLElement; if (props.show && target.contains(props.overlayReference())) { const referenceDomRect: DOMRect = props.overlayReference().getBoundingClientRect(); if (referenceDomRect.bottom < 0 || referenceDomRect.right < 0 || referenceDomRect.left > window.innerWidth || referenceDomRect.top > window.innerHeight) { overlayContentRef?.current?.blur(); } - setPlacementWithCoords(overlayPositionChecker.getPosition(props.position || "top")); + overlayPositionChecker.getPosition(props.position || "top").then((position: ElementPlacementWithCoord) => { + setPlacementWithCoords(position); + }); } } /** Get position within view port */ - function getWithinViewportPosition(disableFocus?: boolean): void { - !disableFocus && overlayContentRef.current.focus(); - const placementCoords: ElementPlacementWithCoord = overlayPositionChecker.getPosition(props.position || "top"); - setPlacementWithCoords(placementCoords); + async function getWithinViewportPosition(disableFocus?: boolean) { + overlayPositionChecker.getPosition(props.position || "top").then((position: ElementPlacementWithCoord) => { + setPlacementWithCoords(position); + !disableFocus && overlayContentRef.current.focus(); + }); } return createPortal( diff --git a/src/__utils/Overlay/placement.test.tsx b/src/__utils/Overlay/placement.test.tsx index 16279e5f5..c71071b1a 100644 --- a/src/__utils/Overlay/placement.test.tsx +++ b/src/__utils/Overlay/placement.test.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { unmountComponentAtNode, render } from "react-dom"; import { act } from "react-dom/test-utils"; -import { OverlayPositionChecker, ElementPosition } from "./placement"; +import { OverlayPositionChecker, ElementPosition, ElementPlacementWithCoord } from "./placement"; // type TriggerTestCase = { // toggleEvent: Event; @@ -36,13 +36,29 @@ describe("Placement class", () => { }); it("Should render", () => { - act(() => { render(
ref
overlay
, container); }); + act(() => { + render( +
+
ref
+
overlay
+
, + container + ); + }); const checker: OverlayPositionChecker = new OverlayPositionChecker(container.querySelector(".reference"), false); expect(checker).toBeDefined(); }); it("Should throw error if overlay container is not set", () => { - act(() => { render(
ref
overlay
, container); }); + act(() => { + render( +
+
ref
+
overlay
+
, + container + ); + }); const checker: OverlayPositionChecker = new OverlayPositionChecker(container.querySelector(".reference"), true); checker.addOverlayContainer(null); try { @@ -53,11 +69,19 @@ describe("Placement class", () => { } }); - it("Should render null position", () => { - act(() => { render(
ref
overlay
, container); }); + it("Should render null position", async () => { + act(() => { + render( +
+
ref
+
overlay
+
, + container + ); + }); const checker: OverlayPositionChecker = new OverlayPositionChecker(container.querySelector(".reference"), true); checker.addOverlayContainer(container.querySelector(".overlay")); - expect(checker.getPosition(null)).toStrictEqual({ coord: null, position: null }); + expect(await checker.getPosition(null)).toStrictEqual({ coord: null, position: null }); }); describe("Should render in all allowed positions", () => { @@ -124,9 +148,17 @@ describe("Placement class", () => { }, ]; for (const testCase of positionTestCases) { - test(`able to render on ${testCase.position}`, () => { + test(`able to render on ${testCase.position}`, async () => { container.style.position = "relative"; - act(() => { render(
ref
overlay
, container); }); + act(() => { + render( +
+
ref
+
overlay
+
, + container + ); + }); const referenceElement: HTMLDivElement = container.querySelector(".reference"); const overlayElement: HTMLDivElement = container.querySelector(".overlay"); const checker: OverlayPositionChecker = new OverlayPositionChecker(referenceElement, false); @@ -137,7 +169,8 @@ describe("Placement class", () => { (overlayElement.getBoundingClientRect as any) = jest.fn(() => { return testCase.mockTooltipBoundingClientRect; }); - expect(checker.getPosition(testCase.position).position === testCase.position).toBeTruthy(); + const coord: ElementPlacementWithCoord = await checker.getPosition(testCase.position); + expect(coord.position === testCase.position).toBeTruthy(); }); } }); @@ -148,79 +181,87 @@ describe("Placement class", () => { position: "top", mockRefBoundingClientRect: { top: -100, bottom: 0, right: 200, left: 200, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "bottom" + relativePosition: "bottom", }, { position: "right", mockRefBoundingClientRect: { top: -100, bottom: 0, right: 768, left: 200, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "bottom" + relativePosition: "bottom", }, { position: "bottom", mockRefBoundingClientRect: { top: 150, bottom: 780, right: 768, left: 200, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: 0, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "top" + relativePosition: "top", }, { position: "left", mockRefBoundingClientRect: { top: -100, bottom: 0, right: 768, left: 0, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "bottom-left" + relativePosition: "bottom-left", }, { position: "top-left", mockRefBoundingClientRect: { top: 200, bottom: 768, right: 200, left: -100, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "right" + relativePosition: "right", }, { position: "top-right", mockRefBoundingClientRect: { top: 0, bottom: 600, right: 1025, left: 0, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: 0, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "bottom-left" + relativePosition: "bottom-left", }, { position: "bottom-left", mockRefBoundingClientRect: { top: 200, bottom: 300, right: 200, left: -500, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "right" + relativePosition: "right", }, { position: "bottom-right", mockRefBoundingClientRect: { top: 200, bottom: 300, right: 1025, left: 300, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "top" + relativePosition: "top", }, { position: "left-top", mockRefBoundingClientRect: { top: 50, bottom: 50, right: 200, left: 0, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "bottom-left" + relativePosition: "bottom-left", }, { position: "left-bottom", mockRefBoundingClientRect: { top: -100, bottom: 0, right: 500, left: -500, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "bottom-right" + relativePosition: "bottom-right", }, { position: "right-top", mockRefBoundingClientRect: { top: -100, bottom: 0, right: 768, left: 200, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "bottom" + relativePosition: "bottom", }, { position: "right-bottom", mockRefBoundingClientRect: { top: -100, bottom: 0, right: window.innerWidth + 200, left: 200, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "bottom" + relativePosition: "bottom", }, ]; for (const testCase of positionTestCases) { - test(`able to render on ${testCase.relativePosition} when tooltip is overflow on ${testCase.position}`, () => { + test(`able to render on ${testCase.relativePosition} when tooltip is overflow on ${testCase.position}`, async () => { container.style.position = "relative"; - act(() => { render(
ref
overlay
, container); }); + act(() => { + render( +
+
ref
+
overlay
+
, + container + ); + }); const referenceElement: HTMLDivElement = container.querySelector(".reference"); const overlayElement: HTMLDivElement = container.querySelector(".overlay"); const checker: OverlayPositionChecker = new OverlayPositionChecker(referenceElement, false); @@ -231,7 +272,8 @@ describe("Placement class", () => { (overlayElement.getBoundingClientRect as any) = jest.fn(() => { return testCase.mockTooltipBoundingClientRect; }); - expect(checker.getPosition(testCase.position).position === testCase.relativePosition).toBeTruthy(); + const coord: ElementPlacementWithCoord = await checker.getPosition(testCase.position); + expect(coord.position === testCase.relativePosition).toBeTruthy(); }); } }); @@ -242,79 +284,87 @@ describe("Placement class", () => { position: "top", mockRefBoundingClientRect: { top: -100, bottom: 0, right: 200, left: 200, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "bottom" + relativePosition: "bottom", }, { position: "right", mockRefBoundingClientRect: { top: -100, bottom: 0, right: 768, left: 200, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "bottom" + relativePosition: "bottom", }, { position: "bottom", mockRefBoundingClientRect: { top: 150, bottom: 780, right: 768, left: 200, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: 0, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "top" + relativePosition: "top", }, { position: "left", mockRefBoundingClientRect: { top: -100, bottom: 0, right: 768, left: 0, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "bottom-left" + relativePosition: "bottom-left", }, { position: "top-left", mockRefBoundingClientRect: { top: 200, bottom: 768, right: 200, left: -100, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "right" + relativePosition: "right", }, { position: "top-right", mockRefBoundingClientRect: { top: 0, bottom: 600, right: 1025, left: 0, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: 0, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "bottom-left" + relativePosition: "bottom-left", }, { position: "bottom-left", mockRefBoundingClientRect: { top: 200, bottom: 300, right: 200, left: -500, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "right" + relativePosition: "right", }, { position: "bottom-right", mockRefBoundingClientRect: { top: 200, bottom: 300, right: 1025, left: 300, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "top" + relativePosition: "top", }, { position: "left-top", mockRefBoundingClientRect: { top: 50, bottom: 50, right: 200, left: 0, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "bottom-left" + relativePosition: "bottom-left", }, { position: "left-bottom", mockRefBoundingClientRect: { top: -100, bottom: 0, right: 500, left: -500, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "bottom-right" + relativePosition: "bottom-right", }, { position: "right-top", mockRefBoundingClientRect: { top: -100, bottom: 0, right: 768, left: 200, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "bottom" + relativePosition: "bottom", }, { position: "right-bottom", mockRefBoundingClientRect: { top: -100, bottom: 0, right: window.innerWidth + 200, left: 200, height: 100, width: 50 }, mockTooltipBoundingClientRect: { top: -100, bottom: 0, right: 0, left: 0, height: 100, width: 100 }, - relativePosition: "bottom" + relativePosition: "bottom", }, ]; for (const testCase of positionTestCases) { - test(`able to render on ${testCase.position} when tooltip is overflow on ${testCase.position}`, () => { + test(`able to render on ${testCase.position} when tooltip is overflow on ${testCase.position}`, async () => { container.style.position = "relative"; - act(() => { render(
ref
overlay
, container); }); + act(() => { + render( +
+
ref
+
overlay
+
, + container + ); + }); const referenceElement: HTMLDivElement = container.querySelector(".reference"); const overlayElement: HTMLDivElement = container.querySelector(".overlay"); const checker: OverlayPositionChecker = new OverlayPositionChecker(referenceElement, false); @@ -326,7 +376,8 @@ describe("Placement class", () => { return testCase.mockTooltipBoundingClientRect; }); checker.disableAutoPlacement(true); - expect(checker.getPosition(testCase.position).position === testCase.position).toBeTruthy(); + const coord: ElementPlacementWithCoord = await checker.getPosition(testCase.position); + expect(coord.position === testCase.position).toBeTruthy(); }); } }); diff --git a/src/__utils/Overlay/placement.ts b/src/__utils/Overlay/placement.ts index ff4e3cd2f..38abf7780 100644 --- a/src/__utils/Overlay/placement.ts +++ b/src/__utils/Overlay/placement.ts @@ -3,20 +3,20 @@ export type ElementPosition = "top" | "bottom" | "left" | "right" | "top-right" type PointPositionLabel = "vertical-center" | "horizontal-center" | "side-top" | "side-bottom" | "side-left" | "side-right" | "bottom" | "left" | "right" | "top"; type ElementPositionCoord = { - left?: number, - top?: number, - right?: number, - bottom?: number + left?: number; + top?: number; + right?: number; + bottom?: number; }; type ElementStylePosition = { - left: string, - top: string + left: string; + top: string; }; type ElementPlacement = { - x: PointPositionLabel, - y: PointPositionLabel + x: PointPositionLabel; + y: PointPositionLabel; }; export type ElementPlacements = { @@ -29,58 +29,71 @@ export type ElementPlacementWithCoord = { }; export const elementPlacements: ElementPlacements = { - "top": { + top: { x: "horizontal-center", - y: "top" + y: "top", }, - "right": { + right: { x: "right", - y: "vertical-center" + y: "vertical-center", }, - "bottom": { + bottom: { x: "horizontal-center", - y: "bottom" + y: "bottom", }, - "left": { + left: { x: "left", - y: "vertical-center" + y: "vertical-center", }, "top-left": { x: "side-left", - y: "top" + y: "top", }, "top-right": { x: "side-right", - y: "top" + y: "top", }, "bottom-left": { x: "side-left", - y: "bottom" + y: "bottom", }, "bottom-right": { x: "side-right", - y: "bottom" + y: "bottom", }, "left-top": { x: "left", - y: "side-top" + y: "side-top", }, "left-bottom": { x: "left", - y: "side-bottom" + y: "side-bottom", }, "right-top": { x: "right", - y: "side-top" + y: "side-top", }, "right-bottom": { x: "right", - y: "side-bottom" + y: "side-bottom", }, }; export class OverlayPositionChecker { - private defaultPositionsList: Array = ["top", "left", "right", "bottom", "bottom-left", "bottom-right", "left-bottom", "left-top", "right-bottom", "right-top", "top-left", "top-right"]; + private defaultPositionsList: Array = [ + "top", + "left", + "right", + "bottom", + "bottom-left", + "bottom-right", + "left-bottom", + "left-top", + "right-bottom", + "right-top", + "top-left", + "top-right", + ]; private referenceElement: HTMLDivElement; private overlayElement: HTMLDivElement; private disableAutoPosition: boolean; @@ -115,14 +128,16 @@ export class OverlayPositionChecker { * @param position selected position * @returns overlay position and coordinates */ - getPosition(position: ElementPosition): ElementPlacementWithCoord { - if (this.disableAutoPosition) { - this.getOverlayPositionCoord(position); - this.currentPlacementWithCoord = this.getPlacement(position); - } else { - this.currentPlacementWithCoord = this.setPositionWithinViewport(position); - } - return this.currentPlacementWithCoord; + async getPosition(position: ElementPosition): Promise { + return new Promise((resolve: (value?: ElementPlacementWithCoord | PromiseLike) => void) => { + if (this.disableAutoPosition) { + this.getOverlayPositionCoord(position); + this.currentPlacementWithCoord = this.getPlacement(position); + } else { + this.currentPlacementWithCoord = this.setPositionWithinViewport(position); + } + resolve(this.currentPlacementWithCoord); + }); } /** @@ -143,6 +158,7 @@ export class OverlayPositionChecker { }); return placementWithCoord; } else { + this.getOverlayPositionCoord(position); return this.getPlacement(position); } } @@ -155,7 +171,7 @@ export class OverlayPositionChecker { if (this.currentPosition) { return { left: `${this.currentPosition.left}px`, - top: `${this.currentPosition.top}px` + top: `${this.currentPosition.top}px`, }; } return null; @@ -179,7 +195,7 @@ export class OverlayPositionChecker { top: top + scrollTop, left: left + scrollLeft, bottom: top + overlayRect.height, - right: left + overlayRect.width + right: left + overlayRect.width, }; } else { this.currentPosition = null; @@ -196,16 +212,35 @@ export class OverlayPositionChecker { const overlayRect: ClientRect = this.overlayElement.getBoundingClientRect(); let calculatedPosition: number = 0; switch (point) { - case "vertical-center": calculatedPosition = referenceRect.bottom - (overlayRect.height / 2) - (referenceRect.height / 2); break; - case "horizontal-center": calculatedPosition = referenceRect.left - (overlayRect.width / 2) + (referenceRect.width / 2); break; - case "side-top": calculatedPosition = referenceRect.bottom - referenceRect.height; break; - case "side-bottom": calculatedPosition = referenceRect.bottom - overlayRect.height; break; - case "side-left": calculatedPosition = referenceRect.left; break; - case "side-right": calculatedPosition = referenceRect.right - overlayRect.width; break; - case "bottom": calculatedPosition = referenceRect.bottom; break; - case "left": calculatedPosition = referenceRect.left - overlayRect.width; break; - case "right": calculatedPosition = referenceRect.right; break; - default: calculatedPosition = referenceRect.top - overlayRect.height; + case "vertical-center": + calculatedPosition = referenceRect.bottom - overlayRect.height / 2 - referenceRect.height / 2; + break; + case "horizontal-center": + calculatedPosition = referenceRect.left - overlayRect.width / 2 + referenceRect.width / 2; + break; + case "side-top": + calculatedPosition = referenceRect.bottom - referenceRect.height; + break; + case "side-bottom": + calculatedPosition = referenceRect.bottom - overlayRect.height; + break; + case "side-left": + calculatedPosition = referenceRect.left; + break; + case "side-right": + calculatedPosition = referenceRect.right - overlayRect.width; + break; + case "bottom": + calculatedPosition = referenceRect.bottom; + break; + case "left": + calculatedPosition = referenceRect.left - overlayRect.width; + break; + case "right": + calculatedPosition = referenceRect.right; + break; + default: + calculatedPosition = referenceRect.top - overlayRect.height; } return calculatedPosition; } @@ -216,8 +251,9 @@ export class OverlayPositionChecker { */ private isElementOverflow(position: ElementPosition): boolean { this.getOverlayPositionCoord(position); - return !this.currentPosition || - (this.currentPosition.left < 0 || this.currentPosition.top < 0 || this.currentPosition.right > window.innerWidth || this.currentPosition.bottom > window.innerHeight); + return ( + !this.currentPosition || this.currentPosition.left < 0 || this.currentPosition.top < 0 || this.currentPosition.right > window.innerWidth || this.currentPosition.bottom > window.innerHeight + ); } /** @@ -228,7 +264,7 @@ export class OverlayPositionChecker { private getPlacement(pos: ElementPosition): ElementPlacementWithCoord { return { coord: this.getNewStyle(), - position: pos + position: pos, }; } }