Skip to content
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

[core] feat: Overlay2 component #6656

Merged
merged 24 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
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) {
adidahiya marked this conversation as resolved.
Show resolved Hide resolved
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)}
adidahiya marked this conversation as resolved.
Show resolved Hide resolved
>
<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