Dialog.tsx 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. import clsx from "clsx";
  2. import React, { useEffect } from "react";
  3. import { useCallbackRefState } from "../hooks/useCallbackRefState";
  4. import { t } from "../i18n";
  5. import { 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 const Dialog = (props: {
  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. const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
  22. useEffect(() => {
  23. if (!islandNode) {
  24. return;
  25. }
  26. const focusableElements = queryFocusableElements(islandNode);
  27. if (focusableElements.length > 0 && props.autofocus !== false) {
  28. // If there's an element other than close, focus it.
  29. (focusableElements[1] || focusableElements[0]).focus();
  30. }
  31. const handleKeyDown = (event: KeyboardEvent) => {
  32. if (event.key === KEYS.TAB) {
  33. const focusableElements = queryFocusableElements(islandNode);
  34. const { activeElement } = document;
  35. const currentIndex = focusableElements.findIndex(
  36. (element) => element === activeElement,
  37. );
  38. if (currentIndex === 0 && event.shiftKey) {
  39. focusableElements[focusableElements.length - 1].focus();
  40. event.preventDefault();
  41. } else if (
  42. currentIndex === focusableElements.length - 1 &&
  43. !event.shiftKey
  44. ) {
  45. focusableElements[0].focus();
  46. event.preventDefault();
  47. }
  48. }
  49. };
  50. islandNode.addEventListener("keydown", handleKeyDown);
  51. return () => islandNode.removeEventListener("keydown", handleKeyDown);
  52. }, [islandNode, props.autofocus]);
  53. const queryFocusableElements = (node: HTMLElement) => {
  54. const focusableElements = node.querySelectorAll<HTMLElement>(
  55. "button, a, input, select, textarea, div[tabindex]",
  56. );
  57. return focusableElements ? Array.from(focusableElements) : [];
  58. };
  59. return (
  60. <Modal
  61. className={clsx("Dialog", props.className)}
  62. labelledBy="dialog-title"
  63. maxWidth={props.small ? 550 : 800}
  64. onCloseRequest={props.onCloseRequest}
  65. theme={props.theme}
  66. >
  67. <Island ref={setIslandNode}>
  68. <h2 id="dialog-title" className="Dialog__title">
  69. <span className="Dialog__titleContent">{props.title}</span>
  70. <button
  71. className="Modal__close"
  72. onClick={props.onCloseRequest}
  73. aria-label={t("buttons.close")}
  74. >
  75. {useIsMobile() ? back : close}
  76. </button>
  77. </h2>
  78. <div className="Dialog__content">{props.children}</div>
  79. </Island>
  80. </Modal>
  81. );
  82. };