Skip to content

[♻️]: Refactor to defer opening state of poppers to client-side #49

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions lib/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@ type OwnProps = {
*/
className?: string | ((ctx: { open: boolean }) => string);
/**
* The anchor element for the menu.
* A function that will resolve the anchor element for the menu.
*
* It has to return `HTMLElement`, or a `VirtualElement`, or `null`.
* A VirtualElement is an object that implements `getBoundingClientRect(): ClientRect`.
*
* If nothing is resolved, the menu won't show up.
*
* Please note that this function is only called on the client-side.
*/
anchorElement:
| React.RefObject<HTMLElement>
| HTMLElement
| VirtualElement
| string;
resolveAnchor: () => HTMLElement | VirtualElement | null;
/**
* The menu positioning alignment.
* @default "start"
Expand Down Expand Up @@ -82,7 +85,7 @@ const MenuBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
className: classNameProp,
children: childrenProp,
alignment = "start",
anchorElement,
resolveAnchor,
keepMounted: keepMountedProp,
disabledKeySearch = false,
open = false,
Expand Down Expand Up @@ -362,14 +365,12 @@ const MenuBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
? classNameProp({ open })
: classNameProp;

if (!anchorElement) return null;

return (
<Popper
autoPlacement
keepMounted={keepMounted}
open={open}
anchorElement={anchorElement}
resolveAnchor={resolveAnchor}
computationMiddlewareOrder="afterAutoPlacement"
computationMiddleware={popperComputationMiddleware}
offset={0}
Expand Down
4 changes: 3 additions & 1 deletion lib/Menu/components/Sub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const SubBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {

if (!menuItemCtx) return null;

const resolveAnchor = () => document.getElementById(menuItemCtx.id);

menuItemCtx.registerSubMenu(rootRef, id);

return (
Expand All @@ -40,7 +42,7 @@ const SubBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
id={id}
ref={handleRootRef}
className={className}
anchorElement={menuItemCtx.id}
resolveAnchor={resolveAnchor}
data-slot={SubRootSlot}
data-submenu
>
Expand Down
48 changes: 19 additions & 29 deletions lib/Popper/Popper.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import * as React from "react";
import Portal from "../Portal";
import { SystemError } from "../internals";
import type { MergeElementProps, RequireOnlyOne } from "../types";
import {
componentWithForwardedRef,
useDeterministicId,
useDirection,
useForkedRefs,
useIsomorphicLayoutEffect,
useIsomorphicValue,
useRegisterNodeRef,
type ClientRect,
} from "../utils";
import * as Slots from "./slots";
import { computePosition, getAnchor, translate } from "./utils";
import { computePosition, translate } from "./utils";

export type Alignment = "start" | "end";
export type Side = "top" | "right" | "bottom" | "left";
Expand Down Expand Up @@ -162,14 +162,16 @@ type OwnProps = {
recompute: () => void;
}>;
/**
* Works as an anchor for the popper.\
* This enables things like positioning context menus or following the cursor.
* A function that will resolve the anchor element for the popper.
*
* It has to return `HTMLElement`, or a `VirtualElement`, or `null`.
* A VirtualElement is an object that implements `getBoundingClientRect(): ClientRect`.
*
* If nothing is resolved, the popper won't show up.
*
* Please note that this function is only called on the client-side.
*/
anchorElement:
| React.RefObject<HTMLElement>
| HTMLElement
| VirtualElement
| string;
resolveAnchor: () => HTMLElement | VirtualElement | null;
/**
* Used to keep mounting when more control is needed.\
* Useful when controlling animation with React animation libraries.
Expand All @@ -189,14 +191,14 @@ export type Props = Omit<

const PopperBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
const {
open,
open: openProp,
actions,
style: styleProp,
id: idProp,
className: classNameProp,
children: childrenProp,
computationMiddleware,
anchorElement,
resolveAnchor,
offset = 8,
side = "top",
keepMounted = false,
Expand All @@ -207,18 +209,7 @@ const PopperBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
...otherProps
} = props;

if (!anchorElement) {
throw new SystemError(
[
"Invalid `anchorElement` property.",
"The `anchorElement` property must be either a `id (string)`, " +
"`HTMLElement`, " +
"`RefObject<HTMLElement>`, or in shape of " +
"`VirtualElement { getBoundingClientRect(): ClientRect }`",
].join("\n"),
"Popper",
);
}
const open = useIsomorphicValue(openProp, false);

const isRtl = useDirection() === "rtl";
const id = useDeterministicId(idProp, "styleless-ui__popper");
Expand Down Expand Up @@ -248,7 +239,7 @@ const PopperBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
};

const updatePosition = () => {
const anchor = getAnchor(anchorElement);
const anchor = resolveAnchor();

if (anchor && popperRef.current) {
const position = computePosition(anchor, popperRef.current, config);
Expand All @@ -267,9 +258,8 @@ const PopperBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
}));

useIsomorphicLayoutEffect(() => {
const anchor = getAnchor(anchorElement);
const anchor = resolveAnchor();

if (!anchor) return;
if (id && anchor instanceof HTMLElement) {
anchor.setAttribute("aria-describedby", id);
}
Expand Down Expand Up @@ -312,12 +302,12 @@ const PopperBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
>
<div
{...otherProps}
tabIndex={-1}
data-slot={Slots.Root}
id={id}
className={className}
ref={registerRef}
style={style}
className={className}
tabIndex={-1}
data-slot={Slots.Root}
data-open={open ? "" : undefined}
>
{children}
Expand Down
15 changes: 0 additions & 15 deletions lib/Popper/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
type MiddlewareResult,
type OffsetMiddleware,
type Placement,
type Props,
type Rect,
type Side,
type Strategy,
Expand Down Expand Up @@ -762,17 +761,3 @@ export const translate = ({ x, y }: Coordinates) => {
msTransform: transformValue,
};
};

export const getAnchor = (anchorElement: Props["anchorElement"]) => {
const isServer = typeof document !== "undefined";

if (typeof anchorElement === "string") {
if (isServer) return null;

return document.getElementById(anchorElement);
}

if ("current" in anchorElement) return anchorElement.current;

return anchorElement;
};
24 changes: 10 additions & 14 deletions lib/Portal/Portal.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { usePortalConfig } from "../PortalConfigProvider";
import { useIsServerHandoffComplete } from "../utils";
import { getContainer } from "./utils";
import { useIsomorphicValue } from "../utils";

export type Props = {
/**
* A string containing one selector to match.
* This string must be a valid CSS selector string;
* if it isn't, a `SyntaxError` exception is thrown.
* A function that will resolve the container element for the portal.
*
* Please note that this function is only called on the client-side.
*/
containerQuerySelector?: string;
resolveContainer?: () => HTMLElement | null;
/**
* The children to render into the container.
*/
Expand All @@ -23,17 +22,14 @@ export type Props = {
};

const Portal = (props: Props) => {
const { containerQuerySelector, children, disabled = false } = props;
const { resolveContainer, children, disabled = false } = props;

const { destinationQuery } = usePortalConfig();
const isServerHandoffComplete = useIsServerHandoffComplete();
const portalConfig = usePortalConfig();

const containerQuery = containerQuerySelector || destinationQuery;
const containerResolver =
resolveContainer ?? portalConfig?.resolveContainer ?? (() => document.body);

const container: HTMLElement | null = React.useMemo(
() => (isServerHandoffComplete ? getContainer(containerQuery) : null),
[containerQuery, isServerHandoffComplete],
);
const container = useIsomorphicValue(containerResolver, null);

if (disabled) return <>{children}</>;
if (!container) return null;
Expand Down
11 changes: 7 additions & 4 deletions lib/PortalConfigProvider/context.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import * as React from "react";

type ContextValue = {
destinationQuery?: string;
/**
* A function that will resolve the container element for the portals.
*
* Please note that this function is only called on the client-side.
*/
resolveContainer: () => HTMLElement | null;
};

const Context = React.createContext<ContextValue>({
destinationQuery: undefined,
});
const Context = React.createContext<ContextValue | null>(null);

if (process.env.NODE_ENV !== "production")
Context.displayName = "PortalConfigContext";
Expand Down
14 changes: 8 additions & 6 deletions lib/Tooltip/Tooltip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import Tooltip from "./Tooltip";
describe("Tooltip", () => {
afterEach(jest.clearAllMocks);

const anchorResolver = () => document.getElementById("anchor");

it(`component could be updated and unmounted without errors`, () => {
const Component = (
<>
<div id="anchor">Anchor</div>
<Tooltip anchorElement="anchor">
<Tooltip resolveAnchor={anchorResolver}>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Mollitia
magnam ad excepturi ipsa exercitationem cum?
</Tooltip>
Expand All @@ -30,7 +32,7 @@ describe("Tooltip", () => {
<>
<div id="anchor">Anchor</div>
<Tooltip
anchorElement="anchor"
resolveAnchor={anchorResolver}
defaultOpen={true}
ref={ref}
>
Expand Down Expand Up @@ -64,7 +66,7 @@ describe("Tooltip", () => {
<>
<div id="anchor">Anchor</div>
<Tooltip
anchorElement="anchor"
resolveAnchor={anchorResolver}
defaultOpen={true}
style={style}
>
Expand Down Expand Up @@ -96,7 +98,7 @@ describe("Tooltip", () => {
<>
<div id="anchor">Anchor</div>
<Tooltip
anchorElement="anchor"
resolveAnchor={anchorResolver}
defaultOpen={true}
data-other-attribute="test"
>
Expand All @@ -117,7 +119,7 @@ describe("Tooltip", () => {
<>
<div id="anchor">Anchor</div>
<Tooltip
anchorElement="anchor"
resolveAnchor={anchorResolver}
defaultOpen={true}
data-other-attribute="test"
className={({ placement, openState }) =>
Expand Down Expand Up @@ -145,7 +147,7 @@ describe("Tooltip", () => {
Anchor
</div>
<Tooltip
anchorElement="anchor"
resolveAnchor={anchorResolver}
defaultOpen={true}
data-other-attribute="test"
className={({ placement, openState }) =>
Expand Down
Loading