From ad6f210043fb469efdc12aea5306c72bca3e34df Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:56:59 -0400 Subject: [PATCH 1/3] feat(code-block): stub wrapCode implementation --- packages/code-block/docs.mdx | 34 +++++- packages/code-block/index.tsx | 4 + .../code-block/partials/code-lines/index.js | 100 +++++++++++++++--- .../partials/code-lines/style.module.css | 43 ++++++++ 4 files changed, 163 insertions(+), 18 deletions(-) diff --git a/packages/code-block/docs.mdx b/packages/code-block/docs.mdx index bf85a4a76..73676dd91 100644 --- a/packages/code-block/docs.mdx +++ b/packages/code-block/docs.mdx @@ -34,6 +34,7 @@ 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. ``` ```` @@ -41,8 +42,35 @@ A line that goes on for a very long time so that it overflows the container in w + + + +#### 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 would overflow the container in which it is located, which might be a pretty wide container, but it wraps instead. +This is a second line of code. +``` +```` + +`Result` + + #### Syntax Highlighting @@ -219,7 +247,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..efca3ef5b 100644 --- a/packages/code-block/partials/code-lines/index.js +++ b/packages/code-block/partials/code-lines/index.js @@ -16,6 +16,7 @@ function CodeLines({ lineNumbers, highlight, hasFloatingCopyButton, + wrapCode, }) { const linesOfCode = useMemo(() => { const isHtmlString = typeof code === 'string' @@ -28,7 +29,7 @@ function CodeLines({ return (
       
-        {lineNumbers ? (
+        {lineNumbers && !wrapCode ? (
           
             {linesOfCode.map((_lineChildren, stableIdx) => {
               const number = stableIdx + 1
@@ -44,39 +45,105 @@ function CodeLines({
                   lineCount={lineCount}
                   isHighlighted={isHighlighted}
                   isNotHighlighted={isNotHighlighted}
+                  wrapCode={false}
                 />
               )
             })}
           
         ) : null}
-        
-          
+        
+          {wrapCode ? (
+            
+          ) : null}
+          
             {linesOfCode.map((lineChildren, stableIdx) => {
               const number = stableIdx + 1
               const isHighlighted = highlightedLines.indexOf(number) !== -1
               const isNotHighlighted = highlightedLines.length && !isHighlighted
               return (
-                
-                  {lineChildren}
-                  {'\n'}
-                
+                // This array is stable, so we can use index as key
+                // eslint-disable-next-line react/no-array-index-key
+                
+ {lineNumbers && wrapCode ? ( + + ) : null} + + {lineChildren} + {'\n'} + +
) })}
+ {wrapCode ? ( + + ) : null}
) } -function LineNumber({ number, isHighlighted, isNotHighlighted, lineCount }) { +function LineSpacer({ lineCount, hasFloatingCopyButton }) { + return ( +
+ + + {''} + {'\n'} + +
+ ) +} + +function LineNumber({ + number, + isHighlighted, + isNotHighlighted, + lineCount, + wrapCode, +}) { const padLevel = Math.max(lineCount.toString().length, 1) const paddedNumber = number.toString().padEnd(padLevel) return ( @@ -84,6 +151,7 @@ function LineNumber({ number, isHighlighted, isNotHighlighted, lineCount }) { className={classNames(s.LineNumber, { [s.isHighlighted]: isHighlighted, [s.isNotHighlighted]: isNotHighlighted, + [s.wrapCode]: wrapCode, })} dangerouslySetInnerHTML={{ __html: paddedNumber }} /> @@ -95,6 +163,7 @@ function LineOfCode({ isHighlighted, isNotHighlighted, hasFloatingCopyButton, + wrapCode, }) { 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..0b81a6526 100644 --- a/packages/code-block/partials/code-lines/style.module.css +++ b/packages/code-block/partials/code-lines/style.module.css @@ -54,6 +54,10 @@ pre.pre { */ overflow-x: auto; overflow-y: hidden; + + &.wrapCode { + padding: 0; + } } .styledScrollbars { @@ -82,6 +86,10 @@ pre.pre { min-width: 100%; flex-direction: column; flex-shrink: 0; + + &.wrapCode { + width: 100%; + } } .numbersColumn { @@ -100,6 +108,10 @@ pre.pre { padding: 0 var(--code-padding); color: var(--text-color-faded); + &.wrapCode { + border-right: 1px solid var(--divider-line-color); + } + &.isHighlighted { background: var(--background-highlighted-line); color: var(--text-color-base); @@ -121,6 +133,12 @@ pre.pre { min-width: max-content; white-space: pre; + &.wrapCode { + white-space: pre-wrap; + overflow-wrap: break-word; + min-width: 0; + } + &.hasFloatingCopyButton { /* Adds right padding so that the floating copy button does not obscure content at the end of the line */ @@ -143,3 +161,28 @@ pre.pre { } } } + +/* + +Additions to make code-wrap work as expected + +TODO: refactor this whole component so there's less conditional stuff going on. + +*/ + +.lineWrapper { + display: flex; + flex-wrap: nowrap; +} + +/* adds space at the top and bottom of the block, + while supporting a continuous border for line numbers */ +.linesSpacer { + display: flex; + flex-wrap: nowrap; + height: var(--code-padding); + + &.wrapCode { + display: flex; + } +} From 6bab8c4e09e6228d9c1e30430bcbfec9c70f33c0 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:59:24 -0400 Subject: [PATCH 2/3] chore(code-block): add changeset --- .changeset/two-impalas-speak.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/two-impalas-speak.md 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. From bbdedb30021ee50056edfbcf854814b4ee71ea4a Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Tue, 8 Aug 2023 20:03:14 -0400 Subject: [PATCH 3/3] refactor: make it composable --- packages/code-block/docs.mdx | 40 ++- .../code-block/partials/code-lines/index.js | 271 ++++++++++-------- .../partials/code-lines/style.module.css | 170 +++++------ 3 files changed, 264 insertions(+), 217 deletions(-) diff --git a/packages/code-block/docs.mdx b/packages/code-block/docs.mdx index 73676dd91..6fbf2b870 100644 --- a/packages/code-block/docs.mdx +++ b/packages/code-block/docs.mdx @@ -35,6 +35,8 @@ 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,14 +44,16 @@ This is a second line of code. +Note this also works with line numbers and line highlighting. + #### Wrap Code @@ -60,17 +64,35 @@ In cases where wrapping code to new lines is preferred over horizontal scrolling ```` ``` -A line that goes on for a very long time so that it would overflow the container in which it is located, which might be a pretty wide container, but it wraps instead. +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 diff --git a/packages/code-block/partials/code-lines/index.js b/packages/code-block/partials/code-lines/index.js index efca3ef5b..e5e683100 100644 --- a/packages/code-block/partials/code-lines/index.js +++ b/packages/code-block/partials/code-lines/index.js @@ -3,13 +3,26 @@ * 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, @@ -18,160 +31,166 @@ function CodeLines({ 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 ( -
-      
-        {lineNumbers && !wrapCode ? (
+  // 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} - - {wrapCode ? ( - - ) : null} - - {linesOfCode.map((lineChildren, stableIdx) => { - const number = stableIdx + 1 - const isHighlighted = highlightedLines.indexOf(number) !== -1 - const isNotHighlighted = highlightedLines.length && !isHighlighted - return ( - // This array is stable, so we can use index as key - // eslint-disable-next-line react/no-array-index-key -
- {lineNumbers && wrapCode ? ( - - ) : null} - - {lineChildren} - {'\n'} - -
- ) - })} + + + {linesOfCode.map(({ children, highlight, dim }, idx) => ( + + {children} + {'\n'} + + ))} - {wrapCode ? ( - - ) : null} +
+ ) + } +} + +/** + * Set up the `
` + `` container
+ * which is necessary for language-specific syntax highlighting styles
+ */
+function PreCode({ children, language }) {
+  return (
+    
+      
+        {children}
       
     
) } -function LineSpacer({ lineCount, hasFloatingCopyButton }) { +/** + * 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 ( -
- - - {''} - {'\n'} - +
+ {number ? : null} + {'\n'}
) } -function LineNumber({ - number, - isHighlighted, - isNotHighlighted, - lineCount, - wrapCode, -}) { - const padLevel = Math.max(lineCount.toString().length, 1) - const paddedNumber = number.toString().padEnd(padLevel) +/** + * 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, - wrapCode, -}) { +/** + * 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 0b81a6526..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,73 +38,6 @@ pre.pre { overflow: hidden; } -.linesColumn { - display: block; - flex-grow: 1; - padding: var(--code-padding) 0; - - /* - - Ran into weird issues in Safari with overflow here. - Sometimes overflow-y appears... but toggling LITERALLY ANY - CSS property in dev tools, ANYWHERE IN THE DOCUMENT, - immediately resolves the issue 😳 - - Should note as well this does NOT seem to be related to - the custom scrollbar styles below... I've tried disabling them, - but I still wind up with the same issue. - - TLDR;, would love if this could just be "overflow: auto;", - but Safari-specific issues when macOS's "Always show scrollbars" pref is enabled - means I have to set overflow-y to "hidden" to reduce the impact of the bug - */ - overflow-x: auto; - overflow-y: hidden; - - &.wrapCode { - padding: 0; - } -} - -.styledScrollbars { - /* web standard Firefox */ - scrollbar-width: thin; - scrollbar-color: var(--scrollbar-foreground) transparent; - - /* webkit prefixed Safari, Chrome, chromium-based browsers */ - &::-webkit-scrollbar { - width: 11px; - height: 11px; - } - &::-webkit-scrollbar-track { - background: transparent; - } - &::-webkit-scrollbar-thumb { - background-color: var(--scrollbar-foreground); - border-radius: 6px; - border: 3px solid var(--scrollbar-background); - } -} - -.linesWrapper { - display: flex; - width: max-content; - min-width: 100%; - flex-direction: column; - flex-shrink: 0; - - &.wrapCode { - width: 100%; - } -} - -.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; @@ -108,16 +47,16 @@ pre.pre { padding: 0 var(--code-padding); color: var(--text-color-faded); - &.wrapCode { + &.wrap { border-right: 1px solid var(--divider-line-color); } - &.isHighlighted { + &.highlight { background: var(--background-highlighted-line); color: var(--text-color-base); } - &.isNotHighlighted > * { + &.dim > * { color: var(--text-color-faded); } } @@ -133,23 +72,24 @@ pre.pre { min-width: max-content; white-space: pre; - &.wrapCode { + &.wrap { white-space: pre-wrap; overflow-wrap: break-word; min-width: 0; + width: 100%; } - &.hasFloatingCopyButton { + &.padRight { /* Adds right padding so that the floating copy button does not obscure content at the end of the line */ padding-right: 96px; } - &.isHighlighted { + &.highlight { background: var(--background-highlighted-line); } - &.isNotHighlighted { + &.dim { opacity: 0.7; &, @@ -164,25 +104,91 @@ pre.pre { /* -Additions to make code-wrap work as expected +SCROLL OVERFLOW LAYOUT -TODO: refactor this whole component so there's less conditional stuff going on. +*/ + +.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; + padding: var(--code-padding) 0; + + /* + + Ran into weird issues in Safari with overflow here. + Sometimes overflow-y appears... but toggling LITERALLY ANY + CSS property in dev tools, ANYWHERE IN THE DOCUMENT, + immediately resolves the issue 😳 + + Should note as well this does NOT seem to be related to + the custom scrollbar styles below... I've tried disabling them, + but I still wind up with the same issue. + + TLDR;, would love if this could just be "overflow: auto;", + but Safari-specific issues when macOS's "Always show scrollbars" pref is enabled + means I have to set overflow-y to "hidden" to reduce the impact of the bug + */ + overflow-x: auto; + overflow-y: hidden; +} + +.styledScrollbars { + /* web standard Firefox */ + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-foreground) transparent; + + /* webkit prefixed Safari, Chrome, chromium-based browsers */ + &::-webkit-scrollbar { + width: 11px; + height: 11px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background-color: var(--scrollbar-foreground); + border-radius: 6px; + border: 3px solid var(--scrollbar-background); + } +} + +.linesScrollContainer { + display: flex; + width: max-content; + min-width: 100%; + flex-direction: column; + flex-shrink: 0; +} + +/* + +WRAP LAYOUT */ -.lineWrapper { +.wrappedLinesContainer { + display: flex; + width: 100%; + flex-direction: column; + flex-shrink: 0; +} + +.wrappedLine { display: flex; flex-wrap: nowrap; } /* adds space at the top and bottom of the block, while supporting a continuous border for line numbers */ -.linesSpacer { +.wrappedLinesSpacer { display: flex; flex-wrap: nowrap; height: var(--code-padding); - - &.wrapCode { - display: flex; - } }