mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-30 18:34:22 +01:00 
			
		
		
		
	Fix 'Dialog' keydown event and prop type warning (#1305)
This commit is contained in:
		| @@ -1,9 +1,10 @@ | |||||||
| import React from "react"; | import React, { useEffect, useRef } from "react"; | ||||||
| import { Modal } from "./Modal"; | import { Modal } from "./Modal"; | ||||||
| import { Island } from "./Island"; | import { Island } from "./Island"; | ||||||
| import { t } from "../i18n"; | import { t } from "../i18n"; | ||||||
| import useIsMobile from "../is-mobile"; | import useIsMobile from "../is-mobile"; | ||||||
| import { back, close } from "./icons"; | import { back, close } from "./icons"; | ||||||
|  | import { KEYS } from "../keys"; | ||||||
|  |  | ||||||
| import "./Dialog.scss"; | import "./Dialog.scss"; | ||||||
|  |  | ||||||
| @@ -12,9 +13,59 @@ export function Dialog(props: { | |||||||
|   className?: string; |   className?: string; | ||||||
|   maxWidth?: number; |   maxWidth?: number; | ||||||
|   onCloseRequest(): void; |   onCloseRequest(): void; | ||||||
|   closeButtonRef?: React.Ref<HTMLButtonElement>; |  | ||||||
|   title: React.ReactNode; |   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 ( |   return ( | ||||||
|     <Modal |     <Modal | ||||||
|       className={`${props.className ?? ""} Dialog`} |       className={`${props.className ?? ""} Dialog`} | ||||||
| @@ -22,14 +73,13 @@ export function Dialog(props: { | |||||||
|       maxWidth={props.maxWidth} |       maxWidth={props.maxWidth} | ||||||
|       onCloseRequest={props.onCloseRequest} |       onCloseRequest={props.onCloseRequest} | ||||||
|     > |     > | ||||||
|       <Island padding={4}> |       <Island padding={4} ref={islandRef}> | ||||||
|         <h2 id="dialog-title" className="Dialog__title"> |         <h2 id="dialog-title" className="Dialog__title"> | ||||||
|           <span className="Dialog__titleContent">{props.title}</span> |           <span className="Dialog__titleContent">{props.title}</span> | ||||||
|           <button |           <button | ||||||
|             className="Modal__close" |             className="Modal__close" | ||||||
|             onClick={props.onCloseRequest} |             onClick={props.onCloseRequest} | ||||||
|             aria-label={t("buttons.close")} |             aria-label={t("buttons.close")} | ||||||
|             ref={props.closeButtonRef} |  | ||||||
|           > |           > | ||||||
|             {useIsMobile() ? back : close} |             {useIsMobile() ? back : close} | ||||||
|           </button> |           </button> | ||||||
|   | |||||||
| @@ -11,8 +11,6 @@ import { ActionsManagerInterface } from "../actions/types"; | |||||||
| import Stack from "./Stack"; | import Stack from "./Stack"; | ||||||
| import { t } from "../i18n"; | import { t } from "../i18n"; | ||||||
|  |  | ||||||
| import { KEYS } from "../keys"; |  | ||||||
|  |  | ||||||
| import { probablySupportsClipboardBlob } from "../clipboard"; | import { probablySupportsClipboardBlob } from "../clipboard"; | ||||||
| import { getSelectedElements, isSomeElementSelected } from "../scene"; | import { getSelectedElements, isSomeElementSelected } from "../scene"; | ||||||
| import useIsMobile from "../is-mobile"; | import useIsMobile from "../is-mobile"; | ||||||
| @@ -35,7 +33,6 @@ function ExportModal({ | |||||||
|   onExportToSvg, |   onExportToSvg, | ||||||
|   onExportToClipboard, |   onExportToClipboard, | ||||||
|   onExportToBackend, |   onExportToBackend, | ||||||
|   closeButton, |  | ||||||
| }: { | }: { | ||||||
|   appState: AppState; |   appState: AppState; | ||||||
|   elements: readonly ExcalidrawElement[]; |   elements: readonly ExcalidrawElement[]; | ||||||
| @@ -46,15 +43,12 @@ function ExportModal({ | |||||||
|   onExportToClipboard: ExportCB; |   onExportToClipboard: ExportCB; | ||||||
|   onExportToBackend: ExportCB; |   onExportToBackend: ExportCB; | ||||||
|   onCloseRequest: () => void; |   onCloseRequest: () => void; | ||||||
|   closeButton: React.RefObject<HTMLButtonElement>; |  | ||||||
| }) { | }) { | ||||||
|   const someElementIsSelected = isSomeElementSelected(elements, appState); |   const someElementIsSelected = isSomeElementSelected(elements, appState); | ||||||
|   const [scale, setScale] = useState(defaultScale); |   const [scale, setScale] = useState(defaultScale); | ||||||
|   const [exportSelected, setExportSelected] = useState(someElementIsSelected); |   const [exportSelected, setExportSelected] = useState(someElementIsSelected); | ||||||
|   const previewRef = useRef<HTMLDivElement>(null); |   const previewRef = useRef<HTMLDivElement>(null); | ||||||
|   const { exportBackground, viewBackgroundColor } = appState; |   const { exportBackground, viewBackgroundColor } = appState; | ||||||
|   const pngButton = useRef<HTMLButtonElement>(null); |  | ||||||
|   const onlySelectedInput = useRef<HTMLInputElement>(null); |  | ||||||
|  |  | ||||||
|   const exportedElements = exportSelected |   const exportedElements = exportSelected | ||||||
|     ? getSelectedElements(elements, appState) |     ? getSelectedElements(elements, appState) | ||||||
| @@ -85,33 +79,8 @@ function ExportModal({ | |||||||
|     scale, |     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 ( |   return ( | ||||||
|     <div onKeyDown={handleKeyDown} className="ExportDialog"> |     <div className="ExportDialog"> | ||||||
|       <div className="ExportDialog__preview" ref={previewRef}></div> |       <div className="ExportDialog__preview" ref={previewRef}></div> | ||||||
|       <Stack.Col gap={2} align="center"> |       <Stack.Col gap={2} align="center"> | ||||||
|         <div className="ExportDialog__actions"> |         <div className="ExportDialog__actions"> | ||||||
| @@ -122,7 +91,6 @@ function ExportModal({ | |||||||
|               title={t("buttons.exportToPng")} |               title={t("buttons.exportToPng")} | ||||||
|               aria-label={t("buttons.exportToPng")} |               aria-label={t("buttons.exportToPng")} | ||||||
|               onClick={() => onExportToPng(exportedElements, scale)} |               onClick={() => onExportToPng(exportedElements, scale)} | ||||||
|               ref={pngButton} |  | ||||||
|             /> |             /> | ||||||
|             <ToolButton |             <ToolButton | ||||||
|               type="button" |               type="button" | ||||||
| @@ -177,7 +145,6 @@ function ExportModal({ | |||||||
|                 onChange={(event) => |                 onChange={(event) => | ||||||
|                   setExportSelected(event.currentTarget.checked) |                   setExportSelected(event.currentTarget.checked) | ||||||
|                 } |                 } | ||||||
|                 ref={onlySelectedInput} |  | ||||||
|               />{" "} |               />{" "} | ||||||
|               {t("labels.onlySelected")} |               {t("labels.onlySelected")} | ||||||
|             </label> |             </label> | ||||||
| @@ -209,7 +176,6 @@ export function ExportDialog({ | |||||||
| }) { | }) { | ||||||
|   const [modalIsShown, setModalIsShown] = useState(false); |   const [modalIsShown, setModalIsShown] = useState(false); | ||||||
|   const triggerButton = useRef<HTMLButtonElement>(null); |   const triggerButton = useRef<HTMLButtonElement>(null); | ||||||
|   const closeButton = useRef<HTMLButtonElement>(null); |  | ||||||
|  |  | ||||||
|   const handleClose = React.useCallback(() => { |   const handleClose = React.useCallback(() => { | ||||||
|     setModalIsShown(false); |     setModalIsShown(false); | ||||||
| @@ -232,7 +198,6 @@ export function ExportDialog({ | |||||||
|           maxWidth={800} |           maxWidth={800} | ||||||
|           onCloseRequest={handleClose} |           onCloseRequest={handleClose} | ||||||
|           title={t("buttons.export")} |           title={t("buttons.export")} | ||||||
|           closeButtonRef={closeButton} |  | ||||||
|         > |         > | ||||||
|           <ExportModal |           <ExportModal | ||||||
|             elements={elements} |             elements={elements} | ||||||
| @@ -244,7 +209,6 @@ export function ExportDialog({ | |||||||
|             onExportToClipboard={onExportToClipboard} |             onExportToClipboard={onExportToClipboard} | ||||||
|             onExportToBackend={onExportToBackend} |             onExportToBackend={onExportToBackend} | ||||||
|             onCloseRequest={handleClose} |             onCloseRequest={handleClose} | ||||||
|             closeButton={closeButton} |  | ||||||
|           /> |           /> | ||||||
|         </Dialog> |         </Dialog> | ||||||
|       )} |       )} | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ export function Modal(props: { | |||||||
|       aria-modal="true" |       aria-modal="true" | ||||||
|       onKeyDown={handleKeydown} |       onKeyDown={handleKeydown} | ||||||
|       aria-labelledby={props.labelledBy} |       aria-labelledby={props.labelledBy} | ||||||
|  |       tabIndex={-1} | ||||||
|     > |     > | ||||||
|       <div className="Modal__background" onClick={props.onCloseRequest}></div> |       <div className="Modal__background" onClick={props.onCloseRequest}></div> | ||||||
|       <div |       <div | ||||||
|   | |||||||
| @@ -14,7 +14,6 @@ const ShortcutIsland = (props: { | |||||||
|       border: "1px solid #ced4da", |       border: "1px solid #ced4da", | ||||||
|       marginBottom: "16px", |       marginBottom: "16px", | ||||||
|     }} |     }} | ||||||
|     {...props} |  | ||||||
|   > |   > | ||||||
|     <h3 |     <h3 | ||||||
|       style={{ |       style={{ | ||||||
| @@ -39,7 +38,6 @@ const Shortcut = (props: { | |||||||
|     style={{ |     style={{ | ||||||
|       borderTop: "1px solid #ced4da", |       borderTop: "1px solid #ced4da", | ||||||
|     }} |     }} | ||||||
|     {...props} |  | ||||||
|   > |   > | ||||||
|     <div |     <div | ||||||
|       style={{ |       style={{ | ||||||
| @@ -68,12 +66,12 @@ const Shortcut = (props: { | |||||||
|         }} |         }} | ||||||
|       > |       > | ||||||
|         {props.shortcuts.map((shortcut, index) => ( |         {props.shortcuts.map((shortcut, index) => ( | ||||||
|           <> |           <React.Fragment key={index}> | ||||||
|             <ShortcutKey>{shortcut}</ShortcutKey> |             <ShortcutKey>{shortcut}</ShortcutKey> | ||||||
|             {props.isOr && |             {props.isOr && | ||||||
|               index !== props.shortcuts.length - 1 && |               index !== props.shortcuts.length - 1 && | ||||||
|               t("shortcutsDialog.or")} |               t("shortcutsDialog.or")} | ||||||
|           </> |           </React.Fragment> | ||||||
|         ))} |         ))} | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Sanghyeon Lee
					Sanghyeon Lee