diff --git a/package.json b/package.json index 2afa23c..711d2b1 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "size-limit": [ { "path": "lib/components/index.js", - "limit": "6.4kb" + "limit": "6.58kb" }, { "path": "lib/components/code-view/highlight/javascript.js", diff --git a/pages/code-view/simple.page.tsx b/pages/code-view/simple.page.tsx index 4f2a634..0d3d00e 100644 --- a/pages/code-view/simple.page.tsx +++ b/pages/code-view/simple.page.tsx @@ -8,6 +8,9 @@ export default function CodeViewPage() {

Code View

+
); } diff --git a/pages/code-view/with-actions-button.page.tsx b/pages/code-view/with-actions-button.page.tsx index 411115e..b0cf4cb 100644 --- a/pages/code-view/with-actions-button.page.tsx +++ b/pages/code-view/with-actions-button.page.tsx @@ -15,6 +15,12 @@ export default function CodeViewPage() { actions={} 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.`} /> + } + 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.`} + /> ); diff --git a/pages/code-view/with-line-wrapping.page.tsx b/pages/code-view/with-line-wrapping.page.tsx new file mode 100644 index 0000000..15ee1d0 --- /dev/null +++ b/pages/code-view/with-line-wrapping.page.tsx @@ -0,0 +1,51 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Box, SpaceBetween } from "@cloudscape-design/components"; + +import { CodeView } from "../../lib/components"; +import cppHighlight from "../../lib/components/code-view/highlight/cpp"; +import { ScreenshotArea } from "../screenshot-area"; + +export default function CodeViewPage() { + return ( + +

Code View

+ + No wrapping, no line numbers + + No wrapping, line numbers + + Wrapping, no line numbers + + Wrapping, line numbers + + Full example with indentation and code highlighting + > &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 >::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::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}`} + /> + Long word + + +
+ ); +} diff --git a/pages/code-view/with-syntax-highlighting.page.tsx b/pages/code-view/with-syntax-highlighting.page.tsx index 246f7f3..f9ae1b4 100644 --- a/pages/code-view/with-syntax-highlighting.page.tsx +++ b/pages/code-view/with-syntax-highlighting.page.tsx @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { SpaceBetween } from "@cloudscape-design/components"; +import { Box, SpaceBetween } from "@cloudscape-design/components"; import { CodeView } from "../../lib/components"; import htmlHighlight from "../../lib/components/code-view/highlight/html"; @@ -23,32 +23,32 @@ export default function CodeViewPage() {

Code View

- JavaScript + JavaScript - TypeScript + TypeScript - XML + XML Hello, world!`} highlight={xmlHighlight} /> - Markdown (MDX) + Markdown (MDX) - Bash (Shell Script) + Bash (Shell Script) - CSS + CSS - HTML + HTML Hello, World!`} highlight={htmlHighlight} /> - Java + Java - JSON + JSON - PHP + PHP `} highlight={phpHighlight} /> - Python + Python - YAML + YAML
diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index b9c601b..eded683 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -38,6 +38,14 @@ Defaults to \`false\`. "optional": true, "type": "boolean", }, + { + "description": "Controls whether line-wrapping is enabled when content would overflow the component. +Defaults to \`false\`. +", + "name": "wrapLines", + "optional": true, + "type": "boolean", + }, ], "regions": [ { diff --git a/src/code-view/__tests__/code-view.test.tsx b/src/code-view/__tests__/code-view.test.tsx index 33640c9..0f2bd8e 100644 --- a/src/code-view/__tests__/code-view.test.tsx +++ b/src/code-view/__tests__/code-view.test.tsx @@ -13,10 +13,17 @@ describe("CodeView", () => { afterEach(() => { cleanup(); }); - test("correctly renders component content", () => { + test("correctly renders simple content", () => { render(); const wrapper = createWrapper()!.findCodeView(); - expect(wrapper!.findContent().getElement().textContent).toBe("Hello World"); + expect(wrapper!.findContent().getElement()).toHaveTextContent("Hello World"); + }); + + test("correctly renders multi line content", () => { + render(); + const wrapper = createWrapper()!.findCodeView()!; + const content = wrapper.findContent(); + expect(content.getElement()).toHaveTextContent("# Hello World This is a markdown example."); }); test("correctly renders copy button slot", () => { @@ -28,7 +35,9 @@ describe("CodeView", () => { test("correctly renders line numbers", () => { render(); const wrapper = createWrapper()!.findCodeView(); - expect(wrapper!.findByClassName(styles["line-numbers"])!.getElement()).toHaveTextContent("123"); + expect(wrapper!.findAllByClassName(styles["line-number"])[0]!.getElement()).toHaveTextContent("1"); + expect(wrapper!.findAllByClassName(styles["line-number"])[1]!.getElement()).toHaveTextContent("2"); + expect(wrapper!.findAllByClassName(styles["line-number"])[2]!.getElement()).toHaveTextContent("3"); }); test("correctly renders aria-label", () => { @@ -59,7 +68,7 @@ describe("CodeView", () => { >
, ); const wrapper = createWrapper().findCodeView()!; - expect(wrapper!.findContent().getElement().innerHTML).toContain('class="tokenized"'); + expect(wrapper!.findContent().getElement().innerHTML).toContain("tokenized"); }); test("correctly tokenizes content if highlight is set to language rules", () => { @@ -73,4 +82,25 @@ describe("CodeView", () => { expect(getByText(element, "string")).toHaveClass("ace_type"); expect(getByText(element, '"world"')).toHaveClass("ace_string"); }); + + test("sets nowrap class to line if linesWrapping undefined", () => { + render(); + const wrapper = createWrapper().findCodeView()!; + const element = wrapper!.findContent().getElement(); + expect(element.outerHTML).toContain("code-line-nowrap"); + }); + + test("sets nowrap class to line if linesWrapping false", () => { + render(); + const wrapper = createWrapper().findCodeView()!; + const element = wrapper!.findContent().getElement(); + expect(element.outerHTML).toContain("code-line-nowrap"); + }); + + test("sets wrap class to line if linesWrapping true", () => { + render(); + const wrapper = createWrapper().findCodeView()!; + const element = wrapper!.findContent().getElement(); + expect(element.outerHTML).toContain("code-line-wrap"); + }); }); diff --git a/src/code-view/highlight/index.tsx b/src/code-view/highlight/index.tsx index aba720d..d37f53d 100644 --- a/src/code-view/highlight/index.tsx +++ b/src/code-view/highlight/index.tsx @@ -7,7 +7,9 @@ import { tokenize } from "ace-code/src/ext/simple_tokenizer"; import "ace-code/styles/theme/cloud_editor.css"; import "ace-code/styles/theme/cloud_editor_dark.css"; -export function createHighlight(rules: Ace.HighlightRules) { +type CreateHighlightType = (code: string) => React.ReactNode; + +export function createHighlight(rules: Ace.HighlightRules): CreateHighlightType { return (code: string) => { const tokens = tokenize(code, rules); return ( diff --git a/src/code-view/interfaces.ts b/src/code-view/interfaces.ts index e6131de..e27e995 100644 --- a/src/code-view/interfaces.ts +++ b/src/code-view/interfaces.ts @@ -24,6 +24,13 @@ export interface CodeViewProps { */ lineNumbers?: boolean; + /** + * Controls whether line-wrapping is enabled when content would overflow the component. + * + * Defaults to `false`. + */ + wrapLines?: boolean; + /** * An optional slot to display a button to enable users to perform actions, such as copy or download the code snippet. * diff --git a/src/code-view/internal.tsx b/src/code-view/internal.tsx index ddb3566..34507ce 100644 --- a/src/code-view/internal.tsx +++ b/src/code-view/internal.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { useRef } from "react"; +import { Children, createElement, Fragment, ReactElement, useRef } from "react"; import clsx from "clsx"; import { useCurrentMode } from "@cloudscape-design/component-toolkit/internal"; @@ -13,32 +13,50 @@ import styles from "./styles.css.js"; const ACE_CLASSES = { light: "ace-cloud_editor", dark: "ace-cloud_editor_dark" }; -function getLineNumbers(content: string) { - return content.split("\n").map((_, n) => n + 1); -} - type InternalCodeViewProps = CodeViewProps & InternalBaseComponentProps; +// Breaks down the input code for non-highlighted code-view into React +// Elements similar to how a highlight function would do. +const textHighlight = (code: string) => { + const lines = code.split("\n"); + return ( + + {lines.map((line, lineIndex) => ( + + {line} + {"\n"} + + ))} + + ); +}; + export function InternalCodeView({ content, actions, lineNumbers, + wrapLines, highlight, ariaLabel, ariaLabelledby, __internalRootRef = null, ...props }: InternalCodeViewProps) { - const code = highlight ? highlight(content) : {content}; const baseProps = getBaseProps(props); const preRef = useRef(null); const darkMode = useCurrentMode(preRef) === "dark"; const regionProps = ariaLabel || ariaLabelledby ? { role: "region" } : {}; + // Create tokenized React nodes of the content. + const code = highlight ? highlight(content) : textHighlight(content); + // Create elements from the nodes. + const codeElementWrapper: ReactElement = createElement(Fragment, null, code); + const codeElement = Children.only(codeElementWrapper.props.children); + return (
-
- - {lineNumbers && ( -
- {getLineNumbers(content).map((number) => ( - {number} - ))} -
- )} -
-
+        
-          
-            {code}
-          
-        
-        {actions && 
{actions}
} + + + + + + {Children.map(codeElement.props.children, (child, index) => { + return ( + + {lineNumbers && ( + + )} + + + ); + })} + +
+ + {index + 1} + + + + + {child} + + +
+ {actions &&
{actions}
}
); } diff --git a/src/code-view/styles.scss b/src/code-view/styles.scss index a289065..dbaf0c1 100644 --- a/src/code-view/styles.scss +++ b/src/code-view/styles.scss @@ -10,65 +10,73 @@ $color-background-code-view-dark: #282c34; .root { position: relative; - &-with-numbers { - display: flex; - align-items: stretch; + background-color: $color-background-code-view-light; + :global(.awsui-dark-mode) &, + :global(.awsui-polaris-dark-mode) & { + background-color: $color-background-code-view-dark; } } -.code { - background-color: $color-background-code-view-light; - display: flex; +.scroll-container { + overflow-x: auto; +} + +.code-table { border-start-start-radius: cs.$border-radius-tiles; border-start-end-radius: cs.$border-radius-tiles; border-end-start-radius: cs.$border-radius-tiles; border-end-end-radius: cs.$border-radius-tiles; - padding-block: cs.$space-static-xs; - padding-inline: cs.$space-static-xs; - margin-block: 0; - margin-inline: 0; - overflow: auto; - :global(.awsui-dark-mode) &, - :global(.awsui-polaris-dark-mode) & { - background-color: $color-background-code-view-dark; - } - &-with-line-numbers { - border-start-start-radius: 0; - border-end-start-radius: 0; - flex: 1; - } + padding-block-start: cs.$space-static-xs; + padding-block-end: cs.$space-static-xs; + table-layout: auto; + width: 100%; + border-spacing: 0; + &-with-actions { - min-block-size: cs.$space-scaled-xxl; - padding-inline-end: calc(2 * cs.$space-static-xxxl); - align-items: center; + min-height: calc(2 * cs.$space-scaled-xs + cs.$space-scaled-xxl); } } -.line-numbers { - border-start-start-radius: cs.$border-radius-tiles; - border-end-start-radius: cs.$border-radius-tiles; +.code-table-with-actions.code-table-with-line-wrapping { + padding-inline-end: cs.$space-static-xxl; +} + +.line-number { + border-right-color: cs.$color-border-divider-default; background-color: $color-background-code-view-light; - padding-block: cs.$space-static-xs; - padding-inline: cs.$space-static-xs; - display: flex; - flex-direction: column; - align-items: flex-end; - border-inline-end-width: 1px; - border-inline-end-style: solid; - border-inline-end-color: cs.$color-border-divider-default; - justify-content: center; + vertical-align: text-top; + position: sticky; + left: 0; + border-right-width: 1px; + border-right-style: solid; + padding-left: cs.$space-static-xs; + padding-right: cs.$space-static-xs; :global(.awsui-dark-mode) &, :global(.awsui-polaris-dark-mode) & { background-color: $color-background-code-view-dark; } - &-with-actions { - min-block-size: cs.$space-scaled-xxl; +} + +.unselectable { + -webkit-user-select: none; + user-select: none; +} + +.code-line { + padding-left: cs.$space-static-xs; + padding-right: cs.$space-static-xs; + &-wrap { + white-space: pre-wrap; + word-break: break-word; + } + &-nowrap { + white-space: pre; } } .actions { position: absolute; - inset-block-start: cs.$space-static-xs; - inset-inline-end: cs.$space-static-xs; + inset-block-start: cs.$space-scaled-xs; + inset-inline-end: cs.$space-scaled-xs; padding-inline-start: cs.$space-container-horizontal; } diff --git a/src/test-utils/dom/code-view/index.ts b/src/test-utils/dom/code-view/index.ts index db8f985..92df5c2 100644 --- a/src/test-utils/dom/code-view/index.ts +++ b/src/test-utils/dom/code-view/index.ts @@ -8,7 +8,7 @@ export default class CodeViewWrapper extends ComponentWrapper { static rootSelector: string = styles.root; findContent(): ElementWrapper { - return this.findByClassName(styles.code)!; + return this.find("tbody")!; } findActions(): ElementWrapper | null {