From 9edb9765db1e9febead948442f31e3893bd174ba Mon Sep 17 00:00:00 2001 From: Anthony Le Courric Date: Thu, 3 Aug 2023 15:38:06 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(react)=20position=20top=20datepicker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Position the datepicker on top of the input when the input is at the bottom of the screen and whenthere is enough space to display the datepicker on the top. --- .changeset/wet-keys-occur.md | 5 + .../src/components/Popover/index.spec.tsx | 121 ++++++++++++++++++ .../src/components/Popover/index.stories.tsx | 40 +++--- .../react/src/components/Popover/index.tsx | 49 ++++++- 4 files changed, 198 insertions(+), 17 deletions(-) create mode 100644 .changeset/wet-keys-occur.md create mode 100644 packages/react/src/components/Popover/index.spec.tsx diff --git a/.changeset/wet-keys-occur.md b/.changeset/wet-keys-occur.md new file mode 100644 index 0000000..12ec744 --- /dev/null +++ b/.changeset/wet-keys-occur.md @@ -0,0 +1,5 @@ +--- +"@openfun/cunningham-react": minor +--- + +Position Datepicker popover on top or bottom depending space available diff --git a/packages/react/src/components/Popover/index.spec.tsx b/packages/react/src/components/Popover/index.spec.tsx new file mode 100644 index 0000000..e5df328 --- /dev/null +++ b/packages/react/src/components/Popover/index.spec.tsx @@ -0,0 +1,121 @@ +import React, { useRef } from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Popover } from "."; + +interface TestComponentProps { + borderless?: boolean; + onClickOutside?: () => void; +} +const TestComponent = ({ + borderless, + onClickOutside = () => {}, +}: TestComponentProps) => { + const parentRef = useRef(null); + + return ( +
+
Other container
+
Parent container
+ + Hello Popover + +
+ ); +}; + +describe("", () => { + it("checks the render", async () => { + render(); + + expect(screen.getByText("Parent container")).toBeInTheDocument(); + expect(screen.getByText("Hello Popover")).toBeInTheDocument(); + expect(screen.queryByRole("dialog")).not.toHaveClass( + "c__popover--borderless", + ); + }); + + it("checks the borderless prop", async () => { + render(); + expect(screen.getByRole("dialog")).toHaveClass("c__popover--borderless"); + }); + + it("checks onClickOutside prop", async () => { + const mockOnClickOutside = vi.fn(); + render(); + + await userEvent.click(screen.getByText("Parent container")); + expect(mockOnClickOutside).not.toHaveBeenCalled(); + + await userEvent.click(screen.getByText("Other container")); + expect(mockOnClickOutside).toHaveBeenCalledTimes(1); + }); + + it("checks popover position top with parent not as origin", async () => { + // by default window.innerHeight === 768 + const ref = React.createRef() as any; + ref.current = { + offsetTop: 400, + clientHeight: 100, + getBoundingClientRect: () => ({ + top: 700, + }), + }; + + render( {}} parentRef={ref} />); + + waitFor( + () => { + expect(screen.getByRole("dialog")).toHaveStyle({ + top: "400px", + }); + }, + { timeout: 1000 }, + ); + }); + + it("checks popover position top with parent as origin", async () => { + // by default window.innerHeight === 768 + const ref = React.createRef() as any; + ref.current = { + offsetTop: 400, + clientHeight: 0, + getBoundingClientRect: () => ({ + top: 800, + }), + }; + + render( {}} parentRef={ref} />); + + waitFor( + () => { + expect(screen.getByRole("dialog")).toHaveStyle({ + top: "0px", + }); + }, + { timeout: 1000 }, + ); + }); + + it("checks popover position bottom", async () => { + // by default window.innerHeight === 768 + const ref = React.createRef() as any; + ref.current = { + offsetTop: 500, + clientHeight: 100, + getBoundingClientRect: () => ({ + top: 200, + }), + }; + + render( {}} parentRef={ref} />); + + expect(await screen.findByRole("dialog")).toHaveStyle({ + top: undefined, + }); + }); +}); diff --git a/packages/react/src/components/Popover/index.stories.tsx b/packages/react/src/components/Popover/index.stories.tsx index 6998933..c7aac27 100644 --- a/packages/react/src/components/Popover/index.stories.tsx +++ b/packages/react/src/components/Popover/index.stories.tsx @@ -14,23 +14,31 @@ export const Default = () => { const parentRef = useRef(null); return ( -
- - {isOpen && ( - setIsOpen(false)}> -
+
+ + {isOpen && ( + setIsOpen(false)} > - I am open -
- - )} +
+ I am open +
+ + )} +
); }; diff --git a/packages/react/src/components/Popover/index.tsx b/packages/react/src/components/Popover/index.tsx index db42a4a..15ba616 100644 --- a/packages/react/src/components/Popover/index.tsx +++ b/packages/react/src/components/Popover/index.tsx @@ -1,4 +1,10 @@ -import React, { PropsWithChildren, RefObject } from "react"; +import React, { + PropsWithChildren, + RefObject, + useLayoutEffect, + useRef, + useState, +} from "react"; import classNames from "classnames"; import { useHandleClickOutside } from ":/hooks/useHandleClickOutside"; @@ -14,13 +20,54 @@ export const Popover = ({ onClickOutside, borderless = false, }: PopoverProps) => { + const popoverRef = useRef(null); useHandleClickOutside(parentRef, onClickOutside); + const timeout = useRef>(); + const [topPosition, setTopPosition] = useState(); + + useLayoutEffect(() => { + const setPopoverTopPosition = () => { + if (!parentRef.current || !popoverRef.current) return; + + const parentBounds = parentRef.current.getBoundingClientRect(); + const popoverBounds = popoverRef.current.getBoundingClientRect(); + + const hasNotEnoughBottomPlace = + window.innerHeight - parentBounds.bottom < popoverBounds.height; + + const hasEnoughTopPlace = parentBounds.top >= popoverBounds.height; + + if (hasNotEnoughBottomPlace && hasEnoughTopPlace) { + setTopPosition(-popoverBounds.height); + } else { + setTopPosition(undefined); + } + }; + + const handleWindowResize = () => { + if (timeout.current) clearTimeout(timeout.current); + timeout.current = setTimeout(setPopoverTopPosition, 1000 / 30); + }; + + window.addEventListener("resize", handleWindowResize); + setPopoverTopPosition(); + + return () => { + window.removeEventListener("resize", handleWindowResize); + if (timeout.current) clearTimeout(timeout.current); + }; + }, []); return (
{children}