From 895325915fe798e474f58095617f83daa0aac742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Thu, 7 Aug 2025 16:20:45 +0200 Subject: [PATCH 1/3] IBX-10469: Stateful Inputs HOC --- .../inputs/InputText/InputText.stories.tsx | 32 +++--------------- .../InputText/InputText.test.stories.tsx | 17 +++++++--- .../src/inputs/InputText/InputText.tsx | 7 ++-- .../components/src/inputs/InputText/index.ts | 3 +- .../src/internal/hoc/withStateValue.tsx | 33 +++++++++++++++++++ 5 files changed, 57 insertions(+), 35 deletions(-) create mode 100644 packages/components/src/internal/hoc/withStateValue.tsx diff --git a/packages/components/src/inputs/InputText/InputText.stories.tsx b/packages/components/src/inputs/InputText/InputText.stories.tsx index c6cf951e..09b5995d 100644 --- a/packages/components/src/inputs/InputText/InputText.stories.tsx +++ b/packages/components/src/inputs/InputText/InputText.stories.tsx @@ -1,13 +1,11 @@ -import React, { useState } from 'react'; - import type { Meta, StoryObj } from '@storybook/react'; import { action } from 'storybook/actions'; import { INPUT_SIZE_VALUES, INPUT_TYPE_VALUES } from './InputText.types'; -import InputText from './InputText'; +import { InputTextStateful } from './InputText'; -const meta: Meta = { - component: InputText, +const meta: Meta = { + component: InputTextStateful, parameters: { layout: 'centered', }, @@ -37,33 +35,11 @@ const meta: Meta = { onFocus: action('on-focus'), onInput: action('on-input'), }, - decorators: [ - (Story, { args }) => { - const [value, setValue] = useState(args.value ?? ''); - const onChange = (changedValue: string, event?: React.ChangeEvent) => { - setValue(changedValue); - - if (args.onChange) { - args.onChange(changedValue, event); - } - }; - - return ( - - ); - }, - ], }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Empty: Story = { name: 'Empty', diff --git a/packages/components/src/inputs/InputText/InputText.test.stories.tsx b/packages/components/src/inputs/InputText/InputText.test.stories.tsx index 31a09891..74101ea7 100644 --- a/packages/components/src/inputs/InputText/InputText.test.stories.tsx +++ b/packages/components/src/inputs/InputText/InputText.test.stories.tsx @@ -1,10 +1,10 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect, fn, userEvent, within } from 'storybook/test'; -import InputText from './InputText'; +import { InputTextStateful } from './InputText'; -const meta: Meta = { - component: InputText, +const meta: Meta = { + component: InputTextStateful, parameters: { layout: 'centered', }, @@ -20,7 +20,7 @@ const meta: Meta = { export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { name: 'Default', @@ -59,5 +59,14 @@ export const Default: Story = { await expect(args.onChange).toHaveBeenCalledTimes(insertTextLength); await expect(args.onInput).toHaveBeenCalledTimes(insertTextLength); }); + + const clearBtn = canvas.getByRole('button'); + + await step('InputText handles clear event', async () => { + await userEvent.click(clearBtn); + + await expect(args.onChange).toHaveBeenLastCalledWith(''); + await expect(input).toHaveValue(''); + }); }, }; diff --git a/packages/components/src/inputs/InputText/InputText.tsx b/packages/components/src/inputs/InputText/InputText.tsx index a6f2d4c2..48ef7fd0 100644 --- a/packages/components/src/inputs/InputText/InputText.tsx +++ b/packages/components/src/inputs/InputText/InputText.tsx @@ -3,11 +3,12 @@ import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; import BaseInput from '@ids-internal/partials/BaseInput'; import ClearBtn from '../../ui/ClearBtn'; import { createCssClassNames } from '@ids-internal/shared/css.class.names'; +import withStateValue from '@ids-internal/hoc/withStateValue'; import { ComponentEntryDataType } from '@ids-types/general'; import { InputTextProps } from './InputText.types'; -const Input = ({ +const InputText = ({ name, onBlur = () => undefined, onChange = () => undefined, @@ -116,4 +117,6 @@ const Input = ({ ); }; -export default Input; +export default InputText; + +export const InputTextStateful = withStateValue(InputText); diff --git a/packages/components/src/inputs/InputText/index.ts b/packages/components/src/inputs/InputText/index.ts index df97352e..d4ccdb82 100644 --- a/packages/components/src/inputs/InputText/index.ts +++ b/packages/components/src/inputs/InputText/index.ts @@ -1,5 +1,6 @@ -import InputText from './InputText'; +import InputText, { InputTextStateful } from './InputText'; import { InputTextProps } from './InputText.types'; export default InputText; +export { InputTextStateful }; export type { InputTextProps }; diff --git a/packages/components/src/internal/hoc/withStateValue.tsx b/packages/components/src/internal/hoc/withStateValue.tsx new file mode 100644 index 00000000..cf795a34 --- /dev/null +++ b/packages/components/src/internal/hoc/withStateValue.tsx @@ -0,0 +1,33 @@ +import React, { FC, useState } from 'react'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +interface WrappedComponentProps { + onChange?: (value: string, ...restArgs: any[]) => void; + value: any; + [key: string]: any; +} + +export default (WrappedComponent: FC) => { + const WrapperComponent = ({ value, onChange, ...restProps }: WrappedComponentProps) => { + const [componentValue, setComponentValue] = useState(value); // eslint-disable-line @typescript-eslint/no-unsafe-assignment + + const handleChange = (newValue: string, ...restArgs: any[]) => { + setComponentValue(newValue); + + if (onChange) { + onChange(newValue, ...restArgs); // eslint-disable-line @typescript-eslint/no-unsafe-argument + } + }; + + return ( + + ); + }; + + return WrapperComponent; +}; +/* eslint-enable @typescript-eslint/no-explicit-any */ From 31b8ddf4e5a73a82e7c9090c9927fd1510936ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Mon, 18 Aug 2025 13:43:14 +0200 Subject: [PATCH 2/3] copilot suggestion fixes --- packages/components/src/internal/hoc/withStateValue.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/components/src/internal/hoc/withStateValue.tsx b/packages/components/src/internal/hoc/withStateValue.tsx index cf795a34..840eadfd 100644 --- a/packages/components/src/internal/hoc/withStateValue.tsx +++ b/packages/components/src/internal/hoc/withStateValue.tsx @@ -28,6 +28,8 @@ export default (WrappedComponent: FC) => { ); }; + WrapperComponent.displayName = `withStateValue(${WrappedComponent.displayName ?? WrappedComponent.name})`; + return WrapperComponent; }; /* eslint-enable @typescript-eslint/no-explicit-any */ From b0e73f0ab8cdb1a2710223b168c42c9df6bb1103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Tue, 19 Aug 2025 13:24:14 +0200 Subject: [PATCH 3/3] added generic type --- .../src/inputs/InputText/InputText.tsx | 2 +- .../src/internal/hoc/withStateValue.tsx | 32 ++++++++----------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/packages/components/src/inputs/InputText/InputText.tsx b/packages/components/src/inputs/InputText/InputText.tsx index 48ef7fd0..dfc92b9b 100644 --- a/packages/components/src/inputs/InputText/InputText.tsx +++ b/packages/components/src/inputs/InputText/InputText.tsx @@ -119,4 +119,4 @@ const InputText = ({ export default InputText; -export const InputTextStateful = withStateValue(InputText); +export const InputTextStateful = withStateValue(InputText); diff --git a/packages/components/src/internal/hoc/withStateValue.tsx b/packages/components/src/internal/hoc/withStateValue.tsx index 840eadfd..c9c45cac 100644 --- a/packages/components/src/internal/hoc/withStateValue.tsx +++ b/packages/components/src/internal/hoc/withStateValue.tsx @@ -1,35 +1,29 @@ import React, { FC, useState } from 'react'; -/* eslint-disable @typescript-eslint/no-explicit-any */ -interface WrappedComponentProps { - onChange?: (value: string, ...restArgs: any[]) => void; - value: any; - [key: string]: any; +type OnChangeFn = (value: T, ...args: any[]) => any; // eslint-disable-line @typescript-eslint/no-explicit-any +interface WrappedComponentProps { + onChange?: OnChangeFn; + value: T; + [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any } -export default (WrappedComponent: FC) => { - const WrapperComponent = ({ value, onChange, ...restProps }: WrappedComponentProps) => { - const [componentValue, setComponentValue] = useState(value); // eslint-disable-line @typescript-eslint/no-unsafe-assignment +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default (WrappedComponent: FC) => { + const WrapperComponent = ({ value, onChange, ...restProps }: WrappedComponentProps) => { + const [componentValue, setComponentValue] = useState(value); - const handleChange = (newValue: string, ...restArgs: any[]) => { - setComponentValue(newValue); + const handleChange = (...args: Parameters>): ReturnType> => { + setComponentValue(args[0]); if (onChange) { - onChange(newValue, ...restArgs); // eslint-disable-line @typescript-eslint/no-unsafe-argument + onChange(...args); } }; - return ( - - ); + return ; }; WrapperComponent.displayName = `withStateValue(${WrappedComponent.displayName ?? WrappedComponent.name})`; return WrapperComponent; }; -/* eslint-enable @typescript-eslint/no-explicit-any */