Skip to content

Commit 59ecbce

Browse files
committed
feat: add option for line-wrapping to prevent horizontal scrolling
1 parent 9f814a2 commit 59ecbce

File tree

9 files changed

+158
-75
lines changed

9 files changed

+158
-75
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"prebuild": "rm -rf lib dist .cache",
1111
"build": "npm-run-all build:pkg --parallel build:src:* --parallel build:pages:* build:themeable",
1212
"lint": "eslint --ignore-path .gitignore --ext ts,tsx,js . && stylelint --ignore-path .gitignore '{src,pages}/**/*.{css,scss}'",
13+
"fix": "eslint --fix --ignore-path .gitignore --ext ts,tsx,js . && stylelint --ignore-path .gitignore '{src,pages}/**/*.{css,scss}'",
1314
"prepare": "husky install",
1415
"test:unit": "vitest run --config vite.unit.config.mjs",
1516
"test:visual": "run-p -r preview test:visual:vitest",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { SpaceBetween } from "@cloudscape-design/components";
5+
import { CodeView } from "../../lib/components";
6+
import { ScreenshotArea } from "../screenshot-area";
7+
export default function CodeViewPage() {
8+
return (
9+
<ScreenshotArea>
10+
<h1>Code View</h1>
11+
<SpaceBetween direction="vertical" size="l">
12+
No wrapping, no line numbers
13+
<CodeView
14+
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.`}
15+
/>
16+
No wrapping, line numbers
17+
<CodeView
18+
lineNumbers={true}
19+
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.`}
20+
/>
21+
Wrapping, no line numbers
22+
<CodeView
23+
lineWrapping={true}
24+
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.`}
25+
/>
26+
Wrapping, line numbers
27+
<CodeView
28+
lineWrapping={true}
29+
lineNumbers={true}
30+
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.`}
31+
/>
32+
</SpaceBetween>
33+
</ScreenshotArea>
34+
);
35+
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ exports[`definition for code-view matches the snapshot > code-view 1`] = `
2828
"description": "A function to perform custom syntax highlighting.",
2929
"name": "highlight",
3030
"optional": true,
31-
"type": "(code: string) => React.ReactNode",
31+
"type": "(code: string) => ReactElement",
3232
},
3333
{
3434
"description": "Controls the display of line numbers.
@@ -38,6 +38,14 @@ Defaults to \`false\`.
3838
"optional": true,
3939
"type": "boolean",
4040
},
41+
{
42+
"description": "Controls lines wrap when overflowing on the right side.
43+
Defaults to \`false\`.
44+
",
45+
"name": "lineWrapping",
46+
"optional": true,
47+
"type": "boolean",
48+
},
4149
],
4250
"regions": [
4351
{

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe("CodeView", () => {
1414
test("correctly renders component content", () => {
1515
render(<CodeView content={"Hello World"}></CodeView>);
1616
const wrapper = createWrapper()!.findCodeView();
17-
expect(wrapper!.findContent().getElement().textContent).toBe("Hello World");
17+
expect(wrapper!.findContent()[0].getElement()).toHaveTextContent("Hello World");
1818
});
1919

2020
test("correctly renders copy button slot", () => {
@@ -26,7 +26,9 @@ describe("CodeView", () => {
2626
test("correctly renders line numbers", () => {
2727
render(<CodeView content={`Hello\nWorld\n!`} lineNumbers={true}></CodeView>);
2828
const wrapper = createWrapper()!.findCodeView();
29-
expect(wrapper!.findByClassName(styles["line-numbers"])!.getElement()).toHaveTextContent("123");
29+
expect(wrapper!.findAllByClassName(styles["line-number"])[0]!.getElement()).toHaveTextContent("1");
30+
expect(wrapper!.findAllByClassName(styles["line-number"])[1]!.getElement()).toHaveTextContent("2");
31+
expect(wrapper!.findAllByClassName(styles["line-number"])[2]!.getElement()).toHaveTextContent("3");
3032
});
3133

3234
test("correctly renders aria-label", () => {
@@ -57,18 +59,39 @@ describe("CodeView", () => {
5759
></CodeView>
5860
);
5961
const wrapper = createWrapper().findCodeView()!;
60-
expect(wrapper!.findContent().getElement().innerHTML).toContain('class="tokenized"');
62+
expect(wrapper!.findContent()[0].getElement().innerHTML).toContain("tokenized");
6163
});
6264

6365
test("correctly tokenizes content if highlight is set to language rules", () => {
6466
render(<CodeView content={'const hello: string = "world";'} highlight={typescriptHighlightRules}></CodeView>);
6567
const wrapper = createWrapper().findCodeView()!;
66-
const element = wrapper!.findContent().getElement();
68+
const element = wrapper!.findContent()[0].getElement();
6769

6870
// Check that the content is tokenized following typescript rules.
6971
expect(getByText(element, "const")).toHaveClass("ace_type");
7072
expect(getByText(element, "hello")).toHaveClass("ace_identifier");
7173
expect(getByText(element, "string")).toHaveClass("ace_type");
7274
expect(getByText(element, '"world"')).toHaveClass("ace_string");
7375
});
76+
77+
test("sets nowrap class to line if linesWrapping undefined", () => {
78+
render(<CodeView content={"Hello World"}></CodeView>);
79+
const wrapper = createWrapper().findCodeView()!;
80+
const element = wrapper!.findContent()[0].getElement();
81+
expect(element.outerHTML).toContain("code-line-nowrap");
82+
});
83+
84+
test("sets nowrap class to line if linesWrapping false", () => {
85+
render(<CodeView lineWrapping={false} content={"Hello World"}></CodeView>);
86+
const wrapper = createWrapper().findCodeView()!;
87+
const element = wrapper!.findContent()[0].getElement();
88+
expect(element.outerHTML).toContain("code-line-nowrap");
89+
});
90+
91+
test("sets wrap class to line if linesWrapping true", () => {
92+
render(<CodeView lineWrapping={true} content={"Hello World"}></CodeView>);
93+
const wrapper = createWrapper().findCodeView()!;
94+
const element = wrapper!.findContent()[0].getElement();
95+
expect(element.outerHTML).toContain("code-line-wrap");
96+
});
7497
});

src/code-view/highlight/index.tsx

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

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

src/code-view/interfaces.ts

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

27+
/**
28+
* Controls lines wrap when overflowing on the right side.
29+
*
30+
* Defaults to `false`.
31+
*/
32+
lineWrapping?: 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
*
@@ -34,5 +41,5 @@ export interface CodeViewProps {
3441
* A function to perform custom syntax highlighting.
3542
*
3643
*/
37-
highlight?: (code: string) => React.ReactNode;
44+
highlight?: (code: string) => React.ReactElement;
3845
}

src/code-view/internal.tsx

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,71 +2,84 @@
22
// SPDX-License-Identifier: Apache-2.0
33
import { useCurrentMode } from "@cloudscape-design/component-toolkit/internal";
44
import Box from "@cloudscape-design/components/box";
5+
import { TextHighlightRules } from "ace-code/src/mode/text_highlight_rules";
56
import clsx from "clsx";
67
import { useRef } from "react";
8+
import { Children } from "react";
79
import { InternalBaseComponentProps, getBaseProps } from "../internal/base-component/use-base-component";
10+
import { createHighlight } from "./highlight";
811
import { CodeViewProps } from "./interfaces";
912
import styles from "./styles.css.js";
1013

1114
const ACE_CLASSES = { light: "ace-cloud_editor", dark: "ace-cloud_editor_dark" };
1215

13-
function getLineNumbers(content: string) {
14-
return content.split("\n").map((_, n) => n + 1);
15-
}
16-
1716
type InternalCodeViewProps = CodeViewProps & InternalBaseComponentProps;
1817

18+
const textHighlight = createHighlight(new TextHighlightRules());
19+
1920
export function InternalCodeView({
2021
content,
2122
actions,
2223
lineNumbers,
24+
lineWrapping,
2325
highlight,
2426
ariaLabel,
2527
ariaLabelledby,
2628
__internalRootRef = null,
2729
...props
2830
}: InternalCodeViewProps) {
29-
const code = highlight ? highlight(content) : <span>{content}</span>;
3031
const baseProps = getBaseProps(props);
3132
const preRef = useRef<HTMLPreElement>(null);
3233
const darkMode = useCurrentMode(preRef) === "dark";
3334

3435
const regionProps = ariaLabel || ariaLabelledby ? { role: "region" } : {};
3536

37+
// Create tokenized elements of the content.
38+
const code = highlight ? highlight(content) : textHighlight(content);
39+
3640
return (
3741
<div
38-
className={styles.root}
42+
className={clsx(darkMode ? ACE_CLASSES.dark : ACE_CLASSES.light, styles.root)}
3943
{...regionProps}
4044
{...baseProps}
4145
aria-label={ariaLabel}
4246
aria-labelledby={ariaLabelledby}
4347
ref={__internalRootRef}
4448
>
45-
<div className={clsx(lineNumbers && styles["root-with-numbers"], actions && styles["root-with-actions"])}>
46-
<Box color="text-status-inactive" fontSize="body-m">
47-
{lineNumbers && (
48-
<div className={styles["line-numbers"]} aria-hidden={true}>
49-
{getLineNumbers(content).map((number) => (
50-
<span key={number}>{number}</span>
51-
))}
52-
</div>
53-
)}
54-
</Box>
55-
<pre
56-
ref={preRef}
57-
className={clsx(
58-
darkMode ? ACE_CLASSES.dark : ACE_CLASSES.light,
59-
styles.code,
60-
lineNumbers && styles["code-with-line-numbers"],
61-
actions && styles["code-with-actions"]
62-
)}
63-
>
64-
<Box color="inherit" variant="code" fontSize="body-m">
65-
{code}
66-
</Box>
67-
</pre>
68-
{actions && <div className={styles.actions}>{actions}</div>}
69-
</div>
49+
<table className={clsx(styles["code-table"])}>
50+
<colgroup>
51+
<col style={{ width: 1 } /* shrink to fit content */} />
52+
<col style={{ width: "auto" }} />
53+
</colgroup>
54+
<tbody>
55+
{Children.map(code.props.children, (child, index) => {
56+
return (
57+
<tr key={index}>
58+
{lineNumbers && (
59+
<td className={styles["line-number"]} aria-hidden={true}>
60+
<Box color="text-status-inactive" fontSize="body-m">
61+
{index + 1}
62+
</Box>
63+
</td>
64+
)}
65+
<td className={styles["code-line"]}>
66+
<Box color="text-status-inactive" variant="code" fontSize="body-m">
67+
<span
68+
className={clsx(
69+
code.props.className,
70+
lineWrapping ? styles["code-line-wrap"] : styles["code-line-nowrap"]
71+
)}
72+
>
73+
{child}
74+
</span>
75+
</Box>
76+
</td>
77+
</tr>
78+
);
79+
})}
80+
</tbody>
81+
</table>
82+
{actions && <div className={styles.actions}>{actions}</div>}
7083
</div>
7184
);
7285
}

src/code-view/styles.scss

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,57 +5,51 @@ $color-background-code-view-dark: #282c34;
55

66
.root {
77
position: relative;
8-
&-with-numbers {
9-
display: flex;
10-
align-items: stretch;
11-
}
12-
}
13-
14-
.code {
158
:global(.awsui-dark-mode) &,
169
:global(.awsui-polaris-dark-mode) & {
1710
background-color: $color-background-code-view-dark;
1811
}
19-
&-with-line-numbers {
20-
border-start-start-radius: 0;
21-
border-end-start-radius: 0;
22-
flex: 1;
23-
}
24-
&-with-actions {
25-
min-block-size: cs.$space-scaled-xxl;
26-
padding-inline-end: calc(2 * cs.$space-static-xxxl);
27-
align-items: center;
28-
}
2912
background-color: $color-background-code-view-light;
30-
display: flex;
13+
overflow-x: auto;
14+
}
15+
16+
.code-table {
3117
border-start-start-radius: cs.$border-radius-tiles;
3218
border-start-end-radius: cs.$border-radius-tiles;
3319
border-end-start-radius: cs.$border-radius-tiles;
3420
border-end-end-radius: cs.$border-radius-tiles;
35-
padding-block: cs.$space-static-xs;
36-
padding-inline: cs.$space-static-xs;
37-
margin-block: 0;
38-
margin-inline: 0;
39-
overflow: auto;
21+
padding-top: cs.$space-static-xs;
22+
padding-bottom: cs.$space-static-xs;
23+
table-layout: auto;
24+
width: 100%;
25+
border-spacing: 0;
4026
}
4127

42-
.line-numbers {
28+
.line-number {
29+
border-right-color: cs.$color-border-divider-default;
4330
:global(.awsui-dark-mode) &,
4431
:global(.awsui-polaris-dark-mode) & {
4532
background-color: $color-background-code-view-dark;
4633
}
47-
border-start-start-radius: cs.$border-radius-tiles;
48-
border-end-start-radius: cs.$border-radius-tiles;
4934
background-color: $color-background-code-view-light;
50-
padding-block: cs.$space-static-xs;
51-
padding-inline: cs.$space-static-xs;
52-
display: flex;
53-
flex-direction: column;
54-
align-items: flex-end;
55-
border-inline-end-width: 1px;
56-
border-inline-end-style: solid;
57-
border-inline-end-color: cs.$color-border-divider-default;
58-
justify-content: center;
35+
vertical-align: text-top;
36+
position: sticky;
37+
left: 0;
38+
border-right-width: 1px;
39+
border-right-style: solid;
40+
padding-left: cs.$space-static-xs;
41+
padding-right: cs.$space-static-xs;
42+
}
43+
44+
.code-line {
45+
padding-left: cs.$space-static-xs;
46+
padding-right: cs.$space-static-xs;
47+
&-wrap {
48+
white-space: pre-wrap;
49+
}
50+
&-nowrap {
51+
white-space: nowrap;
52+
}
5953
}
6054

6155
.actions {

src/test-utils/dom/code-view/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import styles from "../../../code-view/styles.selectors.js";
66
export default class CodeViewWrapper extends ComponentWrapper {
77
static rootSelector: string = styles.root;
88

9-
findContent(): ElementWrapper {
10-
return this.findByClassName(styles.code)!;
9+
findContent() {
10+
return this.findAllByClassName(styles["code-line"])!;
1111
}
1212

1313
findActions(): ElementWrapper | null {

0 commit comments

Comments
 (0)