From 450a8d707c7c2dc3b8d65a50a4020c43daba3be8 Mon Sep 17 00:00:00 2001 From: Paul Gottschling Date: Mon, 25 Nov 2024 08:10:28 -0500 Subject: [PATCH] Support expandable images (#31) Closes #26 Eject `MDXComponents/Img` using Docusaurus swizzling. Adapt the expandable image logic from the `Image` component in `gravitational/docs` to the Docusaurus site, copying the `ModalImage` and `PlainImage` components, plus their supporting hooks, into the ejected `MDXImg` component. --- package.json | 1 + src/theme/MDXComponents/Img/index.tsx | 124 ++++++++++++++++++ src/theme/MDXComponents/Img/styles.module.css | 45 +++++++ yarn.lock | 5 + 4 files changed, 175 insertions(+) create mode 100644 src/theme/MDXComponents/Img/index.tsx create mode 100644 src/theme/MDXComponents/Img/styles.module.css diff --git a/package.json b/package.json index 628f02d..52292be 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@docusaurus/theme-classic": "^3.5.0", "@inkeep/widgets": "^0.2.288", "@mdx-js/react": "^3.0.0", + "classnames": "^2.3", "clsx": "^2.1.1", "date-fns": "^3.6.0", "prism-react-renderer": "^2.3.0", diff --git a/src/theme/MDXComponents/Img/index.tsx b/src/theme/MDXComponents/Img/index.tsx new file mode 100644 index 0000000..ad55734 --- /dev/null +++ b/src/theme/MDXComponents/Img/index.tsx @@ -0,0 +1,124 @@ +import cn from "classnames"; +import { React, useEffect, useState, useCallback, useRef } from "react"; +import clsx from "clsx"; +import type { Props } from "@theme/MDXComponents/Img"; + +import styles from "./styles.module.css"; + +function useClickInside(ref, handler) { + useEffect(() => { + const listener = (e: MouseEvent) => { + if (ref.current) { + handler(e); + } + }; + document.addEventListener("mousedown", listener); + return () => { + document.removeEventListener("mousedown", listener); + }; + }, [ref, handler]); +} + +function useEscape(handler) { + useEffect(() => { + const listener = (e: KeyboardEvent) => { + if (e.key === "Escape") { + handler(e); + } + }; + document.addEventListener("keydown", listener); + return () => { + document.removeEventListener("keydown", listener); + }; + }, [handler]); +} + +function useDisableBodyScroll(shouldDisable) { + useEffect(() => { + if (shouldDisable) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "unset"; + } + }, [shouldDisable]); +} + +function transformImgClassName(className?: string): string { + return clsx(className, styles.img); +} + +//** Maximum width of content block where images are placed. +/* If the original image is smaller, then there is no sense to expand the image by clicking. */ +const MAX_CONTENT_WIDTH = 900; + +type ModalImageProps = { + setShowExpandedImage: Dispatch>; +} & ImageProps; + +const ModalImage = ({ setShowExpandedImage, ...props }: ModalImageProps) => { + const closeHandler = useCallback( + () => setShowExpandedImage(false), + [setShowExpandedImage] + ); + const modalRef = useRef(); + useClickInside(modalRef, closeHandler); + useEscape(closeHandler); + + return ( +
+
+
+ +
+
+ ); +}; + +export default function MDXImg(props: Props): JSX.Element { + const [showExpandedImage, setShowExpandedImage] = useState(false); + const shouldExpand = props.width > MAX_CONTENT_WIDTH; + useDisableBodyScroll(showExpandedImage); + const handleClickImage = () => { + if (shouldExpand) { + setShowExpandedImage(true); + } + }; + const PlainImage = () => { + if (shouldExpand) { + return ( + + ); + } else + return ( + + ); + }; + + return ( + <> + + + + {showExpandedImage && ( + + )} + + ); +} diff --git a/src/theme/MDXComponents/Img/styles.module.css b/src/theme/MDXComponents/Img/styles.module.css new file mode 100644 index 0000000..a628969 --- /dev/null +++ b/src/theme/MDXComponents/Img/styles.module.css @@ -0,0 +1,45 @@ +.img { + height: auto; +} + +.overlay { + background-color: rgba(0, 0, 0, 0.7); + width: 100vw; + height: 100%; + min-width: 100%; + top: 50%; + left: 50%; + z-index: 3002; + transform: translate(-50%, -50%); + position: fixed; + cursor: zoom-out; +} + +.zoomable { + /* + * The background-color and border styles override browser defaults for the + * button element. + */ + background-color: inherit; + border: inherit; + + cursor: zoom-in; + cursor: -moz-zoom-in; + cursor: -webkit-zoom-in; +} + +.dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0,0,0,0); + cursor: zoom-out; + cursor: -moz-zoom-out; + cursor: -webkit-zoom-out; + z-index: 3002; + & .img { + max-width: 95vw; + max-height: 95vh; + } +} diff --git a/yarn.lock b/yarn.lock index 2990a2d..452ec6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5138,6 +5138,11 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== +classnames@^2.3: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + clean-css@^5.2.2, clean-css@^5.3.2, clean-css@~5.3.2: version "5.3.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd"