From 149f88e95772c58554c26ebaa78bcdadb509ec7f Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Mon, 8 Jun 2026 15:07:58 +0200 Subject: [PATCH 1/8] [bulk-textarea] added cleanup in willUnmount for correct work in strict mode --- semcore/bulk-textarea/src/components/InputField/InputField.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/semcore/bulk-textarea/src/components/InputField/InputField.tsx b/semcore/bulk-textarea/src/components/InputField/InputField.tsx index 1aeaeed44e..160901d1d6 100644 --- a/semcore/bulk-textarea/src/components/InputField/InputField.tsx +++ b/semcore/bulk-textarea/src/components/InputField/InputField.tsx @@ -110,7 +110,7 @@ class InputField extends Component< } componentDidMount() { - const { autoFocus, disabled, readonly } = this.asProps; + const { autoFocus, disabled } = this.asProps; this.containerRef.current?.append(this.textarea); @@ -243,6 +243,7 @@ class InputField extends Component< componentWillUnmount() { this.removeEventListeners(this.textarea); + this.containerRef.current?.removeChild(this.textarea); this.observer.disconnect(); } From fb6bc9533d1354f2056a0bd590176b89a10b60c8 Mon Sep 17 00:00:00 2001 From: Valeryia Zimnitskaya Date: Tue, 9 Jun 2026 18:56:14 +0200 Subject: [PATCH 2/8] [chore] add storybook toggle for test strict mode --- .storybook/preview.tsx | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 48541478e6..f82209a5cc 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -51,10 +51,29 @@ const preview: Preview = { dynamicTitle: true, }, }, + strictMode: { + description: 'React StrictMode', + toolbar: { + title: 'StrictMode', + icon: 'circle', + items: [ + { + value: 'off', + title: 'StrictMode off', + }, + { + value: 'on', + title: 'StrictMode on', + }, + ], + dynamicTitle: true, + }, + }, }, initialGlobals: { theme: 'new', + strictMode: 'off', }, decorators: [ (Story, params) => { @@ -67,6 +86,14 @@ const preview: Preview = { ? 'assets/theme/highlights-light.css' : 'assets/core/highlights-light.css'; + const story = params.globals.strictMode === 'on' + ? ( + + + + ) + : ; + if (params.parameters.layout === 'fullscreen') { return ( <> @@ -74,7 +101,7 @@ const preview: Preview = {
- + {story}
@@ -89,7 +116,7 @@ const preview: Preview = {
- + {story}
From 34a2944d3658ce0643dc25ca08735a5e916afdc7 Mon Sep 17 00:00:00 2001 From: Valeryia Zimnitskaya Date: Tue, 9 Jun 2026 18:58:25 +0200 Subject: [PATCH 3/8] [chore] add storybook toggle for test strict mode --- .storybook/preview.tsx | 100 +++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 48 deletions(-) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index f82209a5cc..8f444b2553 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -4,6 +4,57 @@ import React from 'react'; // import '@semcore/theme/lib/highlights-light.css'; +type PreviewDecorator = NonNullable[number]; + +const withStrictMode: PreviewDecorator = (Story, params) => { + const rootRef = React.useRef(null); + const stylesheet = params.globals.theme === 'new' + ? 'assets/theme/light.css' + : 'assets/core/light.css'; + + const stylesheetHighlight = params.globals.theme === 'new' + ? 'assets/theme/highlights-light.css' + : 'assets/core/highlights-light.css'; + + const story = params.globals.strictMode === 'on' + ? ( + + + + ) + : ; + + if (params.parameters.layout === 'fullscreen') { + return ( + <> + + + +
+ {story} +
+
+ + ); + } + + return ( + <> + + +
+
+ +
+ {story} +
+
+
+
+ + ); +}; + const preview: Preview = { parameters: { options: { @@ -76,54 +127,7 @@ const preview: Preview = { strictMode: 'off', }, decorators: [ - (Story, params) => { - const rootRef = React.useRef(null); - const stylesheet = params.globals.theme === 'new' - ? 'assets/theme/light.css' - : 'assets/core/light.css'; - - const stylesheetHighlight = params.globals.theme === 'new' - ? 'assets/theme/highlights-light.css' - : 'assets/core/highlights-light.css'; - - const story = params.globals.strictMode === 'on' - ? ( - - - - ) - : ; - - if (params.parameters.layout === 'fullscreen') { - return ( - <> - - - -
- {story} -
-
- - ); - } - - return ( - <> - - -
-
- -
- {story} -
-
-
-
- - ); - }, + withStrictMode, ], }; From f6c3b1e5690dbbb3217b2ca6b638eed62f3d061b Mon Sep 17 00:00:00 2001 From: Valeryia Zimnitskaya Date: Tue, 9 Jun 2026 19:01:30 +0200 Subject: [PATCH 4/8] [chore] add storybook toggle for test strict mode --- .storybook/preview.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 8f444b2553..05f065605d 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -6,7 +6,22 @@ import React from 'react'; type PreviewDecorator = NonNullable[number]; -const withStrictMode: PreviewDecorator = (Story, params) => { +type StorybookStory = Parameters[0]; + +type StorybookDecoratorParams = Parameters[1] & { + globals: Record & { + theme?: 'new' | 'old'; + strictMode?: 'on' | 'off'; + }; + parameters: Record & { + layout?: string; + }; +}; + +const withStrictMode: PreviewDecorator = ( + Story: StorybookStory, + params: StorybookDecoratorParams, +) => { const rootRef = React.useRef(null); const stylesheet = params.globals.theme === 'new' ? 'assets/theme/light.css' From d7b602d9f3e5c153bae4aa4b612c45661da95617 Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Wed, 10 Jun 2026 06:51:02 +0200 Subject: [PATCH 5/8] [bulk-textarea] added textarea creation in didMount for correct work in strict mode --- .storybook/preview.tsx | 81 +++++++++++-------- .../src/components/InputField/InputField.tsx | 1 + 2 files changed, 49 insertions(+), 33 deletions(-) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 05f065605d..7e2acfdf76 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -2,8 +2,6 @@ import { PortalProvider } from '@semcore/base-components'; import type { Preview } from '@storybook/react-vite'; import React from 'react'; -// import '@semcore/theme/lib/highlights-light.css'; - type PreviewDecorator = NonNullable[number]; type StorybookStory = Parameters[0]; @@ -18,58 +16,72 @@ type StorybookDecoratorParams = Parameters[1] & { }; }; -const withStrictMode: PreviewDecorator = ( +const withStrictMode = ( Story: StorybookStory, params: StorybookDecoratorParams, ) => { - const rootRef = React.useRef(null); - const stylesheet = params.globals.theme === 'new' - ? 'assets/theme/light.css' - : 'assets/core/light.css'; - - const stylesheetHighlight = params.globals.theme === 'new' - ? 'assets/theme/highlights-light.css' - : 'assets/core/highlights-light.css'; - - const story = params.globals.strictMode === 'on' + return params.globals.strictMode === 'on' ? ( ) : ; +}; +const withLayout = ( + Story: StorybookStory, + params: StorybookDecoratorParams, +) => { if (params.parameters.layout === 'fullscreen') { - return ( - <> - - - -
- {story} -
-
- - ); + return ; } + return ( +
+
+ +
+
+ ); +}; + +const withTheme = ( + Story: StorybookStory, + params: StorybookDecoratorParams, +) => { + const stylesheet = params.globals.theme === 'new' + ? 'assets/theme/light.css' + : 'assets/core/light.css'; + + const stylesheetHighlight = params.globals.theme === 'new' + ? 'assets/theme/highlights-light.css' + : 'assets/core/highlights-light.css'; + return ( <> -
-
- -
- {story} -
-
-
-
+ ); }; +const withPortalProvider = ( + Story: StorybookStory, + params: StorybookDecoratorParams, +) => { + const rootRef = React.useRef(null); + + return ( + +
+ +
+
+ ); +}; + const preview: Preview = { parameters: { options: { @@ -143,6 +155,9 @@ const preview: Preview = { }, decorators: [ withStrictMode, + withTheme, + withLayout, + withPortalProvider, ], }; diff --git a/semcore/bulk-textarea/src/components/InputField/InputField.tsx b/semcore/bulk-textarea/src/components/InputField/InputField.tsx index 160901d1d6..35c29dcbd8 100644 --- a/semcore/bulk-textarea/src/components/InputField/InputField.tsx +++ b/semcore/bulk-textarea/src/components/InputField/InputField.tsx @@ -112,6 +112,7 @@ class InputField extends Component< componentDidMount() { const { autoFocus, disabled } = this.asProps; + this.textarea = this.createContentEditableElement(this.asProps); this.containerRef.current?.append(this.textarea); this.handleValueOutChange(); From a29ac3c2a845f008db98bb7c2807ef20c14d0161 Mon Sep 17 00:00:00 2001 From: Valeryia Zimnitskaya Date: Fri, 12 Jun 2026 12:27:11 +0200 Subject: [PATCH 6/8] [chore] udate test --- .../__tests__/bulk-textarea.browser-test.tsx | 57 +++++++++++++++++++ .../bulk-textarea/__tests__/index.test.tsx | 26 +++++++++ .../tests/bulk-textarea.stories.tsx | 1 + .../tests/examples/basic-props.tsx | 36 +++++++++++- 4 files changed, 118 insertions(+), 2 deletions(-) diff --git a/semcore/bulk-textarea/__tests__/bulk-textarea.browser-test.tsx b/semcore/bulk-textarea/__tests__/bulk-textarea.browser-test.tsx index 9cecfa83ee..9383242948 100644 --- a/semcore/bulk-textarea/__tests__/bulk-textarea.browser-test.tsx +++ b/semcore/bulk-textarea/__tests__/bulk-textarea.browser-test.tsx @@ -99,6 +99,63 @@ Keyboard and mouse interactions - no snapshots here. We verify states, visibility, and attributes. ===================================================== */ test.describe(`${TAG.FUNCTIONAL}`, () => { + test.describe('StrictMode', () => { + test('Verify single textbox after StrictMode remount', { + tag: [TAG.PRIORITY_HIGH, + '@bulk-textarea'], + }, async ({ page }) => { + const errors: string[] = []; + page.on('pageerror', (error) => errors.push(error.message)); + page.on('console', (message) => { + const text = message.text(); + if ( + message.type() === 'error' && + !text.includes('ReactDOM.render is no longer supported in React 18') + ) { + errors.push(text); + } + }); + + await loadPage( + page, + 'stories/components/bulk-textarea/tests/examples/basic-props.tsx', + 'en', + { maxLines: 15, strictMode: true }, + ); + + await expect(locators.textbox(page)).toBeVisible(); + await expect(locators.textbox(page)).toHaveCount(1); + await expect(locators.counter(page)).toHaveText('0/15of 15 lines'); + await expect.poll(() => errors).toHaveLength(0); + }); + + test('Verify editing states work in StrictMode', { + tag: [TAG.PRIORITY_HIGH, + TAG.KEYBOARD, + '@bulk-textarea'], + }, async ({ page }) => { + await loadPage( + page, + 'stories/components/bulk-textarea/tests/examples/basic-props.tsx', + 'en', + { autoFocus: true, maxLines: 15, strictMode: true }, + ); + + await expect(locators.textbox(page)).toBeFocused(); + await expect(locators.textbox(page)).toHaveCount(1); + + await page.keyboard.type('Testhttp://,test2', { delay: 10 }); + await expect(locators.counter(page)).toHaveText('2/15of 15 lines'); + await expect(locators.button(page, 'Clear all')).toBeVisible(); + + await locators.button(page, 'Clear all').click(); + await expect(locators.textbox(page)).toBeFocused(); + await expect(locators.textbox(page)).toHaveText(''); + await expect(locators.button(page, 'Clear all')).not.toBeVisible(); + await expect(locators.counter(page)).toHaveText('0/15of 15 lines'); + }); + }); + test.describe('Counter and Clear all', () => { test('Verify counter functionality', { tag: [TAG.PRIORITY_HIGH, diff --git a/semcore/bulk-textarea/__tests__/index.test.tsx b/semcore/bulk-textarea/__tests__/index.test.tsx index 89bd747cae..e4a5d93241 100644 --- a/semcore/bulk-textarea/__tests__/index.test.tsx +++ b/semcore/bulk-textarea/__tests__/index.test.tsx @@ -118,3 +118,29 @@ describe('BulkTextarea onImmediatelyChange', () => { expect(handleImmediatelyChange).toHaveBeenLastCalledWith(['O'], 'O'); }); }); + +describe('BulkTextarea StrictMode', () => { + beforeEach(() => { + cleanup(); + }); + + afterEach(() => { + cleanup(); + }); + + test('Verify textbox is mounted and cleaned up in StrictMode', () => { + const { getAllByRole, unmount } = render( + + { }}> + + + , + ); + + expect(getAllByRole('textbox')).toHaveLength(1); + + unmount(); + + expect(document.querySelectorAll('[role="textbox"]')).toHaveLength(0); + }); +}); diff --git a/stories/components/bulk-textarea/tests/bulk-textarea.stories.tsx b/stories/components/bulk-textarea/tests/bulk-textarea.stories.tsx index 7ab2425e8c..8ca3f2236a 100644 --- a/stories/components/bulk-textarea/tests/bulk-textarea.stories.tsx +++ b/stories/components/bulk-textarea/tests/bulk-textarea.stories.tsx @@ -44,6 +44,7 @@ const sharedArgTypes = { const basicPropsArgTypes = { ...sharedArgTypes, + strictMode: { control: { type: 'boolean' } }, pasteDelimiter: { control: { type: 'select' }, options: ['newline', 'comma', 'semicolon', 'space', 'undefined'], diff --git a/stories/components/bulk-textarea/tests/examples/basic-props.tsx b/stories/components/bulk-textarea/tests/examples/basic-props.tsx index e4793ee538..c7be833d26 100644 --- a/stories/components/bulk-textarea/tests/examples/basic-props.tsx +++ b/stories/components/bulk-textarea/tests/examples/basic-props.tsx @@ -5,6 +5,7 @@ import type { BulkTextareaProps, ErrorItem } from '@semcore/ui/bulk-textarea'; import Button from '@semcore/ui/button'; import { Text } from '@semcore/ui/typography'; import React from 'react'; +import { createRoot } from 'react-dom/client'; const validateRow = (line: string, lines: string[]) => { let isValid = true; @@ -55,6 +56,7 @@ type PasteProps = NonNullable['pasteProps']>; type ExampleProps = Omit, 'linesDelimiters'> & { autoFocus?: boolean; + strictMode?: boolean; w?: BoxProps['w']; pasteDelimiter?: PasteProps['delimiter']; pasteSkipEmptyLines?: boolean; @@ -75,15 +77,39 @@ export const defaultBulkTextareaProps: ExampleProps = { showErrors: undefined, validateOn: ['blur'], autoFocus: false, + strictMode: false, pasteDelimiter: pasteDelimiterOptions.newline, pasteSkipEmptyLines: true, pasteLineProcessing: pasteLineProcessingOptions['remove-http'], linesDelimiters: [...linesDelimiterOptions.comma], }; +const StrictModeRoot = ({ children }: { children: React.ReactNode }) => { + const hostRef = React.useRef(null); + const rootRef = React.useRef | null>(null); + + React.useLayoutEffect(() => { + if (!hostRef.current) return; + + rootRef.current = createRoot(hostRef.current); + + return () => { + rootRef.current?.unmount(); + rootRef.current = null; + }; + }, []); + + React.useLayoutEffect(() => { + rootRef.current?.render({children}); + }); + + return
; +}; + const Demo = (props: Partial) => { const { autoFocus, + strictMode, pasteDelimiter, pasteSkipEmptyLines, pasteLineProcessing, @@ -111,8 +137,8 @@ const Demo = (props: Partial) => { setShowErrors(true); }, [value]); - return ( - + const bulkTextarea = ( + <> ) => { + + ); + + return ( + + {strictMode ? {bulkTextarea} : bulkTextarea} ); }; From 907d6018442f9d6d0ce2678529f2ad4cb1efdb4e Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Fri, 12 Jun 2026 15:28:59 +0200 Subject: [PATCH 7/8] [bulk-textarea] hide clearAll after clear all --- semcore/bulk-textarea/src/BulkTextarea.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/semcore/bulk-textarea/src/BulkTextarea.tsx b/semcore/bulk-textarea/src/BulkTextarea.tsx index 20949ebd89..003e9a5272 100644 --- a/semcore/bulk-textarea/src/BulkTextarea.tsx +++ b/semcore/bulk-textarea/src/BulkTextarea.tsx @@ -240,7 +240,7 @@ class BulkTextareaRoot extends Component< handleClickClearAll = (e: Event) => { this.handlers.showErrors(false); this.handlers.errors([]); - this.setState({ errorIndex: -1 }); + this.setState({ errorIndex: -1, isEmptyText: true }); // @ts-ignore this.handlers.value('', e); this.handlers.state('normal'); From 0ad25abf11784d8f08f8444ff73184c0e06c6d3e Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Fri, 12 Jun 2026 15:31:10 +0200 Subject: [PATCH 8/8] [bulk-textarea] cleanup in tests --- .../__tests__/bulk-textarea.browser-test.tsx | 57 ------------------- .../bulk-textarea/__tests__/index.test.tsx | 26 --------- .../tests/examples/basic-props.tsx | 36 +----------- 3 files changed, 2 insertions(+), 117 deletions(-) diff --git a/semcore/bulk-textarea/__tests__/bulk-textarea.browser-test.tsx b/semcore/bulk-textarea/__tests__/bulk-textarea.browser-test.tsx index 9383242948..9cecfa83ee 100644 --- a/semcore/bulk-textarea/__tests__/bulk-textarea.browser-test.tsx +++ b/semcore/bulk-textarea/__tests__/bulk-textarea.browser-test.tsx @@ -99,63 +99,6 @@ Keyboard and mouse interactions - no snapshots here. We verify states, visibility, and attributes. ===================================================== */ test.describe(`${TAG.FUNCTIONAL}`, () => { - test.describe('StrictMode', () => { - test('Verify single textbox after StrictMode remount', { - tag: [TAG.PRIORITY_HIGH, - '@bulk-textarea'], - }, async ({ page }) => { - const errors: string[] = []; - page.on('pageerror', (error) => errors.push(error.message)); - page.on('console', (message) => { - const text = message.text(); - if ( - message.type() === 'error' && - !text.includes('ReactDOM.render is no longer supported in React 18') - ) { - errors.push(text); - } - }); - - await loadPage( - page, - 'stories/components/bulk-textarea/tests/examples/basic-props.tsx', - 'en', - { maxLines: 15, strictMode: true }, - ); - - await expect(locators.textbox(page)).toBeVisible(); - await expect(locators.textbox(page)).toHaveCount(1); - await expect(locators.counter(page)).toHaveText('0/15of 15 lines'); - await expect.poll(() => errors).toHaveLength(0); - }); - - test('Verify editing states work in StrictMode', { - tag: [TAG.PRIORITY_HIGH, - TAG.KEYBOARD, - '@bulk-textarea'], - }, async ({ page }) => { - await loadPage( - page, - 'stories/components/bulk-textarea/tests/examples/basic-props.tsx', - 'en', - { autoFocus: true, maxLines: 15, strictMode: true }, - ); - - await expect(locators.textbox(page)).toBeFocused(); - await expect(locators.textbox(page)).toHaveCount(1); - - await page.keyboard.type('Testhttp://,test2', { delay: 10 }); - await expect(locators.counter(page)).toHaveText('2/15of 15 lines'); - await expect(locators.button(page, 'Clear all')).toBeVisible(); - - await locators.button(page, 'Clear all').click(); - await expect(locators.textbox(page)).toBeFocused(); - await expect(locators.textbox(page)).toHaveText(''); - await expect(locators.button(page, 'Clear all')).not.toBeVisible(); - await expect(locators.counter(page)).toHaveText('0/15of 15 lines'); - }); - }); - test.describe('Counter and Clear all', () => { test('Verify counter functionality', { tag: [TAG.PRIORITY_HIGH, diff --git a/semcore/bulk-textarea/__tests__/index.test.tsx b/semcore/bulk-textarea/__tests__/index.test.tsx index e4a5d93241..89bd747cae 100644 --- a/semcore/bulk-textarea/__tests__/index.test.tsx +++ b/semcore/bulk-textarea/__tests__/index.test.tsx @@ -118,29 +118,3 @@ describe('BulkTextarea onImmediatelyChange', () => { expect(handleImmediatelyChange).toHaveBeenLastCalledWith(['O'], 'O'); }); }); - -describe('BulkTextarea StrictMode', () => { - beforeEach(() => { - cleanup(); - }); - - afterEach(() => { - cleanup(); - }); - - test('Verify textbox is mounted and cleaned up in StrictMode', () => { - const { getAllByRole, unmount } = render( - - { }}> - - - , - ); - - expect(getAllByRole('textbox')).toHaveLength(1); - - unmount(); - - expect(document.querySelectorAll('[role="textbox"]')).toHaveLength(0); - }); -}); diff --git a/stories/components/bulk-textarea/tests/examples/basic-props.tsx b/stories/components/bulk-textarea/tests/examples/basic-props.tsx index c7be833d26..e4793ee538 100644 --- a/stories/components/bulk-textarea/tests/examples/basic-props.tsx +++ b/stories/components/bulk-textarea/tests/examples/basic-props.tsx @@ -5,7 +5,6 @@ import type { BulkTextareaProps, ErrorItem } from '@semcore/ui/bulk-textarea'; import Button from '@semcore/ui/button'; import { Text } from '@semcore/ui/typography'; import React from 'react'; -import { createRoot } from 'react-dom/client'; const validateRow = (line: string, lines: string[]) => { let isValid = true; @@ -56,7 +55,6 @@ type PasteProps = NonNullable['pasteProps']>; type ExampleProps = Omit, 'linesDelimiters'> & { autoFocus?: boolean; - strictMode?: boolean; w?: BoxProps['w']; pasteDelimiter?: PasteProps['delimiter']; pasteSkipEmptyLines?: boolean; @@ -77,39 +75,15 @@ export const defaultBulkTextareaProps: ExampleProps = { showErrors: undefined, validateOn: ['blur'], autoFocus: false, - strictMode: false, pasteDelimiter: pasteDelimiterOptions.newline, pasteSkipEmptyLines: true, pasteLineProcessing: pasteLineProcessingOptions['remove-http'], linesDelimiters: [...linesDelimiterOptions.comma], }; -const StrictModeRoot = ({ children }: { children: React.ReactNode }) => { - const hostRef = React.useRef(null); - const rootRef = React.useRef | null>(null); - - React.useLayoutEffect(() => { - if (!hostRef.current) return; - - rootRef.current = createRoot(hostRef.current); - - return () => { - rootRef.current?.unmount(); - rootRef.current = null; - }; - }, []); - - React.useLayoutEffect(() => { - rootRef.current?.render({children}); - }); - - return
; -}; - const Demo = (props: Partial) => { const { autoFocus, - strictMode, pasteDelimiter, pasteSkipEmptyLines, pasteLineProcessing, @@ -137,8 +111,8 @@ const Demo = (props: Partial) => { setShowErrors(true); }, [value]); - const bulkTextarea = ( - <> + return ( + ) => { - - ); - - return ( - - {strictMode ? {bulkTextarea} : bulkTextarea} ); };