You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Dialog.tsx 3.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import clsx from "clsx";
  2. import React, { useEffect, useState } from "react";
  3. import { useCallbackRefState } from "../hooks/useCallbackRefState";
  4. import { t } from "../i18n";
  5. import { useExcalidrawContainer, useIsMobile } from "../components/App";
  6. import { KEYS } from "../keys";
  7. import "./Dialog.scss";
  8. import { back, close } from "./icons";
  9. import { Island } from "./Island";
  10. import { Modal } from "./Modal";
  11. import { AppState } from "../types";
  12. export interface DialogProps {
  13. children: React.ReactNode;
  14. className?: string;
  15. small?: boolean;
  16. onCloseRequest(): void;
  17. title: React.ReactNode;
  18. autofocus?: boolean;
  19. theme?: AppState["theme"];
  20. }
  21. export const Dialog = (props: DialogProps) => {
  22. const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
  23. const [lastActiveElement] = useState(document.activeElement);
  24. const { id } = useExcalidrawContainer();
  25. useEffect(() => {
  26. if (!islandNode) {
  27. return;
  28. }
  29. const focusableElements = queryFocusableElements(islandNode);
  30. if (focusableElements.length > 0 && props.autofocus !== false) {
  31. // If there's an element other than close, focus it.
  32. (focusableElements[1] || focusableElements[0]).focus();
  33. }
  34. const handleKeyDown = (event: KeyboardEvent) => {
  35. if (event.key === KEYS.TAB) {
  36. const focusableElements = queryFocusableElements(islandNode);
  37. const { activeElement } = document;
  38. const currentIndex = focusableElements.findIndex(
  39. (element) => element === activeElement,
  40. );
  41. if (currentIndex === 0 && event.shiftKey) {
  42. focusableElements[focusableElements.length - 1].focus();
  43. event.preventDefault();
  44. } else if (
  45. currentIndex === focusableElements.length - 1 &&
  46. !event.shiftKey
  47. ) {
  48. focusableElements[0].focus();
  49. event.preventDefault();
  50. }
  51. }
  52. };
  53. islandNode.addEventListener("keydown", handleKeyDown);
  54. return () => islandNode.removeEventListener("keydown", handleKeyDown);
  55. }, [islandNode, props.autofocus]);
  56. const queryFocusableElements = (node: HTMLElement) => {
  57. const focusableElements = node.querySelectorAll<HTMLElement>(
  58. "button, a, input, select, textarea, div[tabindex]",
  59. );
  60. return focusableElements ? Array.from(focusableElements) : [];
  61. };
  62. const onClose = () => {
  63. (lastActiveElement as HTMLElement).focus();
  64. props.onCloseRequest();
  65. };
  66. return (
  67. <Modal
  68. className={clsx("Dialog", props.className)}
  69. labelledBy="dialog-title"
  70. maxWidth={props.small ? 550 : 800}
  71. onCloseRequest={onClose}
  72. theme={props.theme}
  73. >
  74. <Island ref={setIslandNode}>
  75. <h2 id={`${id}-dialog-title`} className="Dialog__title">
  76. <span className="Dialog__titleContent">{props.title}</span>
  77. <button
  78. className="Modal__close"
  79. onClick={onClose}
  80. aria-label={t("buttons.close")}
  81. >
  82. {useIsMobile() ? back : close}
  83. </button>
  84. </h2>
  85. <div className="Dialog__content">{props.children}</div>
  86. </Island>
  87. </Modal>
  88. );
  89. };