FezVrasta/popper.js

View on GitHub
website/lib/components/Dialog.js

Summary

Maintainability
D
2 days
Test Coverage
import {
  FloatingFocusManager,
  FloatingOverlay,
  FloatingPortal,
  useClick,
  useDismiss,
  useFloating,
  useId,
  useInteractions,
  useMergeRefs,
  useRole,
  useTransitionStyles,
} from '@floating-ui/react';
import * as React from 'react';

export function useDialog({
  initialOpen = false,
  open: controlledOpen,
  onOpenChange: setControlledOpen,
}) {
  const [uncontrolledOpen, setUncontrolledOpen] =
    React.useState(initialOpen);
  const [labelId, setLabelId] = React.useState();
  const [descriptionId, setDescriptionId] = React.useState();

  const open = controlledOpen ?? uncontrolledOpen;
  const setOpen = setControlledOpen ?? setUncontrolledOpen;

  const data = useFloating({
    open,
    onOpenChange: setOpen,
  });

  const context = data.context;

  const click = useClick(context, {
    enabled: controlledOpen == null,
  });
  const dismiss = useDismiss(context, {
    outsidePressEvent: 'mousedown',
  });
  const role = useRole(context);

  const interactions = useInteractions([click, dismiss, role]);

  return React.useMemo(
    () => ({
      open,
      setOpen,
      ...interactions,
      ...data,
      labelId,
      descriptionId,
      setLabelId,
      setDescriptionId,
    }),
    [open, setOpen, interactions, data, labelId, descriptionId],
  );
}

const DialogContext = React.createContext(null);

export const useDialogContext = () => {
  const context = React.useContext(DialogContext);

  if (context == null) {
    throw new Error(
      'Dialog components must be wrapped in <Dialog />',
    );
  }

  return context;
};

export function Dialog({children, ...options}) {
  const dialog = useDialog(options);
  return (
    <DialogContext.Provider value={dialog}>
      {children}
    </DialogContext.Provider>
  );
}

export const DialogTrigger = React.forwardRef(
  function DialogTrigger(
    {children, asChild = false, ...props},
    propRef,
  ) {
    const context = useDialogContext();
    const childrenRef = children.ref;
    const ref = useMergeRefs([
      context.refs.setReference,
      propRef,
      childrenRef,
    ]);

    // `asChild` allows the user to pass any element as the anchor
    if (asChild && React.isValidElement(children)) {
      return React.cloneElement(
        children,
        context.getReferenceProps({
          ref,
          ...props,
          ...children.props,
          'data-state': context.open ? 'open' : 'closed',
        }),
      );
    }

    return (
      <button
        ref={ref}
        // The user can style the trigger based on the state
        data-state={context.open ? 'open' : 'closed'}
        {...context.getReferenceProps(props)}
      >
        {children}
      </button>
    );
  },
);

export const DialogContent = React.forwardRef(
  function DialogContent(props, propRef) {
    const {context: floatingContext, ...context} =
      useDialogContext();
    const ref = useMergeRefs([
      context.refs.setFloating,
      propRef,
    ]);

    const backdropTransition = useTransitionStyles(
      floatingContext,
      {duration: {open: 200, close: 100}},
    );

    const floatingTransition = useTransitionStyles(
      floatingContext,
      {
        duration: {open: 400, close: 100},
        initial: {
          opacity: 0,
          transform: 'translateY(0.25rem)',
        },
        close: {
          opacity: 0,
          transform: 'scale(0.95)',
        },
      },
    );

    return (
      <FloatingPortal>
        {backdropTransition.isMounted && (
          <>
            <FloatingOverlay
              className="grid place-items-center bg-gray-1000/60 backdrop-blur-sm"
              lockScroll
              style={backdropTransition.styles}
            />
            <div className="fixed top-0 right-0 left-0 bottom-0 grid place-items-center text-black">
              <FloatingFocusManager context={floatingContext}>
                <div
                  ref={ref}
                  aria-labelledby={context.labelId}
                  aria-describedby={context.descriptionId}
                  style={floatingTransition.styles}
                  {...context.getFloatingProps(props)}
                >
                  {props.children}
                </div>
              </FloatingFocusManager>
            </div>
          </>
        )}
      </FloatingPortal>
    );
  },
);

export const DialogHeading = React.forwardRef(
  function DialogHeading({children, ...props}, ref) {
    const {setLabelId} = useDialogContext();
    const id = useId();

    // Only sets `aria-labelledby` on the Dialog root element
    // if this component is mounted inside it.
    React.useLayoutEffect(() => {
      setLabelId(id);
      return () => setLabelId(undefined);
    }, [id, setLabelId]);

    return (
      <h2 {...props} ref={ref} id={id}>
        {children}
      </h2>
    );
  },
);

export const DialogDescription = React.forwardRef(
  function DialogDescription({children, ...props}, ref) {
    const {setDescriptionId} = useDialogContext();
    const id = useId();

    // Only sets `aria-describedby` on the Dialog root element
    // if this component is mounted inside it.
    React.useLayoutEffect(() => {
      setDescriptionId(id);
      return () => setDescriptionId(undefined);
    }, [id, setDescriptionId]);

    return (
      <p {...props} ref={ref} id={id}>
        {children}
      </p>
    );
  },
);

export const DialogClose = React.forwardRef(function DialogClose(
  {children, ...props},
  ref,
) {
  const {setOpen} = useDialogContext();
  return (
    <button
      type="button"
      {...props}
      ref={ref}
      onClick={() => setOpen(false)}
    >
      {children}
    </button>
  );
});