diff --git a/.changeset/two-impalas-speak.md b/.changeset/two-impalas-speak.md new file mode 100644 index 000000000..e7fbce9ad --- /dev/null +++ b/.changeset/two-impalas-speak.md @@ -0,0 +1,5 @@ +--- +'@hashicorp/react-code-block': minor +--- + +Implements support for code wrapping, through an options.wrapCode boolean property. diff --git a/packages/code-block/docs.mdx b/packages/code-block/docs.mdx index bf85a4a76..6fbf2b870 100644 --- a/packages/code-block/docs.mdx +++ b/packages/code-block/docs.mdx @@ -34,6 +34,9 @@ Longer lines of code may take up more space than the available content width. In ```` ``` A line that goes on for a very long time so that it overflows the container in which it is located, which might be a pretty wide container. +This is a second line of code. +And a third line. +And another line, this is the fourth line. ``` ```` @@ -42,7 +45,54 @@ A line that goes on for a very long time so that it overflows the container in w + +Note this also works with line numbers and line highlighting. + + + +#### Wrap Code + +In cases where wrapping code to new lines is preferred over horizontal scrolling, the `options.wrapCode` prop can be set to `true`. Note that this option is not yet supported in MDX contexts. + +`Source` + +```` +``` +A line that goes on for a very long time so that it overflows the container in which it is located, which might be a pretty wide container. +This is a second line of code. +And a third line. +And another line, this is the fourth line. +``` +```` + +`Result` + + + +Note this also works with line numbers and line highlighting. + + #### Syntax Highlighting @@ -219,7 +269,7 @@ function hello() { options={{ lineNumbers: true, showClipboard: true }} code={`const foo = 'bar' function hello() { -{/* */} return Math.random() > 0.5 ? 'Hello' : 'Bonjour' + return Math.random() > 0.5 ? 'Hello' : 'Bonjour' }`} /> diff --git a/packages/code-block/index.tsx b/packages/code-block/index.tsx index bfc9eda7c..03093c0ce 100644 --- a/packages/code-block/index.tsx +++ b/packages/code-block/index.tsx @@ -27,6 +27,7 @@ export interface CodeBlockOptions { showWindowBar?: boolean filename?: string heading?: string + wrapCode?: boolean } export interface CodeBlockProps { @@ -52,6 +53,7 @@ function CodeBlock({ lineNumbers: false, showClipboard: false, showWindowBar: false, + wrapCode: false, }, }: CodeBlockProps) { const copyRef = useRef() @@ -76,6 +78,7 @@ function CodeBlock({ lineNumbers, showClipboard, showWindowBar, + wrapCode, } = options if (showWindowBar) { console.warn( @@ -119,6 +122,7 @@ function CodeBlock({ highlight={highlight} lineNumbers={lineNumbers} hasFloatingCopyButton={hasFloatingCopyButton} + wrapCode={wrapCode} /> {hasFloatingCopyButton ? (
diff --git a/packages/code-block/partials/code-lines/index.js b/packages/code-block/partials/code-lines/index.js index c9c80aa1e..e5e683100 100644 --- a/packages/code-block/partials/code-lines/index.js +++ b/packages/code-block/partials/code-lines/index.js @@ -3,105 +3,194 @@ * SPDX-License-Identifier: MPL-2.0 */ +/** + * Note: lines of code are expected to be stable. If we need to work with + * dynamic code blocks in the future, we could assign random unique IDs + * to each line during the `linesOfCode` `useMemo` function. + * + * For now, we disable react/no-array-index key for the entire file. + */ +/* eslint-disable react/no-array-index-key */ + import React, { useMemo } from 'react' +import classNames from 'classnames' import parseHighlightedLines from '../../utils/parse-highlighted-lines' import splitJsxIntoLines from './utils/split-jsx-into-lines' import splitHtmlIntoLines from './utils/split-html-into-lines' -import classNames from 'classnames' import s from './style.module.css' +/** + * Render the provided code into separate line elements, + * accounting for all provided options. + */ function CodeLines({ code, language, lineNumbers, highlight, hasFloatingCopyButton, + wrapCode, }) { + // Parse out an array of integers representing which lines to highlight + const highlightedLines = parseHighlightedLines(highlight) + + /** + * Split the incoming code into lines. + * We need to do this in order to render each line of code in a + * separate element, which is necessary for features such as highlighting + * specific lines and allowing code to wrap. + */ const linesOfCode = useMemo(() => { const isHtmlString = typeof code === 'string' - return isHtmlString ? splitHtmlIntoLines(code) : splitJsxIntoLines(code) - }, [code]) + const lineElements = isHtmlString + ? splitHtmlIntoLines(code) + : splitJsxIntoLines(code) + const lineCount = lineElements.length + const padLevel = Math.max(lineCount.toString().length, 1) + return lineElements.map((children, index) => { + const number = index + 1 + const numberPadded = number.toString().padEnd(padLevel) + const highlight = highlightedLines.indexOf(number) !== -1 + const dim = highlightedLines.length > 0 && !highlight + return { children, number: numberPadded, highlight, dim } + }) + }, [code, highlightedLines]) - const lineCount = linesOfCode.length - const highlightedLines = parseHighlightedLines(highlight) + // When rendering wrapped code with line numbers shown, we need a spacer value + // that matches the padding inset of all other line numbers + let numberSpacer = null + if (lineNumbers) { + const padLevel = Math.max(linesOfCode.length.toString().length, 1) + numberSpacer = ''.padEnd(padLevel) + } - return ( -
-      
+  // When the floating copy button is present, we add padding to many lines
+  const padRight = hasFloatingCopyButton
+
+  if (wrapCode) {
+    /**
+     * For wrapped code, we use a single-column flex layout.
+     * Lines of code are stacked in a single container, and each line row renders
+     * its own line number, which ensures that when lines wrap, the line numbers
+     * are aligned as expected
+     */
+    return (
+      
+        
+ + {linesOfCode.map(({ number, children, highlight, dim }, idx) => ( +
+ {lineNumbers ? ( + + ) : null} + + {children} + {'\n'} + +
+ ))} + +
+
+ ) + } else { + /** + * For overflowing code, we use a two-column layout. + * The first column contains line numbers, and is effectively fixed. + * The second column contains the lines themselves, and is an overflow + * container to allow extra long lines to scroll as needed. + */ + return ( + {lineNumbers ? ( - {linesOfCode.map((_lineChildren, stableIdx) => { - const number = stableIdx + 1 - const isHighlighted = highlightedLines.indexOf(number) !== -1 - const isNotHighlighted = - highlightedLines.length > 0 && !isHighlighted - return ( - - ) - })} + {linesOfCode.map(({ number, highlight, dim }, idx) => ( + + ))} ) : null} - - {linesOfCode.map((lineChildren, stableIdx) => { - const number = stableIdx + 1 - const isHighlighted = highlightedLines.indexOf(number) !== -1 - const isNotHighlighted = highlightedLines.length && !isHighlighted - return ( - - {lineChildren} - {'\n'} - - ) - })} + + {linesOfCode.map(({ children, highlight, dim }, idx) => ( + + {children} + {'\n'} + + ))} + + ) + } +} + +/** + * Set up the `
` + `` container
+ * which is necessary for language-specific syntax highlighting styles
+ */
+function PreCode({ children, language }) {
+  return (
+    
+      
+        {children}
       
     
) } -function LineNumber({ number, isHighlighted, isNotHighlighted, lineCount }) { - const padLevel = Math.max(lineCount.toString().length, 1) - const paddedNumber = number.toString().padEnd(padLevel) +/** + * Provides "padding" at the top and bottom of a code block with wrapping lines + * while retaining the "numbers" and "lines" separation border. + * + * For context, with wrapped code, we don't have separate "numbers" and "lines" + * columns as we would with overflowing code. So, we can't add padding + * to those columns as we do with overflowing code. + * + * To create padding-equivalent space, while also rendering a continuous border + * between the "numbers" and "lines" columns, we use this component, + * which is essentially and empty line of code that's been shortened a bit. + */ +function WrappedLinesSpacer({ number, padRight }) { + return ( +
+ {number ? : null} + {'\n'} +
+ ) +} + +/** + * Renders a line number. + * + * Note the `number` is rendered in monospace in a whitespace-sensitive way, + * so that if a padded string is passed, we can allow for table-like alignment + * of line numbers, and consistent horizontal width of numbers across all lines. + */ +function LineNumber({ number, highlight, dim, wrap }) { return ( + > + {number} + ) } -function LineOfCode({ - children, - isHighlighted, - isNotHighlighted, - hasFloatingCopyButton, -}) { +/** + * Renders a line of code + */ +function LineOfCode({ children, highlight, dim, padRight, wrap }) { return ( {children} diff --git a/packages/code-block/partials/code-lines/style.module.css b/packages/code-block/partials/code-lines/style.module.css index 875e10bb6..a3611cc08 100644 --- a/packages/code-block/partials/code-lines/style.module.css +++ b/packages/code-block/partials/code-lines/style.module.css @@ -9,6 +9,12 @@ code-block/theme-(dark|light).module.css to be present. */ +/* + +SHARED + +*/ + pre.pre { --code-padding: 16px; --code-font-size: 0.84375rem; /* 13.5 px at default text size */ @@ -32,6 +38,83 @@ pre.pre { overflow: hidden; } +.LineNumber { + composes: g-type-code from global; + display: block; + font-size: var(--code-font-size); + line-height: var(--code-line-height); + white-space: pre; + padding: 0 var(--code-padding); + color: var(--text-color-faded); + + &.wrap { + border-right: 1px solid var(--divider-line-color); + } + + &.highlight { + background: var(--background-highlighted-line); + color: var(--text-color-base); + } + + &.dim > * { + color: var(--text-color-faded); + } +} + +.LineOfCode { + composes: g-type-code from global; + display: block; + color: var(--text-color-base); + padding-right: calc(var(--code-padding) * 2); + padding-left: var(--code-padding); + font-size: var(--code-font-size); + line-height: var(--code-line-height); + min-width: max-content; + white-space: pre; + + &.wrap { + white-space: pre-wrap; + overflow-wrap: break-word; + min-width: 0; + width: 100%; + } + + &.padRight { + /* Adds right padding so that the floating copy button + does not obscure content at the end of the line */ + padding-right: 96px; + } + + &.highlight { + background: var(--background-highlighted-line); + } + + &.dim { + opacity: 0.7; + + &, + & * { + /* !important is necessary here to override + syntax highlight color styles, which are + globally scoped by necessity */ + color: var(--text-color-faded) !important; + } + } +} + +/* + +SCROLL OVERFLOW LAYOUT + +*/ + +.numbersColumn { + display: block; + border-right: 1px solid var(--divider-line-color); + flex-shrink: 0; + padding: var(--code-padding) 0; +} + .linesColumn { display: block; flex-grow: 1; @@ -76,7 +159,7 @@ pre.pre { } } -.linesWrapper { +.linesScrollContainer { display: flex; width: max-content; min-width: 100%; @@ -84,62 +167,28 @@ pre.pre { flex-shrink: 0; } -.numbersColumn { - display: block; - border-right: 1px solid var(--divider-line-color); - flex-shrink: 0; - padding: var(--code-padding) 0; -} +/* -.LineNumber { - composes: g-type-code from global; - display: block; - font-size: var(--code-font-size); - line-height: var(--code-line-height); - white-space: pre; - padding: 0 var(--code-padding); - color: var(--text-color-faded); +WRAP LAYOUT - &.isHighlighted { - background: var(--background-highlighted-line); - color: var(--text-color-base); - } +*/ - &.isNotHighlighted > * { - color: var(--text-color-faded); - } +.wrappedLinesContainer { + display: flex; + width: 100%; + flex-direction: column; + flex-shrink: 0; } -.LineOfCode { - composes: g-type-code from global; - display: block; - color: var(--text-color-base); - padding-right: calc(var(--code-padding) * 2); - padding-left: var(--code-padding); - font-size: var(--code-font-size); - line-height: var(--code-line-height); - min-width: max-content; - white-space: pre; - - &.hasFloatingCopyButton { - /* Adds right padding so that the floating copy button - does not obscure content at the end of the line */ - padding-right: 96px; - } - - &.isHighlighted { - background: var(--background-highlighted-line); - } - - &.isNotHighlighted { - opacity: 0.7; +.wrappedLine { + display: flex; + flex-wrap: nowrap; +} - &, - & * { - /* !important is necessary here to override - syntax highlight color styles, which are - globally scoped by necessity */ - color: var(--text-color-faded) !important; - } - } +/* adds space at the top and bottom of the block, + while supporting a continuous border for line numbers */ +.wrappedLinesSpacer { + display: flex; + flex-wrap: nowrap; + height: var(--code-padding); }