Return

All tests / src/components/CodeFirstPaint index.tsx

100% Statements 18/18
66.66% Branches 6/9
100% Functions 5/5
100% Lines 15/15

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 702x 2x 2x         2x                             2x 3x 3x   3x 6x   3x 3x 3x                             4x               4x                           2x  
import React from 'react';
import { Code } from '@code-hike/mdx/dist/components.cjs.js';
import styles from './codeFirstPaint.module.css';
 
type Props = React.ComponentProps<typeof Code>;
 
// Code Hike always pads the line-number column to at least two digits.
const MIN_LINE_NUMBER = 10;
 
/**
 * @description `CH.Code` with the active file painted in the server HTML.
 *
 * Code Hike lays code out from measurements it can only take in the browser, so until the page
 * hydrates it renders the lines at `opacity: 0` and the window is an empty dark box. On a cold load
 * that is the whole JS download. This paints the same highlighted tokens, which the MDX compiler
 * already passes in `files`, over that box until Code Hike has measured. Both happen in layout
 * effects of the same commit, so the swap never reaches the screen as an empty frame.
 *
 * `aria-hidden`: the text is also in Code Hike's own hidden layer, and a reader must not hear it twice.
 * @param {Props} props - The props `CH.Code` receives from the compiled MDX.
 * @returns {JSX.Element}
 */
const CodeFirstPaint = (props: Props) => {
    const [measured, setMeasured] = React.useState(false);
    React.useLayoutEffect(() => setMeasured(true), []);
 
    const { files, northPanel, lineNumbers, codeConfig, style } = props;
    const file = files.find(({ name }) => name === northPanel.active) ?? files[0];
    // `IRawTheme` leaves `colors` untyped; the MDX pipeline passes a VS Code theme (mdx.plugins.ts).
    const colors = (codeConfig.theme as { colors?: Record<string, string> }).colors ?? {};
    const lines = file?.code.lines ?? [];
    const numberWidth = `${String(Math.max(lines.length, MIN_LINE_NUMBER)).length + 2.5}ch`;
 
    return (
        <div className={styles.root} style={{ height: style?.height }}>
            <Code {...props} />
            {!measured && file && (
                <div
                    className={styles.firstPaint}
                    aria-hidden="true"
                    data-testid="code-first-paint"
                    style={{ background: colors['editor.background'], color: colors['editor.foreground'] }}
                >
                    <code className="ch-code-scroll-parent">
                        {lines.map(({ tokens }, index) => (
                            // Lines have no identity beyond their position, and the list never reorders.
                            <div key={index}>
                                {lineNumbers && (
                                    <span className="ch-code-line-number" style={{ width: numberWidth }}>
                                        {index + 1}
                                    </span>
                                )}
                                <span>
                                    {tokens.map(({ content, props: tokenProps }, tokenIndex) => (
                                        <span key={tokenIndex} style={tokenProps?.style}>
                                            {content}
                                        </span>
                                    ))}
                                </span>
                            </div>
                        ))}
                    </code>
                </div>
            )}
        </div>
    );
};
 
export default CodeFirstPaint;