Skip to content

Commit

Permalink
[core] feat: Overlay2 component (#6656)
Browse files Browse the repository at this point in the history
  • Loading branch information
adidahiya authored Jan 24, 2024
1 parent c7791e3 commit 9dfba77
Show file tree
Hide file tree
Showing 50 changed files with 2,683 additions and 466 deletions.
3 changes: 3 additions & 0 deletions packages/core/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ module.exports = async function (config) {
"src/common/abstractComponent*",
"src/common/abstractPureComponent*",
"src/components/html/html.tsx",
// focus mangement is difficult to test, and this function may no longer be required
// if we use the react-focus-lock library in Overlay2.
"src/components/overlay/overlayUtils.ts",

// HACKHACK: for karma upgrade only
"src/common/refs.ts",
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,10 @@ export const DIALOG_WARN_NO_HEADER_CLOSE_BUTTON =

export const DRAWER_ANGLE_POSITIONS_ARE_CASTED =
ns + ` <Drawer> all angle positions are casted into pure position (TOP, BOTTOM, LEFT or RIGHT)`;

export const OVERLAY_CHILD_REF_AND_REFS_MUTEX =
ns + ` <Overlay2> cannot use childRef and childRefs props simultaneously`;
export const OVERLAY_WITH_MULTIPLE_CHILDREN_REQUIRES_CHILD_REFS =
ns + ` <Overlay2> requires childRefs prop when rendering multiple child elements`;
export const OVERLAY_CHILD_REQUIRES_KEY =
ns + ` <Overlay2> requires each child element to have a unique key prop when childRefs is used`;
6 changes: 5 additions & 1 deletion packages/core/src/common/refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ export function getRef<T>(ref: T | React.RefObject<T> | null): T | null {
return null;
}

return (ref as React.RefObject<T>).current ?? (ref as T);
if (typeof (ref as React.RefObject<T>).current === "undefined") {
return ref as T;
}

return (ref as React.RefObject<T>).current;
}

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/common/utils/domUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
* limitations under the License.
*/

/** @returns true if React is running in a client environment, and false if it's in a server */
export function hasDOMEnvironment(): boolean {
return typeof window !== "undefined" && window.document != null;
}

export function elementIsOrContains(element: HTMLElement, testElement: HTMLElement) {
return element === testElement || element.contains(testElement);
}
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/common/utils/jsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,10 @@ export function uniqueId(namespace: string) {
uniqueCountForNamespace.set(namespace, curCount + 1);
return `${namespace}-${curCount}`;
}

/**
* @returns `true` if the value is an empty string after trimming whitespace
*/
export function isEmptyString(val: any) {
return typeof val === "string" && val.trim().length === 0;
}
53 changes: 40 additions & 13 deletions packages/core/src/common/utils/reactUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

import * as React from "react";

import { isEmptyString } from "./jsUtils";

/**
* Returns true if `node` is null/undefined, false, empty string, or an array
* composed of those. If `node` is an array, only one level of the array is
Expand Down Expand Up @@ -45,35 +47,60 @@ export function isReactChildrenElementOrElements(
}

/**
* Converts a React node to an element: non-empty string or number or
* `React.Fragment` (React 16.3+) is wrapped in given tag name; empty strings
* and booleans are discarded.
* Converts a React node to an element. Non-empty strings, numbers, and Fragments will be wrapped in given tag name;
* empty strings and booleans will be discarded.
*
* @param child the React node to convert
* @param tagName the HTML tag name to use when a wrapper element is needed
* @param props additional props to spread onto the element, if any. If the child is a React element and this argument
* is defined, the child will be cloned and these props will be merged in.
*/
export function ensureElement(child: React.ReactNode | undefined, tagName: keyof React.JSX.IntrinsicElements = "span") {
if (child == null || typeof child === "boolean") {
export function ensureElement(
child: React.ReactNode | undefined,
tagName: keyof React.JSX.IntrinsicElements = "span",
props: React.HTMLProps<HTMLElement> = {},
) {
if (child == null || typeof child === "boolean" || isEmptyString(child)) {
return undefined;
} else if (typeof child === "string") {
// cull whitespace strings
return child.trim().length > 0 ? React.createElement(tagName, {}, child) : undefined;
} else if (typeof child === "number" || typeof (child as any).type === "symbol" || Array.isArray(child)) {
// React.Fragment has a symbol type, ReactNodeArray extends from Array
return React.createElement(tagName, {}, child);
} else if (
typeof child === "string" ||
typeof child === "number" ||
isReactFragment(child) ||
isReactNodeArray(child)
) {
// wrap the child element
return React.createElement(tagName, props, child);
} else if (isReactElement(child)) {
return child;
if (Object.keys(props).length > 0) {
// clone the element and merge props
return React.cloneElement(child, props);
} else {
// nothing to do, it's a valid ReactElement
return child;
}
} else {
// child is inferred as {}
return undefined;
}
}

function isReactElement<T = any>(child: React.ReactNode): child is React.ReactElement<T> {
export function isReactElement<T = any>(child: React.ReactNode): child is React.ReactElement<T> {
return (
typeof child === "object" &&
typeof (child as any).type !== "undefined" &&
typeof (child as any).props !== "undefined"
);
}

function isReactFragment(child: React.ReactNode): child is React.ReactFragment {
// bit hacky, but generally works
return typeof (child as any).type === "symbol";
}

function isReactNodeArray(child: React.ReactNode): child is React.ReactNodeArray {
return Array.isArray(child);
}

/**
* Returns true if the given JSX element matches the given component type.
*
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/alert/alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
import { Button } from "../button/buttons";
import { Dialog } from "../dialog/dialog";
import { Icon, type IconName } from "../icon/icon";
import type { OverlayLifecycleProps } from "../overlay/overlay";
import type { OverlayLifecycleProps } from "../overlay/overlayProps";

export interface AlertProps extends OverlayLifecycleProps, Props {
/**
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
@## Overlays

@page overlay
@page overlay2
@page portal
@page alert
@page context-menu
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import type { OverlayLifecycleProps } from "../overlay/overlay";
import type { OverlayLifecycleProps } from "../overlay/overlayProps";
import type { PopoverProps } from "../popover/popover";

export type Offset = {
Expand Down
40 changes: 30 additions & 10 deletions packages/core/src/components/dialog/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,21 @@ import * as React from "react";

import { type IconName, IconSize, SmallCross } from "@blueprintjs/icons";

import { AbstractPureComponent, Classes, DISPLAYNAME_PREFIX, type MaybeElement, type Props } from "../../common";
import {
AbstractPureComponent,
Classes,
DISPLAYNAME_PREFIX,
type MaybeElement,
mergeRefs,
type Props,
} from "../../common";
import * as Errors from "../../common/errors";
import { uniqueId } from "../../common/utils";
import { Button } from "../button/buttons";
import { H6 } from "../html/html";
import { Icon } from "../icon/icon";
import { type BackdropProps, Overlay, type OverlayableProps } from "../overlay/overlay";
import type { BackdropProps, OverlayableProps } from "../overlay/overlayProps";
import { Overlay2 } from "../overlay2/overlay2";

export interface DialogProps extends OverlayableProps, BackdropProps, Props {
/** Dialog contents. */
Expand Down Expand Up @@ -77,7 +85,7 @@ export interface DialogProps extends OverlayableProps, BackdropProps, Props {
transitionName?: string;

/**
* Ref supplied to the `Classes.DIALOG_CONTAINER` element.
* Ref attached to the `Classes.DIALOG_CONTAINER` element.
*/
containerRef?: React.Ref<HTMLDivElement>;

Expand Down Expand Up @@ -106,6 +114,8 @@ export class Dialog extends AbstractPureComponent<DialogProps> {
isOpen: false,
};

private childRef = React.createRef<HTMLDivElement>();

private titleId: string;

public static displayName = `${DISPLAYNAME_PREFIX}.Dialog`;
Expand All @@ -118,21 +128,31 @@ export class Dialog extends AbstractPureComponent<DialogProps> {
}

public render() {
const { className, children, containerRef, style, title, ...overlayProps } = this.props;

return (
<Overlay {...this.props} className={Classes.OVERLAY_SCROLL_CONTAINER} hasBackdrop={true}>
<div className={Classes.DIALOG_CONTAINER} ref={this.props.containerRef}>
<Overlay2
{...overlayProps}
className={Classes.OVERLAY_SCROLL_CONTAINER}
childRef={this.childRef}
hasBackdrop={true}
>
<div
className={Classes.DIALOG_CONTAINER}
ref={containerRef === undefined ? this.childRef : mergeRefs(containerRef, this.childRef)}
>
<div
className={classNames(Classes.DIALOG, this.props.className)}
className={classNames(Classes.DIALOG, className)}
role="dialog"
aria-labelledby={this.props["aria-labelledby"] || (this.props.title ? this.titleId : undefined)}
aria-labelledby={this.props["aria-labelledby"] || (title ? this.titleId : undefined)}
aria-describedby={this.props["aria-describedby"]}
style={this.props.style}
style={style}
>
{this.maybeRenderHeader()}
{this.props.children}
{children}
</div>
</div>
</Overlay>
</Overlay2>
);
}

Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/components/drawer/drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import { DISPLAYNAME_PREFIX, type MaybeElement } from "../../common/props";
import { Button } from "../button/buttons";
import { H4 } from "../html/html";
import { Icon } from "../icon/icon";
import { type BackdropProps, Overlay, type OverlayableProps } from "../overlay/overlay";
import type { BackdropProps, OverlayableProps } from "../overlay/overlayProps";
import { Overlay2 } from "../overlay2/overlay2";

export enum DrawerSize {
SMALL = "360px",
Expand Down Expand Up @@ -134,12 +135,12 @@ export class Drawer extends AbstractPureComponent<DrawerProps> {
return (
// N.B. the `OVERLAY_CONTAINER` class is a bit of a misnomer since it is only being used by the Drawer
// component, but we keep it for backwards compatibility.
<Overlay {...overlayProps} className={classNames({ [Classes.OVERLAY_CONTAINER]: hasBackdrop })}>
<Overlay2 {...overlayProps} className={classNames({ [Classes.OVERLAY_CONTAINER]: hasBackdrop })}>
<div className={classes} style={styleProp}>
{this.maybeRenderHeader()}
{children}
</div>
</Overlay>
</Overlay2>
);
}

Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ export { NavbarGroup, type NavbarGroupProps } from "./navbar/navbarGroup";
export { NavbarHeading, type NavbarHeadingProps } from "./navbar/navbarHeading";
export { NonIdealState, type NonIdealStateProps, NonIdealStateIconSize } from "./non-ideal-state/nonIdealState";
export { OverflowList, type OverflowListProps } from "./overflow-list/overflowList";
export { Overlay, type OverlayLifecycleProps, type OverlayProps, type OverlayableProps } from "./overlay/overlay";
// eslint-disable-next-line deprecation/deprecation
export { Overlay } from "./overlay/overlay";
export type { OverlayLifecycleProps, OverlayProps, OverlayableProps } from "./overlay/overlayProps";
export { Overlay2, type Overlay2Props, type OverlayInstance } from "./overlay2/overlay2";
export { Text, type TextProps } from "./text/text";
// eslint-disable-next-line deprecation/deprecation
export { PanelStack, type PanelStackProps } from "./panel-stack/panelStack";
Expand Down Expand Up @@ -120,7 +123,10 @@ export { Tag, type TagProps } from "./tag/tag";
export { TagInput, type TagInputProps, type TagInputAddMethod } from "./tag-input/tagInput";
export { OverlayToaster, type OverlayToasterCreateOptions } from "./toast/overlayToaster";
export type { OverlayToasterProps, ToasterPosition } from "./toast/overlayToasterProps";
export { Toast, type ToastProps } from "./toast/toast";
// eslint-disable-next-line deprecation/deprecation
export { Toast } from "./toast/toast";
export { Toast2 } from "./toast/toast2";
export type { ToastProps } from "./toast/toastProps";
export { Toaster, type ToastOptions } from "./toast/toaster";
export { type TooltipProps, Tooltip } from "./tooltip/tooltip";
export { Tree, type TreeProps } from "./tree/tree";
Expand Down
25 changes: 19 additions & 6 deletions packages/core/src/components/overlay/overlay.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
@# Overlay

__Overlay__ is a generic low-level component for rendering content _on top of_ its siblings or above the entire
<div class="@ns-callout @ns-intent-danger @ns-icon-error @ns-callout-has-body-content">
<h5 class="@ns-heading">

Deprecated: use [**Overlay2**](#core/components/overlay2)

</h5>

This component is **deprecated since @blueprintjs/core v5.9.0** in favor of the new
**Overlay2** component which is compatible with React 18 strict mode. You should migrate to the
new API which will become the standard in a future major version of Blueprint.

</div>

**Overlay** is a generic low-level component for rendering content _on top of_ its siblings or above the entire
application.

It combines the functionality of the [__Portal__](#core/components/portal) component (which allows React elements to
escape their current DOM hierarchy) with a [__CSSTransitionGroup__](https://reactcommunity.org/react-transition-group/)
It combines the functionality of the [**Portal**](#core/components/portal) component (which allows React elements to
escape their current DOM hierarchy) with a [**CSSTransitionGroup**](https://reactcommunity.org/react-transition-group/)
(to show elegant enter and leave transitions).

An optional "backdrop" element can be rendered behind the overlaid children to provide modal behavior, whereby the
overlay prevents interaction with anything behind it.

__Overlay__ is the backbone of all the components listed in the **Overlays** group in the sidebar. Using __Overlay__
**Overlay** is the backbone of all the components listed in the **Overlays** group in the sidebar. Using **Overlay**
directly should be rare in your application; it should only be necessary if no existing component meets your needs.

@reactExample OverlayExample

@## Usage

__Overlay__ is a controlled component that renders its children only when `isOpen={true}`. The optional backdrop element
**Overlay** is a controlled component that renders its children only when `isOpen={true}`. The optional backdrop element
will be inserted before the children if `hasBackdrop={true}`.

The `onClose` callback prop is invoked when user interaction causes the overlay to close, but your application is
Expand Down Expand Up @@ -50,7 +63,7 @@ scrollable, so any overflowing content will be hidden. Fortunately, making an ov
<Overlay className={Classes.OVERLAY_SCROLL_CONTAINER} />
```

Note that the [__Dialog__](https://blueprintjs.com/docs/#core/components/dialog) component applies this CSS class
Note that the [**Dialog**](https://blueprintjs.com/docs/#core/components/dialog) component applies this CSS class
automatically.

@## Props interface
Expand Down
Loading

1 comment on commit 9dfba77

@adidahiya
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[core] feat: Overlay2 component (#6656)

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

Please sign in to comment.