Skip to content

Commit 3cc6942

Browse files
authored
Replace the Docusaurus Details component with a HTML native solution (#564)
* Replace the Docusaurus Details component with html native solution * Adjust styles * Fix the close animation not working * Adjust the bottom padding of details-content * Replicate the transition duration calculation logic from the Docusaurus Details component * Apply the transitions only if interpolate-size is supported * Clarify the comment about the source of getAutoHeightDuration
1 parent cdde9bc commit 3cc6942

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed

src/theme/Details/index.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React, { type ReactNode, useRef, useCallback, useEffect } from "react";
2+
import clsx from "clsx";
3+
import type { Props } from "@theme/Details";
4+
import { prefersReducedMotion } from "@docusaurus/theme-common";
5+
6+
import styles from "./styles.module.css";
7+
8+
const InfimaClasses = "alert alert--info";
9+
10+
/*
11+
Calculates a duration for the transition based on the content height. Adapted from Docusaurus's Collapsible component:
12+
https://github.com/facebook/docusaurus/blob/d5509e329d090ca3ab1da94d41834ddd51f11937/packages/docusaurus-theme-common/src/components/Collapsible/index.tsx#L74-L82
13+
*/
14+
function getAutoHeightDuration(height: number) {
15+
if (prefersReducedMotion()) {
16+
return 1;
17+
}
18+
const constant = height / 36;
19+
return Math.round((4 + 15 * constant ** 0.25 + constant / 5) * 10);
20+
}
21+
22+
export default function Details({ summary, ...props }: Props): ReactNode {
23+
const summaryElement = React.isValidElement(summary) ? (
24+
summary
25+
) : (
26+
<summary>{summary ?? "Details"}</summary>
27+
);
28+
29+
const detailsRef = useRef<HTMLDetailsElement>(null);
30+
31+
const applyTransitionVars = useCallback((el: HTMLDetailsElement) => {
32+
let contentHeight = 0;
33+
for (const child of Array.from(el.children)) {
34+
if ((child as HTMLElement).tagName.toLowerCase() !== "summary") {
35+
contentHeight += (child as HTMLElement).scrollHeight;
36+
}
37+
}
38+
const durationMs = getAutoHeightDuration(contentHeight);
39+
const durationS = durationMs / 1000;
40+
const delayS = Math.max(0, (durationMs - 20) / 1000);
41+
el.style.setProperty("--details-transition-duration", `${durationS}s`);
42+
el.style.setProperty("--details-transition-delay", `${delayS}s`);
43+
}, []);
44+
45+
// Apply the variables on mount so that elements with `open` by default are covered.
46+
useEffect(() => {
47+
if (detailsRef.current) {
48+
applyTransitionVars(detailsRef.current);
49+
}
50+
}, [applyTransitionVars]);
51+
52+
const handleToggle = useCallback(
53+
(e: React.SyntheticEvent<HTMLDetailsElement>) => {
54+
applyTransitionVars(e.currentTarget);
55+
},
56+
[applyTransitionVars],
57+
);
58+
59+
return (
60+
<details
61+
{...props}
62+
ref={detailsRef}
63+
onToggle={handleToggle}
64+
className={clsx(styles.details, InfimaClasses, props.className)}
65+
>
66+
{summaryElement}
67+
{props.children}
68+
</details>
69+
);
70+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
.details {
2+
--docusaurus-details-decoration-color: var(--ifm-alert-border-color);
3+
--docusaurus-details-transition: transform var(--ifm-transition-fast) ease;
4+
--docusaurus-details-summary-arrow-size: 0.38rem;
5+
interpolate-size: allow-keywords;
6+
margin: 0 0 var(--ifm-spacing-vertical);
7+
border: 1px solid var(--ifm-alert-border-color);
8+
overflow: hidden;
9+
padding-bottom: 0;
10+
11+
> :global(summary) {
12+
position: relative;
13+
padding-left: 1rem;
14+
list-style: none;
15+
16+
&:hover {
17+
cursor: pointer;
18+
}
19+
20+
&::before {
21+
content: "";
22+
border-width: var(--docusaurus-details-summary-arrow-size);
23+
border-style: solid;
24+
border-color: transparent transparent transparent
25+
var(--docusaurus-details-decoration-color);
26+
transition: var(--docusaurus-details-transition);
27+
transform-origin: calc(var(--docusaurus-details-summary-arrow-size) / 2)
28+
50%;
29+
position: absolute;
30+
top: 0.45rem;
31+
left: 0;
32+
transform: rotate(0);
33+
}
34+
}
35+
36+
&[open] {
37+
> :global(summary) {
38+
&::before {
39+
transform: rotate(90deg);
40+
}
41+
}
42+
}
43+
}
44+
45+
.details :global(summary)::after {
46+
content: "";
47+
display: block;
48+
position: absolute;
49+
bottom: 0;
50+
left: 0;
51+
width: 100%;
52+
transform: translateY(calc(var(--ifm-alert-padding-horizontal) + 1px));
53+
height: 1px;
54+
opacity: 0;
55+
transition: opacity 0.1s var(--details-transition-delay, 0.38s);
56+
background-color: var(--docusaurus-details-decoration-color);
57+
transition-behavior: allow-discrete;
58+
}
59+
60+
.details[open] :global(summary)::after {
61+
opacity: 1;
62+
transition: opacity 0s;
63+
}
64+
65+
.details::details-content {
66+
block-size: 0;
67+
padding-top: 1rem;
68+
margin-top: 0;
69+
@supports (interpolate-size: allow-keywords) {
70+
transition:
71+
block-size var(--details-transition-duration, 0.4s) ease-in-out,
72+
content-visibility var(--details-transition-duration, 0.4s) ease-in-out,
73+
margin-top 0.1s var(--details-transition-delay, 0.38s);
74+
transition-behavior: allow-discrete !important;
75+
}
76+
}
77+
78+
.details[open]::details-content {
79+
margin-top: 1rem;
80+
block-size: auto;
81+
padding: 1rem 0;
82+
@supports (interpolate-size: allow-keywords) {
83+
transition:
84+
block-size var(--details-transition-duration, 0.4s) ease-in-out,
85+
content-visibility var(--details-transition-duration, 0.4s) ease-in-out,
86+
margin-top 0s;
87+
}
88+
}

0 commit comments

Comments
 (0)