diff --git a/src/components/ApiDocs/ApiComment.tsx b/src/components/ApiDocs/ApiComment.tsx index 4b984b6a320..0d4f689319e 100644 --- a/src/components/ApiDocs/ApiComment.tsx +++ b/src/components/ApiDocs/ApiComment.tsx @@ -8,8 +8,10 @@ interface ApiCommentProps { export const ApiComment = ({ apiComment, codeBlock }: ApiCommentProps) => { if (!apiComment) return null; - const firstItem = apiComment[0]; - if (!firstItem.text.replaceAll('-', '').trim()) { + const firstItem = apiComment[0]?.text + ? apiComment[0].text.replaceAll('-', '').trim() + : null; + if (!firstItem) { apiComment.shift(); } const commentList = apiComment.map((snippet, idx) => { diff --git a/src/components/ApiDocs/ApiModalProvider.tsx b/src/components/ApiDocs/ApiModalProvider.tsx index c9f28d7d434..2052328b31c 100644 --- a/src/components/ApiDocs/ApiModalProvider.tsx +++ b/src/components/ApiDocs/ApiModalProvider.tsx @@ -1,10 +1,21 @@ -import { useState, createContext } from 'react'; +import { useState, createContext, useRef, RefObject } from 'react'; import { LinkDataType } from './display/TypeLink'; import { ApiModal } from './display/ApiModal'; -export const TypeContext = createContext({ +interface TypeContextInterface { + setModalData: (data: any) => void; + setModalTriggerRef: (ref: RefObject | null) => void; + modalTriggerRef: RefObject | null; + openModal: () => void; + addBreadCrumb: (data: any) => void; + setBC: (data: any) => void; +} + +export const TypeContext = createContext({ setModalData: (data) => data, - modalOpen: () => {}, + setModalTriggerRef: (ref) => ref, + modalTriggerRef: null, + openModal: () => {}, addBreadCrumb: (data) => data, setBC: (data) => data }); @@ -13,12 +24,26 @@ export const ApiModalProvider = ({ children }) => { const [modalData, setModalData] = useState({}); const [showModal, setShowModal] = useState(false); const [breadCrumbs, setBreadCrumbs] = useState([]); + const [modalTriggerRef, setModalTriggerRef] = + useState | null>(null); + + const modalRef = useRef(null); - const modalOpen = () => { + const openModal = () => { setShowModal(true); + setTimeout(() => { + // Focus the dialog element after modal is set to open + modalRef?.current?.focus(); + }, 0); }; const closeModal = () => { setShowModal(false); + // Focus the original modal trigger button after dialog is closed, + // otherwise, focus will be lost on the page + setTimeout(() => { + modalTriggerRef?.current?.focus(); + setModalTriggerRef(null); + }, 0); }; const addBreadCrumb = (bc) => { @@ -36,7 +61,9 @@ export const ApiModalProvider = ({ children }) => { const value = { setModalData, - modalOpen, + setModalTriggerRef, + modalTriggerRef, + openModal, addBreadCrumb, setBC }; @@ -44,6 +71,7 @@ export const ApiModalProvider = ({ children }) => { return ( void; breadCrumbs: LinkDataType[]; clearBC: () => void; + close: () => void; + data: any; + modalRef: React.RefObject; + showModal?: boolean; } export const ApiModal = ({ - data, - showModal = false, - close, breadCrumbs, - clearBC + clearBC, + close, + data, + modalRef, + showModal = false }: ApiModalInterface) => { if (data.type === 'reference') { data = references[data.target]; } const description = data?.comment?.summary; - const closeModal = () => { + const closeModal = useCallback(() => { clearBC(); close(); - }; + }, [clearBC, close]); + + const handleEscape = useCallback( + (event: KeyboardEvent) => { + if (event.key === 'Escape') { + closeModal(); + } + }, + [closeModal] + ); + + // Use esc key to close modal + useEffect(() => { + const modal = modalRef.current; + if (showModal && modal) { + window.addEventListener('keyup', handleEscape); + + return () => { + window.removeEventListener('keyup', handleEscape); + }; + } + }, [showModal, handleEscape, modalRef]); let name = data.name; let typeParameters = data.typeArguments; @@ -86,13 +110,21 @@ export const ApiModal = ({ return ( - + + ); diff --git a/src/styles/reference.scss b/src/styles/reference.scss index 3fe95441783..f4a44c650f4 100644 --- a/src/styles/reference.scss +++ b/src/styles/reference.scss @@ -17,20 +17,33 @@ left: 0; width: 100vw; height: 100vh; - background-color: rgba(0, 0, 0, 0.5); + align-items: center; justify-content: center; - z-index: 99999; + z-index: 5; &--open { display: flex; } } +.api-modal-backdrop { + position: fixed; + z-index: 1; + background-color: rgba(0, 0, 0, 0.5); + width: 100vw; + height: 100vh; +} + .api-modal { + z-index: 2; width: 800px; max-width: 90vw; max-height: 90vh; border-radius: var(--amplify-radii-medium); + &:focus-visible { + outline: 2px solid var(--amplify-colors-border-focus); + outline-offset: 2px; + } } .api-model__header { @@ -44,6 +57,10 @@ overflow: scroll; align-items: baseline; gap: 2px; + &:focus-visible { + outline: 2px solid var(--amplify-colors-border-focus); + outline-offset: 2px; + } } .api-modal__breadcrumbs__current {