diff --git a/packages/react/src/components/Dialog/Dialog-story.js b/packages/react/src/components/Dialog/Dialog-story.js
index d3a9ecaf7733..7bbcc6445c21 100644
--- a/packages/react/src/components/Dialog/Dialog-story.js
+++ b/packages/react/src/components/Dialog/Dialog-story.js
@@ -7,11 +7,10 @@
/* eslint-disable storybook/story-exports */
-import * as React from 'react';
-import { FocusScope } from '../FocusScope';
-import { Dialog } from '../Dialog';
-import { useId } from '../../internal/useId';
-import { Portal } from '../Portal';
+import React, { useEffect, useState } from 'react';
+import Dialog from './';
+import Button from '../Button';
+import { action } from '@storybook/addon-actions';
export default {
title: 'Experimental/unstable_Dialog',
@@ -19,137 +18,65 @@ export default {
includeStories: [],
};
-export const Default = () => {
- function DemoComponent() {
- const [open, setOpen] = React.useState(false);
- const ref = React.useRef(null);
+export const Default = ({ open: _open, ...args }) => {
+ const [open, setOpen] = useState(_open);
- return (
-
-
{
- setOpen(true);
- }}>
- Open
-
- {open ? (
-
-
-
- Elit hic at labore culpa itaque fugiat. Consequuntur iure autem
- autem officiis dolores facilis nulla earum! Neque quia nemo
- sequi assumenda ratione officia Voluptate beatae eligendi
- placeat nemo laborum, ratione.
-
-
-
{
- setOpen(false);
- }}>
- Close
-
-
-
- ) : null}
-
- );
- }
- return (
- <>
-
- Hello
- >
- );
-};
+ const handleOpenDialog = () => {
+ setOpen(!open);
+ };
-export const DialogExample = () => {
- function Example() {
- const [open, setOpen] = React.useState(false);
- const id = useId();
+ const closeAction = action('Close action');
- return (
-
-
- First
-
-
{
- setOpen(true);
- }}>
- Open
-
- {open ? (
-
-
- {
- setOpen(false);
- }}
- style={{
- position: 'relative',
- zIndex: 9999,
- padding: '1rem',
- background: 'white',
- }}>
-
- Hello
-
-
-
-
- {
- setOpen(false);
- }}>
- Close
-
-
-
- ) : null}
+ const handleCloseEvent = () => {
+ closeAction();
+ // keep local state the same as the dialog otherwise open will
+ // need two clicks after a close
+ setOpen(false);
+ };
-
- Last
-
-
- );
- }
+ const handleCloseClick = () => {
+ setOpen(false);
+ };
- return ;
-};
+ useEffect(() => {
+ setOpen(_open);
+ }, [_open]);
-const FullPage = React.forwardRef(function FullPage(props, ref) {
return (
-
+
+
+ Toggle open
+
+
+
+ Elit hic at labore culpa itaque fugiat. Consequuntur iure autem autem
+ officiis dolores facilis nulla earum! Neque quia nemo sequi assumenda
+ ratione officia Voluptate beatae eligendi placeat nemo laborum,
+ ratione.
+
+
+
+ Elit hic at labore culpa itaque fugiat. Consequuntur iure autem autem
+ officiis dolores facilis nulla earum! Neque quia nemo sequi assumenda
+ ratione officia Voluptate beatae eligendi placeat nemo laborum,
+ ratione.
+
+
+
+ Elit hic at labore culpa itaque fugiat. Consequuntur iure autem autem
+ officiis dolores facilis nulla earum! Neque quia nemo sequi assumenda
+ ratione officia Voluptate beatae eligendi placeat nemo laborum,
+ ratione.
+
+
+
+ Close
+
+
+
);
-});
+};
diff --git a/packages/react/src/components/Dialog/index.js b/packages/react/src/components/Dialog/index.js
deleted file mode 100644
index 4f13a9273c07..000000000000
--- a/packages/react/src/components/Dialog/index.js
+++ /dev/null
@@ -1,153 +0,0 @@
-/**
- * Copyright IBM Corp. 2016, 2023
- *
- * This source code is licensed under the Apache-2.0 license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-import 'wicg-inert';
-import PropTypes from 'prop-types';
-import React, { useEffect, useRef } from 'react';
-import { FocusScope } from '../FocusScope';
-import { useMergedRefs } from '../../internal/useMergedRefs';
-import { useSavedCallback } from '../../internal/useSavedCallback';
-import { match, keys } from '../../internal/keyboard';
-
-/**
- * @see https://www.tpgi.com/the-current-state-of-modal-dialog-accessibility/
- */
-const Dialog = React.forwardRef(function Dialog(props, forwardRef) {
- const { 'aria-labelledby': labelledBy, children, onDismiss, ...rest } = props;
- const dialogRef = useRef(null);
- const ref = useMergedRefs([dialogRef, forwardRef]);
- const savedOnDismiss = useSavedCallback(onDismiss);
-
- function onKeyDown(event) {
- if (match(event, keys.Escape)) {
- event.stopPropagation();
- savedOnDismiss();
- }
- }
-
- useEffect(() => {
- const changes = hide(document.body, dialogRef.current);
- return () => {
- show(changes);
- };
- }, []);
-
- return (
-
- {children}
-
- );
-});
-
-Dialog.propTypes = {
- /**
- * Provide the associated element that labels the Dialog
- */
- 'aria-labelledby': PropTypes.string.isRequired,
-
- /**
- * Provide children to be rendered inside of the Dialog
- */
- children: PropTypes.node,
-
- /**
- * Provide a handler that is called when the Dialog is requesting to be closed
- */
- onDismiss: PropTypes.func.isRequired,
-};
-
-if (__DEV__) {
- Dialog.displayName = 'Dialog';
-}
-
-function hide(root, dialog) {
- const changes = [];
- const queue = Array.from(root.childNodes);
-
- while (queue.length !== 0) {
- const node = queue.shift();
-
- if (node.nodeType !== Node.ELEMENT_NODE) {
- continue;
- }
-
- // If a node is the dialog, do nothing
- if (node === dialog) {
- continue;
- }
-
- // If a tree contains our dialog, traverse its children
- if (node.contains(dialog)) {
- queue.push(...Array.from(node.childNodes));
- continue;
- }
-
- // If a node is a bumper, do nothing
- if (
- node.hasAttribute('data-carbon-focus-scope') &&
- (dialog.previousSibling === node || dialog.nextSibling === node)
- ) {
- continue;
- }
-
- if (node.getAttribute('aria-hidden') === 'true') {
- continue;
- }
-
- if (node.hasAttribute('inert')) {
- continue;
- }
-
- if (node.getAttribute('aria-hidden') === 'false') {
- node.setAttribute('aria-hidden', 'true');
- node.setAttribute('inert', '');
- changes.push({
- node,
- attributes: {
- 'aria-hidden': 'false',
- },
- });
- continue;
- }
-
- // Otherwise, set it to inert and set aria-hidden to true
- node.setAttribute('aria-hidden', 'true');
- node.setAttribute('inert', '');
-
- changes.push({
- node,
- });
- }
-
- return changes;
-}
-
-function show(changes) {
- changes.forEach(({ node, attributes }) => {
- node.removeAttribute('inert');
- // This mutation needs to be asynchronous to allow the polyfill time to
- // observe the change and allow mutations to occur
- // https://github.com/WICG/inert#performance-and-gotchas
- setTimeout(() => {
- if (attributes && attributes['aria-hidden']) {
- node.setAttribute('aria-hidden', attributes['aria-hidden']);
- } else {
- node.removeAttribute('aria-hidden');
- }
- }, 0);
- });
-}
-
-export { Dialog };
diff --git a/packages/react/src/components/Dialog/index.tsx b/packages/react/src/components/Dialog/index.tsx
new file mode 100644
index 000000000000..dcc9cd3f5032
--- /dev/null
+++ b/packages/react/src/components/Dialog/index.tsx
@@ -0,0 +1,150 @@
+/**
+ * Copyright IBM Corp. 2014, 2024
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import 'wicg-inert';
+import PropTypes from 'prop-types';
+import React, { MutableRefObject, useEffect, useRef } from 'react';
+import { usePrefix } from '../../internal/usePrefix';
+import cx from 'classnames';
+import { Close } from '@carbon/icons-react';
+import { IconButton } from '../IconButton';
+import { noopFn } from '../../internal/noopFn';
+import { ReactAttr } from '../../types/common';
+
+export interface DialogProps extends ReactAttr {
+ /**
+ * Provide the contents of the Dialog
+ */
+ children?: React.ReactNode;
+
+ /**
+ * Specifies whether the dialog is modal or non-modal
+ */
+ modal?: boolean;
+
+ /**
+ * Specify a handler for closing Dialog.
+ * The handler should care of closing Dialog, e.g. changing `open` prop.
+ */
+ onRequestClose?: React.ReactEventHandler;
+
+ /**
+ * Specify whether the Dialog is currently open
+ */
+ open?: boolean;
+}
+
+const Dialog = React.forwardRef(
+ (
+ {
+ children,
+ modal,
+ onRequestClose = noopFn,
+ open = false,
+ ...rest
+ }: DialogProps,
+ ref
+ ) => {
+ const backupRef = useRef(null);
+ const localRef = (ref ?? backupRef) as MutableRefObject;
+
+ const prefix = usePrefix();
+
+ const handleClose = (ev) => {
+ if (onRequestClose) {
+ onRequestClose(ev);
+ }
+ };
+
+ const handleCancel = () => {
+ localRef.current?.close();
+ };
+
+ const handleBackdropClick = (ev) => {
+ if (ev.target === localRef.current) {
+ localRef.current?.close();
+ }
+ };
+
+ useEffect(() => {
+ if (localRef.current) {
+ if (open) {
+ if (modal) {
+ localRef.current.showModal();
+ } else {
+ localRef.current.open = true;
+ }
+ } else {
+ localRef.current.close();
+ }
+ }
+ }, [localRef, modal, open]);
+
+ return (
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
+
+
+
+ {children}
+
+ );
+ }
+);
+
+Dialog.displayName = 'Dialog';
+
+Dialog.propTypes = {
+ /**
+ * Provide children to be rendered inside of the Dialog
+ */
+ children: PropTypes.node,
+
+ /**
+ * Modal specifies whether the Dialog is modal or non-modal
+ */
+ modal: PropTypes.bool,
+
+ /**
+ * Specify a handler for closing Dialog.
+ * The handler should care of closing Dialog, e.g. changing `open` prop.
+ */
+ onRequestClose: PropTypes.func,
+
+ /**
+ * open initial state
+ */
+ open: PropTypes.bool,
+};
+
+export { Dialog };
+export default Dialog;
diff --git a/packages/react/src/components/FocusScope/index.js b/packages/react/src/components/FocusScope/index.js
deleted file mode 100644
index 9c59e549b077..000000000000
--- a/packages/react/src/components/FocusScope/index.js
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * Copyright IBM Corp. 2016, 2023
- *
- * This source code is licensed under the Apache-2.0 license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-import PropTypes from 'prop-types';
-import * as React from 'react';
-import { useMergedRefs } from '../../internal/useMergedRefs';
-import { useAutoFocus } from './useAutoFocus';
-import { useFocusScope } from './useFocusScope';
-import { useRestoreFocus } from './useRestoreFocus';
-
-const FocusScope = React.forwardRef(function FocusScope(props, forwardRef) {
- const {
- as: BaseComponent = 'div',
- children,
- initialFocusRef,
- ...rest
- } = props;
- const containerRef = React.useRef(null);
- const focusScope = useFocusScope(containerRef);
- const ref = useMergedRefs([forwardRef, containerRef]);
-
- useRestoreFocus(containerRef);
- useAutoFocus(() => {
- if (initialFocusRef) {
- return initialFocusRef;
- }
- return focusScope.current.getFirstDescendant();
- });
-
- return (
- <>
- {
- focusScope.current.focusLastDescendant();
- }}
- />
-
- {children}
-
- {
- focusScope.current.focusFirstDescendant();
- }}
- />
- >
- );
-});
-
-if (__DEV__) {
- FocusScope.displayName = 'FocusScope';
-}
-
-FocusScope.propTypes = {
- /**
- * Provide a custom element type for the containing element
- */
- as: PropTypes.oneOfType([
- PropTypes.func,
- PropTypes.string,
- PropTypes.elementType,
- ]),
-
- /**
- * Provide the children to be rendered inside of the `FocusScope`
- */
- children: PropTypes.node,
-
- /**
- * Provide a `ref` that is used to place focus when the `FocusScope` is
- * initially opened
- */
- initialFocusRef: PropTypes.shape({
- current: PropTypes.any,
- }),
-};
-
-const bumperStyle = {
- outline: 'none',
- opacity: '0',
- position: 'fixed',
- pointerEvents: 'none',
-};
-
-function FocusScopeBumper(props) {
- return (
-
- );
-}
-
-export { FocusScope };
diff --git a/packages/react/src/components/FocusScope/useAutoFocus.js b/packages/react/src/components/FocusScope/useAutoFocus.js
deleted file mode 100644
index 4a1c082fbb49..000000000000
--- a/packages/react/src/components/FocusScope/useAutoFocus.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Copyright IBM Corp. 2016, 2023
- *
- * This source code is licensed under the Apache-2.0 license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-import { useEffect, useRef } from 'react';
-import { focus } from '../../internal/focus';
-
-export function useAutoFocus(getElementOrRef) {
- const callbackRef = useRef(getElementOrRef);
-
- useEffect(() => {
- if (callbackRef.current) {
- const elementOrRef = callbackRef.current();
- const element = elementOrRef.current || elementOrRef;
- if (element) {
- focus(element);
- }
- }
- }, []);
-}
diff --git a/packages/react/src/components/FocusScope/useFocusScope.js b/packages/react/src/components/FocusScope/useFocusScope.js
deleted file mode 100644
index 41ab64a28ab6..000000000000
--- a/packages/react/src/components/FocusScope/useFocusScope.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * Copyright IBM Corp. 2016, 2023
- *
- * This source code is licensed under the Apache-2.0 license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-import { useRef } from 'react';
-import { focus } from '../../internal/focus';
-
-export function useFocusScope(containerRef) {
- const focusScope = useRef(null);
-
- if (focusScope.current === null) {
- focusScope.current = createFocusScope(containerRef);
- }
-
- return focusScope;
-}
-
-function createFocusWalker(container) {
- return document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
- acceptNode(node) {
- if (node.tabIndex >= 0 && !node.disabled) {
- return NodeFilter.FILTER_ACCEPT;
- }
- return NodeFilter.FILTER_SKIP;
- },
- });
-}
-
-function createFocusScope(root) {
- const focusScope = {
- getFirstDescendant() {
- const walker = createFocusWalker(root.current);
- return walker.firstChild();
- },
- focusFirstDescendant() {
- const walker = createFocusWalker(root.current);
- const firstChild = walker.firstChild();
- if (firstChild) {
- focus(firstChild);
- }
- },
- focusLastDescendant() {
- const walker = createFocusWalker(root.current);
- const lastChild = walker.lastChild();
- if (lastChild) {
- focus(lastChild);
- }
- },
- };
-
- return focusScope;
-}
diff --git a/packages/react/src/components/FocusScope/useRestoreFocus.js b/packages/react/src/components/FocusScope/useRestoreFocus.js
deleted file mode 100644
index 16667593efd2..000000000000
--- a/packages/react/src/components/FocusScope/useRestoreFocus.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * Copyright IBM Corp. 2016, 2023
- *
- * This source code is licensed under the Apache-2.0 license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-import { useEffect, useRef } from 'react';
-import { focus } from '../../internal/focus';
-
-export function useRestoreFocus(container) {
- const containsFocus = useRef(false);
-
- useEffect(() => {
- const initialActiveElement = document.activeElement;
-
- if (container.current && container.current.contains) {
- containsFocus.current = container.current.contains(
- document.activeElement
- );
- }
-
- function onFocusIn() {
- containsFocus.current = true;
- }
-
- function onFocusOut(event) {
- if (container.current && container.current.contains) {
- containsFocus.current = container.current.contains(event.relatedTarget);
- }
- }
-
- const { current: element } = container;
-
- element.addEventListener('focusin', onFocusIn);
- element.addEventListener('focusout', onFocusOut);
-
- return () => {
- element.removeEventListener('focusin', onFocusIn);
- element.removeEventListener('focusout', onFocusOut);
-
- if (containsFocus.current === true) {
- setTimeout(() => {
- focus(initialActiveElement);
- }, 0);
- }
- };
- }, [container]);
-}
diff --git a/packages/styles/scss/components/dialog/_dialog.scss b/packages/styles/scss/components/dialog/_dialog.scss
new file mode 100644
index 000000000000..76d9bdc93345
--- /dev/null
+++ b/packages/styles/scss/components/dialog/_dialog.scss
@@ -0,0 +1,155 @@
+//
+// Copyright IBM Corp. 2014, 2024
+//
+// This source code is licensed under the Apache-2.0 license found in the
+// LICENSE file in the root directory of this source tree.
+//
+
+@use 'sass:list';
+@use '../button';
+@use '../../config' as *;
+@use '../../breakpoint' as *;
+@use '../../motion' as *;
+@use '../../spacing' as *;
+@use '../../theme' as *;
+@use '../../type' as *;
+@use '../../utilities/ai-gradient' as *;
+@use '../../utilities/convert';
+@use '../../utilities/component-reset';
+@use '../../utilities/focus-outline' as *;
+@use '../../utilities/high-contrast-mode' as *;
+@use '../../utilities/z-index' as *;
+
+/// Dialog styles
+/// @access public
+/// @group dialog
+@mixin dialog {
+ .#{$prefix}--dialog {
+ /* size */
+ padding: 0;
+ border: 1px solid $border-subtle-01;
+ background-color: $layer;
+ color: $text-primary;
+ inline-size: 48rem;
+ max-block-size: 50%;
+ max-inline-size: 100%;
+ opacity: 0;
+ transform: translateY(calc(-1 * #{$spacing-06}));
+
+ /** opening and closing is used in as allow-discrete is not currently supported wide enough
+ * https://caniuse.com/mdn-css_properties_display_is_transitionable
+ */
+ transition: opacity $duration-moderate-02 motion(exit, expressive),
+ transform $duration-moderate-02 motion(exit, expressive),
+ overlay $duration-moderate-02 motion(exit, expressive) allow-discrete,
+ display $duration-moderate-02 motion(exit, expressive) allow-discrete;
+
+ @media (prefers-reduced-motion) {
+ transition: none;
+ }
+
+ @include breakpoint(md) {
+ max-inline-size: 84%;
+ }
+ @include breakpoint(lg) {
+ max-inline-size: 72%;
+ }
+ @include breakpoint(xlg) {
+ max-inline-size: 64%;
+ }
+ @include breakpoint(xlg) {
+ max-inline-size: 60%;
+ }
+
+ &[open] {
+ opacity: 1;
+ transform: translateY(0);
+
+ transition: opacity $duration-moderate-02 motion(entrance, expressive),
+ transform $duration-moderate-02 motion(entrance, expressive),
+ overlay $duration-moderate-02 motion(entrance, expressive)
+ allow-discrete,
+ display $duration-moderate-02 motion(entrance, expressive)
+ allow-discrete;
+
+ @media (prefers-reduced-motion) {
+ transition: none;
+ }
+ }
+
+ /** starting style also not supported widely
+ * https://caniuse.com/mdn-css_at-rules_starting-style
+ */
+ /* Before-open state */
+ /* Needs to be after the previous dialog[open] rule to take effect,
+ as the specificity is the same */
+ /* stylelint-disable-next-line scss/at-rule-no-unknown */
+ @starting-style {
+ &[open] {
+ opacity: 0;
+ transform: translateY(calc(-1 * #{$spacing-06}));
+ }
+ }
+ }
+
+ .#{$prefix}--dialog__header {
+ position: relative;
+ overflow: visible;
+ inline-size: 100%;
+ min-block-size: $spacing-09;
+ }
+
+ .#{$prefix}--dialog__content {
+ padding: $spacing-05;
+ block-size: 100%;
+ }
+
+ .#{$prefix}--dialog--modal {
+ border: 1px solid transparent;
+ }
+
+ /* Transition the :backdrop when the dialog modal is promoted to the top layer */
+ .#{$prefix}--dialog::backdrop {
+ background-color: $overlay;
+ opacity: 0;
+ /* opening and closing is used in as allow-discrete is not currently supported wide enough
+ * https://caniuse.com/mdn-css_properties_display_is_transitionable
+ */
+ transition: background-color $duration-moderate-02
+ motion(entrance, expressive),
+ opacity $duration-moderate-02 motion(entrance, expressive);
+
+ @media (prefers-reduced-motion) {
+ transition: none;
+ }
+ }
+
+ .#{$prefix}--dialog[open]::backdrop {
+ opacity: 1;
+
+ transition: background-color $duration-moderate-02 motion(exit, expressive),
+ opacity $duration-moderate-02 motion(exit, expressive);
+
+ @media (prefers-reduced-motion) {
+ transition: none;
+ }
+ }
+
+ /** starting style also not supported widely
+ * https://caniuse.com/mdn-css_at-rules_starting-style
+ */
+ /* This starting-style rule cannot be nested inside the above selector
+because the nesting selector cannot represent pseudo-elements. */
+ /* stylelint-disable-next-line scss/at-rule-no-unknown */
+ @starting-style {
+ .#{$prefix}--dialog[open]::backdrop {
+ opacity: 0;
+ }
+ }
+
+ .#{$prefix}--dialog__header-controls {
+ position: absolute;
+ inset-block-start: 0;
+ inset-inline-end: 0;
+ }
+}
diff --git a/packages/styles/scss/components/dialog/_index.scss b/packages/styles/scss/components/dialog/_index.scss
new file mode 100644
index 000000000000..c53a964fb276
--- /dev/null
+++ b/packages/styles/scss/components/dialog/_index.scss
@@ -0,0 +1,11 @@
+//
+// Copyright IBM Corp. 2018, 2023
+//
+// This source code is licensed under the Apache-2.0 license found in the
+// LICENSE file in the root directory of this source tree.
+//
+
+@forward 'dialog';
+@use 'dialog';
+
+@include dialog.dialog;