@arbor-education/design-system.components 0.0.1 → 0.0.3
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/.github/workflows/design-system-pr-checks.yml +37 -0
- package/.husky/pre-commit +2 -1
- package/.storybook/main.ts +12 -0
- package/.storybook/preview.ts +1 -1
- package/README.md +7 -4
- package/dist/components/button/Button.test.d.ts +2 -0
- package/dist/components/button/Button.test.d.ts.map +1 -0
- package/dist/components/button/Button.test.js +44 -0
- package/dist/components/button/Button.test.js.map +1 -0
- package/dist/components/card/Card.test.d.ts +2 -0
- package/dist/components/card/Card.test.d.ts.map +1 -0
- package/dist/components/card/Card.test.js +169 -0
- package/dist/components/card/Card.test.js.map +1 -0
- package/dist/components/formField/FormField.d.ts +20 -0
- package/dist/components/formField/FormField.d.ts.map +1 -0
- package/dist/components/formField/FormField.js +25 -0
- package/dist/components/formField/FormField.js.map +1 -0
- package/dist/components/formField/FormField.stories.d.ts +58 -0
- package/dist/components/formField/FormField.stories.d.ts.map +1 -0
- package/dist/components/formField/FormField.stories.js +75 -0
- package/dist/components/formField/FormField.stories.js.map +1 -0
- package/dist/components/formField/FormField.test.d.ts +2 -0
- package/dist/components/formField/FormField.test.d.ts.map +1 -0
- package/dist/components/formField/FormField.test.js +37 -0
- package/dist/components/formField/FormField.test.js.map +1 -0
- package/dist/components/formField/inputs/text/TextInput.d.ts +7 -0
- package/dist/components/formField/inputs/text/TextInput.d.ts.map +1 -0
- package/dist/components/formField/inputs/text/TextInput.js +11 -0
- package/dist/components/formField/inputs/text/TextInput.js.map +1 -0
- package/dist/components/formField/inputs/text/TextInput.stories.d.ts +31 -0
- package/dist/components/formField/inputs/text/TextInput.stories.d.ts.map +1 -0
- package/dist/components/formField/inputs/text/TextInput.stories.js +37 -0
- package/dist/components/formField/inputs/text/TextInput.stories.js.map +1 -0
- package/dist/components/formField/inputs/text/TextInput.test.d.ts +2 -0
- package/dist/components/formField/inputs/text/TextInput.test.d.ts.map +1 -0
- package/dist/components/formField/inputs/text/TextInput.test.js +29 -0
- package/dist/components/formField/inputs/text/TextInput.test.js.map +1 -0
- package/dist/components/formField/inputs/textArea/TextArea.d.ts +7 -0
- package/dist/components/formField/inputs/textArea/TextArea.d.ts.map +1 -0
- package/dist/components/formField/inputs/textArea/TextArea.js +26 -0
- package/dist/components/formField/inputs/textArea/TextArea.js.map +1 -0
- package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts +11 -0
- package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts.map +1 -0
- package/dist/components/formField/inputs/textArea/TextArea.stories.js +14 -0
- package/dist/components/formField/inputs/textArea/TextArea.stories.js.map +1 -0
- package/dist/components/formField/inputs/textArea/TextArea.test.d.ts +2 -0
- package/dist/components/formField/inputs/textArea/TextArea.test.d.ts.map +1 -0
- package/dist/components/formField/inputs/textArea/TextArea.test.js +19 -0
- package/dist/components/formField/inputs/textArea/TextArea.test.js.map +1 -0
- package/dist/components/formField/label/Label.d.ts +3 -0
- package/dist/components/formField/label/Label.d.ts.map +1 -0
- package/dist/components/formField/label/Label.js +8 -0
- package/dist/components/formField/label/Label.js.map +1 -0
- package/dist/components/formField/label/Label.stories.d.ts +10 -0
- package/dist/components/formField/label/Label.stories.d.ts.map +1 -0
- package/dist/components/formField/label/Label.stories.js +12 -0
- package/dist/components/formField/label/Label.stories.js.map +1 -0
- package/dist/components/formField/label/Label.test.d.ts +2 -0
- package/dist/components/formField/label/Label.test.d.ts.map +1 -0
- package/dist/components/formField/label/Label.test.js +10 -0
- package/dist/components/formField/label/Label.test.js.map +1 -0
- package/dist/components/heading/Heading.test.d.ts +2 -0
- package/dist/components/heading/Heading.test.d.ts.map +1 -0
- package/dist/components/heading/Heading.test.js +20 -0
- package/dist/components/heading/Heading.test.js.map +1 -0
- package/dist/components/icon/Icon.test.d.ts +2 -0
- package/dist/components/icon/Icon.test.d.ts.map +1 -0
- package/dist/components/icon/Icon.test.js +17 -0
- package/dist/components/icon/Icon.test.js.map +1 -0
- package/dist/components/icon/customIcons/CheckSolid.js.map +1 -1
- package/dist/components/icon/customIcons/XSolid.js.map +1 -1
- package/dist/components/pill/Pill.test.d.ts +2 -0
- package/dist/components/pill/Pill.test.d.ts.map +1 -0
- package/dist/components/pill/Pill.test.js +20 -0
- package/dist/components/pill/Pill.test.js.map +1 -0
- package/dist/components/tabs/Tabs.d.ts +21 -0
- package/dist/components/tabs/Tabs.d.ts.map +1 -0
- package/dist/components/tabs/Tabs.js +43 -0
- package/dist/components/tabs/Tabs.js.map +1 -0
- package/dist/components/tabs/Tabs.stories.d.ts +11 -0
- package/dist/components/tabs/Tabs.stories.d.ts.map +1 -0
- package/dist/components/tabs/Tabs.stories.js +74 -0
- package/dist/components/tabs/Tabs.stories.js.map +1 -0
- package/dist/components/tabs/Tabs.test.d.ts +2 -0
- package/dist/components/tabs/Tabs.test.d.ts.map +1 -0
- package/dist/components/tabs/Tabs.test.js +115 -0
- package/dist/components/tabs/Tabs.test.js.map +1 -0
- package/dist/index.css +132 -24
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +11 -4
- package/src/components/card/card.scss +1 -1
- package/src/components/formField/FormField.stories.tsx +112 -0
- package/src/components/formField/FormField.test.tsx +41 -0
- package/src/components/formField/FormField.tsx +73 -0
- package/src/components/formField/formField.scss +41 -0
- package/src/components/formField/inputs/input.scss +75 -0
- package/src/components/formField/inputs/text/TextInput.stories.tsx +42 -0
- package/src/components/formField/inputs/text/TextInput.test.tsx +31 -0
- package/src/components/formField/inputs/text/TextInput.tsx +35 -0
- package/src/components/formField/inputs/textArea/TextArea.stories.tsx +18 -0
- package/src/components/formField/inputs/textArea/TextArea.test.tsx +19 -0
- package/src/components/formField/inputs/textArea/TextArea.tsx +56 -0
- package/src/components/formField/label/Label.stories.tsx +15 -0
- package/src/components/formField/label/Label.test.tsx +9 -0
- package/src/components/formField/label/Label.tsx +8 -0
- package/src/components/formField/label/label.scss +9 -0
- package/src/components/icon/customIcons/CheckSolid.tsx +1 -1
- package/src/components/icon/customIcons/XSolid.tsx +1 -1
- package/src/index.scss +10 -7
- package/src/index.ts +8 -6
- package/src/tokens.scss +25 -22
- package/tsconfig.json +8 -4
- package/vitest.config.ts +7 -16
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { fn } from 'storybook/test';
|
|
3
|
+
|
|
4
|
+
import { FormField } from './FormField';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof FormField> = {
|
|
7
|
+
title: 'Components/FormField',
|
|
8
|
+
component: FormField,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const Default = {
|
|
12
|
+
args: {
|
|
13
|
+
id: 'text-input',
|
|
14
|
+
label: 'Text Input',
|
|
15
|
+
inputProps: {
|
|
16
|
+
placeholder: 'Enter your text',
|
|
17
|
+
onChange: fn(),
|
|
18
|
+
},
|
|
19
|
+
helperLinkText: 'More information',
|
|
20
|
+
helperLinkUrl: 'https://www.google.com',
|
|
21
|
+
errorText: 'This is some error text',
|
|
22
|
+
fieldDescription: 'This is some descriptive text for the field',
|
|
23
|
+
inputType: 'text',
|
|
24
|
+
},
|
|
25
|
+
argTypes: {
|
|
26
|
+
'helperLinkText': {
|
|
27
|
+
control: 'text',
|
|
28
|
+
description: 'Helper link text',
|
|
29
|
+
},
|
|
30
|
+
'helperLinkUrl': {
|
|
31
|
+
control: 'text',
|
|
32
|
+
description: 'Helper link URL',
|
|
33
|
+
},
|
|
34
|
+
'errorText': {
|
|
35
|
+
control: 'text',
|
|
36
|
+
description: 'Error text',
|
|
37
|
+
},
|
|
38
|
+
'fieldDescription': {
|
|
39
|
+
control: 'text',
|
|
40
|
+
description: 'Field description',
|
|
41
|
+
},
|
|
42
|
+
'inputType': {
|
|
43
|
+
control: 'select',
|
|
44
|
+
options: ['text', 'textarea'],
|
|
45
|
+
description: 'Input type',
|
|
46
|
+
},
|
|
47
|
+
'inputProps.size': {
|
|
48
|
+
control: 'select',
|
|
49
|
+
options: ['M', 'S'],
|
|
50
|
+
description: 'Input size',
|
|
51
|
+
},
|
|
52
|
+
'inputProps.disabled': {
|
|
53
|
+
control: 'boolean',
|
|
54
|
+
description: 'Disable the input',
|
|
55
|
+
},
|
|
56
|
+
'inputProps.placeholder': {
|
|
57
|
+
control: 'text',
|
|
58
|
+
description: 'Input placeholder text',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type Story = StoryObj<typeof meta>;
|
|
64
|
+
|
|
65
|
+
// Form example with multiple inputs
|
|
66
|
+
export const FormExample: Story = {
|
|
67
|
+
render: () => (
|
|
68
|
+
<div style={{ width: '400px', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
|
69
|
+
<FormField
|
|
70
|
+
id="first-name"
|
|
71
|
+
label="First Name"
|
|
72
|
+
inputProps={{
|
|
73
|
+
placeholder: 'Enter your first name',
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
<FormField
|
|
77
|
+
id="last-name"
|
|
78
|
+
label="Last Name"
|
|
79
|
+
inputProps={{
|
|
80
|
+
placeholder: 'Enter your first name',
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
<FormField
|
|
84
|
+
id="email"
|
|
85
|
+
label="Email"
|
|
86
|
+
inputProps={{
|
|
87
|
+
placeholder: 'Enter your email',
|
|
88
|
+
}}
|
|
89
|
+
helperLinkText="More information"
|
|
90
|
+
helperLinkUrl="https://www.google.com"
|
|
91
|
+
/>
|
|
92
|
+
<FormField
|
|
93
|
+
id="password"
|
|
94
|
+
label="Password"
|
|
95
|
+
inputProps={{
|
|
96
|
+
placeholder: 'Enter your first name',
|
|
97
|
+
type: 'password',
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
<FormField
|
|
101
|
+
id="message"
|
|
102
|
+
label="Message"
|
|
103
|
+
inputType="textarea"
|
|
104
|
+
inputProps={{
|
|
105
|
+
placeholder: 'Enter a lovely message',
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default meta;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { expect, test, describe } from 'vitest';
|
|
2
|
+
import { FormField } from './FormField';
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import '@testing-library/jest-dom/vitest';
|
|
5
|
+
|
|
6
|
+
describe('Input component', () => {
|
|
7
|
+
test('renders a form field', () => {
|
|
8
|
+
render(<FormField inputType="text" label="Email" id="niceid" />);
|
|
9
|
+
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('renders helper link when provided', () => {
|
|
13
|
+
render(<FormField helperLinkText="This is helper text" helperLinkUrl="https://www.google.com" inputProps={{ placeholder: 'Enter text' }} />);
|
|
14
|
+
expect(screen.getByText('This is helper text')).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('renders error text when provided', () => {
|
|
18
|
+
render(<FormField errorText="This is an error" inputProps={{ placeholder: 'Enter text' }} />);
|
|
19
|
+
expect(screen.getByText('This is an error')).toBeInTheDocument();
|
|
20
|
+
const input = screen.getByPlaceholderText('Enter text');
|
|
21
|
+
expect(input).toHaveClass('ds-input--error');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('applies disabled class and attribute', () => {
|
|
25
|
+
render(<FormField id="niceid" inputProps={{ disabled: true, placeholder: 'Enter text' }} />);
|
|
26
|
+
const input = screen.getByPlaceholderText('Enter text');
|
|
27
|
+
expect(input).toBeDisabled();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('renders a textinput by default', () => {
|
|
31
|
+
render(<FormField id="niceid" inputProps={{ placeholder: 'Enter text' }} />);
|
|
32
|
+
const input = screen.getByPlaceholderText('Enter text');
|
|
33
|
+
expect(input).toHaveClass('ds-text-input');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('renders a textarea when inputType is textarea', () => {
|
|
37
|
+
render(<FormField id="niceid" inputType="textarea" inputProps={{ placeholder: 'Enter text' }} />);
|
|
38
|
+
const input = screen.getByPlaceholderText('Enter text');
|
|
39
|
+
expect(input).toHaveClass('ds-textarea');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { Label } from './label/Label';
|
|
3
|
+
import { Icon } from '../icon/Icon';
|
|
4
|
+
import { TextInput, type TextInputProps } from './inputs/text/TextInput';
|
|
5
|
+
import { TextArea, type TextAreaProps } from './inputs/textArea/TextArea';
|
|
6
|
+
|
|
7
|
+
type FormFieldProps = {
|
|
8
|
+
className?: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
id?: string;
|
|
11
|
+
fieldDescription?: React.ReactNode;
|
|
12
|
+
helperLinkText?: string;
|
|
13
|
+
helperLinkUrl?: string;
|
|
14
|
+
errorText?: string;
|
|
15
|
+
} & (
|
|
16
|
+
| { inputType?: 'text'; inputProps?: TextInputProps }
|
|
17
|
+
| { inputType?: 'textarea'; inputProps?: TextAreaProps }
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export const FormField = (props: FormFieldProps) => {
|
|
21
|
+
const { className, label, id, inputType = 'text', helperLinkText, helperLinkUrl, errorText, inputProps, fieldDescription } = props;
|
|
22
|
+
const classes = classNames('ds-form-field', className);
|
|
23
|
+
const describedByIds = [];
|
|
24
|
+
if (fieldDescription) {
|
|
25
|
+
describedByIds.push(`${id}-description`);
|
|
26
|
+
}
|
|
27
|
+
if (errorText) {
|
|
28
|
+
describedByIds.push(`${id}-error`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const sharedProps = {
|
|
32
|
+
id,
|
|
33
|
+
'aria-describedby': describedByIds.join(' '),
|
|
34
|
+
'hasError': !!errorText,
|
|
35
|
+
'aria-invalid': !!errorText,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={classes}>
|
|
40
|
+
{label && (
|
|
41
|
+
<Label htmlFor={id}>
|
|
42
|
+
{label}
|
|
43
|
+
</Label>
|
|
44
|
+
)}
|
|
45
|
+
{fieldDescription && (
|
|
46
|
+
<span id={`${id}-description`} className="ds-form-field__description">{fieldDescription}</span>
|
|
47
|
+
)}
|
|
48
|
+
{inputType === 'text' && (
|
|
49
|
+
<TextInput {...sharedProps} {...(inputProps as TextInputProps)} />
|
|
50
|
+
)}
|
|
51
|
+
{inputType === 'textarea' && (
|
|
52
|
+
<TextArea {...sharedProps} {...(inputProps as TextAreaProps)} />
|
|
53
|
+
)}
|
|
54
|
+
{((helperLinkText && helperLinkUrl) || errorText) && (
|
|
55
|
+
<div className="ds-form-field__message">
|
|
56
|
+
{errorText && (
|
|
57
|
+
<span className="ds-form-field__message--error" id={`${id}-error`}>
|
|
58
|
+
<Icon size={12} name="triangle-alert" />
|
|
59
|
+
{errorText}
|
|
60
|
+
</span>
|
|
61
|
+
)}
|
|
62
|
+
{helperLinkText && helperLinkUrl && (
|
|
63
|
+
<a href={helperLinkUrl} aria-label={`${label} helper link`} className="ds-form-field__message--helper">
|
|
64
|
+
{helperLinkText}
|
|
65
|
+
{' '}
|
|
66
|
+
<Icon size={12} name="arrow-up-right" />
|
|
67
|
+
</a>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
.ds-form-field {
|
|
2
|
+
font-family: var(--font-family-standard);
|
|
3
|
+
font-size: var(--font-size-2-13);
|
|
4
|
+
line-height: 150%;
|
|
5
|
+
width: 100%;
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
|
|
8
|
+
&__description {
|
|
9
|
+
margin-bottom: var(--form-field-spacing-y);
|
|
10
|
+
color: var(--form-field-description-color-text);
|
|
11
|
+
display: block;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
&__message {
|
|
15
|
+
margin-top: var(--form-field-spacing-y);
|
|
16
|
+
|
|
17
|
+
&--error, &--helper {
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
margin-top: var(--form-field-spacing-y);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&--error {
|
|
24
|
+
gap: var(--form-field-spacing-x);
|
|
25
|
+
color: var(--form-field-error-color-error-text);
|
|
26
|
+
|
|
27
|
+
.ds-icon {
|
|
28
|
+
color: var(--form-field-error-color-error-icon);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&--helper {
|
|
33
|
+
color: var(--form-field-help-text-default-color-text);
|
|
34
|
+
|
|
35
|
+
a {
|
|
36
|
+
color: var(--form-field-help-text-default-color-text);
|
|
37
|
+
text-decoration: underline;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Base input styles
|
|
2
|
+
|
|
3
|
+
.ds-input {
|
|
4
|
+
width: 100%;
|
|
5
|
+
font-family: var(--font-family-standard);
|
|
6
|
+
font-weight: var(--font-weight-regular);
|
|
7
|
+
border: 1px solid var(--form-field-placeholder-color-border);
|
|
8
|
+
border-radius: var(--form-field-radius);
|
|
9
|
+
color: var(--colour-grey-900);
|
|
10
|
+
transition: border-color 0.2s, box-shadow 0.2s, background-color 0.2s;
|
|
11
|
+
line-height: 150%;
|
|
12
|
+
background-color: var(--form-field-placeholder-color-background);
|
|
13
|
+
padding: 0 var(--form-field-spacing-x);
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
|
|
16
|
+
&::placeholder {
|
|
17
|
+
color: var(--form-field-pill-placeholder-color-text);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
&:focus {
|
|
21
|
+
border-color: var(--form-field-focus-color-border);
|
|
22
|
+
background-color: var(--form-field-focus-color-background);
|
|
23
|
+
outline: 2px solid var(--button-medium-primary-focus-color-focus);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Disabled state
|
|
27
|
+
&:disabled {
|
|
28
|
+
background-color: var(--colour-grey-100);
|
|
29
|
+
border-color: var(--colour-grey-200);
|
|
30
|
+
color: var(--colour-grey-500);
|
|
31
|
+
cursor: not-allowed;
|
|
32
|
+
|
|
33
|
+
&::placeholder {
|
|
34
|
+
color: var(--colour-grey-400);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&:hover {
|
|
38
|
+
border-color: var(--colour-grey-200);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&:hover:not(:disabled, :focus) {
|
|
43
|
+
border-color: var(--form-field-hover-color-border);
|
|
44
|
+
background-color: var(--form-field-hover-color-background);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Input sizes
|
|
48
|
+
&--M {
|
|
49
|
+
font-size: var(--font-size-3-14);
|
|
50
|
+
height: var(--text-field-medium-height);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&--S {
|
|
54
|
+
font-size: var(--font-size-2-13);
|
|
55
|
+
height: var(--text-field-small-height);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Error state
|
|
59
|
+
&--error {
|
|
60
|
+
border-color: var(--colour-semantic-destructive-500);
|
|
61
|
+
|
|
62
|
+
&:focus {
|
|
63
|
+
border-color: var(--colour-semantic-destructive-500);
|
|
64
|
+
box-shadow: 0 0 0 2px var(--colour-semantic-destructive-100-hover);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
&:hover:not(:disabled, :focus) {
|
|
68
|
+
border-color: var(--colour-semantic-destructive-300);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.ds-textarea {
|
|
74
|
+
padding: var(--form-field-spacing-y) var(--form-field-spacing-x);
|
|
75
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { fn } from 'storybook/test';
|
|
3
|
+
|
|
4
|
+
import { TextInput } from './TextInput';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'Components/FormField/Inputs/TextInput',
|
|
8
|
+
component: TextInput,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'centered',
|
|
11
|
+
},
|
|
12
|
+
tags: ['autodocs'],
|
|
13
|
+
args: {
|
|
14
|
+
onChange: fn(),
|
|
15
|
+
},
|
|
16
|
+
argTypes: {
|
|
17
|
+
size: {
|
|
18
|
+
control: 'select',
|
|
19
|
+
options: ['M', 'S'],
|
|
20
|
+
description: 'Input size',
|
|
21
|
+
},
|
|
22
|
+
disabled: {
|
|
23
|
+
control: 'boolean',
|
|
24
|
+
description: 'Disable the input',
|
|
25
|
+
},
|
|
26
|
+
placeholder: {
|
|
27
|
+
control: 'text',
|
|
28
|
+
description: 'Input placeholder text',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
} satisfies Meta<typeof TextInput>;
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<typeof meta>;
|
|
35
|
+
|
|
36
|
+
// Default input
|
|
37
|
+
export const Default: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
placeholder: 'Enter text...',
|
|
40
|
+
size: 'M',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { expect, test, describe, vi } from 'vitest';
|
|
2
|
+
import { TextInput } from './TextInput';
|
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
4
|
+
import '@testing-library/jest-dom/vitest';
|
|
5
|
+
|
|
6
|
+
describe('Input component', () => {
|
|
7
|
+
test('renders input element', () => {
|
|
8
|
+
render(<TextInput placeholder="Enter text" />);
|
|
9
|
+
expect(screen.getByPlaceholderText('Enter text')).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('calls onChange when input changes', () => {
|
|
13
|
+
const onChange = vi.fn();
|
|
14
|
+
render(<TextInput placeholder="Enter text" onChange={onChange} />);
|
|
15
|
+
fireEvent.change(screen.getByPlaceholderText('Enter text'), { target: { value: 'Hello' } });
|
|
16
|
+
expect(onChange).toHaveBeenCalled();
|
|
17
|
+
expect((screen.getByPlaceholderText('Enter text') as HTMLInputElement).value).toBe('Hello');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('applies default size class', () => {
|
|
21
|
+
render(<TextInput placeholder="Enter text" />);
|
|
22
|
+
const input = screen.getByPlaceholderText('Enter text');
|
|
23
|
+
expect(input).toHaveClass('ds-input--M');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('applies correct size class when specified', () => {
|
|
27
|
+
render(<TextInput size="S" placeholder="Enter text" />);
|
|
28
|
+
const input = screen.getByPlaceholderText('Enter text');
|
|
29
|
+
expect(input).toHaveClass('ds-input--S');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { type InputHTMLAttributes } from 'react';
|
|
3
|
+
|
|
4
|
+
export type TextInputProps = {
|
|
5
|
+
size?: 'M' | 'S';
|
|
6
|
+
hasError?: boolean;
|
|
7
|
+
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>;
|
|
8
|
+
|
|
9
|
+
export const TextInput = (props: TextInputProps) => {
|
|
10
|
+
const {
|
|
11
|
+
size = 'M',
|
|
12
|
+
hasError,
|
|
13
|
+
className = '',
|
|
14
|
+
disabled = false,
|
|
15
|
+
...rest
|
|
16
|
+
} = props;
|
|
17
|
+
|
|
18
|
+
const inputClasses = classNames(
|
|
19
|
+
'ds-text-input',
|
|
20
|
+
'ds-input',
|
|
21
|
+
`ds-input--${size}`,
|
|
22
|
+
{
|
|
23
|
+
'ds-input--error': hasError,
|
|
24
|
+
},
|
|
25
|
+
className,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<input
|
|
30
|
+
className={inputClasses}
|
|
31
|
+
disabled={disabled}
|
|
32
|
+
{...rest}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Meta } from '@storybook/react-vite';
|
|
2
|
+
import { fn } from 'storybook/test';
|
|
3
|
+
|
|
4
|
+
import { TextArea } from './TextArea';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof TextArea> = {
|
|
7
|
+
title: 'Components/FormField/Inputs/TextArea',
|
|
8
|
+
component: TextArea,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const Default = {
|
|
12
|
+
args: {
|
|
13
|
+
title: 'titleValue',
|
|
14
|
+
onChange: fn(),
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { expect, describe, test, vi } from 'vitest';
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import { TextArea } from './TextArea';
|
|
4
|
+
import '@testing-library/jest-dom/vitest';
|
|
5
|
+
|
|
6
|
+
describe('TextArea component', () => {
|
|
7
|
+
test('TextArea says hello', () => {
|
|
8
|
+
render(<TextArea placeholder="Hello I'm a TextArea!" />);
|
|
9
|
+
expect(screen.getByPlaceholderText("Hello I'm a TextArea!")).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('calls onChange when input changes', () => {
|
|
13
|
+
const onChange = vi.fn();
|
|
14
|
+
render(<TextArea placeholder="Enter text" onChange={onChange} />);
|
|
15
|
+
fireEvent.change(screen.getByPlaceholderText('Enter text'), { target: { value: 'Hello' } });
|
|
16
|
+
expect(onChange).toHaveBeenCalled();
|
|
17
|
+
expect((screen.getByPlaceholderText('Enter text') as HTMLInputElement).value).toBe('Hello');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import type { ChangeEvent, TextareaHTMLAttributes } from 'react';
|
|
4
|
+
|
|
5
|
+
export type TextAreaProps = {
|
|
6
|
+
hasError?: boolean;
|
|
7
|
+
autoSize?: boolean;
|
|
8
|
+
} & Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'size'>;
|
|
9
|
+
|
|
10
|
+
export const TextArea = (props: TextAreaProps) => {
|
|
11
|
+
const {
|
|
12
|
+
hasError,
|
|
13
|
+
className = '',
|
|
14
|
+
disabled = false,
|
|
15
|
+
autoSize = true,
|
|
16
|
+
onChange,
|
|
17
|
+
...rest
|
|
18
|
+
} = props;
|
|
19
|
+
|
|
20
|
+
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
|
21
|
+
|
|
22
|
+
const inputClasses = classNames(
|
|
23
|
+
'ds-textarea',
|
|
24
|
+
'ds-input',
|
|
25
|
+
{
|
|
26
|
+
'ds-input--error': hasError,
|
|
27
|
+
},
|
|
28
|
+
className,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const autosizeTextArea = () => {
|
|
32
|
+
if (inputRef.current) {
|
|
33
|
+
inputRef.current.style.height = 'auto';
|
|
34
|
+
const verticalPadding = 4;
|
|
35
|
+
const newHeight = inputRef.current.scrollHeight;
|
|
36
|
+
inputRef.current.style.height = `${newHeight + verticalPadding}px`;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleOnChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
41
|
+
onChange?.(e);
|
|
42
|
+
if (autoSize) {
|
|
43
|
+
autosizeTextArea();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<textarea
|
|
49
|
+
ref={inputRef}
|
|
50
|
+
className={inputClasses}
|
|
51
|
+
disabled={disabled}
|
|
52
|
+
onChange={handleOnChange}
|
|
53
|
+
{...rest}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Meta } from '@storybook/react-vite';
|
|
2
|
+
import { Label } from './Label';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Label> = {
|
|
5
|
+
title: 'Components/FormField/Inputs/Label',
|
|
6
|
+
component: Label,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const Default = {
|
|
10
|
+
args: {
|
|
11
|
+
children: 'Label',
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { Label } from './Label';
|
|
4
|
+
import '@testing-library/jest-dom/vitest';
|
|
5
|
+
|
|
6
|
+
test('Label says hello', () => {
|
|
7
|
+
render(<Label>Hello I'm a Label!</Label>);
|
|
8
|
+
expect(screen.getByText("Hello I'm a Label!")).toBeInTheDocument();
|
|
9
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import type { HTMLProps } from 'react';
|
|
3
|
+
|
|
4
|
+
export const Label = (props: HTMLProps<HTMLLabelElement>) => {
|
|
5
|
+
const { className, htmlFor, children } = props;
|
|
6
|
+
const classes = classNames('ds-label', className);
|
|
7
|
+
return <label className={classes} htmlFor={htmlFor}>{children}</label>;
|
|
8
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
.ds-label {
|
|
2
|
+
font-family: var(--font-family-standard);
|
|
3
|
+
font-size: var(--font-size-2-13);
|
|
4
|
+
font-weight: var(--font-weight-bold);
|
|
5
|
+
color: var(--form-field-label-color-text);
|
|
6
|
+
line-height: 150%;
|
|
7
|
+
margin-bottom: var(--form-field-spacing-y);
|
|
8
|
+
display: block;
|
|
9
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CustomIconProps } from '../types';
|
|
2
|
-
import { useMemoGenerateUuid } from '
|
|
2
|
+
import { useMemoGenerateUuid } from 'Utils/hooks/useMemoGenerateUuid';
|
|
3
3
|
|
|
4
4
|
export const CheckSolid = (props: CustomIconProps) => {
|
|
5
5
|
const { size, color, ...rest } = props;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CustomIconProps } from '../types';
|
|
2
|
-
import { useMemoGenerateUuid } from '
|
|
2
|
+
import { useMemoGenerateUuid } from 'Utils/hooks/useMemoGenerateUuid';
|
|
3
3
|
|
|
4
4
|
export const XSolid = (props: CustomIconProps) => {
|
|
5
5
|
const { size, color, ...rest } = props;
|
package/src/index.scss
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
@use
|
|
2
|
-
@use
|
|
3
|
-
@use
|
|
4
|
-
@use
|
|
5
|
-
@use
|
|
6
|
-
@use "./components/
|
|
7
|
-
@
|
|
1
|
+
@use './tokens.scss';
|
|
2
|
+
@use './global.scss';
|
|
3
|
+
@use './components/button/button.scss';
|
|
4
|
+
@use './components/heading/heading.scss';
|
|
5
|
+
@use './components/card/card.scss';
|
|
6
|
+
@use "./components/formField/formField.scss";
|
|
7
|
+
@use "./components/formField/inputs/input.scss";
|
|
8
|
+
@use "./components/formField/label/label.scss";
|
|
9
|
+
@use './components/pill/pill.scss';
|
|
10
|
+
@import 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap';
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
export { Button } from '
|
|
2
|
-
export { Heading } from '
|
|
3
|
-
export { HeadingInnerContainer } from '
|
|
4
|
-
export { Icon } from '
|
|
5
|
-
export { Card } from '
|
|
6
|
-
export { Pill } from '
|
|
1
|
+
export { Button } from 'Components/button/Button';
|
|
2
|
+
export { Heading } from 'Components/heading/Heading';
|
|
3
|
+
export { HeadingInnerContainer } from 'Components/heading/HeadingInnerContainer';
|
|
4
|
+
export { Icon } from 'Components/icon/Icon';
|
|
5
|
+
export { Card } from 'Components/card/Card';
|
|
6
|
+
export { Pill } from 'Components/pill/Pill';
|
|
7
|
+
export { TextInput } from './components/formField/inputs/text/TextInput';
|
|
8
|
+
export { TextArea } from './components/formField/inputs/textArea/TextArea';
|