Back to React Spectrum

useModalOverlay

packages/react-aria/docs/overlays/useModalOverlay.mdx

2022-12-169.9 KB
Original Source

{/* Copyright 2020 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */}

import {Layout} from '@react-spectrum/docs'; export default Layout;

import docs from 'docs:@react-aria/overlays'; import focusDocs from 'docs:@react-aria/focus'; import dialogDocs from 'docs:@react-aria/dialog'; import statelyDocs from 'docs:@react-stately/overlays'; import {HeaderInfo, FunctionAPI, TypeContext, InterfaceType, TypeLink, PageDescription} from '@react-spectrum/docs'; import {Keyboard} from '@react-spectrum/text'; import packageData from '@react-aria/overlays/package.json'; import ChevronRight from '@spectrum-icons/workflow/ChevronRight'; import Anatomy from './modal-anatomy.svg';


category: Overlays keywords: [overlays, dialog, modal, aria]

useModalOverlay

<PageDescription>{docs.exports.useModalOverlay.description}</PageDescription>

<HeaderInfo packageData={packageData} componentNames={['useModalOverlay']} sourceData={[ {type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/'} ]} />

API

<FunctionAPI function={docs.exports.useModalOverlay} links={docs.links} />

Features

The HTML <dialog> element can be used to build modal overlays. However, it is not yet widely supported across browsers, and can be difficult to style and customize. useModalOverlay, helps achieve accessible modal overlays that can be styled as needed.

  • Accessible – Content outside the modal is hidden from assistive technologies while it is open. The modal optionally closes when interacting outside, or pressing the <Keyboard>Escape</Keyboard> key.
  • Focus management – Focus is moved into the modal on mount, and restored to the trigger element on unmount. While open, focus is contained within the modal, preventing the user from tabbing outside.
  • Scroll locking – Scrolling the page behind the modal is prevented while it is open, including in mobile browsers.

Note: useModalOverlay only handles the overlay itself. It should be combined with useDialog to create fully accessible modal dialogs. Other overlays such as menus may also be placed in a modal overlay.

Anatomy

<Anatomy />

A modal overlay consists of an overlay container element, and an underlay. The overlay may contain a dialog, or another element such as a menu or listbox when used within a component such as a select or combobox. The underlay is typically a partially transparent element that covers the rest of the screen behind the overlay, and prevents the user from interacting with the elements behind it.

useModalOverlay returns props that you should spread onto the overlay and underlay elements:

<TypeContext.Provider value={docs.links}> <InterfaceType properties={docs.links[docs.exports.useModalOverlay.return.id].properties} /> </TypeContext.Provider>

State is managed by the <TypeLink links={statelyDocs.links} type={statelyDocs.exports.useOverlayTriggerState} /> hook in @react-stately/overlays. The state object should be passed as an argument to useModalOverlay.

Example

This example shows how to build a typical modal dialog, by combining useModalOverlay with useDialog. The Dialog component used in this example can also be reused within a popover or other types of overlays.

The Modal component uses an <<TypeLink links={docs.links} type={docs.exports.Overlay} />> to render its contents in a React Portal at the end of the document body, which ensures it is not clipped by other elements. It also acts as a focus scope, containing focus within the modal and restoring it to the trigger when it unmounts. <TypeLink links={docs.links} type={docs.exports.useModalOverlay} /> handles preventing page scrolling while the modal is open, hiding content outside the modal from screen readers, and optionally closing it when the user interacts outside or presses the <Keyboard>Escape</Keyboard> key.

tsx
import {Overlay, useModalOverlay} from '@react-aria/overlays';
import {useViewportSize} from '@react-aria/utils';

function Modal({state, children, ...props}) {
  let ref = React.useRef(null);
  let {modalProps, underlayProps} = useModalOverlay(props, state, ref);

  return (
    <Overlay>
      <div
        style={{
          position: 'absolute',
          zIndex: 100,
          top: 0,
          left: 0,
          width: '100%',
          height: document.body.clientHeight,
          background: 'rgba(0, 0, 0, 0.5)'
        }}
        {...underlayProps} />
      <div
        style={{
          position: 'fixed',
          top: 0,
          left: 0,
          width: '100%',
          height: useViewportSize().height + 'px',
          zIndex: 101,
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center'
        }}>
        <div
          {...modalProps}
          ref={ref}
          style={{
            background: 'var(--page-background)',
            border: '1px solid gray'
          }}>
          {children}
        </div>
      </div>
    </Overlay>
  );
}

The below ModalTrigger component uses the <TypeLink links={docs.links} type={docs.exports.useOverlayTrigger} /> hook to show the modal when a button is pressed. It accepts a function as children, which is called with a callback that closes the modal. This can be used to implement a close button.

tsx
import {useOverlayTriggerState} from '@react-stately/overlays';
import {useOverlayTrigger} from '@react-aria/overlays';

// Reuse the Button from your component library. See below for details.
import {Button} from 'your-component-library';

function ModalTrigger({label, children, ...props}) {
  let state = useOverlayTriggerState(props);
  let {triggerProps, overlayProps} = useOverlayTrigger({type: 'dialog'}, state);

  return <>
    <Button {...triggerProps}>Open Dialog</Button>
    {state.isOpen &&
      <Modal {...props} state={state}>
        {React.cloneElement(children(state.close), overlayProps)}
      </Modal>
    }
  </>;
}

Now, we can render an example modal containing a dialog, with a button that closes it using the function provided by ModalTrigger.

tsx
// Reuse the Dialog from your component library. See below for details.
import {Dialog} from 'your-component-library';

<ModalTrigger label="Open Dialog">
  {close =>
    <Dialog title="Enter your name">
      <form style={{display: 'flex', flexDirection: 'column'}}>
        <label htmlFor="first-name">First Name:</label>
        <input id="first-name" />
        <label htmlFor="last-name">Last Name:</label>
        <input id="last-name" />
        <Button
          onPress={close}
          style={{marginTop: 10}}>
          Submit
        </Button>
      </form>
    </Dialog>
  }
</ModalTrigger>

Dialog

The Dialog component is rendered within the ModalOverlay component. It is built using the useDialog hook, and can also be used in other overlay containers such as popovers.

<details> <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show code</summary>
tsx
import type {AriaDialogProps} from '@react-aria/dialog';
import {useDialog} from '@react-aria/dialog';

interface DialogProps extends AriaDialogProps {
  title?: React.ReactNode,
  children: React.ReactNode
}

function Dialog({title, children, ...props}: DialogProps) {
  let ref = React.useRef(null);
  let {dialogProps, titleProps} = useDialog(props, ref);

  return (
    <div {...dialogProps} ref={ref} style={{padding: 30}}>
      {title &&
        <h3 {...titleProps} style={{marginTop: 0}}>
          {title}
        </h3>
      }
      {children}
    </div>
  );
}
</details>

Button

The Button component is used in the above example to toggle the popover. It is built using the useButton hook, and can be shared with many other components.

<details> <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show code</summary>
tsx
import {useButton} from '@react-aria/button';

function Button(props) {
  let ref = props.buttonRef;
  let {buttonProps} = useButton(props, ref);
  return <button {...buttonProps} ref={ref} style={props.style}>{props.children}</button>;
}
</details>

Usage

The following examples show how to use the Modal and ModalTrigger components created in the above example.

Dismissable

If your modal doesn't require the user to make a confirmation, you can set isDismissable on the Modal. This allows the user to click outside to close the dialog.

tsx
<ModalTrigger isDismissable label="Open Dialog">
  {() =>
    <Dialog title="Notice">
      Click outside to close this dialog.
    </Dialog>
  }
</ModalTrigger>

Keyboard dismiss disabled

By default, modals can be closed by pressing the <Keyboard>Escape</Keyboard> key. This can be disabled with the isKeyboardDismissDisabled prop.

tsx
<ModalTrigger isKeyboardDismissDisabled label="Open Dialog">
  {close =>
    <Dialog title="Notice">
      <p>You must close this dialog using the button below.</p>
      <Button onPress={close}>Close</Button>
    </Dialog>
  }
</ModalTrigger>