'use client' import React, {type PropsWithChildren, useCallback, useState, useRef, useEffect, useId} from 'react' import clsx from 'clsx' import {LiveProvider, LiveEditor, LiveError, LivePreview} from 'react-live' import {useColorMode} from '../../context/color-modes/useColorMode' import {useConfig} from '../../context/useConfig' import styles from './ReactCodeBlock.module.css' import {ActionBar, Button, ThemeProvider} from '@primer/react' import {Text} from '@primer/react-brand' import {CopyIcon, MoonIcon, SunIcon, UndoIcon, UnfoldIcon, FoldIcon} from '@primer/octicons-react' import {colorModes} from '../../context/color-modes/context' import {lightTheme, darkTheme} from './syntax-highlighting-themes' import {codeTransformer} from './code-transformer' const COLLAPSE_HEIGHT = 400 // TODO: Hoist this to config to make user customizable eventually type ReactCodeBlockProps = { 'data-language': string 'data-filename'?: string jsxScope: Record } & PropsWithChildren const getFocusableElements = () => { const focusableElementsQuery = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' return Array.from(document.querySelectorAll(focusableElementsQuery)).filter(el => { const style = window.getComputedStyle(el) return style.display !== 'none' && style.visibility !== 'hidden' && !el.hasAttribute('disabled') }) } export function ReactCodeBlock(props: ReactCodeBlockProps) { const uniqueId = useId() const {colorMode, setColorMode} = useColorMode() const {basePath} = useConfig() const codeSourceRef = useRef(null) const [code, setCode] = useState('') const [isReady, setIsReady] = useState(false) const rootRef = useRef(null) const [isCodePaneCollapsed, setIsCodePaneCollapsed] = useState(null) const [initialPosition, setInitialPosition] = useState(null) const editorRef = useRef(null) const resetButtonRef = useRef(null) const shouldShowPreview = ['tsx', 'jsx'].includes(props['data-language']) useEffect(() => { if (codeSourceRef.current && !isReady) { const textContent = codeSourceRef.current.textContent || '' if (textContent) { setCode(textContent) setIsReady(true) } } }, [isReady, props.children]) // scroll back to the initial y pos on collapse state change useEffect(() => { if (rootRef.current && initialPosition === null) { const rect = rootRef.current.getBoundingClientRect() const scrollTop = window.pageYOffset || document.documentElement.scrollTop setInitialPosition(rect.top + scrollTop) } }, [initialPosition]) useEffect(() => { if (editorRef.current && isCodePaneCollapsed === null) { const editorHeight = editorRef.current.scrollHeight setIsCodePaneCollapsed(editorHeight > COLLAPSE_HEIGHT) } }, [code, isCodePaneCollapsed]) const shouldShowCollapse = isCodePaneCollapsed !== null && (editorRef.current?.scrollHeight || 0) > COLLAPSE_HEIGHT /** * Transforms code to prepend basePath to img src attributes */ const transformCodeWithBasePath = useCallback( (sourceCode: string) => codeTransformer(sourceCode, basePath), [basePath], ) const handleReset = useCallback(() => { if (codeSourceRef.current) { setCode(codeSourceRef.current.textContent || '') } }, []) const handleCopy = useCallback(() => { navigator.clipboard.writeText(code) }, [code]) const handleCollapsibleCodePane = useCallback(() => { const newCollapsedState = !isCodePaneCollapsed if (!isCodePaneCollapsed && newCollapsedState && initialPosition !== null) { requestAnimationFrame(() => { window.scrollTo({ top: initialPosition, behavior: 'smooth', }) }) } setIsCodePaneCollapsed(newCollapsedState) }, [isCodePaneCollapsed, initialPosition]) const noInline = props['data-filename'] === 'noinline' || false useEffect(() => { const editor = editorRef.current if (!editor) return const onKeyDown = (e: KeyboardEvent) => { if (e.key !== 'Tab') { return } if (e.shiftKey) { e.preventDefault() // We know that the previous focusable element is always the reset button resetButtonRef.current?.focus() return } const focusableElements = getFocusableElements() const currentIndex = focusableElements.findIndex(el => el === resetButtonRef.current) if (currentIndex !== -1) { e.preventDefault() const nextIndex = currentIndex + 1 focusableElements[nextIndex]?.focus() } } editor.addEventListener('keydown', onKeyDown) return () => { editor.removeEventListener('keydown', onKeyDown) } }, []) return ( <> {/* Hidden el to render children and extract text content */} {isReady && (
{shouldShowPreview && (
{colorModes.map((mode, index) => { const Icon = mode === 'light' ? SunIcon : MoonIcon return ( setColorMode(mode)} /> ) })}
)}
{shouldShowCollapse && ( )}
{shouldShowPreview && (
)}
)} ) }