Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn run lint-staged
if command -v yarn >/dev/null 2>&1; then
yarn run lint-staged
elif command -v corepack >/dev/null 2>&1; then
corepack yarn run lint-staged
elif command -v npm >/dev/null 2>&1; then
npm exec -- lint-staged
else
echo "Error: yarn/corepack/npm not found in PATH"
exit 127
fi
61 changes: 61 additions & 0 deletions src/components/MDXComponents/ExpandableImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as React from "react";

export function ExpandableImage(
props: React.ImgHTMLAttributes<HTMLImageElement>
) {
const [open, setOpen] = React.useState(false);

React.useEffect(() => {
if (!open) return;

const originalOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";

const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setOpen(false);
}
};

document.addEventListener("keydown", onKeyDown);

return () => {
document.body.style.overflow = originalOverflow;
document.removeEventListener("keydown", onKeyDown);
};
}, [open]);

return (
<div className="expandable-image">
<img
{...props}
className={`expandable-inline-image${
props.className ? ` ${props.className}` : ""
}`}
onClick={(event) => {
props.onClick?.(event);
if (!event.defaultPrevented) {
setOpen(true);
}
}}
/>
{open && (
<div
className="expandable-modal-backdrop"
role="presentation"
onClick={() => setOpen(false)}
>
<div
className="expandable-modal-content expandable-image-modal-content"
role="dialog"
aria-modal="true"
>
<div className="expandable-modal-scroll">
<img {...props} className="expandable-modal-image" />
</div>
</div>
</div>
)}
</div>
);
}
101 changes: 101 additions & 0 deletions src/components/MDXComponents/ExpandableTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as React from "react";
import { useI18next } from "gatsby-plugin-react-i18next";
import { MdCloseFullscreen, MdOpenInFull } from "react-icons/md";

const TABLE_LABELS = {
en: {
expand: "Expand table",
collapse: "Collapse table",
},
zh: {
expand: "Expand table",
collapse: "Collapse table",
},
ja: {
expand: "Expand table",
collapse: "Collapse table",
},
} as const;

function getTableLabel(language: string, expanded: boolean) {
const lang = language.startsWith("zh")
? "zh"
: language.startsWith("ja")
? "ja"
: "en";
return expanded ? TABLE_LABELS[lang].collapse : TABLE_LABELS[lang].expand;
}
Comment on lines +20 to +27

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This getTableLabel function and its language detection logic are duplicated in ExpandableImage.tsx. To improve maintainability and follow the DRY (Don't Repeat Yourself) principle, please consider extracting the language key detection logic into a shared utility function that both components can use.


export function ExpandableTable(
props: React.TableHTMLAttributes<HTMLTableElement>
) {
const [open, setOpen] = React.useState(false);
const { language } = useI18next();
const expandLabel = getTableLabel(language, false);
const collapseLabel = getTableLabel(language, true);

React.useEffect(() => {
if (!open) return;

const originalOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";

const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setOpen(false);
}
};

document.addEventListener("keydown", onKeyDown);

return () => {
document.body.style.overflow = originalOverflow;
document.removeEventListener("keydown", onKeyDown);
};
}, [open]);

return (
<div className="expandable-table">
<button
type="button"
className="expandable-toggle-button"
onClick={() => setOpen(true)}
aria-expanded={open}
>
<span className="expandable-button-content">
<MdOpenInFull aria-hidden="true" />
<span>{expandLabel}</span>
</span>
</button>
<table {...props} />
{open && (
<div
className="expandable-modal-backdrop"
role="presentation"
onClick={() => setOpen(false)}
>
<div
className="expandable-modal-content expandable-table-modal-content"
role="dialog"
aria-modal="true"
onClick={(event) => event.stopPropagation()}
>
<button
type="button"
className="expandable-modal-collapse"
onClick={() => setOpen(false)}
>
<span className="expandable-button-content">
<MdCloseFullscreen aria-hidden="true" />
<span>{collapseLabel}</span>
</span>
</button>
<div className="expandable-modal-scroll">
<table {...props} />
</div>
</div>
</div>
)}
</div>
);
}
2 changes: 2 additions & 0 deletions src/components/MDXComponents/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ export {
} from "components/MDXComponents/developer";

export { TargetLink as Link } from "./Link";
export { ExpandableTable as table } from "./ExpandableTable";
export { ExpandableImage as img } from "./ExpandableImage";
105 changes: 105 additions & 0 deletions src/styles/docTemplate.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,111 @@
margin: auto;
}

.expandable-toggle-button {
display: block;
width: fit-content;
margin: 0 0 8px auto;
padding: 0;
border: 0;
background: none;
color: var(--tiui-palette-secondary);
text-decoration: underline;
text-underline-offset: 2px;
font-size: 14px;
line-height: 1.4;
cursor: pointer;
}

.expandable-button-content {
display: inline-flex;
align-items: center;
gap: 4px;
}

.expandable-table {
margin: 16px 0;
}

.expandable-image {
display: block;
margin: 16px 0;
}

.expandable-image > img {
max-width: 100%;
height: auto;
}

.expandable-inline-image {
cursor: zoom-in;
}

.expandable-modal-backdrop {
position: fixed;
inset: 0;
z-index: 1300;
display: flex;
align-items: center;
justify-content: center;
background: rgba(14, 35, 54, 0.6);
padding: 24px;
box-sizing: border-box;
}

.expandable-modal-content {
width: min(1200px, 100%);
max-height: 100%;
background: #fff;
border-radius: var(--tiui-shape-border-radius);
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
padding: 12px 12px 16px;
box-sizing: border-box;
}

.expandable-modal-collapse {
margin-left: auto;
margin-bottom: 8px;
padding: 0;
border: 0;
background: none;
color: var(--tiui-palette-secondary);
text-decoration: underline;
text-underline-offset: 2px;
font-size: 14px;
line-height: 1.4;
cursor: pointer;
display: block;
}

.expandable-modal-scroll {
max-height: calc(100vh - 120px);
overflow: auto;
}

.expandable-table-modal-content table {
margin: 0;
min-width: 100%;
}

.expandable-image-modal-content {
padding: 0;
overflow: hidden;
}

.expandable-image-modal-content .expandable-modal-scroll {
text-align: center;
max-height: calc(100vh - 48px);
}

.expandable-modal-image {
width: 100%;
max-width: 100%;
max-height: calc(100vh - 48px);
height: auto;
margin: 0 auto;
object-fit: contain;
}

code {
background-color: var(--tiui-palette-carbon-200);
border-radius: var(--tiui-shape-border-radius);
Expand Down