✨(react) position top datepicker
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.
This commit is contained in:
committed by
Jean-Baptiste PENRATH
parent
6ea8544fed
commit
9edb9765db
5
.changeset/wet-keys-occur.md
Normal file
5
.changeset/wet-keys-occur.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@openfun/cunningham-react": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Position Datepicker popover on top or bottom depending space available
|
||||||
121
packages/react/src/components/Popover/index.spec.tsx
Normal file
121
packages/react/src/components/Popover/index.spec.tsx
Normal file
@@ -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<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>Other container</div>
|
||||||
|
<div ref={parentRef}>Parent container</div>
|
||||||
|
<Popover
|
||||||
|
parentRef={parentRef}
|
||||||
|
onClickOutside={onClickOutside}
|
||||||
|
borderless={borderless}
|
||||||
|
>
|
||||||
|
Hello Popover
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("<Popover />", () => {
|
||||||
|
it("checks the render", async () => {
|
||||||
|
render(<TestComponent />);
|
||||||
|
|
||||||
|
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(<TestComponent borderless />);
|
||||||
|
expect(screen.getByRole("dialog")).toHaveClass("c__popover--borderless");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("checks onClickOutside prop", async () => {
|
||||||
|
const mockOnClickOutside = vi.fn();
|
||||||
|
render(<TestComponent onClickOutside={mockOnClickOutside} />);
|
||||||
|
|
||||||
|
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<HTMLDivElement>() as any;
|
||||||
|
ref.current = {
|
||||||
|
offsetTop: 400,
|
||||||
|
clientHeight: 100,
|
||||||
|
getBoundingClientRect: () => ({
|
||||||
|
top: 700,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<Popover onClickOutside={() => {}} 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<HTMLDivElement>() as any;
|
||||||
|
ref.current = {
|
||||||
|
offsetTop: 400,
|
||||||
|
clientHeight: 0,
|
||||||
|
getBoundingClientRect: () => ({
|
||||||
|
top: 800,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<Popover onClickOutside={() => {}} 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<HTMLDivElement>() as any;
|
||||||
|
ref.current = {
|
||||||
|
offsetTop: 500,
|
||||||
|
clientHeight: 100,
|
||||||
|
getBoundingClientRect: () => ({
|
||||||
|
top: 200,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<Popover onClickOutside={() => {}} parentRef={ref} />);
|
||||||
|
|
||||||
|
expect(await screen.findByRole("dialog")).toHaveStyle({
|
||||||
|
top: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,10 +14,17 @@ export const Default = () => {
|
|||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={parentRef} style={{ width: "fit-content" }}>
|
<div style={{ display: "grid", height: "120vh", placeItems: "center" }}>
|
||||||
|
<div
|
||||||
|
ref={parentRef}
|
||||||
|
style={{ width: "fit-content", position: "relative" }}
|
||||||
|
>
|
||||||
<Button onClick={() => setIsOpen(!isOpen)}>Toggle</Button>
|
<Button onClick={() => setIsOpen(!isOpen)}>Toggle</Button>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<Popover parentRef={parentRef} onClickOutside={() => setIsOpen(false)}>
|
<Popover
|
||||||
|
parentRef={parentRef}
|
||||||
|
onClickOutside={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: "200px",
|
height: "200px",
|
||||||
@@ -32,5 +39,6 @@ export const Default = () => {
|
|||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import React, { PropsWithChildren, RefObject } from "react";
|
import React, {
|
||||||
|
PropsWithChildren,
|
||||||
|
RefObject,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useHandleClickOutside } from ":/hooks/useHandleClickOutside";
|
import { useHandleClickOutside } from ":/hooks/useHandleClickOutside";
|
||||||
|
|
||||||
@@ -14,13 +20,54 @@ export const Popover = ({
|
|||||||
onClickOutside,
|
onClickOutside,
|
||||||
borderless = false,
|
borderless = false,
|
||||||
}: PopoverProps) => {
|
}: PopoverProps) => {
|
||||||
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
useHandleClickOutside(parentRef, onClickOutside);
|
useHandleClickOutside(parentRef, onClickOutside);
|
||||||
|
const timeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
const [topPosition, setTopPosition] = useState<number | undefined>();
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={popoverRef}
|
||||||
className={classNames("c__popover", {
|
className={classNames("c__popover", {
|
||||||
"c__popover--borderless": borderless,
|
"c__popover--borderless": borderless,
|
||||||
})}
|
})}
|
||||||
|
style={{
|
||||||
|
top: topPosition,
|
||||||
|
}}
|
||||||
|
role="dialog"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user