Skip to content

Commit

Permalink
Support expandable images (#31)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ptgott authored Nov 25, 2024
1 parent 3fda3f4 commit 450a8d7
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
124 changes: 124 additions & 0 deletions src/theme/MDXComponents/Img/index.tsx
Original file line number Diff line number Diff line change
@@ -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<SetStateAction<boolean>>;
} & ImageProps;

const ModalImage = ({ setShowExpandedImage, ...props }: ModalImageProps) => {
const closeHandler = useCallback(
() => setShowExpandedImage(false),
[setShowExpandedImage]
);
const modalRef = useRef<HTMLDivElement>();
useClickInside(modalRef, closeHandler);
useEscape(closeHandler);

return (
<div ref={modalRef}>
<div className={styles.overlay} />
<div className={styles.dialog}>
<img
decoding="async"
loading="lazy"
{...props}
className={transformImgClassName(props.className)}
/>
</div>
</div>
);
};

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 (
<button onClick={handleClickImage} className={styles.zoomable}>
<img
decoding="async"
loading="lazy"
{...props}
className={transformImgClassName(props.className)}
/>
</button>
);
} else
return (
<img
decoding="async"
loading="lazy"
{...props}
className={transformImgClassName(props.className)}
/>
);
};

return (
<>
<span className={cn(styles.wrapper)}>
<PlainImage />
</span>
{showExpandedImage && (
<ModalImage setShowExpandedImage={setShowExpandedImage} {...props} />
)}
</>
);
}
45 changes: 45 additions & 0 deletions src/theme/MDXComponents/Img/styles.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 450a8d7

Please sign in to comment.