@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.
Files changed (117) hide show
  1. package/.github/workflows/design-system-pr-checks.yml +37 -0
  2. package/.husky/pre-commit +2 -1
  3. package/.storybook/main.ts +12 -0
  4. package/.storybook/preview.ts +1 -1
  5. package/README.md +7 -4
  6. package/dist/components/button/Button.test.d.ts +2 -0
  7. package/dist/components/button/Button.test.d.ts.map +1 -0
  8. package/dist/components/button/Button.test.js +44 -0
  9. package/dist/components/button/Button.test.js.map +1 -0
  10. package/dist/components/card/Card.test.d.ts +2 -0
  11. package/dist/components/card/Card.test.d.ts.map +1 -0
  12. package/dist/components/card/Card.test.js +169 -0
  13. package/dist/components/card/Card.test.js.map +1 -0
  14. package/dist/components/formField/FormField.d.ts +20 -0
  15. package/dist/components/formField/FormField.d.ts.map +1 -0
  16. package/dist/components/formField/FormField.js +25 -0
  17. package/dist/components/formField/FormField.js.map +1 -0
  18. package/dist/components/formField/FormField.stories.d.ts +58 -0
  19. package/dist/components/formField/FormField.stories.d.ts.map +1 -0
  20. package/dist/components/formField/FormField.stories.js +75 -0
  21. package/dist/components/formField/FormField.stories.js.map +1 -0
  22. package/dist/components/formField/FormField.test.d.ts +2 -0
  23. package/dist/components/formField/FormField.test.d.ts.map +1 -0
  24. package/dist/components/formField/FormField.test.js +37 -0
  25. package/dist/components/formField/FormField.test.js.map +1 -0
  26. package/dist/components/formField/inputs/text/TextInput.d.ts +7 -0
  27. package/dist/components/formField/inputs/text/TextInput.d.ts.map +1 -0
  28. package/dist/components/formField/inputs/text/TextInput.js +11 -0
  29. package/dist/components/formField/inputs/text/TextInput.js.map +1 -0
  30. package/dist/components/formField/inputs/text/TextInput.stories.d.ts +31 -0
  31. package/dist/components/formField/inputs/text/TextInput.stories.d.ts.map +1 -0
  32. package/dist/components/formField/inputs/text/TextInput.stories.js +37 -0
  33. package/dist/components/formField/inputs/text/TextInput.stories.js.map +1 -0
  34. package/dist/components/formField/inputs/text/TextInput.test.d.ts +2 -0
  35. package/dist/components/formField/inputs/text/TextInput.test.d.ts.map +1 -0
  36. package/dist/components/formField/inputs/text/TextInput.test.js +29 -0
  37. package/dist/components/formField/inputs/text/TextInput.test.js.map +1 -0
  38. package/dist/components/formField/inputs/textArea/TextArea.d.ts +7 -0
  39. package/dist/components/formField/inputs/textArea/TextArea.d.ts.map +1 -0
  40. package/dist/components/formField/inputs/textArea/TextArea.js +26 -0
  41. package/dist/components/formField/inputs/textArea/TextArea.js.map +1 -0
  42. package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts +11 -0
  43. package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts.map +1 -0
  44. package/dist/components/formField/inputs/textArea/TextArea.stories.js +14 -0
  45. package/dist/components/formField/inputs/textArea/TextArea.stories.js.map +1 -0
  46. package/dist/components/formField/inputs/textArea/TextArea.test.d.ts +2 -0
  47. package/dist/components/formField/inputs/textArea/TextArea.test.d.ts.map +1 -0
  48. package/dist/components/formField/inputs/textArea/TextArea.test.js +19 -0
  49. package/dist/components/formField/inputs/textArea/TextArea.test.js.map +1 -0
  50. package/dist/components/formField/label/Label.d.ts +3 -0
  51. package/dist/components/formField/label/Label.d.ts.map +1 -0
  52. package/dist/components/formField/label/Label.js +8 -0
  53. package/dist/components/formField/label/Label.js.map +1 -0
  54. package/dist/components/formField/label/Label.stories.d.ts +10 -0
  55. package/dist/components/formField/label/Label.stories.d.ts.map +1 -0
  56. package/dist/components/formField/label/Label.stories.js +12 -0
  57. package/dist/components/formField/label/Label.stories.js.map +1 -0
  58. package/dist/components/formField/label/Label.test.d.ts +2 -0
  59. package/dist/components/formField/label/Label.test.d.ts.map +1 -0
  60. package/dist/components/formField/label/Label.test.js +10 -0
  61. package/dist/components/formField/label/Label.test.js.map +1 -0
  62. package/dist/components/heading/Heading.test.d.ts +2 -0
  63. package/dist/components/heading/Heading.test.d.ts.map +1 -0
  64. package/dist/components/heading/Heading.test.js +20 -0
  65. package/dist/components/heading/Heading.test.js.map +1 -0
  66. package/dist/components/icon/Icon.test.d.ts +2 -0
  67. package/dist/components/icon/Icon.test.d.ts.map +1 -0
  68. package/dist/components/icon/Icon.test.js +17 -0
  69. package/dist/components/icon/Icon.test.js.map +1 -0
  70. package/dist/components/icon/customIcons/CheckSolid.js.map +1 -1
  71. package/dist/components/icon/customIcons/XSolid.js.map +1 -1
  72. package/dist/components/pill/Pill.test.d.ts +2 -0
  73. package/dist/components/pill/Pill.test.d.ts.map +1 -0
  74. package/dist/components/pill/Pill.test.js +20 -0
  75. package/dist/components/pill/Pill.test.js.map +1 -0
  76. package/dist/components/tabs/Tabs.d.ts +21 -0
  77. package/dist/components/tabs/Tabs.d.ts.map +1 -0
  78. package/dist/components/tabs/Tabs.js +43 -0
  79. package/dist/components/tabs/Tabs.js.map +1 -0
  80. package/dist/components/tabs/Tabs.stories.d.ts +11 -0
  81. package/dist/components/tabs/Tabs.stories.d.ts.map +1 -0
  82. package/dist/components/tabs/Tabs.stories.js +74 -0
  83. package/dist/components/tabs/Tabs.stories.js.map +1 -0
  84. package/dist/components/tabs/Tabs.test.d.ts +2 -0
  85. package/dist/components/tabs/Tabs.test.d.ts.map +1 -0
  86. package/dist/components/tabs/Tabs.test.js +115 -0
  87. package/dist/components/tabs/Tabs.test.js.map +1 -0
  88. package/dist/index.css +132 -24
  89. package/dist/index.css.map +1 -1
  90. package/dist/index.d.ts +2 -0
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +2 -0
  93. package/dist/index.js.map +1 -1
  94. package/package.json +11 -4
  95. package/src/components/card/card.scss +1 -1
  96. package/src/components/formField/FormField.stories.tsx +112 -0
  97. package/src/components/formField/FormField.test.tsx +41 -0
  98. package/src/components/formField/FormField.tsx +73 -0
  99. package/src/components/formField/formField.scss +41 -0
  100. package/src/components/formField/inputs/input.scss +75 -0
  101. package/src/components/formField/inputs/text/TextInput.stories.tsx +42 -0
  102. package/src/components/formField/inputs/text/TextInput.test.tsx +31 -0
  103. package/src/components/formField/inputs/text/TextInput.tsx +35 -0
  104. package/src/components/formField/inputs/textArea/TextArea.stories.tsx +18 -0
  105. package/src/components/formField/inputs/textArea/TextArea.test.tsx +19 -0
  106. package/src/components/formField/inputs/textArea/TextArea.tsx +56 -0
  107. package/src/components/formField/label/Label.stories.tsx +15 -0
  108. package/src/components/formField/label/Label.test.tsx +9 -0
  109. package/src/components/formField/label/Label.tsx +8 -0
  110. package/src/components/formField/label/label.scss +9 -0
  111. package/src/components/icon/customIcons/CheckSolid.tsx +1 -1
  112. package/src/components/icon/customIcons/XSolid.tsx +1 -1
  113. package/src/index.scss +10 -7
  114. package/src/index.ts +8 -6
  115. package/src/tokens.scss +25 -22
  116. package/tsconfig.json +8 -4
  117. 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 '../../../utils/hooks/useMemoGenerateUuid';
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 '../../../utils/hooks/useMemoGenerateUuid';
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 "./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/pill/pill.scss";
7
- @import "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap";
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 './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';
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';