mirror of
https://github.com/block/buzz.git
synced 2026-11-12 20:31:12 +00:00
Signed-off-by: Aaron Goldsmith <aargoldsmith@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
149 lines
4.2 KiB
TypeScript
149 lines
4.2 KiB
TypeScript
import * as React from "react";
|
|
|
|
import { hasBlockMedia } from "../markdownUtils";
|
|
import { SpoilerParticles } from "../SpoilerParticles";
|
|
|
|
/**
|
|
* True for descendants of a spoiler that is currently hidden. Consumers (e.g.
|
|
* `MaskedLinkTooltip`) use it to suppress hover/focus affordances that would
|
|
* otherwise leak masked content before the spoiler is revealed. Default
|
|
* `false` — content outside any spoiler is never hidden.
|
|
*/
|
|
export const SpoilerHiddenContext = React.createContext(false);
|
|
|
|
export function SpoilerInline({
|
|
block = false,
|
|
children,
|
|
interactive = true,
|
|
}: {
|
|
block?: boolean;
|
|
children?: React.ReactNode;
|
|
interactive?: boolean;
|
|
}) {
|
|
const [revealed, setRevealed] = React.useState(false);
|
|
const contentRef = React.useRef<HTMLElement | null>(null);
|
|
const isBlock = block || hasBlockMedia(React.Children.toArray(children));
|
|
|
|
const setContentElement = React.useCallback((node: HTMLElement | null) => {
|
|
contentRef.current = node;
|
|
}, []);
|
|
|
|
const toggleRevealed = React.useCallback(() => {
|
|
setRevealed((value) => !value);
|
|
}, []);
|
|
|
|
const handlePointerDownCapture = React.useCallback(
|
|
(event: React.PointerEvent<HTMLElement>) => {
|
|
if (revealed) return;
|
|
event.stopPropagation();
|
|
},
|
|
[revealed],
|
|
);
|
|
|
|
const handleClickCapture = React.useCallback(
|
|
(event: React.MouseEvent<HTMLElement>) => {
|
|
if (revealed) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
toggleRevealed();
|
|
},
|
|
[revealed, toggleRevealed],
|
|
);
|
|
|
|
const handleClick = React.useCallback(
|
|
(event: React.MouseEvent<HTMLElement>) => {
|
|
if (revealed && isBlock && event.target !== event.currentTarget) return;
|
|
toggleRevealed();
|
|
},
|
|
[isBlock, revealed, toggleRevealed],
|
|
);
|
|
|
|
const handleKeyDown = React.useCallback(
|
|
(event: React.KeyboardEvent<HTMLElement>) => {
|
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
event.preventDefault();
|
|
toggleRevealed();
|
|
},
|
|
[toggleRevealed],
|
|
);
|
|
|
|
const revealProps = {
|
|
"aria-label": revealed ? "Hide spoiler" : "Reveal spoiler",
|
|
"aria-pressed": revealed,
|
|
onClick: handleClick,
|
|
onClickCapture: handleClickCapture,
|
|
onKeyDown: handleKeyDown,
|
|
onPointerDownCapture: handlePointerDownCapture,
|
|
role: "button",
|
|
tabIndex: 0,
|
|
} as const;
|
|
|
|
if (!interactive) {
|
|
if (isBlock) {
|
|
return (
|
|
<div
|
|
className="buzz-spoiler buzz-spoiler--block buzz-spoiler--inert"
|
|
data-revealed="false"
|
|
data-spoiler=""
|
|
>
|
|
<SpoilerParticles active contentRef={contentRef} />
|
|
<div className="buzz-spoiler__content" ref={setContentElement}>
|
|
<SpoilerHiddenContext.Provider value={true}>
|
|
{children}
|
|
</SpoilerHiddenContext.Provider>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<span
|
|
className="buzz-spoiler buzz-spoiler--inert"
|
|
data-revealed="false"
|
|
data-spoiler=""
|
|
>
|
|
<SpoilerParticles active contentRef={contentRef} />
|
|
<span className="buzz-spoiler__content" ref={setContentElement}>
|
|
<SpoilerHiddenContext.Provider value={true}>
|
|
{children}
|
|
</SpoilerHiddenContext.Provider>
|
|
</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
if (isBlock) {
|
|
return (
|
|
<div
|
|
{...revealProps}
|
|
className="buzz-spoiler buzz-spoiler--block"
|
|
data-revealed={revealed ? "true" : "false"}
|
|
data-spoiler=""
|
|
>
|
|
<SpoilerParticles active={!revealed} contentRef={contentRef} />
|
|
<div className="buzz-spoiler__content" ref={setContentElement}>
|
|
<SpoilerHiddenContext.Provider value={!revealed}>
|
|
{children}
|
|
</SpoilerHiddenContext.Provider>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<span
|
|
{...revealProps}
|
|
className="buzz-spoiler"
|
|
data-revealed={revealed ? "true" : "false"}
|
|
data-spoiler=""
|
|
>
|
|
<SpoilerParticles active={!revealed} contentRef={contentRef} />
|
|
<span className="buzz-spoiler__content" ref={setContentElement}>
|
|
<SpoilerHiddenContext.Provider value={!revealed}>
|
|
{children}
|
|
</SpoilerHiddenContext.Provider>
|
|
</span>
|
|
</span>
|
|
);
|
|
}
|