Skip to content
Open
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
49 changes: 20 additions & 29 deletions www/src/modules/docs/toggle-code-block.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,26 @@
// Animates a code block's expand/collapse as a height tween. Deliberately not
// a view transition: snapshotting pauses input document-wide (long for a big
// highlighted file) and participating elements skip hit-testing, so even a
// transition scoped to the block reads as a frozen page.
import { flushSync } from "react-dom"

// Expand/collapse morph scoped to one code block. :root is opted out of view
// transitions globally (see styles.css) so the rest of the page keeps its
// hit-testing; only the block itself morphs. It's named just for the
// transition's duration — a static name would duplicate across a page's demos
// and abort the transition.
export function toggleCodeBlock(target: Element, update: () => void) {
const block = target.closest("figure")
if (!block || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
if (
!block ||
!document.startViewTransition ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
) {
update()
return
}
const from = block.getBoundingClientRect().height
update()
// The re-render can land after the current task (DynamicPre defers
// re-highlighting), so tween on the resulting height change instead of
// measuring synchronously.
const ro = new ResizeObserver(() => {
const to = block.getBoundingClientRect().height
if (to === from) return
ro.disconnect()
clearTimeout(bail)
for (const animation of block.getAnimations()) animation.cancel()
block.style.overflow = "hidden"
block
.animate([{ height: `${from}px` }, { height: `${to}px` }], {
duration: 250,
easing: "cubic-bezier(0.215, 0.61, 0.355, 1)",
})
.finished.finally(() => {
block.style.overflow = ""
})
})
ro.observe(block)
// A no-op toggle (same height) would leave the observer running forever.
const bail = setTimeout(() => ro.disconnect(), 500)
block.style.viewTransitionName = "docs-code-block"
document
.startViewTransition(() => {
flushSync(update)
})
.finished.finally(() => {
block.style.viewTransitionName = ""
})
}
Loading