@indico-data/design-system 2.4.2 → 2.5.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/lib/index.css +119 -0
- package/lib/index.esm.css +119 -0
- package/lib/src/components/forms/input/Input.d.ts +18 -0
- package/lib/src/components/forms/input/Input.stories.d.ts +12 -0
- package/lib/src/components/forms/input/__tests__/Input.test.d.ts +1 -0
- package/lib/src/components/forms/input/index.d.ts +1 -0
- package/lib/src/components/forms/subcomponents/ErrorList.d.ts +6 -0
- package/lib/src/components/forms/subcomponents/Label.d.ts +8 -0
- package/lib/src/components/forms/subcomponents/__tests__/ErrorList.test.d.ts +1 -0
- package/lib/src/components/forms/subcomponents/__tests__/Label.test.d.ts +1 -0
- package/lib/src/components/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/forms/input/Input.mdx +19 -0
- package/src/components/forms/input/Input.stories.tsx +301 -0
- package/src/components/forms/input/Input.tsx +86 -0
- package/src/components/forms/input/__tests__/Input.test.tsx +213 -0
- package/src/components/forms/input/index.ts +1 -0
- package/src/components/forms/input/styles/Input.scss +112 -0
- package/src/components/forms/subcomponents/ErrorList.tsx +14 -0
- package/src/components/forms/subcomponents/Label.tsx +20 -0
- package/src/components/forms/subcomponents/__tests__/ErrorList.test.tsx +16 -0
- package/src/components/forms/subcomponents/__tests__/Label.test.tsx +33 -0
- package/src/components/index.ts +1 -0
- package/src/styles/_typography.scss +29 -11
- package/src/styles/index.scss +1 -0
package/lib/index.css
CHANGED
|
@@ -862,6 +862,109 @@
|
|
|
862
862
|
fill: var(--pf-gray-color-300);
|
|
863
863
|
}
|
|
864
864
|
|
|
865
|
+
:root,
|
|
866
|
+
:root [data-theme=light],
|
|
867
|
+
:root [data-theme=dark] {
|
|
868
|
+
--pf-input-background-color: var(--pf-white-color);
|
|
869
|
+
--pf-input-border-color: var(--pf-gray-color);
|
|
870
|
+
--pf-input-text-color: var(--pf-gray-color);
|
|
871
|
+
--pf-input-placeholder-text-color: var(--pf-gray-color-300);
|
|
872
|
+
--pf-input-help-text-color: var(--pf-gray-color-400);
|
|
873
|
+
--pf-input-rounded: var(--pf-rounded);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
:root [data-theme=dark] {
|
|
877
|
+
--pf-input-background-color: var(--pf-primary-color);
|
|
878
|
+
--pf-input-border-color: var(--pf-gray-color-100);
|
|
879
|
+
--pf-input-text-color: var(--pf-gray-color-100);
|
|
880
|
+
--pf-input-placeholder-text-color: var(--pf-gray-color);
|
|
881
|
+
--pf-input-help-text-color: var(--pf-gray-color-200);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
.input {
|
|
885
|
+
background-color: var(--pf-input-background-color);
|
|
886
|
+
border: 1px solid var(--pf-input-border-color);
|
|
887
|
+
border-radius: var(--pf-input-rounded);
|
|
888
|
+
color: var(--pf-input-text-color);
|
|
889
|
+
padding: 10px;
|
|
890
|
+
width: 100%;
|
|
891
|
+
box-sizing: border-box;
|
|
892
|
+
height: 36px;
|
|
893
|
+
}
|
|
894
|
+
.input::-moz-placeholder {
|
|
895
|
+
color: var(--pf-input-placeholder-text-color);
|
|
896
|
+
}
|
|
897
|
+
.input::placeholder {
|
|
898
|
+
color: var(--pf-input-placeholder-text-color);
|
|
899
|
+
}
|
|
900
|
+
.input:focus {
|
|
901
|
+
border-color: var(--pf-primary-color);
|
|
902
|
+
}
|
|
903
|
+
.input.error {
|
|
904
|
+
border-color: var(--pf-error-color);
|
|
905
|
+
}
|
|
906
|
+
.input.success {
|
|
907
|
+
border-color: var(--pf-success-color);
|
|
908
|
+
}
|
|
909
|
+
.input.warning {
|
|
910
|
+
border-color: var(--pf-warning-color);
|
|
911
|
+
}
|
|
912
|
+
.input.info {
|
|
913
|
+
border-color: var(--pf-info-color);
|
|
914
|
+
}
|
|
915
|
+
.input:disabled {
|
|
916
|
+
background-color: var(--pf-gray-color-100);
|
|
917
|
+
border-color: var(--pf-gray-color-300);
|
|
918
|
+
color: var(--pf-gray-color-400);
|
|
919
|
+
}
|
|
920
|
+
.input--has-icon {
|
|
921
|
+
padding-left: var(--pf-padding-7);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
.form-control .error-list {
|
|
925
|
+
list-style: none;
|
|
926
|
+
padding: 0;
|
|
927
|
+
margin: 0;
|
|
928
|
+
margin-top: var(--pf-margin-2);
|
|
929
|
+
margin-bottom: var(--pf-margin-2);
|
|
930
|
+
color: var(--pf-error-color);
|
|
931
|
+
}
|
|
932
|
+
.form-control .help-text {
|
|
933
|
+
margin-top: var(--pf-margin-2);
|
|
934
|
+
margin-bottom: var(--pf-margin-2);
|
|
935
|
+
color: var(--pf-input-help-text-color);
|
|
936
|
+
font-size: var(--pf-font-size-subtitle2);
|
|
937
|
+
}
|
|
938
|
+
.form-control .input-wrapper {
|
|
939
|
+
position: relative;
|
|
940
|
+
}
|
|
941
|
+
.form-control .input-wrapper .embedded-icon {
|
|
942
|
+
position: absolute;
|
|
943
|
+
top: 10px;
|
|
944
|
+
left: var(--pf-margin-2);
|
|
945
|
+
color: var(--pf-input-text-color);
|
|
946
|
+
}
|
|
947
|
+
.form-control .input-wrapper .clearable-icon {
|
|
948
|
+
position: absolute;
|
|
949
|
+
top: var(--pf-margin-3);
|
|
950
|
+
right: var(--pf-margin-2);
|
|
951
|
+
color: var(--pf-input-text-color);
|
|
952
|
+
}
|
|
953
|
+
.form-control .is-visually-hidden {
|
|
954
|
+
position: absolute;
|
|
955
|
+
width: 1px;
|
|
956
|
+
height: 1px;
|
|
957
|
+
padding: 0;
|
|
958
|
+
margin: -1px;
|
|
959
|
+
overflow: hidden;
|
|
960
|
+
clip: rect(0, 0, 0, 0);
|
|
961
|
+
white-space: nowrap;
|
|
962
|
+
border: 0;
|
|
963
|
+
}
|
|
964
|
+
.form-control .form-label {
|
|
965
|
+
margin-bottom: var(--pf-margin-2);
|
|
966
|
+
}
|
|
967
|
+
|
|
865
968
|
:root {
|
|
866
969
|
--pf-font-family-base: "Mulish", sans-serif;
|
|
867
970
|
--pf-font-size-base: 1rem;
|
|
@@ -1119,6 +1222,22 @@ p,
|
|
|
1119
1222
|
overflow-wrap: break-word;
|
|
1120
1223
|
}
|
|
1121
1224
|
|
|
1225
|
+
.text-error {
|
|
1226
|
+
color: var(--pf-error-color);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
.text-warning {
|
|
1230
|
+
color: var(--pf-warning-color);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
.text-success {
|
|
1234
|
+
color: var(--pf-success-color);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
.text-info {
|
|
1238
|
+
color: var(--pf-info-color);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1122
1241
|
.color-primary {
|
|
1123
1242
|
color: var(--pf-primary-color);
|
|
1124
1243
|
}
|
package/lib/index.esm.css
CHANGED
|
@@ -862,6 +862,109 @@
|
|
|
862
862
|
fill: var(--pf-gray-color-300);
|
|
863
863
|
}
|
|
864
864
|
|
|
865
|
+
:root,
|
|
866
|
+
:root [data-theme=light],
|
|
867
|
+
:root [data-theme=dark] {
|
|
868
|
+
--pf-input-background-color: var(--pf-white-color);
|
|
869
|
+
--pf-input-border-color: var(--pf-gray-color);
|
|
870
|
+
--pf-input-text-color: var(--pf-gray-color);
|
|
871
|
+
--pf-input-placeholder-text-color: var(--pf-gray-color-300);
|
|
872
|
+
--pf-input-help-text-color: var(--pf-gray-color-400);
|
|
873
|
+
--pf-input-rounded: var(--pf-rounded);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
:root [data-theme=dark] {
|
|
877
|
+
--pf-input-background-color: var(--pf-primary-color);
|
|
878
|
+
--pf-input-border-color: var(--pf-gray-color-100);
|
|
879
|
+
--pf-input-text-color: var(--pf-gray-color-100);
|
|
880
|
+
--pf-input-placeholder-text-color: var(--pf-gray-color);
|
|
881
|
+
--pf-input-help-text-color: var(--pf-gray-color-200);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
.input {
|
|
885
|
+
background-color: var(--pf-input-background-color);
|
|
886
|
+
border: 1px solid var(--pf-input-border-color);
|
|
887
|
+
border-radius: var(--pf-input-rounded);
|
|
888
|
+
color: var(--pf-input-text-color);
|
|
889
|
+
padding: 10px;
|
|
890
|
+
width: 100%;
|
|
891
|
+
box-sizing: border-box;
|
|
892
|
+
height: 36px;
|
|
893
|
+
}
|
|
894
|
+
.input::-moz-placeholder {
|
|
895
|
+
color: var(--pf-input-placeholder-text-color);
|
|
896
|
+
}
|
|
897
|
+
.input::placeholder {
|
|
898
|
+
color: var(--pf-input-placeholder-text-color);
|
|
899
|
+
}
|
|
900
|
+
.input:focus {
|
|
901
|
+
border-color: var(--pf-primary-color);
|
|
902
|
+
}
|
|
903
|
+
.input.error {
|
|
904
|
+
border-color: var(--pf-error-color);
|
|
905
|
+
}
|
|
906
|
+
.input.success {
|
|
907
|
+
border-color: var(--pf-success-color);
|
|
908
|
+
}
|
|
909
|
+
.input.warning {
|
|
910
|
+
border-color: var(--pf-warning-color);
|
|
911
|
+
}
|
|
912
|
+
.input.info {
|
|
913
|
+
border-color: var(--pf-info-color);
|
|
914
|
+
}
|
|
915
|
+
.input:disabled {
|
|
916
|
+
background-color: var(--pf-gray-color-100);
|
|
917
|
+
border-color: var(--pf-gray-color-300);
|
|
918
|
+
color: var(--pf-gray-color-400);
|
|
919
|
+
}
|
|
920
|
+
.input--has-icon {
|
|
921
|
+
padding-left: var(--pf-padding-7);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
.form-control .error-list {
|
|
925
|
+
list-style: none;
|
|
926
|
+
padding: 0;
|
|
927
|
+
margin: 0;
|
|
928
|
+
margin-top: var(--pf-margin-2);
|
|
929
|
+
margin-bottom: var(--pf-margin-2);
|
|
930
|
+
color: var(--pf-error-color);
|
|
931
|
+
}
|
|
932
|
+
.form-control .help-text {
|
|
933
|
+
margin-top: var(--pf-margin-2);
|
|
934
|
+
margin-bottom: var(--pf-margin-2);
|
|
935
|
+
color: var(--pf-input-help-text-color);
|
|
936
|
+
font-size: var(--pf-font-size-subtitle2);
|
|
937
|
+
}
|
|
938
|
+
.form-control .input-wrapper {
|
|
939
|
+
position: relative;
|
|
940
|
+
}
|
|
941
|
+
.form-control .input-wrapper .embedded-icon {
|
|
942
|
+
position: absolute;
|
|
943
|
+
top: 10px;
|
|
944
|
+
left: var(--pf-margin-2);
|
|
945
|
+
color: var(--pf-input-text-color);
|
|
946
|
+
}
|
|
947
|
+
.form-control .input-wrapper .clearable-icon {
|
|
948
|
+
position: absolute;
|
|
949
|
+
top: var(--pf-margin-3);
|
|
950
|
+
right: var(--pf-margin-2);
|
|
951
|
+
color: var(--pf-input-text-color);
|
|
952
|
+
}
|
|
953
|
+
.form-control .is-visually-hidden {
|
|
954
|
+
position: absolute;
|
|
955
|
+
width: 1px;
|
|
956
|
+
height: 1px;
|
|
957
|
+
padding: 0;
|
|
958
|
+
margin: -1px;
|
|
959
|
+
overflow: hidden;
|
|
960
|
+
clip: rect(0, 0, 0, 0);
|
|
961
|
+
white-space: nowrap;
|
|
962
|
+
border: 0;
|
|
963
|
+
}
|
|
964
|
+
.form-control .form-label {
|
|
965
|
+
margin-bottom: var(--pf-margin-2);
|
|
966
|
+
}
|
|
967
|
+
|
|
865
968
|
:root {
|
|
866
969
|
--pf-font-family-base: "Mulish", sans-serif;
|
|
867
970
|
--pf-font-size-base: 1rem;
|
|
@@ -1119,6 +1222,22 @@ p,
|
|
|
1119
1222
|
overflow-wrap: break-word;
|
|
1120
1223
|
}
|
|
1121
1224
|
|
|
1225
|
+
.text-error {
|
|
1226
|
+
color: var(--pf-error-color);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
.text-warning {
|
|
1230
|
+
color: var(--pf-warning-color);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
.text-success {
|
|
1234
|
+
color: var(--pf-success-color);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
.text-info {
|
|
1238
|
+
color: var(--pf-info-color);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1122
1241
|
.color-primary {
|
|
1123
1242
|
color: var(--pf-primary-color);
|
|
1124
1243
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { IconName } from '@/types';
|
|
3
|
+
export interface InputProps {
|
|
4
|
+
ref?: React.LegacyRef<HTMLInputElement>;
|
|
5
|
+
label: string;
|
|
6
|
+
name: string;
|
|
7
|
+
placeholder: string;
|
|
8
|
+
value: string;
|
|
9
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
10
|
+
isRequired?: boolean;
|
|
11
|
+
isDisabled?: boolean;
|
|
12
|
+
errorList?: string[];
|
|
13
|
+
helpText?: string;
|
|
14
|
+
hasHiddenLabel?: boolean;
|
|
15
|
+
iconName?: IconName;
|
|
16
|
+
isClearable?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare const Input: ({ ref, label, name, placeholder, value, onChange, isRequired, isDisabled, errorList, helpText, iconName, hasHiddenLabel, isClearable, ...rest }: InputProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Input } from './Input';
|
|
3
|
+
declare const meta: Meta;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof Input>;
|
|
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 { Input } from './Input';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Canvas, Meta, Controls } from '@storybook/blocks';
|
|
2
|
+
import * as Input from './Input.stories';
|
|
3
|
+
|
|
4
|
+
<Meta title="Forms/Input" name="Input" />
|
|
5
|
+
|
|
6
|
+
# Input
|
|
7
|
+
|
|
8
|
+
The input component is the building block of any form. Below you will find the accepted properties for this component. It is encouraged to build forms utilizing [React Hook Form](https://react-hook-form.com/) library in your application. This will facilitate form state management and enforce best practices. (***Our components are compatible with but do not provide the plugin***)
|
|
9
|
+
|
|
10
|
+
<Canvas
|
|
11
|
+
of={Input.Default}
|
|
12
|
+
source={{
|
|
13
|
+
code: `
|
|
14
|
+
<Input name="first_name" required={true} isRequired helpText="This Is Help Text" isClearable label="Label Name" iconName="user" />
|
|
15
|
+
`,
|
|
16
|
+
}}
|
|
17
|
+
/>
|
|
18
|
+
|
|
19
|
+
<Controls of={Input.Default} />
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Input, InputProps } from './Input';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { iconNames } from 'build/generated/iconTypes';
|
|
5
|
+
|
|
6
|
+
const meta: Meta = {
|
|
7
|
+
title: 'Forms/Input',
|
|
8
|
+
component: Input,
|
|
9
|
+
argTypes: {
|
|
10
|
+
onChange: {
|
|
11
|
+
control: false,
|
|
12
|
+
description: 'onChange event handler',
|
|
13
|
+
table: {
|
|
14
|
+
category: 'Callbacks',
|
|
15
|
+
type: {
|
|
16
|
+
summary: '(e: React.ChangeEvent<HTMLInputElement>) => void',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
action: 'onChange',
|
|
20
|
+
},
|
|
21
|
+
label: {
|
|
22
|
+
control: 'text',
|
|
23
|
+
description: 'The label for the input field',
|
|
24
|
+
table: {
|
|
25
|
+
category: 'Props',
|
|
26
|
+
type: {
|
|
27
|
+
summary: 'string',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultValue: { summary: '' },
|
|
31
|
+
},
|
|
32
|
+
name: {
|
|
33
|
+
control: 'text',
|
|
34
|
+
description: 'The name for the input field',
|
|
35
|
+
table: {
|
|
36
|
+
category: 'Props',
|
|
37
|
+
type: {
|
|
38
|
+
summary: 'string',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
defaultValue: { summary: '' },
|
|
42
|
+
},
|
|
43
|
+
placeholder: {
|
|
44
|
+
control: 'text',
|
|
45
|
+
description: 'The placeholder for the input field',
|
|
46
|
+
table: {
|
|
47
|
+
category: 'Props',
|
|
48
|
+
type: {
|
|
49
|
+
summary: 'string',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
defaultValue: { summary: '' },
|
|
53
|
+
},
|
|
54
|
+
value: {
|
|
55
|
+
control: 'text',
|
|
56
|
+
description: 'The value for the input field',
|
|
57
|
+
table: {
|
|
58
|
+
category: 'Props',
|
|
59
|
+
type: {
|
|
60
|
+
summary: 'string',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
defaultValue: { summary: '' },
|
|
64
|
+
},
|
|
65
|
+
isRequired: {
|
|
66
|
+
control: 'boolean',
|
|
67
|
+
description: 'Toggles the required astherisc on the label',
|
|
68
|
+
table: {
|
|
69
|
+
category: 'Props',
|
|
70
|
+
type: {
|
|
71
|
+
summary: 'boolean',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
defaultValue: { summary: 'false' },
|
|
75
|
+
},
|
|
76
|
+
isDisabled: {
|
|
77
|
+
control: 'boolean',
|
|
78
|
+
description: 'Toggles the disabled state of the input',
|
|
79
|
+
table: {
|
|
80
|
+
category: 'Props',
|
|
81
|
+
type: {
|
|
82
|
+
summary: 'boolean',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
defaultValue: { summary: 'false' },
|
|
86
|
+
},
|
|
87
|
+
errorList: {
|
|
88
|
+
control: false,
|
|
89
|
+
description: 'An array of error messages',
|
|
90
|
+
table: {
|
|
91
|
+
category: 'Props',
|
|
92
|
+
type: {
|
|
93
|
+
summary: 'string[]',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
defaultValue: { summary: '[]' },
|
|
97
|
+
},
|
|
98
|
+
helpText: {
|
|
99
|
+
control: 'text',
|
|
100
|
+
description: 'The help text for the input field',
|
|
101
|
+
table: {
|
|
102
|
+
category: 'Props',
|
|
103
|
+
type: {
|
|
104
|
+
summary: 'string',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
defaultValue: { summary: '' },
|
|
108
|
+
},
|
|
109
|
+
hasHiddenLabel: {
|
|
110
|
+
control: 'boolean',
|
|
111
|
+
description: 'Hides the label visually (retains it for screen readers)',
|
|
112
|
+
table: {
|
|
113
|
+
category: 'Props',
|
|
114
|
+
type: {
|
|
115
|
+
summary: 'boolean',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
defaultValue: { summary: 'false' },
|
|
119
|
+
},
|
|
120
|
+
iconName: {
|
|
121
|
+
control: 'select',
|
|
122
|
+
options: iconNames,
|
|
123
|
+
description: 'Adds an icon to the left hand side of the input field',
|
|
124
|
+
table: {
|
|
125
|
+
category: 'Props',
|
|
126
|
+
type: {
|
|
127
|
+
summary: 'string',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
defaultValue: { summary: '' },
|
|
131
|
+
},
|
|
132
|
+
isClearable: {
|
|
133
|
+
control: 'boolean',
|
|
134
|
+
description: 'Adds a clear x icon to the right hand side of the input field',
|
|
135
|
+
table: {
|
|
136
|
+
category: 'Props',
|
|
137
|
+
type: {
|
|
138
|
+
summary: 'boolean',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
defaultValue: { summary: 'false' },
|
|
142
|
+
},
|
|
143
|
+
ref: {
|
|
144
|
+
table: {
|
|
145
|
+
disable: true,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export default meta;
|
|
152
|
+
|
|
153
|
+
type Story = StoryObj<typeof Input>;
|
|
154
|
+
|
|
155
|
+
const defaultArgs = {
|
|
156
|
+
label: 'Enter your name',
|
|
157
|
+
name: 'first_name',
|
|
158
|
+
placeholder: 'Please enter a value',
|
|
159
|
+
} as InputProps;
|
|
160
|
+
|
|
161
|
+
export const Default: Story = {
|
|
162
|
+
args: {
|
|
163
|
+
isRequired: true,
|
|
164
|
+
iconName: 'user',
|
|
165
|
+
helpText: 'This Is Help Text',
|
|
166
|
+
isClearable: true,
|
|
167
|
+
label: 'Label Name',
|
|
168
|
+
name: 'firsT_name',
|
|
169
|
+
placeholder: 'Please enter a value',
|
|
170
|
+
hasHiddenLabel: false,
|
|
171
|
+
isDisabled: false,
|
|
172
|
+
errorList: [],
|
|
173
|
+
value: '',
|
|
174
|
+
},
|
|
175
|
+
render: (args) => {
|
|
176
|
+
const [value, setValue] = useState('');
|
|
177
|
+
return (
|
|
178
|
+
<Input
|
|
179
|
+
{...args}
|
|
180
|
+
value={value}
|
|
181
|
+
onChange={(e) => {
|
|
182
|
+
setValue(e.target.value);
|
|
183
|
+
}}
|
|
184
|
+
/>
|
|
185
|
+
);
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const Errors: Story = {
|
|
190
|
+
args: {
|
|
191
|
+
...defaultArgs,
|
|
192
|
+
errorList: ['You require a username value.'],
|
|
193
|
+
},
|
|
194
|
+
render: (args) => {
|
|
195
|
+
const [value, setValue] = useState('');
|
|
196
|
+
return (
|
|
197
|
+
<Input
|
|
198
|
+
{...args}
|
|
199
|
+
value={value}
|
|
200
|
+
onChange={(e) => {
|
|
201
|
+
setValue(e.target.value);
|
|
202
|
+
}}
|
|
203
|
+
/>
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const HiddenLabel: Story = {
|
|
209
|
+
args: {
|
|
210
|
+
...defaultArgs,
|
|
211
|
+
hasHiddenLabel: true,
|
|
212
|
+
},
|
|
213
|
+
render: (args) => {
|
|
214
|
+
const [value, setValue] = useState('');
|
|
215
|
+
return (
|
|
216
|
+
<Input
|
|
217
|
+
{...args}
|
|
218
|
+
value={value}
|
|
219
|
+
onChange={(e) => {
|
|
220
|
+
setValue(e.target.value);
|
|
221
|
+
}}
|
|
222
|
+
/>
|
|
223
|
+
);
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export const HelpText: Story = {
|
|
228
|
+
args: {
|
|
229
|
+
...defaultArgs,
|
|
230
|
+
helpText: 'In order to submit the form, this field is required.',
|
|
231
|
+
},
|
|
232
|
+
render: (args) => {
|
|
233
|
+
const [value, setValue] = useState('');
|
|
234
|
+
return (
|
|
235
|
+
<Input
|
|
236
|
+
{...args}
|
|
237
|
+
value={value}
|
|
238
|
+
onChange={(e) => {
|
|
239
|
+
setValue(e.target.value);
|
|
240
|
+
}}
|
|
241
|
+
/>
|
|
242
|
+
);
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export const Clearable: Story = {
|
|
247
|
+
args: {
|
|
248
|
+
...defaultArgs,
|
|
249
|
+
isClearable: true,
|
|
250
|
+
},
|
|
251
|
+
render: (args) => {
|
|
252
|
+
const [value, setValue] = useState('');
|
|
253
|
+
return (
|
|
254
|
+
<Input
|
|
255
|
+
{...args}
|
|
256
|
+
value={value}
|
|
257
|
+
onChange={(e) => {
|
|
258
|
+
setValue(e.target.value);
|
|
259
|
+
}}
|
|
260
|
+
/>
|
|
261
|
+
);
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export const Icon: Story = {
|
|
266
|
+
args: {
|
|
267
|
+
...defaultArgs,
|
|
268
|
+
iconName: 'user',
|
|
269
|
+
},
|
|
270
|
+
render: (args) => {
|
|
271
|
+
const [value, setValue] = useState('');
|
|
272
|
+
return (
|
|
273
|
+
<Input
|
|
274
|
+
{...args}
|
|
275
|
+
value={value}
|
|
276
|
+
onChange={(e) => {
|
|
277
|
+
setValue(e.target.value);
|
|
278
|
+
}}
|
|
279
|
+
/>
|
|
280
|
+
);
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
export const Required: Story = {
|
|
285
|
+
args: {
|
|
286
|
+
...defaultArgs,
|
|
287
|
+
isRequired: true,
|
|
288
|
+
},
|
|
289
|
+
render: (args) => {
|
|
290
|
+
const [value, setValue] = useState('');
|
|
291
|
+
return (
|
|
292
|
+
<Input
|
|
293
|
+
{...args}
|
|
294
|
+
value={value}
|
|
295
|
+
onChange={(e) => {
|
|
296
|
+
setValue(e.target.value);
|
|
297
|
+
}}
|
|
298
|
+
/>
|
|
299
|
+
);
|
|
300
|
+
},
|
|
301
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Icon } from '@/components/icons';
|
|
3
|
+
import { IconName } from '@/types';
|
|
4
|
+
import { Label } from '../subcomponents/Label';
|
|
5
|
+
import { ErrorList } from '../subcomponents/ErrorList';
|
|
6
|
+
|
|
7
|
+
export interface InputProps {
|
|
8
|
+
ref?: React.LegacyRef<HTMLInputElement>;
|
|
9
|
+
label: string;
|
|
10
|
+
name: string;
|
|
11
|
+
placeholder: string;
|
|
12
|
+
value: string;
|
|
13
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
14
|
+
isRequired?: boolean;
|
|
15
|
+
isDisabled?: boolean;
|
|
16
|
+
errorList?: string[];
|
|
17
|
+
helpText?: string;
|
|
18
|
+
hasHiddenLabel?: boolean;
|
|
19
|
+
iconName?: IconName;
|
|
20
|
+
isClearable?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const Input = ({
|
|
24
|
+
ref,
|
|
25
|
+
label,
|
|
26
|
+
name,
|
|
27
|
+
placeholder,
|
|
28
|
+
value,
|
|
29
|
+
onChange,
|
|
30
|
+
isRequired,
|
|
31
|
+
isDisabled,
|
|
32
|
+
errorList,
|
|
33
|
+
helpText,
|
|
34
|
+
iconName,
|
|
35
|
+
hasHiddenLabel,
|
|
36
|
+
isClearable,
|
|
37
|
+
...rest
|
|
38
|
+
}: InputProps) => {
|
|
39
|
+
const hasErrors = errorList && errorList.length > 0;
|
|
40
|
+
const handleClear = () => {
|
|
41
|
+
onChange({ target: { value: '' } } as React.ChangeEvent<HTMLInputElement>);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="form-control">
|
|
46
|
+
<Label label={label} name={name} isRequired={isRequired} hasHiddenLabel={hasHiddenLabel} />
|
|
47
|
+
<div className="input-wrapper">
|
|
48
|
+
{iconName && (
|
|
49
|
+
<Icon name={iconName} data-testid={`${name}-embedded-icon`} className="embedded-icon" />
|
|
50
|
+
)}
|
|
51
|
+
<input
|
|
52
|
+
ref={ref}
|
|
53
|
+
data-testid={`form-input-${name}`}
|
|
54
|
+
name={name}
|
|
55
|
+
type="text"
|
|
56
|
+
disabled={isDisabled}
|
|
57
|
+
required={isRequired}
|
|
58
|
+
placeholder={placeholder}
|
|
59
|
+
value={value}
|
|
60
|
+
onChange={onChange}
|
|
61
|
+
className={`input ${hasErrors ? 'error' : ''} ${iconName ? 'input--has-icon' : ''}`}
|
|
62
|
+
aria-invalid={hasErrors}
|
|
63
|
+
aria-describedby={hasErrors || helpText ? `${name}-helper` : undefined}
|
|
64
|
+
aria-required={isRequired}
|
|
65
|
+
aria-label={label}
|
|
66
|
+
{...rest}
|
|
67
|
+
/>
|
|
68
|
+
{isClearable && (
|
|
69
|
+
<Icon
|
|
70
|
+
name="x-close"
|
|
71
|
+
data-testid={`${name}-clearable-icon`}
|
|
72
|
+
size="sm"
|
|
73
|
+
onClick={handleClear}
|
|
74
|
+
className="clearable-icon"
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
{hasErrors && <ErrorList errorList={errorList} name={name} />}
|
|
79
|
+
{helpText && (
|
|
80
|
+
<div data-testid={`${name}-help-text`} className="help-text" id={`${name}-helper`}>
|
|
81
|
+
{helpText}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { render, screen, act } from '@testing-library/react';
|
|
2
|
+
import { Input } from '@/components/forms/input/Input';
|
|
3
|
+
import { ChangeEvent } from 'react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
|
|
6
|
+
const handleOnChange = jest.fn();
|
|
7
|
+
|
|
8
|
+
describe('Input', () => {
|
|
9
|
+
it('renders the input field', () => {
|
|
10
|
+
render(
|
|
11
|
+
<Input
|
|
12
|
+
isRequired={true}
|
|
13
|
+
label="Enter your name"
|
|
14
|
+
helpText="In order to submit the form, this field is required."
|
|
15
|
+
name="name"
|
|
16
|
+
placeholder="Please enter a value"
|
|
17
|
+
iconName="user"
|
|
18
|
+
isClearable={true}
|
|
19
|
+
ref={undefined}
|
|
20
|
+
value={''}
|
|
21
|
+
onChange={handleOnChange}
|
|
22
|
+
/>,
|
|
23
|
+
);
|
|
24
|
+
expect(screen.getByText('Enter your name')).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
it('shows an x when the text is clearable', () => {
|
|
27
|
+
render(
|
|
28
|
+
<Input
|
|
29
|
+
isRequired={true}
|
|
30
|
+
label="Enter your name"
|
|
31
|
+
helpText="In order to submit the form, this field is required."
|
|
32
|
+
name="name"
|
|
33
|
+
placeholder="Please enter a value"
|
|
34
|
+
iconName="user"
|
|
35
|
+
isClearable={true}
|
|
36
|
+
ref={undefined}
|
|
37
|
+
value={''}
|
|
38
|
+
onChange={handleOnChange}
|
|
39
|
+
/>,
|
|
40
|
+
);
|
|
41
|
+
const icon = screen.getByTestId('name-clearable-icon');
|
|
42
|
+
expect(icon).toBeInTheDocument();
|
|
43
|
+
expect(icon).toBeVisible();
|
|
44
|
+
});
|
|
45
|
+
it('does not show an x when the text is not clearable', () => {
|
|
46
|
+
render(
|
|
47
|
+
<Input
|
|
48
|
+
isRequired={true}
|
|
49
|
+
label="Enter your name"
|
|
50
|
+
helpText="In order to submit the form, this field is required."
|
|
51
|
+
name="name"
|
|
52
|
+
placeholder="Please enter a value"
|
|
53
|
+
iconName="user"
|
|
54
|
+
isClearable={false}
|
|
55
|
+
ref={undefined}
|
|
56
|
+
value={''}
|
|
57
|
+
onChange={handleOnChange}
|
|
58
|
+
/>,
|
|
59
|
+
);
|
|
60
|
+
const icon = screen.queryByTestId('name-clearable-icon');
|
|
61
|
+
expect(icon).not.toBeInTheDocument();
|
|
62
|
+
expect(icon).toBe(null);
|
|
63
|
+
});
|
|
64
|
+
it('clicking on the x clears the value', async () => {
|
|
65
|
+
render(
|
|
66
|
+
<Input
|
|
67
|
+
isRequired={true}
|
|
68
|
+
label="Enter your name"
|
|
69
|
+
helpText="In order to submit the form, this field is required."
|
|
70
|
+
name="name"
|
|
71
|
+
placeholder="Please enter a value"
|
|
72
|
+
iconName="user"
|
|
73
|
+
isClearable={true}
|
|
74
|
+
ref={undefined}
|
|
75
|
+
value={'test'}
|
|
76
|
+
onChange={handleOnChange}
|
|
77
|
+
/>,
|
|
78
|
+
);
|
|
79
|
+
const input = screen.getByTestId('form-input-name');
|
|
80
|
+
const icon = screen.getByTestId('name-clearable-icon');
|
|
81
|
+
expect(input).toHaveValue('test');
|
|
82
|
+
await userEvent.click(icon);
|
|
83
|
+
expect(handleOnChange).toHaveBeenCalledWith({
|
|
84
|
+
target: { value: '' },
|
|
85
|
+
} as ChangeEvent<HTMLInputElement>);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('it renders an icon on the left when one exists', () => {
|
|
89
|
+
render(
|
|
90
|
+
<Input
|
|
91
|
+
isRequired={true}
|
|
92
|
+
label="Enter your name"
|
|
93
|
+
helpText="In order to submit the form, this field is required."
|
|
94
|
+
name="name"
|
|
95
|
+
placeholder="Please enter a value"
|
|
96
|
+
iconName="user"
|
|
97
|
+
isClearable={true}
|
|
98
|
+
ref={undefined}
|
|
99
|
+
value={'test'}
|
|
100
|
+
onChange={handleOnChange}
|
|
101
|
+
/>,
|
|
102
|
+
);
|
|
103
|
+
const icon = screen.getByTestId('name-embedded-icon');
|
|
104
|
+
expect(icon).toBeInTheDocument();
|
|
105
|
+
expect(icon).toBeVisible();
|
|
106
|
+
});
|
|
107
|
+
it('it does not render an embedded icon when one does not exist', () => {
|
|
108
|
+
render(
|
|
109
|
+
<Input
|
|
110
|
+
isRequired={true}
|
|
111
|
+
label="Enter your name"
|
|
112
|
+
helpText="In order to submit the form, this field is required."
|
|
113
|
+
name="name"
|
|
114
|
+
placeholder="Please enter a value"
|
|
115
|
+
isClearable={true}
|
|
116
|
+
ref={undefined}
|
|
117
|
+
value={'test'}
|
|
118
|
+
onChange={handleOnChange}
|
|
119
|
+
/>,
|
|
120
|
+
);
|
|
121
|
+
const icon = screen.queryByTestId('name-embedded-icon');
|
|
122
|
+
expect(icon).not.toBeInTheDocument();
|
|
123
|
+
expect(icon).toBe(null);
|
|
124
|
+
});
|
|
125
|
+
it('adds the error class when errors exist', () => {
|
|
126
|
+
render(
|
|
127
|
+
<Input
|
|
128
|
+
isRequired={true}
|
|
129
|
+
errorList={['You require a username value.']}
|
|
130
|
+
label="Enter your name"
|
|
131
|
+
helpText="In order to submit the form, this field is required."
|
|
132
|
+
name="name"
|
|
133
|
+
placeholder="Please enter a value"
|
|
134
|
+
isClearable={true}
|
|
135
|
+
ref={undefined}
|
|
136
|
+
value={'test'}
|
|
137
|
+
onChange={handleOnChange}
|
|
138
|
+
/>,
|
|
139
|
+
);
|
|
140
|
+
const input = screen.getByTestId('form-input-name');
|
|
141
|
+
expect(input).toHaveClass('error');
|
|
142
|
+
});
|
|
143
|
+
it('does not highlight the input when no errors exist', () => {
|
|
144
|
+
render(
|
|
145
|
+
<Input
|
|
146
|
+
isRequired={true}
|
|
147
|
+
label="Enter your name"
|
|
148
|
+
helpText="In order to submit the form, this field is required."
|
|
149
|
+
name="name"
|
|
150
|
+
placeholder="Please enter a value"
|
|
151
|
+
isClearable={true}
|
|
152
|
+
ref={undefined}
|
|
153
|
+
value={'test'}
|
|
154
|
+
onChange={handleOnChange}
|
|
155
|
+
/>,
|
|
156
|
+
);
|
|
157
|
+
const input = screen.getByTestId('form-input-name');
|
|
158
|
+
expect(input).not.toHaveClass('error');
|
|
159
|
+
});
|
|
160
|
+
it('renders help text when help text exists', () => {
|
|
161
|
+
render(
|
|
162
|
+
<Input
|
|
163
|
+
isRequired={true}
|
|
164
|
+
label="Enter your name"
|
|
165
|
+
helpText="In order to submit the form, this field is required."
|
|
166
|
+
name="name"
|
|
167
|
+
placeholder="Please enter a value"
|
|
168
|
+
isClearable={true}
|
|
169
|
+
ref={undefined}
|
|
170
|
+
value={'test'}
|
|
171
|
+
onChange={handleOnChange}
|
|
172
|
+
/>,
|
|
173
|
+
);
|
|
174
|
+
const helpText = screen.getByText('In order to submit the form, this field is required.');
|
|
175
|
+
expect(helpText).toBeInTheDocument();
|
|
176
|
+
expect(helpText).toBeVisible();
|
|
177
|
+
});
|
|
178
|
+
it('does not render help text when help text does not exist', () => {
|
|
179
|
+
render(
|
|
180
|
+
<Input
|
|
181
|
+
isRequired={true}
|
|
182
|
+
label="Enter your name"
|
|
183
|
+
name="name"
|
|
184
|
+
placeholder="Please enter a value"
|
|
185
|
+
isClearable={true}
|
|
186
|
+
ref={undefined}
|
|
187
|
+
value={'test'}
|
|
188
|
+
onChange={handleOnChange}
|
|
189
|
+
/>,
|
|
190
|
+
);
|
|
191
|
+
const helpText = screen.queryByTestId('name-help-text');
|
|
192
|
+
expect(helpText).not.toBeInTheDocument();
|
|
193
|
+
expect(helpText).toBeNull();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('emits the value when user types', async () => {
|
|
197
|
+
const handleOnChange = jest.fn();
|
|
198
|
+
render(
|
|
199
|
+
<Input
|
|
200
|
+
isRequired={true}
|
|
201
|
+
label="Enter your name"
|
|
202
|
+
name="name"
|
|
203
|
+
placeholder="Please enter a value"
|
|
204
|
+
ref={undefined}
|
|
205
|
+
value={''}
|
|
206
|
+
onChange={handleOnChange}
|
|
207
|
+
/>,
|
|
208
|
+
);
|
|
209
|
+
const input = screen.getByTestId('form-input-name');
|
|
210
|
+
await userEvent.type(input, 't');
|
|
211
|
+
expect(handleOnChange).toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Input } from './Input';
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Common Variables
|
|
2
|
+
:root,
|
|
3
|
+
:root [data-theme='light'],
|
|
4
|
+
:root [data-theme='dark'] {
|
|
5
|
+
// Typography
|
|
6
|
+
--pf-input-background-color: var(--pf-white-color);
|
|
7
|
+
--pf-input-border-color: var(--pf-gray-color);
|
|
8
|
+
--pf-input-text-color: var(--pf-gray-color);
|
|
9
|
+
--pf-input-placeholder-text-color: var(--pf-gray-color-300);
|
|
10
|
+
--pf-input-help-text-color: var(--pf-gray-color-400);
|
|
11
|
+
|
|
12
|
+
// input Radius
|
|
13
|
+
--pf-input-rounded: var(--pf-rounded);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Dark Theme Specific Variables
|
|
17
|
+
:root [data-theme='dark'] {
|
|
18
|
+
--pf-input-background-color: var(--pf-primary-color);
|
|
19
|
+
--pf-input-border-color: var(--pf-gray-color-100);
|
|
20
|
+
--pf-input-text-color: var(--pf-gray-color-100);
|
|
21
|
+
--pf-input-placeholder-text-color: var(--pf-gray-color);
|
|
22
|
+
--pf-input-help-text-color: var(--pf-gray-color-200);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.input {
|
|
26
|
+
background-color: var(--pf-input-background-color);
|
|
27
|
+
border: 1px solid var(--pf-input-border-color);
|
|
28
|
+
border-radius: var(--pf-input-rounded);
|
|
29
|
+
color: var(--pf-input-text-color);
|
|
30
|
+
padding: 10px;
|
|
31
|
+
width: 100%;
|
|
32
|
+
box-sizing: border-box;
|
|
33
|
+
height: 36px;
|
|
34
|
+
&::placeholder {
|
|
35
|
+
color: var(--pf-input-placeholder-text-color);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&:focus {
|
|
39
|
+
border-color: var(--pf-primary-color);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&.error {
|
|
43
|
+
border-color: var(--pf-error-color);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&.success {
|
|
47
|
+
border-color: var(--pf-success-color);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
&.warning {
|
|
51
|
+
border-color: var(--pf-warning-color);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&.info {
|
|
55
|
+
border-color: var(--pf-info-color);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
&:disabled {
|
|
59
|
+
background-color: var(--pf-gray-color-100);
|
|
60
|
+
border-color: var(--pf-gray-color-300);
|
|
61
|
+
color: var(--pf-gray-color-400);
|
|
62
|
+
}
|
|
63
|
+
&--has-icon {
|
|
64
|
+
padding-left: var(--pf-padding-7);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.form-control {
|
|
69
|
+
.error-list {
|
|
70
|
+
list-style: none;
|
|
71
|
+
padding: 0;
|
|
72
|
+
margin: 0;
|
|
73
|
+
margin-top: var(--pf-margin-2);
|
|
74
|
+
margin-bottom: var(--pf-margin-2);
|
|
75
|
+
color: var(--pf-error-color);
|
|
76
|
+
}
|
|
77
|
+
.help-text {
|
|
78
|
+
margin-top: var(--pf-margin-2);
|
|
79
|
+
margin-bottom: var(--pf-margin-2);
|
|
80
|
+
color: var(--pf-input-help-text-color);
|
|
81
|
+
font-size: var(--pf-font-size-subtitle2);
|
|
82
|
+
}
|
|
83
|
+
.input-wrapper {
|
|
84
|
+
position: relative;
|
|
85
|
+
.embedded-icon {
|
|
86
|
+
position: absolute;
|
|
87
|
+
top: 10px;
|
|
88
|
+
left: var(--pf-margin-2);
|
|
89
|
+
color: var(--pf-input-text-color);
|
|
90
|
+
}
|
|
91
|
+
.clearable-icon {
|
|
92
|
+
position: absolute;
|
|
93
|
+
top: var(--pf-margin-3);
|
|
94
|
+
right: var(--pf-margin-2);
|
|
95
|
+
color: var(--pf-input-text-color);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
.is-visually-hidden {
|
|
99
|
+
position: absolute;
|
|
100
|
+
width: 1px;
|
|
101
|
+
height: 1px;
|
|
102
|
+
padding: 0;
|
|
103
|
+
margin: -1px;
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
clip: rect(0, 0, 0, 0);
|
|
106
|
+
white-space: nowrap;
|
|
107
|
+
border: 0;
|
|
108
|
+
}
|
|
109
|
+
.form-label {
|
|
110
|
+
margin-bottom: var(--pf-margin-2);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface ErrorListProps {
|
|
2
|
+
name: string;
|
|
3
|
+
errorList: string[];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const ErrorList = ({ errorList, name }: ErrorListProps) => {
|
|
7
|
+
return (
|
|
8
|
+
<ul className="error-list" id={`${name}-helper`}>
|
|
9
|
+
{(errorList ?? []).map((error, index) => (
|
|
10
|
+
<li key={index}>{error}</li>
|
|
11
|
+
))}
|
|
12
|
+
</ul>
|
|
13
|
+
);
|
|
14
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface LabelProps {
|
|
2
|
+
label: string;
|
|
3
|
+
name: string;
|
|
4
|
+
isRequired?: boolean;
|
|
5
|
+
hasHiddenLabel?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const Label = ({ label, name, isRequired, hasHiddenLabel }: LabelProps) => {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
data-testid={`${name}-testId`}
|
|
12
|
+
className={`form-label ${hasHiddenLabel ? 'is-visually-hidden' : ''}`}
|
|
13
|
+
>
|
|
14
|
+
<label htmlFor={`${name}`}>
|
|
15
|
+
{label}
|
|
16
|
+
{isRequired ? <span className="text-error"> *</span> : ''}
|
|
17
|
+
</label>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { ErrorList } from '@/components/forms/subcomponents/ErrorList';
|
|
3
|
+
|
|
4
|
+
describe('ErrorList', () => {
|
|
5
|
+
it('renders the error list', () => {
|
|
6
|
+
render(<ErrorList name="name" errorList={['error1', 'error2']} />);
|
|
7
|
+
expect(screen.getByText('error1')).toBeInTheDocument();
|
|
8
|
+
expect(screen.getByText('error2')).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('does not render the error list when it is empty', () => {
|
|
12
|
+
render(<ErrorList name="name" errorList={[]} />);
|
|
13
|
+
expect(screen.queryByText('error1')).not.toBeInTheDocument();
|
|
14
|
+
expect(screen.queryByText('error2')).not.toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { Label } from '@/components/forms/subcomponents/Label';
|
|
3
|
+
|
|
4
|
+
describe('Label', () => {
|
|
5
|
+
it('renders the `required` astherisc when the label is required', () => {
|
|
6
|
+
render(<Label label={'name'} name={'name'} isRequired={true} hasHiddenLabel={false} />);
|
|
7
|
+
expect(screen.getByText('*')).toBeInTheDocument();
|
|
8
|
+
expect(screen.getByText('*')).toBeVisible();
|
|
9
|
+
});
|
|
10
|
+
it('does not render the `required` astherisc when the label is not required', () => {
|
|
11
|
+
render(<Label label={'name'} name={'name'} isRequired={false} hasHiddenLabel={false} />);
|
|
12
|
+
expect(screen.queryByText('*')).not.toBeInTheDocument();
|
|
13
|
+
expect(screen.queryByText('*')).toBeNull();
|
|
14
|
+
});
|
|
15
|
+
it('renders the label for screen readers while hidden visually', () => {
|
|
16
|
+
render(<Label label={'name'} name={'name'} isRequired={false} hasHiddenLabel={true} />);
|
|
17
|
+
expect(screen.getByText('name')).toBeInTheDocument();
|
|
18
|
+
expect(screen.getByText('name')).toBeVisible();
|
|
19
|
+
});
|
|
20
|
+
it('renders the label text', () => {
|
|
21
|
+
render(<Label label={'name'} name={'name'} isRequired={false} hasHiddenLabel={false} />);
|
|
22
|
+
const label = screen.getByTestId('name-testId');
|
|
23
|
+
expect(label).toBeInTheDocument();
|
|
24
|
+
expect(label).toBeVisible();
|
|
25
|
+
expect(label).not.toHaveClass('is-visually-hidden');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('renders the label text when the label is hidden', () => {
|
|
29
|
+
render(<Label label={'name'} name={'name'} isRequired={false} hasHiddenLabel={true} />);
|
|
30
|
+
const label = screen.getByTestId('name-testId');
|
|
31
|
+
expect(label).toHaveClass('is-visually-hidden');
|
|
32
|
+
});
|
|
33
|
+
});
|
package/src/components/index.ts
CHANGED
|
@@ -5,14 +5,17 @@ html {
|
|
|
5
5
|
font-family: var(--pf-font-family-base);
|
|
6
6
|
}
|
|
7
7
|
// Font Sizing and Family
|
|
8
|
-
@each $tag,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
@each $tag,
|
|
9
|
+
$size
|
|
10
|
+
in (
|
|
11
|
+
h1: --pf-font-size-h1,
|
|
12
|
+
h2: --pf-font-size-h2,
|
|
13
|
+
h3: --pf-font-size-h3,
|
|
14
|
+
h4: --pf-font-size-h4,
|
|
15
|
+
h5: --pf-font-size-h5,
|
|
16
|
+
p: --pf-font-size-body
|
|
17
|
+
)
|
|
18
|
+
{
|
|
16
19
|
#{$tag},
|
|
17
20
|
.text-#{$tag} {
|
|
18
21
|
font-size: var(#{$size});
|
|
@@ -21,9 +24,8 @@ html {
|
|
|
21
24
|
padding: 0;
|
|
22
25
|
|
|
23
26
|
// If the tag is not 'p', make it bold
|
|
24
|
-
@if $tag !='p' {
|
|
27
|
+
@if $tag != 'p' {
|
|
25
28
|
font-weight: var(--pf-font-weight-bold);
|
|
26
|
-
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
}
|
|
@@ -46,7 +48,6 @@ html {
|
|
|
46
48
|
font-size: var(--pf-font-size-overine);
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
|
|
50
51
|
// Transform
|
|
51
52
|
.text-capitalize {
|
|
52
53
|
text-transform: capitalize;
|
|
@@ -114,3 +115,20 @@ html {
|
|
|
114
115
|
.text-break--word {
|
|
115
116
|
overflow-wrap: break-word;
|
|
116
117
|
}
|
|
118
|
+
|
|
119
|
+
// Text Colors
|
|
120
|
+
.text-error {
|
|
121
|
+
color: var(--pf-error-color);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.text-warning {
|
|
125
|
+
color: var(--pf-warning-color);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.text-success {
|
|
129
|
+
color: var(--pf-success-color);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.text-info {
|
|
133
|
+
color: var(--pf-info-color);
|
|
134
|
+
}
|
package/src/styles/index.scss
CHANGED