Prechádzať zdrojové kódy

Fix 'Dialog' keydown event and prop type warning (#1305)

Sanghyeon Lee 5 rokov pred
rodič
commit
26facfa710

+ 54 - 4
src/components/Dialog.tsx

@@ -1,9 +1,10 @@
-import React from "react";
+import React, { useEffect, useRef } from "react";
 import { Modal } from "./Modal";
 import { Island } from "./Island";
 import { t } from "../i18n";
 import useIsMobile from "../is-mobile";
 import { back, close } from "./icons";
+import { KEYS } from "../keys";
 
 import "./Dialog.scss";
 
@@ -12,9 +13,59 @@ export function Dialog(props: {
   className?: string;
   maxWidth?: number;
   onCloseRequest(): void;
-  closeButtonRef?: React.Ref<HTMLButtonElement>;
   title: React.ReactNode;
 }) {
+  const islandRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    const focusableElements = queryFocusableElements();
+
+    if (focusableElements.length > 0) {
+      // If there's an element other than close, focus it.
+      (focusableElements[1] || focusableElements[0]).focus();
+    }
+  }, []);
+
+  useEffect(() => {
+    if (!islandRef.current) {
+      return;
+    }
+
+    function handleKeyDown(event: KeyboardEvent) {
+      if (event.key === KEYS.TAB) {
+        const focusableElements = queryFocusableElements();
+        const { activeElement } = document;
+        const currentIndex = focusableElements.findIndex(
+          (element) => element === activeElement,
+        );
+
+        if (currentIndex === 0 && event.shiftKey) {
+          focusableElements[focusableElements.length - 1].focus();
+          event.preventDefault();
+        } else if (
+          currentIndex === focusableElements.length - 1 &&
+          !event.shiftKey
+        ) {
+          focusableElements[0].focus();
+          event.preventDefault();
+        }
+      }
+    }
+
+    const node = islandRef.current;
+    node.addEventListener("keydown", handleKeyDown);
+
+    return () => node.removeEventListener("keydown", handleKeyDown);
+  }, []);
+
+  function queryFocusableElements() {
+    const focusableElements = islandRef.current?.querySelectorAll<HTMLElement>(
+      "button, a, input, select, textarea, div[tabindex]",
+    );
+
+    return focusableElements ? Array.from(focusableElements) : [];
+  }
+
   return (
     <Modal
       className={`${props.className ?? ""} Dialog`}
@@ -22,14 +73,13 @@ export function Dialog(props: {
       maxWidth={props.maxWidth}
       onCloseRequest={props.onCloseRequest}
     >
-      <Island padding={4}>
+      <Island padding={4} ref={islandRef}>
         <h2 id="dialog-title" className="Dialog__title">
           <span className="Dialog__titleContent">{props.title}</span>
           <button
             className="Modal__close"
             onClick={props.onCloseRequest}
             aria-label={t("buttons.close")}
-            ref={props.closeButtonRef}
           >
             {useIsMobile() ? back : close}
           </button>

+ 1 - 37
src/components/ExportDialog.tsx

@@ -11,8 +11,6 @@ import { ActionsManagerInterface } from "../actions/types";
 import Stack from "./Stack";
 import { t } from "../i18n";
 
-import { KEYS } from "../keys";
-
 import { probablySupportsClipboardBlob } from "../clipboard";
 import { getSelectedElements, isSomeElementSelected } from "../scene";
 import useIsMobile from "../is-mobile";
@@ -35,7 +33,6 @@ function ExportModal({
   onExportToSvg,
   onExportToClipboard,
   onExportToBackend,
-  closeButton,
 }: {
   appState: AppState;
   elements: readonly ExcalidrawElement[];
@@ -46,15 +43,12 @@ function ExportModal({
   onExportToClipboard: ExportCB;
   onExportToBackend: ExportCB;
   onCloseRequest: () => void;
-  closeButton: React.RefObject<HTMLButtonElement>;
 }) {
   const someElementIsSelected = isSomeElementSelected(elements, appState);
   const [scale, setScale] = useState(defaultScale);
   const [exportSelected, setExportSelected] = useState(someElementIsSelected);
   const previewRef = useRef<HTMLDivElement>(null);
   const { exportBackground, viewBackgroundColor } = appState;
-  const pngButton = useRef<HTMLButtonElement>(null);
-  const onlySelectedInput = useRef<HTMLInputElement>(null);
 
   const exportedElements = exportSelected
     ? getSelectedElements(elements, appState)
@@ -85,33 +79,8 @@ function ExportModal({
     scale,
   ]);
 
-  useEffect(() => {
-    pngButton.current?.focus();
-  }, []);
-
-  function handleKeyDown(event: React.KeyboardEvent) {
-    if (event.key === KEYS.TAB) {
-      const { activeElement } = document;
-      if (event.shiftKey) {
-        if (activeElement === pngButton.current) {
-          closeButton.current?.focus();
-          event.preventDefault();
-        }
-      } else {
-        if (activeElement === closeButton.current) {
-          pngButton.current?.focus();
-          event.preventDefault();
-        }
-        if (activeElement === onlySelectedInput.current) {
-          closeButton.current?.focus();
-          event.preventDefault();
-        }
-      }
-    }
-  }
-
   return (
-    <div onKeyDown={handleKeyDown} className="ExportDialog">
+    <div className="ExportDialog">
       <div className="ExportDialog__preview" ref={previewRef}></div>
       <Stack.Col gap={2} align="center">
         <div className="ExportDialog__actions">
@@ -122,7 +91,6 @@ function ExportModal({
               title={t("buttons.exportToPng")}
               aria-label={t("buttons.exportToPng")}
               onClick={() => onExportToPng(exportedElements, scale)}
-              ref={pngButton}
             />
             <ToolButton
               type="button"
@@ -177,7 +145,6 @@ function ExportModal({
                 onChange={(event) =>
                   setExportSelected(event.currentTarget.checked)
                 }
-                ref={onlySelectedInput}
               />{" "}
               {t("labels.onlySelected")}
             </label>
@@ -209,7 +176,6 @@ export function ExportDialog({
 }) {
   const [modalIsShown, setModalIsShown] = useState(false);
   const triggerButton = useRef<HTMLButtonElement>(null);
-  const closeButton = useRef<HTMLButtonElement>(null);
 
   const handleClose = React.useCallback(() => {
     setModalIsShown(false);
@@ -232,7 +198,6 @@ export function ExportDialog({
           maxWidth={800}
           onCloseRequest={handleClose}
           title={t("buttons.export")}
-          closeButtonRef={closeButton}
         >
           <ExportModal
             elements={elements}
@@ -244,7 +209,6 @@ export function ExportDialog({
             onExportToClipboard={onExportToClipboard}
             onExportToBackend={onExportToBackend}
             onCloseRequest={handleClose}
-            closeButton={closeButton}
           />
         </Dialog>
       )}

+ 1 - 0
src/components/Modal.tsx

@@ -26,6 +26,7 @@ export function Modal(props: {
       aria-modal="true"
       onKeyDown={handleKeydown}
       aria-labelledby={props.labelledBy}
+      tabIndex={-1}
     >
       <div className="Modal__background" onClick={props.onCloseRequest}></div>
       <div

+ 2 - 4
src/components/ShortcutsDialog.tsx

@@ -14,7 +14,6 @@ const ShortcutIsland = (props: {
       border: "1px solid #ced4da",
       marginBottom: "16px",
     }}
-    {...props}
   >
     <h3
       style={{
@@ -39,7 +38,6 @@ const Shortcut = (props: {
     style={{
       borderTop: "1px solid #ced4da",
     }}
-    {...props}
   >
     <div
       style={{
@@ -68,12 +66,12 @@ const Shortcut = (props: {
         }}
       >
         {props.shortcuts.map((shortcut, index) => (
-          <>
+          <React.Fragment key={index}>
             <ShortcutKey>{shortcut}</ShortcutKey>
             {props.isOr &&
               index !== props.shortcuts.length - 1 &&
               t("shortcutsDialog.or")}
-          </>
+          </React.Fragment>
         ))}
       </div>
     </div>