Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/copysnippet-wrap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@cube-dev/ui-kit': minor
---

`PrismCode` and `CopySnippet` take an `isWrapped` prop that soft-wraps long content instead of scrolling it sideways.

By default both components keep each line on one line and scroll horizontally, which buries long error messages and logs off to the right. `isWrapped` lays the content out on multiple lines instead: unbreakable runs like URLs, tokens and identifiers break too (`overflow-wrap: anywhere`), not just spaces. On `CopySnippet` the block additionally grows vertically to fit — so even a single very long line is fully readable rather than clamped to the collapsed height — and the prop is forwarded to the inner `PrismCode`, which owns the wrapping itself.

```jsx
<PrismCode code={longErrorMessage} language="bash" isWrapped />
<CopySnippet code={longErrorMessage} language="bash" isWrapped />
```

On `CopySnippet` it is a different axis from `nowrap` — `nowrap` collapses real newlines into one scrolling line, `isWrapped` breaks long lines — and `nowrap` wins when both are set. The copy button and syntax highlighting are unchanged.
10 changes: 5 additions & 5 deletions src/components/content/CopySnippet/CopySnippet.docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ A code block with copy-to-clipboard functionality and syntax highlighting. Exten
- **`title`** `string` (default: `'Code example'`) — Accessible title used in the copy toast message
- **`prefix`** `string` (default: `''`) — Prefix for each line of code (e.g. `'$ '` for bash snippets)
- **`nowrap`** `boolean` — Force single-line display even for multi-line code
- **`isWrapped`** `boolean` — Soft-wrap long content onto multiple lines instead of scrolling it horizontally. The block grows to fit, so even a single long line stays fully readable, and unbreakable runs (URLs, tokens, identifiers) wrap too. Useful for error messages and logs. Ignored when `nowrap` is set (a different axis: `nowrap` collapses newlines into one scrolling line, `isWrapped` breaks long lines).
- **`serif`** `boolean` — Use serif (non-monospace) font for the code
- **`hideText`** `string | string[] | boolean` — Hide sensitive text with bullet characters. `true` hides all text, a string hides that substring, an array hides multiple substrings. A toggle button appears to reveal hidden content.
- **`actions`** `ReactNode` — Additional action buttons displayed alongside the copy button. Use `CopySnippet.Button` for consistent styling.
Expand Down Expand Up @@ -55,14 +56,13 @@ These properties allow direct style application without using the `styles` prop:
```jsx
<CopySnippet code="const x = 1;" language="javascript" />

<CopySnippet
code={'{\n "key": "value"\n}'}
language="json"
multiline
/>
<CopySnippet code={'{\n "key": "value"\n}'} language="json" />

<CopySnippet
code={'<button type="button" class="primary">Save</button>'}
language="html"
/>

// Wrap a long single-line error instead of scrolling it sideways
<CopySnippet code={longErrorMessage} language="bash" isWrapped />
```
10 changes: 10 additions & 0 deletions src/components/content/CopySnippet/CopySnippet.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ WithScroll.args = {
},
};

export const Wrapped = Template.bind({});
Wrapped.args = {
code: 'XMLA Internal Error: Arrow error: External error: Database Execution Error: Internal: Error during planning: Error decoding LogicalPlanNode.logical_plan_type:SubqueryAliasNode.input as protobuf message',
language: 'bash',
isWrapped: true,
styles: {
width: 'max 400px',
},
};

export const JavascriptSyntax = Template.bind({});
JavascriptSyntax.args = {
language: 'javascript',
Expand Down
24 changes: 22 additions & 2 deletions src/components/content/CopySnippet/CopySnippet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ const StyledBlock = tasty({
'': 'monospace',
serif: true,
},
// The wrapping itself (`white-space` / `overflow-wrap` on the `<code>`)
// is owned by `PrismCode` via its `isWrapped` prop, which this component
// forwards.
},
},
});
Expand Down Expand Up @@ -160,6 +163,16 @@ export interface CubeCopySnippetProps extends CubeCardProps {
title?: string;
/** Whether the snippet is single-lined */
nowrap?: boolean;
/**
* Soft-wrap long content onto multiple lines instead of scrolling it
* horizontally. The block grows vertically to fit (so even a single long line
* is fully readable rather than clamped), and unbreakable runs like URLs,
* tokens and identifiers wrap too. Useful for error messages and logs.
* Has no effect when `nowrap` is set. Note this is a different axis from
* `nowrap`: `nowrap` collapses real newlines into one scrolling line, while
* `isWrapped` breaks long lines that would otherwise scroll.
*/
isWrapped?: boolean;
/** The prefix for each line of code. Useful for bash snippets. */
prefix?: string;
/** The code language of the snippet */
Expand All @@ -186,6 +199,7 @@ function CopySnippet(allProps: CubeCopySnippetProps) {
code = '',
title = t('copySnippet.title', 'Code example'),
nowrap,
isWrapped,
prefix = '',
language,
serif,
Expand All @@ -208,6 +222,11 @@ function CopySnippet(allProps: CubeCopySnippetProps) {
const pristineCode = code.replace(/\n$/, '');

const multiline = pristineCode.includes('\n') && !nowrap;
// `isWrapped` reuses the multiline block layout (auto height, no right fade,
// copy button on top) so wrapped content grows vertically instead of being
// clamped to the single-line height. `nowrap` (force one scrolling line) wins
// over it.
const shouldWrap = !!isWrapped && !nowrap;
let formattedCode = pristineCode
.replace(/\r/g, '')
.split(/\n/g)
Expand Down Expand Up @@ -236,11 +255,11 @@ function CopySnippet(allProps: CubeCopySnippetProps) {
const mods = useMemo(() => {
return {
nowrap,
multiline,
multiline: multiline || shouldWrap,
serif,
hidden: !!hideText,
};
}, [nowrap, multiline, hideText, serif]);
}, [nowrap, multiline, shouldWrap, hideText, serif]);

const Snippet = (
<CopySnippetElement mods={mods} {...props}>
Expand All @@ -250,6 +269,7 @@ function CopySnippet(allProps: CubeCopySnippetProps) {
style={{ margin: 0, overflow: 'visible' }}
code={formattedCode}
language={language || 'javascript'}
isWrapped={shouldWrap}
/>
</StyledBlock>
<ButtonContainer mods={mods}>
Expand Down
1 change: 1 addition & 0 deletions src/components/content/PrismCode/PrismCode.docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Syntax-highlighted code block using Prism. Supports multiple languages.

- **`code`** `string` (default: `''`) — The code snippet to highlight
- **`language`** `string` (default: `'javascript'`) — Language for syntax highlighting (`javascript`, `typescript`, `json`, `yaml`, `bash`, `sql`, `css`, `html`, etc.)
- **`isWrapped`** `boolean` — Soft-wrap long lines onto multiple lines instead of scrolling them horizontally. Unbreakable runs (URLs, tokens, identifiers) wrap too. Useful for error messages and logs.

### Style Defaults

Expand Down
10 changes: 10 additions & 0 deletions src/components/content/PrismCode/PrismCode.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ export const MultiLine = {
},
};

export const Wrapped = {
render: Template,
args: {
language: 'bash',
isWrapped: true,
width: 'max 400px',
code: 'XMLA Internal Error: Arrow error: External error: Database Execution Error: Internal: Error during planning: Error decoding LogicalPlanNode.logical_plan_type:SubqueryAliasNode.input as protobuf message',
},
};

export const JavascriptSyntax = {
render: Template,
args: {
Expand Down
24 changes: 22 additions & 2 deletions src/components/content/PrismCode/PrismCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ const PreElement = tasty({
Code: {
display: 'block',
preset: 's3',
// The global Prism CSS pins `white-space: pre` on
// `code[class*="language-"]`, so the wrap has to be re-declared right on
// the `<code>` element — `pre-wrap` on the surrounding block alone does
// not cascade past it. `overflow-wrap: anywhere` also breaks runs with no
// spaces (URLs, tokens, identifiers) that `pre-wrap` on its own would let
// overflow.
whiteSpace: {
'': 'pre',
wrapped: 'pre-wrap',
},
overflowWrap: {
'': 'normal',
wrapped: 'anywhere',
},
},
},
});
Expand All @@ -39,6 +53,12 @@ export interface CubePrismCodeProps extends ContainerStyleProps {
/** The CSS style map */
style?: BaseProps['style'];
styles?: Styles;
/**
* Soft-wrap long lines onto multiple lines instead of scrolling them
* horizontally. Unbreakable runs like URLs, tokens and identifiers wrap too.
* Useful for error messages and logs.
*/
isWrapped?: boolean;
/** The code snippet */
code?: string;
/** The language of the code snippet */
Expand Down Expand Up @@ -82,7 +102,7 @@ function isDiffCode(code: string): boolean {
}

function PrismCode(props: CubePrismCodeProps, ref) {
let { code = '', language = 'javascript', ...otherProps } = props;
let { code = '', language = 'javascript', isWrapped, ...otherProps } = props;

if (!code) {
code = '';
Expand All @@ -109,7 +129,7 @@ function PrismCode(props: CubePrismCodeProps, ref) {
}

return (
<PreElement ref={ref} {...otherProps}>
<PreElement ref={ref} {...otherProps} mods={{ wrapped: isWrapped }}>
<Highlight prism={Prism} code={code} language={grammarLang as any}>
{({ className, style, tokens, getLineProps, getTokenProps }) => {
return (
Expand Down
14 changes: 14 additions & 0 deletions src/components/content/PrismCode/__tests__/PrismCode.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,18 @@ describe('PrismCode component', () => {
expect(codeElement?.querySelector('.token.keyword')).toBeInTheDocument();
expect(codeElement?.querySelector('.token.string')).toBeInTheDocument();
});

test('sets the wrapped mod only when isWrapped is passed', () => {
const code = 'a very long single-line error message';

const { container: plain } = render(
<PrismCode code={code} language="bash" />,
);
const { container: wrapped } = render(
<PrismCode isWrapped code={code} language="bash" />,
);

expect(plain.querySelector('pre')).not.toHaveAttribute('data-wrapped');
expect(wrapped.querySelector('pre')).toHaveAttribute('data-wrapped');
});
});
Loading