Skip to content

Commit a67cd76

Browse files
committed
IBX-10307: Form Control - Input Text
1 parent a461b87 commit a67cd76

File tree

6 files changed

+267
-0
lines changed

6 files changed

+267
-0
lines changed

packages/assets/src/scss/inputs/_input-text.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
.ids-input-text {
44
position: relative;
5+
width: fit-content;
56

67
&__actions {
78
position: absolute;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { action } from 'storybook/actions';
3+
4+
import { FormControlInputTextStateful } from './InputText';
5+
6+
const meta: Meta<typeof FormControlInputTextStateful> = {
7+
component: FormControlInputTextStateful,
8+
parameters: {
9+
layout: 'centered',
10+
},
11+
tags: ['autodocs', 'foundation', 'inputs'],
12+
argTypes: {
13+
className: {
14+
control: 'text',
15+
},
16+
title: {
17+
control: 'text',
18+
},
19+
value: {
20+
control: 'text',
21+
},
22+
},
23+
args: {
24+
id: 'default-input',
25+
name: 'default-input',
26+
onChange: action('on-change'),
27+
onValidate: action('on-validate'),
28+
},
29+
};
30+
31+
export default meta;
32+
33+
type Story = StoryObj<typeof FormControlInputTextStateful>;
34+
35+
export const Default: Story = {
36+
name: 'Default',
37+
args: {
38+
helperText: 'This is a helper text',
39+
label: 'Input Label',
40+
input: {
41+
size: 'medium',
42+
type: 'text',
43+
},
44+
},
45+
};
46+
47+
export const Required: Story = {
48+
name: 'Required',
49+
args: {
50+
helperText: 'This is a helper text',
51+
label: 'Input Label',
52+
input: {
53+
size: 'medium',
54+
required: true,
55+
type: 'text',
56+
},
57+
},
58+
};
59+
60+
export const Small: Story = {
61+
name: 'Small',
62+
args: {
63+
helperText: 'This is a helper text',
64+
label: 'Input Label',
65+
input: {
66+
size: 'small',
67+
type: 'text',
68+
},
69+
},
70+
};
71+
72+
export const Number: Story = {
73+
name: 'Number',
74+
args: {
75+
helperText: 'This is a helper text',
76+
label: 'Input Label',
77+
input: {
78+
size: 'medium',
79+
type: 'number',
80+
},
81+
},
82+
};
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { expect, fn, userEvent, within } from 'storybook/test';
3+
4+
import { FormControlInputTextStateful } from './InputText';
5+
6+
const meta: Meta<typeof FormControlInputTextStateful> = {
7+
component: FormControlInputTextStateful,
8+
parameters: {
9+
layout: 'centered',
10+
},
11+
tags: ['!dev'],
12+
args: {
13+
name: 'default-input',
14+
onChange: fn(),
15+
onValidate: fn(),
16+
},
17+
};
18+
19+
export default meta;
20+
21+
type Story = StoryObj<typeof FormControlInputTextStateful>;
22+
23+
export const NotRequired: Story = {
24+
name: 'Not required',
25+
play: async ({ canvasElement, step, args }) => {
26+
const canvas = within(canvasElement);
27+
const input = canvas.getByRole('textbox');
28+
29+
await step('InputText handles change event', async () => {
30+
const insertText = 'Lorem Ipsum';
31+
const insertTextLength = insertText.length;
32+
33+
await userEvent.type(input, insertText);
34+
35+
await expect(args.onChange).toHaveBeenCalledTimes(insertTextLength);
36+
await expect(input).toHaveValue(insertText);
37+
await expect(args.onValidate).toHaveBeenCalledWith(true, []);
38+
});
39+
40+
const clearBtn = canvas.getByRole('button');
41+
42+
await step('InputText handles clear event', async () => {
43+
await userEvent.click(clearBtn);
44+
45+
await expect(args.onChange).toHaveBeenLastCalledWith('');
46+
await expect(input).toHaveValue('');
47+
await expect(args.onValidate).toHaveBeenCalledWith(true, []);
48+
});
49+
},
50+
};
51+
52+
export const Required: Story = {
53+
name: 'Required',
54+
args: {
55+
input: {
56+
required: true,
57+
},
58+
},
59+
play: async ({ canvasElement, step, args }) => {
60+
const canvas = within(canvasElement);
61+
const input = canvas.getByRole('textbox');
62+
63+
await step('InputText handles change event', async () => {
64+
const insertText = 'Lorem Ipsum';
65+
const insertTextLength = insertText.length;
66+
67+
await userEvent.type(input, insertText);
68+
69+
await expect(args.onChange).toHaveBeenCalledTimes(insertTextLength);
70+
await expect(input).toHaveValue(insertText);
71+
await expect(args.onValidate).toHaveBeenCalledWith(true, []);
72+
await expect(input).toHaveAttribute('aria-invalid', 'false');
73+
});
74+
75+
const clearBtn = canvas.getByRole('button');
76+
77+
await step('InputText handles clear event', async () => {
78+
await userEvent.click(clearBtn);
79+
80+
await expect(args.onChange).toHaveBeenLastCalledWith('');
81+
await expect(input).toHaveValue('');
82+
await expect(args.onValidate).toHaveBeenLastCalledWith(false, expect.anything());
83+
await expect(input).toHaveAttribute('aria-invalid', 'true');
84+
});
85+
},
86+
};
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React, { useContext, useEffect, useMemo } from 'react';
2+
3+
import BaseFormControl from '@ids-internal/partials/BaseFormControl';
4+
import InputText from '../../inputs/InputText';
5+
import IsEmptyStringValidator from '@ibexa/ids-core/validators/IsEmptyStringValidator';
6+
import { TranslatorContext } from '@ids-context/Translator';
7+
import { validateInput } from '@ids-internal/shared/validators';
8+
import withStateValue from '@ids-internal/hoc/withStateValue';
9+
10+
import { FormControlInputTextProps } from './InputText.types';
11+
import { ValidationResult } from '@ibexa/ids-core/types/validation';
12+
13+
const FormControlInputText = ({
14+
helperText,
15+
helperTextExtra = {},
16+
id,
17+
input = {},
18+
label,
19+
labelExtra = {},
20+
name,
21+
onChange = () => undefined,
22+
onValidate = () => undefined,
23+
value = '',
24+
}: FormControlInputTextProps) => {
25+
const translator = useContext(TranslatorContext);
26+
const required = input.required ?? false;
27+
const validators = useMemo(() => {
28+
const validatorsList = [];
29+
30+
if (required) {
31+
validatorsList.push(new IsEmptyStringValidator(translator));
32+
}
33+
34+
return validatorsList;
35+
}, [required, translator]);
36+
const { isValid, messages } = useMemo<ValidationResult>(
37+
() => validateInput<string | number>(value, validators),
38+
[value, validators],
39+
);
40+
const helperTextProps = {
41+
children: isValid ? helperText : messages.join(', '),
42+
type: isValid ? ('default' as const) : ('error' as const),
43+
...helperTextExtra,
44+
};
45+
const labelProps = {
46+
children: label,
47+
error: !isValid,
48+
htmlFor: id,
49+
required,
50+
...labelExtra,
51+
};
52+
const inputProps = {
53+
...input,
54+
error: !isValid,
55+
id,
56+
name,
57+
onChange,
58+
value,
59+
};
60+
61+
useEffect(() => {
62+
onValidate(isValid, messages);
63+
}, [isValid, messages, onValidate]);
64+
65+
return (
66+
<BaseFormControl helperText={helperTextProps} label={labelProps} type="input-text">
67+
<InputText {...inputProps} />
68+
</BaseFormControl>
69+
);
70+
};
71+
72+
export default FormControlInputText;
73+
74+
export const FormControlInputTextStateful = withStateValue<string | number>(FormControlInputText);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { BaseComponentAttributes } from '@ids-types/general';
2+
3+
import { InputTextProps as BasicInputTextProps } from '../../inputs/InputText/InputText.types';
4+
import { HelperTextProps } from '../../HelperText/HelperText.types';
5+
import { LabelProps } from '../../Label/Label.types';
6+
7+
export interface FormControlInputTextProps extends BaseComponentAttributes {
8+
id: string;
9+
name: BasicInputTextProps['name'];
10+
input?: Omit<BasicInputTextProps, 'error' | 'name' | 'onChange' | 'value'>;
11+
helperText?: HelperTextProps['children'];
12+
helperTextExtra?: Omit<HelperTextProps, 'children' | 'type'>;
13+
label?: LabelProps['children'];
14+
labelExtra?: Omit<LabelProps, 'children' | 'error' | 'htmlFor' | 'required'>;
15+
onChange?: BasicInputTextProps['onChange'];
16+
onValidate?: (isValid: boolean, messages: string[]) => void;
17+
value?: BasicInputTextProps['value'];
18+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import FormControlInputText, { FormControlInputTextStateful } from './InputText';
2+
import { FormControlInputTextProps } from './InputText.types';
3+
4+
export default FormControlInputText;
5+
export { FormControlInputTextStateful };
6+
export type { FormControlInputTextProps };

0 commit comments

Comments
 (0)