Skip to content

Commit 5381da5

Browse files
akoremanRuben CarvalhogethinwebsterAlice Koreman
authored
feat: Add option for line-wrapping to prevent horizontal scrolling (#20)
* feat: add option for line-wrapping to prevent horizontal scrolling * chore: remove accidentally commited npm script * fix: make line numbers unselectable * chore: replace Ace text highlight rules with in-repo function * chore: Fix minimum-height and actions absolute positioning * fix: Fix code indentation by setting white-space to pre * chore: Fix code color and lines Box variant * chore: Add multi-line test * chore: Increase size-limit by 8 bytes * chore: Use logical properties * chore: Update findContent() test to be backward compatible * Give <table> role="presentation" * feat: add option for line-wrapping to prevent horizontal scrolling * chore: remove accidentally commited npm script * fix: make line numbers unselectable * chore: replace Ace text highlight rules with in-repo function * chore: Fix minimum-height and actions absolute positioning * fix: Fix code indentation by setting white-space to pre * chore: Fix code color and lines Box variant * chore: Add multi-line test * chore: Increase size-limit by 8 bytes * Give <table> role="presentation" * Increase size limit * chore: increase size-limit * small fixes after merging up to main * small fix after merging up to main * bump size limit * keep test util backwards compatible * small fixes from mistakes made while merging * revert highlight function type to return ReactNode * add syntax highlighting example line wrapping page * small refactor * wrap text on pages in Box components * update parameter jsdoc * update documentation snapshot * update prop name to wrapLines * add word break * bump size --------- Co-authored-by: Ruben Carvalho <[email protected]> Co-authored-by: Gethin Webster <[email protected]> Co-authored-by: Gethin Webster <[email protected]> Co-authored-by: Alice Koreman <[email protected]>
1 parent f12b6cc commit 5381da5

File tree

12 files changed

+238
-90
lines changed

12 files changed

+238
-90
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@
146146
"size-limit": [
147147
{
148148
"path": "lib/components/index.js",
149-
"limit": "6.4kb"
149+
"limit": "6.58kb"
150150
},
151151
{
152152
"path": "lib/components/code-view/highlight/javascript.js",

pages/code-view/simple.page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export default function CodeViewPage() {
88
<ScreenshotArea>
99
<h1>Code View</h1>
1010
<CodeView content={"Hello World"} />
11+
<CodeView
12+
content={`public class HelloWorld {\n public static void main(String[] args) {\n System.out.println("Hello, World!");\n }\n}`}
13+
/>
1114
</ScreenshotArea>
1215
);
1316
}

pages/code-view/with-actions-button.page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ export default function CodeViewPage() {
1515
actions={<Button ariaLabel="Copy code" iconName="copy"></Button>}
1616
content={`Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.`}
1717
/>
18+
<CodeView
19+
lineNumbers={true}
20+
wrapLines={true}
21+
actions={<Button ariaLabel="Copy code" iconName="copy"></Button>}
22+
content={`Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.`}
23+
/>
1824
</SpaceBetween>
1925
</ScreenshotArea>
2026
);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { Box, SpaceBetween } from "@cloudscape-design/components";
5+
6+
import { CodeView } from "../../lib/components";
7+
import cppHighlight from "../../lib/components/code-view/highlight/cpp";
8+
import { ScreenshotArea } from "../screenshot-area";
9+
10+
export default function CodeViewPage() {
11+
return (
12+
<ScreenshotArea>
13+
<h1>Code View</h1>
14+
<SpaceBetween direction="vertical" size="l">
15+
<Box>No wrapping, no line numbers</Box>
16+
<CodeView
17+
content={`Hello this is a short line.\nHello this is a long line. Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.\nHello this is another short line.`}
18+
/>
19+
<Box>No wrapping, line numbers</Box>
20+
<CodeView
21+
lineNumbers={true}
22+
content={`Hello this is a short line.\nHello this is a long line. Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.\nHello this is another short line.`}
23+
/>
24+
<Box>Wrapping, no line numbers</Box>
25+
<CodeView
26+
wrapLines={true}
27+
content={`Hello this is a short line.\nHello this is a long line. Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.\nHello this is another short line.`}
28+
/>
29+
<Box>Wrapping, line numbers</Box>
30+
<CodeView
31+
wrapLines={true}
32+
lineNumbers={true}
33+
content={`Hello this is a short line.\nHello this is a long line. Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.\nHello this is another short line.`}
34+
/>
35+
<Box>Full example with indentation and code highlighting</Box>
36+
<CodeView
37+
wrapLines={true}
38+
lineNumbers={true}
39+
highlight={cppHighlight}
40+
content={`void UserQuery(map<string, vector<string> > &svmap) {\n string queryName;\n cout << "Please enter a family name you want to query: ";\n cin >> queryName;\n int i = 0;\n for (map<string, vector<string> >::iterator itr = svmap.begin(), mapEnd = svmap.end(); itr != mapEnd; ++itr) {\n i++;\n if (itr->first == queryName) {\n cout << "The " << itr->first << " family has " << itr->second.size() << " children: ";\n for (vector<string>::iterator itrvec = itr->second.begin(), vecEnd = itr->second.end(); itrvec != vecEnd; ++itrvec)\n cout << *itrvec << " ";\n break;\n }\n }\n if (i >= svmap.size())\n cout << "Sorry, the " << queryName << " family is not found." << endl;\n}`}
41+
/>
42+
<Box>Long word</Box>
43+
<CodeView
44+
wrapLines={true}
45+
lineNumbers={true}
46+
content={`LoremipsumdolorsitametconsecteturadipiscingelitCurabitursagittismetusidornarebibendumLoremipsumdolorsitametconsecteturadipiscingelitCurabitursagittismetusidornarebibendumLoremipsumdolorsitametconsecteturadipiscingelitCurabitursagittismetusidornarebibendumLoremipsumdolorsitametconsecteturadipiscingelitCurabitursagittismetusidornarebibendumLoremipsumdolorsitametconsecteturadipiscingelitCurabitursagittismetusidornarebibendumLoremipsumdolorsitametconsecteturadipiscingelitCurabitursagittismetusidornarebibendumLoremipsumdolorsitametconsecteturadipiscingelitCurabitursagittismetusidornarebibendumLoremipsumdolorsitametconsecteturadipiscingelitCurabitursagittismetusidornarebibendum`}
47+
/>
48+
</SpaceBetween>
49+
</ScreenshotArea>
50+
);
51+
}

pages/code-view/with-syntax-highlighting.page.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { SpaceBetween } from "@cloudscape-design/components";
4+
import { Box, SpaceBetween } from "@cloudscape-design/components";
55

66
import { CodeView } from "../../lib/components";
77
import htmlHighlight from "../../lib/components/code-view/highlight/html";
@@ -23,32 +23,32 @@ export default function CodeViewPage() {
2323
<ScreenshotArea>
2424
<h1>Code View</h1>
2525
<SpaceBetween direction="vertical" size="l">
26-
JavaScript
26+
<Box>JavaScript</Box>
2727
<CodeView content={`const hello = "world";\nconsole.log(hello);`} highlight={javascriptHighlight} />
28-
TypeScript
28+
<Box>TypeScript</Box>
2929
<CodeView content={`let hello: string = "world";\nconsole.log(hello);`} highlight={typescriptHighlight} />
30-
XML
30+
<Box>XML</Box>
3131
<CodeView content={`<greeting>Hello, world!</greeting>`} highlight={xmlHighlight} />
32-
Markdown (MDX)
32+
<Box>Markdown (MDX)</Box>
3333
<CodeView content={`# Hello World\n\nThis is a markdown example.`} highlight={markdownHighlight} />
34-
Bash (Shell Script)
34+
<Box>Bash (Shell Script)</Box>
3535
<CodeView content={`echo "Hello, world!"`} highlight={shHighlight} />
36-
CSS
36+
<Box>CSS</Box>
3737
<CodeView content={`body { background-color: lightblue; }`} highlight={cssHighlight} />
38-
HTML
38+
<Box>HTML</Box>
3939
<CodeView content={`<h1>Hello, World!</h1>`} highlight={htmlHighlight} />
40-
Java
40+
<Box>Java</Box>
4141
<CodeView
4242
content={`public class HelloWorld {\n public static void main(String[] args) {\n System.out.println("Hello, World!");\n }\n}`}
4343
highlight={javaHighlight}
4444
/>
45-
JSON
45+
<Box>JSON</Box>
4646
<CodeView content={`{"greeting": "Hello, world!"}`} highlight={jsonHighlight} />
47-
PHP
47+
<Box>PHP</Box>
4848
<CodeView content={`<?php\necho 'Hello, world!';\n?>`} highlight={phpHighlight} />
49-
Python
49+
<Box>Python</Box>
5050
<CodeView content={`print("Hello, World!")`} highlight={pythonHighlight} />
51-
YAML
51+
<Box>YAML</Box>
5252
<CodeView content={`greeting: Hello, world!`} highlight={yamlHighlight} />
5353
</SpaceBetween>
5454
</ScreenshotArea>

src/__tests__/__snapshots__/documenter.test.ts.snap

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ Defaults to \`false\`.
3838
"optional": true,
3939
"type": "boolean",
4040
},
41+
{
42+
"description": "Controls whether line-wrapping is enabled when content would overflow the component.
43+
Defaults to \`false\`.
44+
",
45+
"name": "wrapLines",
46+
"optional": true,
47+
"type": "boolean",
48+
},
4149
],
4250
"regions": [
4351
{

src/code-view/__tests__/code-view.test.tsx

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,17 @@ describe("CodeView", () => {
1313
afterEach(() => {
1414
cleanup();
1515
});
16-
test("correctly renders component content", () => {
16+
test("correctly renders simple content", () => {
1717
render(<CodeView content={"Hello World"}></CodeView>);
1818
const wrapper = createWrapper()!.findCodeView();
19-
expect(wrapper!.findContent().getElement().textContent).toBe("Hello World");
19+
expect(wrapper!.findContent().getElement()).toHaveTextContent("Hello World");
20+
});
21+
22+
test("correctly renders multi line content", () => {
23+
render(<CodeView content={`# Hello World\n\nThis is a markdown example.`}></CodeView>);
24+
const wrapper = createWrapper()!.findCodeView()!;
25+
const content = wrapper.findContent();
26+
expect(content.getElement()).toHaveTextContent("# Hello World This is a markdown example.");
2027
});
2128

2229
test("correctly renders copy button slot", () => {
@@ -28,7 +35,9 @@ describe("CodeView", () => {
2835
test("correctly renders line numbers", () => {
2936
render(<CodeView content={`Hello\nWorld\n!`} lineNumbers={true}></CodeView>);
3037
const wrapper = createWrapper()!.findCodeView();
31-
expect(wrapper!.findByClassName(styles["line-numbers"])!.getElement()).toHaveTextContent("123");
38+
expect(wrapper!.findAllByClassName(styles["line-number"])[0]!.getElement()).toHaveTextContent("1");
39+
expect(wrapper!.findAllByClassName(styles["line-number"])[1]!.getElement()).toHaveTextContent("2");
40+
expect(wrapper!.findAllByClassName(styles["line-number"])[2]!.getElement()).toHaveTextContent("3");
3241
});
3342

3443
test("correctly renders aria-label", () => {
@@ -59,7 +68,7 @@ describe("CodeView", () => {
5968
></CodeView>,
6069
);
6170
const wrapper = createWrapper().findCodeView()!;
62-
expect(wrapper!.findContent().getElement().innerHTML).toContain('class="tokenized"');
71+
expect(wrapper!.findContent().getElement().innerHTML).toContain("tokenized");
6372
});
6473

6574
test("correctly tokenizes content if highlight is set to language rules", () => {
@@ -73,4 +82,25 @@ describe("CodeView", () => {
7382
expect(getByText(element, "string")).toHaveClass("ace_type");
7483
expect(getByText(element, '"world"')).toHaveClass("ace_string");
7584
});
85+
86+
test("sets nowrap class to line if linesWrapping undefined", () => {
87+
render(<CodeView content={"Hello World"}></CodeView>);
88+
const wrapper = createWrapper().findCodeView()!;
89+
const element = wrapper!.findContent().getElement();
90+
expect(element.outerHTML).toContain("code-line-nowrap");
91+
});
92+
93+
test("sets nowrap class to line if linesWrapping false", () => {
94+
render(<CodeView wrapLines={false} content={"Hello World"}></CodeView>);
95+
const wrapper = createWrapper().findCodeView()!;
96+
const element = wrapper!.findContent().getElement();
97+
expect(element.outerHTML).toContain("code-line-nowrap");
98+
});
99+
100+
test("sets wrap class to line if linesWrapping true", () => {
101+
render(<CodeView wrapLines={true} content={"Hello World"}></CodeView>);
102+
const wrapper = createWrapper().findCodeView()!;
103+
const element = wrapper!.findContent().getElement();
104+
expect(element.outerHTML).toContain("code-line-wrap");
105+
});
76106
});

src/code-view/highlight/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { tokenize } from "ace-code/src/ext/simple_tokenizer";
77
import "ace-code/styles/theme/cloud_editor.css";
88
import "ace-code/styles/theme/cloud_editor_dark.css";
99

10-
export function createHighlight(rules: Ace.HighlightRules) {
10+
type CreateHighlightType = (code: string) => React.ReactNode;
11+
12+
export function createHighlight(rules: Ace.HighlightRules): CreateHighlightType {
1113
return (code: string) => {
1214
const tokens = tokenize(code, rules);
1315
return (

src/code-view/interfaces.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ export interface CodeViewProps {
2424
*/
2525
lineNumbers?: boolean;
2626

27+
/**
28+
* Controls whether line-wrapping is enabled when content would overflow the component.
29+
*
30+
* Defaults to `false`.
31+
*/
32+
wrapLines?: boolean;
33+
2734
/**
2835
* An optional slot to display a button to enable users to perform actions, such as copy or download the code snippet.
2936
*

src/code-view/internal.tsx

Lines changed: 64 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3-
import { useRef } from "react";
3+
import { Children, createElement, Fragment, ReactElement, useRef } from "react";
44
import clsx from "clsx";
55

66
import { useCurrentMode } from "@cloudscape-design/component-toolkit/internal";
@@ -13,67 +13,100 @@ import styles from "./styles.css.js";
1313

1414
const ACE_CLASSES = { light: "ace-cloud_editor", dark: "ace-cloud_editor_dark" };
1515

16-
function getLineNumbers(content: string) {
17-
return content.split("\n").map((_, n) => n + 1);
18-
}
19-
2016
type InternalCodeViewProps = CodeViewProps & InternalBaseComponentProps;
2117

18+
// Breaks down the input code for non-highlighted code-view into React
19+
// Elements similar to how a highlight function would do.
20+
const textHighlight = (code: string) => {
21+
const lines = code.split("\n");
22+
return (
23+
<span>
24+
{lines.map((line, lineIndex) => (
25+
<span key={lineIndex}>
26+
{line}
27+
{"\n"}
28+
</span>
29+
))}
30+
</span>
31+
);
32+
};
33+
2234
export function InternalCodeView({
2335
content,
2436
actions,
2537
lineNumbers,
38+
wrapLines,
2639
highlight,
2740
ariaLabel,
2841
ariaLabelledby,
2942
__internalRootRef = null,
3043
...props
3144
}: InternalCodeViewProps) {
32-
const code = highlight ? highlight(content) : <span>{content}</span>;
3345
const baseProps = getBaseProps(props);
3446
const preRef = useRef<HTMLPreElement>(null);
3547
const darkMode = useCurrentMode(preRef) === "dark";
3648

3749
const regionProps = ariaLabel || ariaLabelledby ? { role: "region" } : {};
3850

51+
// Create tokenized React nodes of the content.
52+
const code = highlight ? highlight(content) : textHighlight(content);
53+
// Create elements from the nodes.
54+
const codeElementWrapper: ReactElement = createElement(Fragment, null, code);
55+
const codeElement = Children.only(codeElementWrapper.props.children);
56+
3957
return (
4058
<div
41-
className={styles.root}
59+
className={clsx(darkMode ? ACE_CLASSES.dark : ACE_CLASSES.light, styles.root)}
4260
{...regionProps}
4361
{...baseProps}
4462
aria-label={ariaLabel}
4563
aria-labelledby={ariaLabelledby}
4664
dir="ltr"
4765
ref={__internalRootRef}
4866
>
49-
<div className={clsx(lineNumbers && styles["root-with-numbers"], actions && styles["root-with-actions"])}>
50-
<Box color="text-status-inactive" fontSize="body-m">
51-
{lineNumbers && (
52-
<div
53-
className={clsx(styles["line-numbers"], actions && styles["line-numbers-with-actions"])}
54-
aria-hidden={true}
55-
>
56-
{getLineNumbers(content).map((number) => (
57-
<span key={number}>{number}</span>
58-
))}
59-
</div>
60-
)}
61-
</Box>
62-
<pre
63-
ref={preRef}
67+
<div className={styles["scroll-container"]}>
68+
<table
69+
role="presentation"
6470
className={clsx(
65-
darkMode ? ACE_CLASSES.dark : ACE_CLASSES.light,
66-
styles.code,
67-
lineNumbers && styles["code-with-line-numbers"],
68-
actions && styles["code-with-actions"],
71+
styles["code-table"],
72+
actions && styles["code-table-with-actions"],
73+
wrapLines && styles["code-table-with-line-wrapping"],
6974
)}
7075
>
71-
<Box color="inherit" variant="code" fontSize="body-m">
72-
{code}
73-
</Box>
74-
</pre>
75-
{actions && <div className={styles.actions}>{actions}</div>}
76+
<colgroup>
77+
<col style={{ width: 1 } /* shrink to fit content */} />
78+
<col style={{ width: "auto" }} />
79+
</colgroup>
80+
<tbody>
81+
{Children.map(codeElement.props.children, (child, index) => {
82+
return (
83+
<tr key={index}>
84+
{lineNumbers && (
85+
<td className={clsx(styles["line-number"], styles.unselectable)} aria-hidden={true}>
86+
<Box variant="code" color="text-status-inactive" fontSize="body-m">
87+
{index + 1}
88+
</Box>
89+
</td>
90+
)}
91+
<td className={styles["code-line"]}>
92+
<Box variant="code" fontSize="body-m">
93+
<span
94+
className={clsx(
95+
codeElement.props.className,
96+
wrapLines ? styles["code-line-wrap"] : styles["code-line-nowrap"],
97+
)}
98+
>
99+
{child}
100+
</span>
101+
</Box>
102+
</td>
103+
</tr>
104+
);
105+
})}
106+
</tbody>
107+
</table>
76108
</div>
109+
{actions && <div className={styles.actions}>{actions}</div>}
77110
</div>
78111
);
79112
}

0 commit comments

Comments
 (0)