@indico-data/design-system 2.33.1 → 2.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/preview-head.html +0 -4
- package/lib/index.css +95 -0
- package/lib/index.d.ts +8 -8
- package/lib/index.esm.css +95 -0
- package/lib/index.esm.js +1 -1
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/src/components/forms/input/Input.d.ts +5 -3
- package/lib/src/components/forms/numberInput/NumberInput.d.ts +11 -0
- package/lib/src/components/forms/numberInput/NumberInput.stories.d.ts +12 -0
- package/lib/src/components/forms/numberInput/__tests__/NumberInput.test.d.ts +1 -0
- package/lib/src/components/forms/numberInput/index.d.ts +1 -0
- package/lib/src/components/forms/passwordInput/PasswordInput.d.ts +3 -3
- package/lib/src/components/forms/subcomponents/Label.d.ts +1 -3
- package/lib/src/components/forms/textarea/Textarea.d.ts +3 -3
- package/lib/src/storybook/formArgTypes.d.ts +5 -0
- package/package.json +1 -1
- package/src/components/forms/input/Input.stories.tsx +2 -96
- package/src/components/forms/input/Input.tsx +5 -3
- package/src/components/forms/numberInput/NumberInput.mdx +32 -0
- package/src/components/forms/numberInput/NumberInput.stories.tsx +215 -0
- package/src/components/forms/numberInput/NumberInput.tsx +90 -0
- package/src/components/forms/numberInput/__tests__/NumberInput.test.tsx +94 -0
- package/src/components/forms/numberInput/index.ts +1 -0
- package/src/components/forms/numberInput/styles/NumberInput.scss +108 -0
- package/src/components/forms/passwordInput/PasswordInput.stories.tsx +3 -70
- package/src/components/forms/passwordInput/PasswordInput.tsx +2 -2
- package/src/components/forms/subcomponents/Label.tsx +1 -4
- package/src/components/forms/textarea/Textarea.stories.tsx +3 -68
- package/src/components/forms/textarea/Textarea.tsx +2 -2
- package/src/storybook/formArgTypes.ts +152 -0
- package/src/styles/index.scss +1 -0
- package/lib/src/storybook/labelArgTypes.d.ts +0 -3
- package/src/storybook/labelArgTypes.ts +0 -50
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { IconName } from '@/types';
|
|
3
|
-
import {
|
|
4
|
-
export interface
|
|
3
|
+
import { LabelProps } from '../subcomponents/Label';
|
|
4
|
+
export interface BaseInputProps {
|
|
5
5
|
value?: string | undefined;
|
|
6
6
|
placeholder?: string;
|
|
7
7
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
@@ -13,5 +13,7 @@ export interface InputProps extends WithLabelProps {
|
|
|
13
13
|
className?: string;
|
|
14
14
|
defaultValue?: string;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
export interface InputProps extends BaseInputProps, LabelProps {
|
|
17
|
+
}
|
|
18
|
+
declare const LabeledInput: React.ForwardRefExoticComponent<Omit<InputProps & React.RefAttributes<HTMLInputElement> & LabelProps, "ref"> & React.RefAttributes<any>>;
|
|
17
19
|
export { LabeledInput as Input };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { LabelProps } from '../subcomponents/Label';
|
|
3
|
+
import { BaseInputProps } from '../input/Input';
|
|
4
|
+
export interface NumberInputProps extends Omit<BaseInputProps, 'value'>, LabelProps {
|
|
5
|
+
value?: number | '';
|
|
6
|
+
min?: number;
|
|
7
|
+
max?: number;
|
|
8
|
+
step?: number;
|
|
9
|
+
}
|
|
10
|
+
declare const LabeledNumberInput: React.ForwardRefExoticComponent<Omit<NumberInputProps & React.RefAttributes<HTMLInputElement> & LabelProps, "ref"> & React.RefAttributes<any>>;
|
|
11
|
+
export { LabeledNumberInput as NumberInput };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { NumberInput } from './NumberInput';
|
|
3
|
+
declare const meta: Meta;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof NumberInput>;
|
|
6
|
+
export declare const Default: Story;
|
|
7
|
+
export declare const Errors: Story;
|
|
8
|
+
export declare const HiddenLabel: Story;
|
|
9
|
+
export declare const HelpText: Story;
|
|
10
|
+
export declare const Clearable: Story;
|
|
11
|
+
export declare const Icon: Story;
|
|
12
|
+
export declare const Required: Story;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { NumberInput } from './NumberInput';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
export interface PasswordInputProps extends
|
|
2
|
+
import { LabelProps } from '../subcomponents/Label';
|
|
3
|
+
export interface PasswordInputProps extends LabelProps {
|
|
4
4
|
ref?: React.LegacyRef<HTMLInputElement>;
|
|
5
5
|
value?: string | undefined;
|
|
6
6
|
placeholder?: string;
|
|
@@ -11,5 +11,5 @@ export interface PasswordInputProps extends WithLabelProps {
|
|
|
11
11
|
hasShowPassword?: boolean;
|
|
12
12
|
defaultValue?: string;
|
|
13
13
|
}
|
|
14
|
-
declare const LabeledPasswordInput: React.ForwardRefExoticComponent<Omit<Omit<PasswordInputProps, "ref"> & React.RefAttributes<HTMLInputElement> &
|
|
14
|
+
declare const LabeledPasswordInput: React.ForwardRefExoticComponent<Omit<Omit<PasswordInputProps, "ref"> & React.RefAttributes<HTMLInputElement> & LabelProps, "ref"> & React.RefAttributes<any>>;
|
|
15
15
|
export { LabeledPasswordInput as PasswordInput };
|
|
@@ -3,9 +3,7 @@ export interface LabelProps {
|
|
|
3
3
|
label: string;
|
|
4
4
|
name: string;
|
|
5
5
|
isRequired?: boolean;
|
|
6
|
-
}
|
|
7
|
-
export interface WithLabelProps extends LabelProps {
|
|
8
6
|
hasHiddenLabel?: boolean;
|
|
9
7
|
}
|
|
10
8
|
export declare const Label: ({ label, name, isRequired }: LabelProps) => import("react/jsx-runtime").JSX.Element;
|
|
11
|
-
export declare function withLabel<P extends object>(WrappedComponent: React.ComponentType<P>): React.ForwardRefExoticComponent<React.PropsWithoutRef<P &
|
|
9
|
+
export declare function withLabel<P extends object>(WrappedComponent: React.ComponentType<P>): React.ForwardRefExoticComponent<React.PropsWithoutRef<P & LabelProps> & React.RefAttributes<any>>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
export interface TextareaProps extends
|
|
2
|
+
import { LabelProps } from '../subcomponents/Label';
|
|
3
|
+
export interface TextareaProps extends LabelProps {
|
|
4
4
|
ref?: React.LegacyRef<HTMLTextAreaElement>;
|
|
5
5
|
placeholder?: string;
|
|
6
6
|
value?: string | undefined;
|
|
@@ -17,5 +17,5 @@ export interface TextareaProps extends WithLabelProps {
|
|
|
17
17
|
autofocus?: boolean;
|
|
18
18
|
defaultValue?: string;
|
|
19
19
|
}
|
|
20
|
-
declare const LabeledTextarea: React.ForwardRefExoticComponent<Omit<Omit<TextareaProps, "ref"> & React.RefAttributes<HTMLTextAreaElement> &
|
|
20
|
+
declare const LabeledTextarea: React.ForwardRefExoticComponent<Omit<Omit<TextareaProps, "ref"> & React.RefAttributes<HTMLTextAreaElement> & LabelProps, "ref"> & React.RefAttributes<any>>;
|
|
21
21
|
export { LabeledTextarea as Textarea };
|
package/package.json
CHANGED
|
@@ -1,107 +1,13 @@
|
|
|
1
1
|
import { Meta, StoryObj } from '@storybook/react';
|
|
2
2
|
import { Input, InputProps } from './Input';
|
|
3
3
|
import { SetStateAction, useEffect, useState } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
import labelArgTypes from '@/storybook/labelArgTypes';
|
|
4
|
+
import { inputArgTypes, labelArgTypes } from '@/storybook/formArgTypes';
|
|
6
5
|
|
|
7
6
|
const meta: Meta = {
|
|
8
7
|
title: 'Forms/Input',
|
|
9
8
|
component: Input,
|
|
10
9
|
argTypes: {
|
|
11
|
-
|
|
12
|
-
control: false,
|
|
13
|
-
description: 'onChange event handler',
|
|
14
|
-
table: {
|
|
15
|
-
category: 'Callbacks',
|
|
16
|
-
type: {
|
|
17
|
-
summary: '(e: React.ChangeEvent<HTMLInputElement>) => void',
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
type: { name: 'function', required: true },
|
|
21
|
-
action: 'onChange',
|
|
22
|
-
},
|
|
23
|
-
placeholder: {
|
|
24
|
-
control: 'text',
|
|
25
|
-
description: 'The placeholder for the input field',
|
|
26
|
-
table: {
|
|
27
|
-
category: 'Props',
|
|
28
|
-
type: {
|
|
29
|
-
summary: 'string',
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
type: { name: 'string', required: false },
|
|
33
|
-
},
|
|
34
|
-
value: {
|
|
35
|
-
control: 'text',
|
|
36
|
-
description: 'The value for the input field',
|
|
37
|
-
table: {
|
|
38
|
-
category: 'Props',
|
|
39
|
-
type: {
|
|
40
|
-
summary: 'string',
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
type: { name: 'string', required: false },
|
|
44
|
-
},
|
|
45
|
-
isDisabled: {
|
|
46
|
-
control: 'boolean',
|
|
47
|
-
description: 'Toggles the disabled state of the input',
|
|
48
|
-
table: {
|
|
49
|
-
category: 'Props',
|
|
50
|
-
type: {
|
|
51
|
-
summary: 'boolean',
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
defaultValue: { summary: 'false' },
|
|
55
|
-
},
|
|
56
|
-
errorMessage: {
|
|
57
|
-
control: 'text',
|
|
58
|
-
description: 'Error message',
|
|
59
|
-
table: {
|
|
60
|
-
category: 'Props',
|
|
61
|
-
type: {
|
|
62
|
-
summary: 'string',
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
defaultValue: { summary: '' },
|
|
66
|
-
},
|
|
67
|
-
helpText: {
|
|
68
|
-
control: 'text',
|
|
69
|
-
description: 'The help text for the input field',
|
|
70
|
-
table: {
|
|
71
|
-
category: 'Props',
|
|
72
|
-
type: {
|
|
73
|
-
summary: 'string',
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
iconName: {
|
|
78
|
-
control: 'select',
|
|
79
|
-
options: iconNames,
|
|
80
|
-
description: 'Adds an icon to the left hand side of the input field',
|
|
81
|
-
table: {
|
|
82
|
-
category: 'Props',
|
|
83
|
-
type: {
|
|
84
|
-
summary: 'string',
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
defaultValue: { summary: '' },
|
|
88
|
-
},
|
|
89
|
-
isClearable: {
|
|
90
|
-
control: 'boolean',
|
|
91
|
-
description: 'Adds a clear x icon to the right hand side of the input field',
|
|
92
|
-
table: {
|
|
93
|
-
category: 'Props',
|
|
94
|
-
type: {
|
|
95
|
-
summary: 'boolean',
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
defaultValue: { summary: 'false' },
|
|
99
|
-
},
|
|
100
|
-
ref: {
|
|
101
|
-
table: {
|
|
102
|
-
disable: true,
|
|
103
|
-
},
|
|
104
|
-
},
|
|
10
|
+
...inputArgTypes,
|
|
105
11
|
...labelArgTypes,
|
|
106
12
|
},
|
|
107
13
|
};
|
|
@@ -3,10 +3,10 @@ import classNames from 'classnames';
|
|
|
3
3
|
|
|
4
4
|
import { Icon } from '@/components/icons';
|
|
5
5
|
import { IconName } from '@/types';
|
|
6
|
-
import { withLabel,
|
|
6
|
+
import { withLabel, LabelProps } from '../subcomponents/Label';
|
|
7
7
|
import { DisplayFormError } from '../subcomponents/DisplayFormError';
|
|
8
8
|
|
|
9
|
-
export interface
|
|
9
|
+
export interface BaseInputProps {
|
|
10
10
|
value?: string | undefined;
|
|
11
11
|
placeholder?: string;
|
|
12
12
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
@@ -19,6 +19,8 @@ export interface InputProps extends WithLabelProps {
|
|
|
19
19
|
defaultValue?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export interface InputProps extends BaseInputProps, LabelProps {}
|
|
23
|
+
|
|
22
24
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
23
25
|
(
|
|
24
26
|
{
|
|
@@ -70,7 +72,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
70
72
|
aria-required={isRequired}
|
|
71
73
|
{...rest}
|
|
72
74
|
/>
|
|
73
|
-
{isClearable && (
|
|
75
|
+
{isClearable && !isDisabled && (
|
|
74
76
|
<Icon
|
|
75
77
|
name="x-close"
|
|
76
78
|
data-testid={`${name}-clearable-icon`}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Canvas, Meta, Controls } from '@storybook/blocks';
|
|
2
|
+
import * as NumberInput from './NumberInput.stories';
|
|
3
|
+
|
|
4
|
+
<Meta title="Forms/Number Input" name="Number Input" />
|
|
5
|
+
|
|
6
|
+
# Number Input
|
|
7
|
+
|
|
8
|
+
The `NumberInput` component is a specialized input field designed to handle numeric values. It supports features such as min and max constraints, step increments, and optional icons or clear buttons. This component is intended to be used within forms where numeric input is required.
|
|
9
|
+
|
|
10
|
+
<Canvas
|
|
11
|
+
of={NumberInput.Default}
|
|
12
|
+
source={{
|
|
13
|
+
code: `
|
|
14
|
+
<NumberInput
|
|
15
|
+
label="Enter a number"
|
|
16
|
+
name="number_input"
|
|
17
|
+
placeholder="Please enter a value"
|
|
18
|
+
helpText="This Is Help Text"
|
|
19
|
+
isRequired
|
|
20
|
+
hasHiddenLabel={false}
|
|
21
|
+
isClearable
|
|
22
|
+
iconName="fa-calculator"
|
|
23
|
+
isDisabled={false}
|
|
24
|
+
errorMessage=""
|
|
25
|
+
value={0}
|
|
26
|
+
onChange={() => {}}
|
|
27
|
+
/>
|
|
28
|
+
`,
|
|
29
|
+
}}
|
|
30
|
+
/>
|
|
31
|
+
|
|
32
|
+
<Controls of={NumberInput.Default} />
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { NumberInput, NumberInputProps } from './NumberInput';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { iconNames } from 'build/generated/iconTypes';
|
|
5
|
+
import { labelArgTypes, inputArgTypes } from '@/storybook/formArgTypes';
|
|
6
|
+
|
|
7
|
+
const meta: Meta = {
|
|
8
|
+
title: 'Forms/Number Input',
|
|
9
|
+
component: NumberInput,
|
|
10
|
+
argTypes: {
|
|
11
|
+
...labelArgTypes,
|
|
12
|
+
...inputArgTypes,
|
|
13
|
+
value: {
|
|
14
|
+
control: 'number',
|
|
15
|
+
description: 'The value for the input field',
|
|
16
|
+
table: {
|
|
17
|
+
category: 'Props',
|
|
18
|
+
type: {
|
|
19
|
+
summary: 'number | ""',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
type: { name: 'number', required: false },
|
|
23
|
+
},
|
|
24
|
+
min: {
|
|
25
|
+
control: 'number',
|
|
26
|
+
description: 'The minimum value for the input field',
|
|
27
|
+
table: {
|
|
28
|
+
category: 'Props',
|
|
29
|
+
type: {
|
|
30
|
+
summary: 'number',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
type: { name: 'number', required: false },
|
|
34
|
+
},
|
|
35
|
+
max: {
|
|
36
|
+
control: 'number',
|
|
37
|
+
description: 'The maximum value for the input field',
|
|
38
|
+
table: {
|
|
39
|
+
category: 'Props',
|
|
40
|
+
type: {
|
|
41
|
+
summary: 'number',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
type: { name: 'number', required: false },
|
|
45
|
+
},
|
|
46
|
+
step: {
|
|
47
|
+
control: 'number',
|
|
48
|
+
description: 'The step value for the input field',
|
|
49
|
+
table: {
|
|
50
|
+
category: 'Props',
|
|
51
|
+
type: {
|
|
52
|
+
summary: 'number',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
type: { name: 'number', required: false },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default meta;
|
|
61
|
+
|
|
62
|
+
type Story = StoryObj<typeof NumberInput>;
|
|
63
|
+
|
|
64
|
+
const defaultArgs: NumberInputProps = {
|
|
65
|
+
label: 'Enter a number',
|
|
66
|
+
name: 'number_input',
|
|
67
|
+
placeholder: 'Please enter a value',
|
|
68
|
+
onChange: () => {},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const Default: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
...defaultArgs,
|
|
74
|
+
isRequired: true,
|
|
75
|
+
iconName: 'fa-calculator',
|
|
76
|
+
helpText: 'This Is Help Text',
|
|
77
|
+
isClearable: true,
|
|
78
|
+
hasHiddenLabel: false,
|
|
79
|
+
isDisabled: false,
|
|
80
|
+
errorMessage: '',
|
|
81
|
+
},
|
|
82
|
+
render: (args) => {
|
|
83
|
+
const [value, setValue] = useState<number | ''>(0);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
setValue(args.value || '');
|
|
87
|
+
}, [args.value]);
|
|
88
|
+
|
|
89
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
90
|
+
setValue(e.target.value === '' ? '' : parseFloat(e.target.value));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return <NumberInput {...args} value={value} onChange={handleChange} />;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const Errors: Story = {
|
|
98
|
+
args: {
|
|
99
|
+
...defaultArgs,
|
|
100
|
+
errorMessage: 'This field requires a number.',
|
|
101
|
+
},
|
|
102
|
+
render: (args) => {
|
|
103
|
+
const [value, setValue] = useState<number | ''>(0);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
setValue(args.value || '');
|
|
107
|
+
}, [args.value]);
|
|
108
|
+
|
|
109
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
110
|
+
setValue(e.target.value === '' ? '' : parseFloat(e.target.value));
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return <NumberInput {...args} value={value} onChange={handleChange} />;
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const HiddenLabel: Story = {
|
|
118
|
+
args: {
|
|
119
|
+
...defaultArgs,
|
|
120
|
+
hasHiddenLabel: true,
|
|
121
|
+
},
|
|
122
|
+
render: (args) => {
|
|
123
|
+
const [value, setValue] = useState<number | ''>(0);
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
setValue(args.value || '');
|
|
127
|
+
}, [args.value]);
|
|
128
|
+
|
|
129
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
130
|
+
setValue(e.target.value === '' ? '' : parseFloat(e.target.value));
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return <NumberInput {...args} value={value} onChange={handleChange} />;
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const HelpText: Story = {
|
|
138
|
+
args: {
|
|
139
|
+
...defaultArgs,
|
|
140
|
+
helpText: 'In order to submit the form, this field is required.',
|
|
141
|
+
},
|
|
142
|
+
render: (args) => {
|
|
143
|
+
const [value, setValue] = useState<number | ''>(0);
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
setValue(args.value || '');
|
|
147
|
+
}, [args.value]);
|
|
148
|
+
|
|
149
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
150
|
+
setValue(e.target.value === '' ? '' : parseFloat(e.target.value));
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return <NumberInput {...args} value={value} onChange={handleChange} />;
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const Clearable: Story = {
|
|
158
|
+
args: {
|
|
159
|
+
...defaultArgs,
|
|
160
|
+
isClearable: true,
|
|
161
|
+
},
|
|
162
|
+
render: (args) => {
|
|
163
|
+
const [value, setValue] = useState<number | ''>(args.value || '');
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
setValue(args.value || '');
|
|
167
|
+
}, [args.value]);
|
|
168
|
+
|
|
169
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
170
|
+
setValue(e.target.value === '' ? '' : parseFloat(e.target.value));
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return <NumberInput {...args} value={value} onChange={handleChange} />;
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export const Icon: Story = {
|
|
178
|
+
args: {
|
|
179
|
+
...defaultArgs,
|
|
180
|
+
iconName: 'fa-calculator',
|
|
181
|
+
},
|
|
182
|
+
render: (args) => {
|
|
183
|
+
const [value, setValue] = useState<number | ''>(0);
|
|
184
|
+
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
setValue(args.value || '');
|
|
187
|
+
}, [args.value]);
|
|
188
|
+
|
|
189
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
190
|
+
setValue(e.target.value === '' ? '' : parseFloat(e.target.value));
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return <NumberInput {...args} value={value} onChange={handleChange} />;
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const Required: Story = {
|
|
198
|
+
args: {
|
|
199
|
+
...defaultArgs,
|
|
200
|
+
isRequired: true,
|
|
201
|
+
},
|
|
202
|
+
render: (args) => {
|
|
203
|
+
const [value, setValue] = useState<number | ''>(0);
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
setValue(args.value || '');
|
|
207
|
+
}, [args.value]);
|
|
208
|
+
|
|
209
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
210
|
+
setValue(e.target.value === '' ? '' : parseFloat(e.target.value));
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
return <NumberInput {...args} value={value} onChange={handleChange} />;
|
|
214
|
+
},
|
|
215
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
|
|
4
|
+
import { Icon } from '@/components/icons';
|
|
5
|
+
import { LabelProps, withLabel } from '../subcomponents/Label';
|
|
6
|
+
import { DisplayFormError } from '../subcomponents/DisplayFormError';
|
|
7
|
+
import { BaseInputProps } from '../input/Input';
|
|
8
|
+
|
|
9
|
+
export interface NumberInputProps extends Omit<BaseInputProps, 'value'>, LabelProps {
|
|
10
|
+
value?: number | '';
|
|
11
|
+
min?: number;
|
|
12
|
+
max?: number;
|
|
13
|
+
step?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
|
|
17
|
+
(
|
|
18
|
+
{
|
|
19
|
+
name,
|
|
20
|
+
onChange,
|
|
21
|
+
isRequired,
|
|
22
|
+
isDisabled,
|
|
23
|
+
isClearable,
|
|
24
|
+
errorMessage,
|
|
25
|
+
helpText,
|
|
26
|
+
iconName,
|
|
27
|
+
className,
|
|
28
|
+
...rest
|
|
29
|
+
},
|
|
30
|
+
ref,
|
|
31
|
+
) => {
|
|
32
|
+
const hasErrors = errorMessage && errorMessage.length > 0;
|
|
33
|
+
|
|
34
|
+
const handleClear = () => {
|
|
35
|
+
onChange({ target: { value: '' } } as React.ChangeEvent<HTMLInputElement>);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const inputClasses = classNames(
|
|
39
|
+
'number-input',
|
|
40
|
+
{
|
|
41
|
+
error: hasErrors,
|
|
42
|
+
'number-input--has-icon': iconName,
|
|
43
|
+
'number-input--is-clearable': isClearable,
|
|
44
|
+
},
|
|
45
|
+
className,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
<div className="number-input-wrapper">
|
|
51
|
+
{iconName && (
|
|
52
|
+
<Icon name={iconName} data-testid={`${name}-embedded-icon`} className="embedded-icon" />
|
|
53
|
+
)}
|
|
54
|
+
<input
|
|
55
|
+
ref={ref}
|
|
56
|
+
data-testid={`form-number-input-${name}`}
|
|
57
|
+
name={name}
|
|
58
|
+
type="number"
|
|
59
|
+
disabled={isDisabled}
|
|
60
|
+
onChange={onChange}
|
|
61
|
+
className={inputClasses}
|
|
62
|
+
aria-invalid={hasErrors ? true : undefined}
|
|
63
|
+
aria-describedby={hasErrors || helpText ? `${name}-helper` : undefined}
|
|
64
|
+
aria-required={isRequired}
|
|
65
|
+
{...rest}
|
|
66
|
+
/>
|
|
67
|
+
{isClearable && !isDisabled && (
|
|
68
|
+
<Icon
|
|
69
|
+
name="x-close"
|
|
70
|
+
data-testid={`${name}-clearable-icon`}
|
|
71
|
+
onClick={handleClear}
|
|
72
|
+
className="clearable-icon"
|
|
73
|
+
size="sm"
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
{hasErrors && <DisplayFormError message={errorMessage} />}
|
|
78
|
+
{helpText && !hasErrors && (
|
|
79
|
+
<div data-testid={`${name}-help-text`} className="help-text" id={`${name}-helper`}>
|
|
80
|
+
{helpText}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const LabeledNumberInput = withLabel(NumberInput);
|
|
89
|
+
LabeledNumberInput.displayName = 'NumberInput';
|
|
90
|
+
export { LabeledNumberInput as NumberInput };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { NumberInput, NumberInputProps } from '../NumberInput';
|
|
4
|
+
|
|
5
|
+
const handleOnChange = jest.fn();
|
|
6
|
+
|
|
7
|
+
const defaultProps: NumberInputProps = {
|
|
8
|
+
isRequired: true,
|
|
9
|
+
label: 'Enter a number',
|
|
10
|
+
helpText: 'In order to submit the form, this field is required.',
|
|
11
|
+
name: 'number',
|
|
12
|
+
placeholder: 'Please enter a value',
|
|
13
|
+
iconName: 'fa-calculator',
|
|
14
|
+
isClearable: true,
|
|
15
|
+
onChange: handleOnChange,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('NumberInput', () => {
|
|
19
|
+
beforeEach(jest.clearAllMocks);
|
|
20
|
+
|
|
21
|
+
it('renders the number input field', () => {
|
|
22
|
+
render(<NumberInput {...defaultProps} />);
|
|
23
|
+
expect(screen.getByText('Enter a number')).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('shows an x clear icon when the number input is clearable', () => {
|
|
27
|
+
render(<NumberInput {...defaultProps} />);
|
|
28
|
+
const icon = screen.getByTestId('number-clearable-icon');
|
|
29
|
+
expect(icon).toBeInTheDocument();
|
|
30
|
+
expect(icon).toBeVisible();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('does not show an x when the number input is not clearable', () => {
|
|
34
|
+
render(<NumberInput {...defaultProps} isClearable={false} />);
|
|
35
|
+
const icon = screen.queryByTestId('number-clearable-icon');
|
|
36
|
+
expect(icon).not.toBeInTheDocument();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('clicking on the x clears the value', async () => {
|
|
40
|
+
render(<NumberInput {...defaultProps} value={42} />);
|
|
41
|
+
const input = screen.getByTestId('form-number-input-number');
|
|
42
|
+
const icon = screen.getByTestId('number-clearable-icon');
|
|
43
|
+
expect(input).toHaveValue(42);
|
|
44
|
+
await userEvent.click(icon);
|
|
45
|
+
expect(handleOnChange).toHaveBeenCalledWith(expect.objectContaining({ target: { value: '' } }));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('renders an icon on the left when one exists', () => {
|
|
49
|
+
render(<NumberInput {...defaultProps} />);
|
|
50
|
+
const icon = screen.getByTestId('number-embedded-icon');
|
|
51
|
+
expect(icon).toBeInTheDocument();
|
|
52
|
+
expect(icon).toBeVisible();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('does not render an embedded icon when one does not exist', () => {
|
|
56
|
+
render(<NumberInput {...defaultProps} iconName={undefined} />);
|
|
57
|
+
const icon = screen.queryByTestId('number-embedded-icon');
|
|
58
|
+
expect(icon).not.toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('adds the error class when errors exist', () => {
|
|
62
|
+
render(
|
|
63
|
+
<NumberInput {...defaultProps} errorMessage="You require a numeric value." value={42} />,
|
|
64
|
+
);
|
|
65
|
+
const input = screen.getByTestId('form-number-input-number');
|
|
66
|
+
expect(input).toHaveClass('error');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('does not highlight the input when no errors exist', () => {
|
|
70
|
+
render(<NumberInput {...defaultProps} value={42} />);
|
|
71
|
+
const input = screen.getByTestId('form-number-input-number');
|
|
72
|
+
expect(input).not.toHaveClass('error');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('renders help text when help text exists', () => {
|
|
76
|
+
render(<NumberInput {...defaultProps} value={42} />);
|
|
77
|
+
const helpText = screen.getByText('In order to submit the form, this field is required.');
|
|
78
|
+
expect(helpText).toBeInTheDocument();
|
|
79
|
+
expect(helpText).toBeVisible();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('does not render help text when help text does not exist', () => {
|
|
83
|
+
render(<NumberInput {...defaultProps} helpText={undefined} value={42} />);
|
|
84
|
+
const helpText = screen.queryByTestId('number-help-text');
|
|
85
|
+
expect(helpText).not.toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('emits the value when user types', async () => {
|
|
89
|
+
render(<NumberInput {...defaultProps} />);
|
|
90
|
+
const input = screen.getByTestId('form-number-input-number');
|
|
91
|
+
await userEvent.type(input, '42');
|
|
92
|
+
expect(handleOnChange).toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
});
|