banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

Floating UI Experience Sharing - Dialog

Previous article: Floating UI Experience Sharing - Popover

In this article, I will share how to use Floating UI to create another common floating UI component—Dialog. A Dialog is a floating element that displays information that requires immediate attention; it appears over the page content and prevents interaction with the page until it is closed.

It has similar interactions to popups, but there are two main differences:

  • It is modal and presents a background behind the dialog, dimming the content behind it and making the rest of the page inaccessible.
  • It is centered in the viewport and is not anchored to any specific reference element.

An accessible dialog component has the following key points:

  • Dismissal: It closes when the user presses the esc key or clicks outside the open dialog.
  • Role: The element is assigned relevant roles and ARIA attributes so that screen readers can access it.
  • Focus management: Focus is completely trapped within the dialog and must be released by the user.

Target Component#

Goal: Implement a Dialog Demo 👇

Pasted image 20230616145507

Next, we need to create a React component named Dialog, which uses the @floating-ui/react library to create an interactive floating dialog. Here is the concept for the component:

Component Parameters#

The Dialog component needs to accept the following parameters:

  • rootId: The root element of the floating element, optional.
  • open: A boolean that controls whether the dialog is open.
  • initialOpen: A boolean indicating whether the dialog is initially open, default is false.
  • onOpenChange: A callback function that is called when the dialog's open state changes, accepting a boolean parameter.
  • render: A function that accepts an object parameter containing a close method to close the dialog. This function returns the React node to be rendered in the dialog.
  • className: CSS class name applied to the dialog.
  • overlayClass: CSS class name applied to the floating overlay.
  • containerClass: CSS class name applied to the dialog container.
  • isDismiss: A boolean that determines whether to enable the feature of closing the dialog by clicking outside, default is true.
  • children: React child elements, which can be a button that opens the dialog when clicked.
  • showCloseButton: A boolean that determines whether to show the close button, default is true.

Component Functionality#

The main functionality of the Dialog component is to create an interactive floating dialog that can be closed by clicking the close button or clicking outside the dialog. The open and close state of the dialog can be controlled through the open and onOpenChange parameters (controlled), or it can be managed automatically through internal state (uncontrolled).

The Dialog component uses several hooks from the @floating-ui/react library:

  • useFloating: Used to manage the open and close state of the dialog.
  • useClick, useRole, and useDismiss: Used to handle interactions with the dialog, such as clicks and role management.
  • useInteractions: Used to get and set interaction properties.

Additionally, the Dialog component also uses FloatingPortal, FloatingOverlay, and FloatingFocusManager components to create the UI for the floating dialog.

Complete Code#

Combining practical aspects, we can write a more complete Dialog example that allows customization of the overlay, internal element styles, controls whether clicking the overlay closes the dialog, and can incorporate animations using Framer-motion (I may write an article about this in the future).

import {  
  FloatingFocusManager,  
  FloatingOverlay,  
  FloatingPortal,  
  useClick,  
  useDismiss,  
  useFloating,  
  useInteractions,  
  useRole,  
} from '@floating-ui/react';  
import clsx from 'clsx';  
import React, { cloneElement, useState } from 'react';  
import { CgClose } from 'react-icons/cg';  
  
type DialogProps = {  
  rootId?: string;  
  open?: boolean;  
  initialOpen?: boolean;  
  onOpenChange?: (open: boolean) => void;  
  children?: JSX.Element;  
  render: (props: { close: () => void }) => React.ReactNode;  
  className?: string;  
  overlayClass?: string;  
  containerClass?: string;  
  isDismiss?: boolean;  
  showCloseButton?: boolean;  
};  
  
export default function Dialog({  
  initialOpen = false,  
  open: controlledOpen,  
  onOpenChange: setControlledOpen,  
  children,  
  className,  
  render,  
  rootId: customRootId,  
  overlayClass,  
  containerClass,  
  showCloseButton = true,  
  isDismiss = true,  
}: DialogProps) {  
  const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen);  
  const open = controlledOpen ?? uncontrolledOpen;  
  const setOpen = setControlledOpen ?? setUncontrolledOpen;  
  
  const { reference, floating, context } = useFloating({  
    open,  
    onOpenChange: setOpen,  
  });  
  
  const click = useClick(context);  
  const role = useRole(context);  
  const dismiss = useDismiss(context, { enabled: isDismiss, outsidePressEvent: 'mousedown' });  
  
  const { getReferenceProps, getFloatingProps } = useInteractions([click, role, dismiss]);  
  
  const onClose = () => setOpen(false);  
  
  return (  
    <>  
      {children && cloneElement(children, getReferenceProps({ ref: reference, ...children.props }))}  
      <FloatingPortal id={customRootId}>  
        {open && (  
          <FloatingOverlay  
            className={clsx('absolute inset-0 z-10 flex h-full w-full items-center', overlayClass ?? 'bg-black/60')}  
            lockScroll  
          >  
            <div className={clsx('m-auto grid place-items-center', containerClass)}>  
              <FloatingFocusManager context={context}>  
                <div  
                  className={clsx('relative overflow-hidden rounded-md bg-white', className ?? 'mx-24')}  
                  ref={floating}  
                  {...getFloatingProps()}  
                >  
                  {showCloseButton && <CgClose className="absolute right-2 top-2 h-6 w-6 cursor-pointer" onClick={onClose} />}  
                  {render({ close: onClose })}  
                </div>  
              </FloatingFocusManager>  
            </div>  
          </FloatingOverlay>  
        )}  
      </FloatingPortal>  
    </>  
  );  
}

Basic Dialog Hooks#

Official example 👉 CodeSandbox demo

This example demonstrates how to create a dialog for a single instance to familiarize yourself with the basics.

Let's take a look at this example:

Open state#

import {useState} from 'react';
 
function Dialog() {
  const [isOpen, setIsOpen] = useState(false);
}

isOpen determines whether the dialog is currently open on the screen. It is used for conditional rendering.

useFloating hook#

useFloating hook

The useFloating() hook provides context for our dialog. We need to pass some information:

  • open: The open state from our useState() hook above.
  • onOpenChange: A callback function called when the dialog opens or closes. We will use it to update our isOpen state.
import {useFloating} from '@floating-ui/react';
 
function Dialog() {
  const [isOpen, setIsOpen] = useState(false);
 
  const {refs, context} = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
  });
}

Interaction hooks#

Interaction hooks

  • useClick() adds the ability to open or close the dialog when clicking the reference element. However, the dialog may not be attached to the reference element, so this is optional. (Generally, dialogs are separated into a Portal, meaning the context is the body.)
  • useDismiss() adds the ability to close the dialog when the user presses the esc key or clicks outside the dialog. You can set its outsidePressEvent option to 'mousedown' so that touch events become lazy and do not pass through the background, as the default behavior is eager. (This is a bit hard to understand, but that's the gist.)
  • useRole() adds the correct ARIA attributes for dialog to the dialog and reference elements.

Finally, useInteractions() merges all their props into prop getters, which can then be used for rendering.

import {
  // ...
  useClick,
  useDismiss,
  useRole,
  useInteractions,
  useId,
} from '@floating-ui/react';
 
function Dialog() {
  const [isOpen, setIsOpen] = useState(false);
 
  const {refs, context} = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
  });
 
  const click = useClick(context);
  const dismiss = useDismiss(context, {
    outsidePressEvent: 'mousedown',
  });
  const role = useRole(context);
 
  // Merge all the interactions into prop getters
  const {getReferenceProps, getFloatingProps} = useInteractions([
    click,
    dismiss,
    role,
  ]);
 
  // Set up label and description ids
  const labelId = useId();
  const descriptionId = useId();
}

Rendering#

Rendering

Now that we have set up all the variables and hooks, we can render our elements.

function Dialog() {
  // ...
  return (
    <>
      <button ref={refs.setReference} {...getReferenceProps()}>
        Reference element
      </button>
      {isOpen && (
        <FloatingOverlay
          lockScroll
          style={{background: 'rgba(0, 0, 0, 0.8)'}}
        >
          <FloatingFocusManager context={context}>
            <div
              ref={refs.setFloating}
              aria-labelledby={labelId}
              aria-describedby={descriptionId}
              {...getFloatingProps()}
            >
              <h2 id={labelId}>Heading element</h2>
              <p id={descriptionId}>Description element</p>
              <button onClick={() => setIsOpen(false)}>
                Close
              </button>
            </div>
          </FloatingFocusManager>
        </FloatingOverlay>
      )}
    </>
  );
}
  • {...getReferenceProps()} / {...getFloatingProps()} as mentioned in the previous article, spreads props from interaction hooks to the relevant elements. They contain props such as onClick, aria-expanded, etc.

FloatingPortal & FloatingOverlay & FloatingFocusManager#

  • <FloatingOverlay /> is a component that renders a background overlay element behind the floating element, with the ability to lock body scroll. FloatingOverlay docs
    • It provides a fixed basic style that dims the background content and prevents pointer events behind the floating element.
    • It is a regular <div/>, so it can be styled using any CSS solution.
  • <FloatingFocusManager />
    • FloatingPortal docs is a component that manages and controls the focus of floating elements on the page.
    • It automatically detects focus changes, adjusts the position and state of floating elements on the page, ensuring accessibility and usability for all elements on the page.
    • By default, it usually captures focus internally.
    • It should directly wrap the floating element and should only be rendered when the dialog is also rendered. FloatingFocusManager docs
      • <FloatingPortal /> transports the floating element to a given container element—by default, outside the application root and into the body.
      • You can customize the root, meaning you can choose a node with an id, or create it and attach it to a specified root (body).
import {FloatingPortal} from '@floating-ui/react';
 
function Tooltip() {
  return (
    isOpen && (
      <FloatingPortal>
        <div>Floating element</div>
      </FloatingPortal>
    )
  );
}
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.