From 7c2ffb5ddfee089e0739b8c18a1e9e57e20c836b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 13:17:50 +0000 Subject: [PATCH 1/6] fix(Alert): preserve line breaks in string text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render `\n` in string `text` and `title` with pre-wrap, and wrap long unbroken tokens. ReactNode content is unchanged. Co-authored-by: Fábio Neves --- .changeset/cui-335-alert-line-breaks.md | 7 +++ src/components/Alert/Alert.module.css | 10 ++++ src/components/Alert/Alert.stories.tsx | 39 +++++++++++++++ src/components/Alert/Alert.test.tsx | 46 +++++++++++++++++- src/components/Alert/Alert.tsx | 20 +++++++- src/components/Alert/Alert.types.ts | 2 + tests/display/alert.spec.ts | 3 ++ ...ong-unbroken-token-dark-chromium-linux.png | Bin 0 -> 8729 bytes ...ng-unbroken-token-light-chromium-linux.png | Bin 0 -> 8007 bytes ...ert-multiline-text-dark-chromium-linux.png | Bin 0 -> 10555 bytes ...rt-multiline-text-light-chromium-linux.png | Bin 0 -> 10120 bytes ...t-single-line-text-dark-chromium-linux.png | Bin 0 -> 3380 bytes ...-single-line-text-light-chromium-linux.png | Bin 0 -> 3293 bytes 13 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 .changeset/cui-335-alert-line-breaks.md create mode 100644 tests/display/alert.spec.ts-snapshots/alert-long-unbroken-token-dark-chromium-linux.png create mode 100644 tests/display/alert.spec.ts-snapshots/alert-long-unbroken-token-light-chromium-linux.png create mode 100644 tests/display/alert.spec.ts-snapshots/alert-multiline-text-dark-chromium-linux.png create mode 100644 tests/display/alert.spec.ts-snapshots/alert-multiline-text-light-chromium-linux.png create mode 100644 tests/display/alert.spec.ts-snapshots/alert-single-line-text-dark-chromium-linux.png create mode 100644 tests/display/alert.spec.ts-snapshots/alert-single-line-text-light-chromium-linux.png diff --git a/.changeset/cui-335-alert-line-breaks.md b/.changeset/cui-335-alert-line-breaks.md new file mode 100644 index 000000000..2ec751518 --- /dev/null +++ b/.changeset/cui-335-alert-line-breaks.md @@ -0,0 +1,7 @@ +--- +'@clickhouse/click-ui': patch +--- + +Preserve line breaks in string `Alert` `text` and `title`, and wrap long unbroken tokens inside the alert. + +Visual: string content uses `white-space: pre-wrap` and `overflow-wrap: anywhere`. Single-line alerts look the same. Multi-line strings now show each `\n` as a line break. ReactNode `text` and `title` are unchanged. diff --git a/src/components/Alert/Alert.module.css b/src/components/Alert/Alert.module.css index b66a44eaa..aeee5d906 100644 --- a/src/components/Alert/Alert.module.css +++ b/src/components/Alert/Alert.module.css @@ -142,6 +142,11 @@ margin: 0; } +.alert__title_pre-wrap { + white-space: pre-wrap; + overflow-wrap: anywhere; +} + .alert__title_size_small { font: var(--click-alert-small-typography-title-default); } @@ -154,6 +159,11 @@ margin: 0; } +.alert__text_pre-wrap { + white-space: pre-wrap; + overflow-wrap: anywhere; +} + .alert__text_size_small { font: var(--click-alert-small-typography-text-default); } diff --git a/src/components/Alert/Alert.stories.tsx b/src/components/Alert/Alert.stories.tsx index 74bece975..6a71f42fa 100644 --- a/src/components/Alert/Alert.stories.tsx +++ b/src/components/Alert/Alert.stories.tsx @@ -173,3 +173,42 @@ export const DismissibleBanner: Story = { dismissible: true, }, }; + +// Whitespace: string text/title keep `\n` and wrap long tokens. + +export const SingleLineText: Story = { + args: { + title: 'Query failed', + text: 'A single-line alert.', + state: 'warning', + size: 'small', + type: 'default', + }, +}; + +export const MultilineText: Story = { + args: { + title: 'Query failed\nCode: 62', + text: 'Syntax error: failed at position 15.\nExpected one of: token, identifier.\n\nSELECT * FROM events\nWHERE id = 1', + state: 'warning', + size: 'small', + type: 'default', + }, +}; + +export const LongUnbrokenToken: Story = { + decorators: [ + Story => ( + + + + ), + ], + args: { + title: 'Unknown identifier', + text: 'DB::Exception: Unknown table expression identifier default.extremely_long_table_name_without_any_spaces_that_should_wrap_inside_the_alert_instead_of_overflowing in scope.', + state: 'danger', + size: 'small', + type: 'default', + }, +}; diff --git a/src/components/Alert/Alert.test.tsx b/src/components/Alert/Alert.test.tsx index 44e55269c..0242f6929 100644 --- a/src/components/Alert/Alert.test.tsx +++ b/src/components/Alert/Alert.test.tsx @@ -1,11 +1,19 @@ -import { Alert, AlertProps } from '@/components/Alert'; +import { Alert, AlertProps, WarningAlert } from '@/components/Alert'; import { fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { renderCUI } from '@/utils/test-utils'; +import styles from './Alert.module.css'; describe('Alert', () => { const renderAlert = (props: AlertProps) => renderCUI(); + const withExactText = (value: string) => (_content: string, node: Element | null) => + Boolean( + node && + node.textContent === value && + Array.from(node.children).every(child => child.textContent !== value) + ); + it('renders the dismiss button with type="button" so it does not submit a form', () => { const handleSubmit = vi.fn(e => e.preventDefault()); const { getByTestId } = renderCUI( @@ -61,4 +69,40 @@ describe('Alert', () => { userEvent.click(dismissButton); await waitFor(() => expect(queryAllByText(text).length).toEqual(0)); }); + + it('preserves line breaks in string text and title', () => { + const { getByText } = renderAlert({ + title: 'Title line 1\nTitle line 2', + text: 'line 1\nline 2', + }); + + const title = getByText(withExactText('Title line 1\nTitle line 2')); + const body = getByText(withExactText('line 1\nline 2')); + + expect(title.tagName).toBe('H6'); + expect(title).toHaveClass(styles['alert__title_pre-wrap']); + expect(body).toHaveClass(styles['alert__text_pre-wrap']); + }); + + it('preserves line breaks on WarningAlert string text', () => { + const { getByText } = renderCUI(); + const body = getByText(withExactText('line 1\nline 2')); + + expect(body).toHaveClass(styles['alert__text_pre-wrap']); + }); + + it('does not apply pre-wrap to ReactNode text or title', () => { + const { getByText } = renderAlert({ + title: Title node, + text: {'line 1\nline 2'}, + }); + + const title = getByText('Title node'); + const body = getByText(withExactText('line 1\nline 2')); + + expect(title.tagName).toBe('SPAN'); + expect(title.parentElement).not.toHaveClass(styles['alert__title_pre-wrap']); + expect(body.tagName).toBe('SPAN'); + expect(body.parentElement).not.toHaveClass(styles['alert__text_pre-wrap']); + }); }); diff --git a/src/components/Alert/Alert.tsx b/src/components/Alert/Alert.tsx index eae28d68b..d31d8bdc8 100644 --- a/src/components/Alert/Alert.tsx +++ b/src/components/Alert/Alert.tsx @@ -150,8 +150,24 @@ export const Alert = ({ )}
- {title &&
{title}
} -
{text}
+ {title ? ( +
+ {title} +
+ ) : null} +
+ {text} +
{dismissible && (