@thecb/components 11.3.5 → 11.4.1-beta.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/dist/index.cjs.js +187 -34
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.esm.js +187 -35
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/.DS_Store +0 -0
- package/src/components/.DS_Store +0 -0
- package/src/components/atoms/.DS_Store +0 -0
- package/src/components/atoms/form-layouts/FormTextarea.js +208 -0
- package/src/components/atoms/form-layouts/FormTextarea.mdx +48 -0
- package/src/components/atoms/form-layouts/FormTextarea.stories.js +265 -0
- package/src/components/atoms/form-layouts/index.d.ts +28 -0
- package/src/components/atoms/form-layouts/index.js +3 -1
- package/src/components/molecules/tabs/Tabs.js +18 -9
- package/src/components/molecules/tabs/index.d.ts +21 -0
package/package.json
CHANGED
package/src/.DS_Store
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import styled, { css } from "styled-components";
|
|
3
|
+
import { fallbackValues } from "./FormLayouts.theme.js";
|
|
4
|
+
import { themeComponent } from "../../../util/themeUtils";
|
|
5
|
+
import { createIdFromString } from "../../../util/general.js";
|
|
6
|
+
import Text from "../text";
|
|
7
|
+
import { Box, Cluster, Stack } from "../layouts";
|
|
8
|
+
import { FONT_WEIGHT_REGULAR } from "../../../constants/style_constants";
|
|
9
|
+
import { ERROR_COLOR, ROYAL_BLUE } from "../../../constants/colors";
|
|
10
|
+
|
|
11
|
+
const TextareaField = styled.textarea`
|
|
12
|
+
border: 1px solid
|
|
13
|
+
${({ field, showErrors, themeValues }) =>
|
|
14
|
+
(field.dirty && field.hasErrors) || (field.hasErrors && showErrors)
|
|
15
|
+
? ERROR_COLOR
|
|
16
|
+
: themeValues.borderColor};
|
|
17
|
+
border-radius: 2px;
|
|
18
|
+
height: ${({ $customHeight }) => ($customHeight ? $customHeight : "auto")};
|
|
19
|
+
width: 100%;
|
|
20
|
+
padding: 1rem;
|
|
21
|
+
min-width: 100px;
|
|
22
|
+
margin: 0;
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
position: relative;
|
|
25
|
+
font-size: 1.1rem;
|
|
26
|
+
font-family: Public Sans;
|
|
27
|
+
line-height: 1.5rem;
|
|
28
|
+
font-weight: ${FONT_WEIGHT_REGULAR};
|
|
29
|
+
background-color: ${({ themeValues }) =>
|
|
30
|
+
themeValues.inputBackgroundColor && themeValues.inputBackgroundColor};
|
|
31
|
+
color: ${({ themeValues }) => themeValues.color && themeValues.color};
|
|
32
|
+
box-shadow: none;
|
|
33
|
+
resize: ${({ resize }) => resize || "vertical"};
|
|
34
|
+
transition: background 0.3s ease;
|
|
35
|
+
|
|
36
|
+
&:focus {
|
|
37
|
+
outline: 3px solid ${ROYAL_BLUE};
|
|
38
|
+
outline-offset: 2px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
${({ disabled }) =>
|
|
42
|
+
disabled &&
|
|
43
|
+
css`
|
|
44
|
+
color: #6e727e;
|
|
45
|
+
background-color: #f7f7f7;
|
|
46
|
+
`}
|
|
47
|
+
|
|
48
|
+
${({ $extraStyles }) =>
|
|
49
|
+
css`
|
|
50
|
+
${$extraStyles}
|
|
51
|
+
`}
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const FormTextarea = ({
|
|
55
|
+
ariaLabelledBy = undefined,
|
|
56
|
+
labelDisplayOverride = null,
|
|
57
|
+
labelTextWhenNoError = "",
|
|
58
|
+
errorMessages,
|
|
59
|
+
helperModal = false,
|
|
60
|
+
field,
|
|
61
|
+
fieldActions,
|
|
62
|
+
showErrors,
|
|
63
|
+
themeValues,
|
|
64
|
+
customHeight,
|
|
65
|
+
extraStyles,
|
|
66
|
+
removeFromValue, // regex of characters to remove before setting value
|
|
67
|
+
dataQa = null,
|
|
68
|
+
isRequired = false,
|
|
69
|
+
errorFieldExtraStyles,
|
|
70
|
+
showFieldErrorRow = true,
|
|
71
|
+
labelTextVariant = "pS",
|
|
72
|
+
errorTextVariant = "pXS",
|
|
73
|
+
resize = "vertical", // none, horizontal, vertical, both
|
|
74
|
+
rows = 5,
|
|
75
|
+
cols,
|
|
76
|
+
placeholder,
|
|
77
|
+
maxLength,
|
|
78
|
+
...props
|
|
79
|
+
}) => {
|
|
80
|
+
const setValue = value => {
|
|
81
|
+
if (removeFromValue !== undefined) {
|
|
82
|
+
return fieldActions.set(value.replace(removeFromValue, ""));
|
|
83
|
+
}
|
|
84
|
+
return fieldActions.set(value);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Stack childGap="0.25rem">
|
|
89
|
+
<Box padding="0">
|
|
90
|
+
{helperModal ? (
|
|
91
|
+
<Cluster justify="space-between" align="center">
|
|
92
|
+
{labelDisplayOverride ? (
|
|
93
|
+
labelDisplayOverride
|
|
94
|
+
) : (
|
|
95
|
+
<Text
|
|
96
|
+
as="label"
|
|
97
|
+
color={themeValues.labelColor}
|
|
98
|
+
variant={labelTextVariant}
|
|
99
|
+
weight={themeValues.fontWeight}
|
|
100
|
+
extraStyles={`word-break: break-word;
|
|
101
|
+
font-family: Public Sans;
|
|
102
|
+
&::first-letter {
|
|
103
|
+
text-transform: uppercase;
|
|
104
|
+
}`}
|
|
105
|
+
id={createIdFromString(labelTextWhenNoError)}
|
|
106
|
+
>
|
|
107
|
+
{labelTextWhenNoError}
|
|
108
|
+
</Text>
|
|
109
|
+
)}
|
|
110
|
+
{helperModal()}
|
|
111
|
+
</Cluster>
|
|
112
|
+
) : (
|
|
113
|
+
<Box padding="0" minWidth="100%">
|
|
114
|
+
<Cluster justify="space-between" align="center">
|
|
115
|
+
{labelDisplayOverride ? (
|
|
116
|
+
labelDisplayOverride
|
|
117
|
+
) : (
|
|
118
|
+
<Text
|
|
119
|
+
as="label"
|
|
120
|
+
color={themeValues.labelColor}
|
|
121
|
+
variant={labelTextVariant}
|
|
122
|
+
fontWeight={themeValues.fontWeight}
|
|
123
|
+
extraStyles={`word-break: break-word;
|
|
124
|
+
font-family: Public Sans;
|
|
125
|
+
&::first-letter {
|
|
126
|
+
text-transform: uppercase;
|
|
127
|
+
}`}
|
|
128
|
+
id={createIdFromString(labelTextWhenNoError)}
|
|
129
|
+
>
|
|
130
|
+
{labelTextWhenNoError}
|
|
131
|
+
</Text>
|
|
132
|
+
)}
|
|
133
|
+
</Cluster>
|
|
134
|
+
</Box>
|
|
135
|
+
)}
|
|
136
|
+
</Box>
|
|
137
|
+
<Box padding="0">
|
|
138
|
+
<TextareaField
|
|
139
|
+
aria-labelledby={
|
|
140
|
+
ariaLabelledBy === undefined
|
|
141
|
+
? createIdFromString(labelTextWhenNoError)
|
|
142
|
+
: ariaLabelledBy
|
|
143
|
+
}
|
|
144
|
+
aria-describedby={createIdFromString(
|
|
145
|
+
labelTextWhenNoError,
|
|
146
|
+
"error message"
|
|
147
|
+
)}
|
|
148
|
+
aria-invalid={
|
|
149
|
+
(field.dirty && field.hasErrors) || (field.hasErrors && showErrors)
|
|
150
|
+
}
|
|
151
|
+
onChange={e => setValue(e.target.value)}
|
|
152
|
+
onBlur={e => handleOnBlur(e.target.value)}
|
|
153
|
+
value={field.rawValue}
|
|
154
|
+
field={field}
|
|
155
|
+
showErrors={showErrors}
|
|
156
|
+
themeValues={themeValues}
|
|
157
|
+
$customHeight={customHeight}
|
|
158
|
+
$extraStyles={extraStyles}
|
|
159
|
+
data-qa={dataQa || labelTextWhenNoError}
|
|
160
|
+
required={isRequired}
|
|
161
|
+
resize={resize}
|
|
162
|
+
rows={rows}
|
|
163
|
+
cols={cols}
|
|
164
|
+
placeholder={placeholder}
|
|
165
|
+
maxLength={maxLength}
|
|
166
|
+
{...props}
|
|
167
|
+
/>
|
|
168
|
+
</Box>
|
|
169
|
+
{showFieldErrorRow && (
|
|
170
|
+
<Stack
|
|
171
|
+
direction="row"
|
|
172
|
+
justify="space-between"
|
|
173
|
+
aria-live="polite"
|
|
174
|
+
aria-atomic={true}
|
|
175
|
+
>
|
|
176
|
+
{(field.hasErrors && field.dirty) ||
|
|
177
|
+
(field.hasErrors && showErrors) ? (
|
|
178
|
+
<Text
|
|
179
|
+
color={ERROR_COLOR}
|
|
180
|
+
variant={errorTextVariant}
|
|
181
|
+
weight={themeValues.fontWeight}
|
|
182
|
+
extraStyles={`word-break: break-word;
|
|
183
|
+
font-family: Public Sans;
|
|
184
|
+
&::first-letter {
|
|
185
|
+
text-transform: uppercase;
|
|
186
|
+
}
|
|
187
|
+
${errorFieldExtraStyles};`}
|
|
188
|
+
id={createIdFromString(labelTextWhenNoError, "error message")}
|
|
189
|
+
>
|
|
190
|
+
{errorMessages[field.errors[0]]}
|
|
191
|
+
</Text>
|
|
192
|
+
) : (
|
|
193
|
+
<Text
|
|
194
|
+
extraStyles={`height: ${themeValues.lineHeight}; ${errorFieldExtraStyles};`}
|
|
195
|
+
/>
|
|
196
|
+
)}
|
|
197
|
+
</Stack>
|
|
198
|
+
)}
|
|
199
|
+
</Stack>
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export default themeComponent(
|
|
204
|
+
FormTextarea,
|
|
205
|
+
"FormTextarea",
|
|
206
|
+
fallbackValues,
|
|
207
|
+
"default"
|
|
208
|
+
);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Canvas, Meta, Title, Story, Controls } from '@storybook/blocks';
|
|
2
|
+
|
|
3
|
+
import * as FormTextareaStories from './FormTextarea.stories.js';
|
|
4
|
+
|
|
5
|
+
<Meta of={FormTextareaStories} />
|
|
6
|
+
|
|
7
|
+
<Title />
|
|
8
|
+
|
|
9
|
+
FormTextarea is a wrapper for `<textarea/>` elements that adds extra functionality. It is meant to be used in forms for multi-line text input. The underlying component is a `textarea` element with additional form integration and styling. Additional props are passed down to the underlying element.
|
|
10
|
+
|
|
11
|
+
## Form Integration
|
|
12
|
+
|
|
13
|
+
FormTextarea requires a `field` and `fieldActions` prop. Both are objects that _can_ be generated with [redux-freeform](https://github.com/CityBaseInc/redux-freeform). Below are example values for each prop with the minimum properties needed for a FormTextarea component.
|
|
14
|
+
|
|
15
|
+
### field
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
{
|
|
19
|
+
"dirty": false,
|
|
20
|
+
"rawValue": "",
|
|
21
|
+
"errors": [
|
|
22
|
+
"error/REQUIRED"
|
|
23
|
+
],
|
|
24
|
+
"hasErrors": true
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### fieldActions
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
{
|
|
32
|
+
set: (value) => {...}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`fieldActions.set()` is called when the textarea value changes.
|
|
37
|
+
|
|
38
|
+
## Textarea-Specific Features
|
|
39
|
+
|
|
40
|
+
FormTextarea includes several features specific to textarea elements:
|
|
41
|
+
|
|
42
|
+
- **Resize Control**: Use the `resize` prop to control how users can resize the textarea (`"none"`, `"horizontal"`, `"vertical"`, or `"both"`)
|
|
43
|
+
- **Rows and Columns**: Set initial size with `rows` and `cols` props
|
|
44
|
+
- **Character Limit**: Use `maxLength` to limit the number of characters
|
|
45
|
+
- **Placeholder Text**: Provide helpful placeholder text with the `placeholder` prop
|
|
46
|
+
- **Custom Height**: Override the default height with the `customHeight` prop
|
|
47
|
+
|
|
48
|
+
<Controls />
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import FormTextarea from "./FormTextarea";
|
|
3
|
+
import { connect, Provider } from "react-redux";
|
|
4
|
+
import { createStore } from "redux";
|
|
5
|
+
import { createFormState, required } from "redux-freeform";
|
|
6
|
+
import Modal from "../../molecules/modal";
|
|
7
|
+
|
|
8
|
+
const { mapStateToProps, mapDispatchToProps, reducer } = createFormState({
|
|
9
|
+
example: {
|
|
10
|
+
validators: [required()]
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const store = createStore(
|
|
15
|
+
reducer,
|
|
16
|
+
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const errorMessages = {
|
|
20
|
+
[required.error]: "This is required!"
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const FormWrapper = props => (
|
|
24
|
+
<FormTextarea
|
|
25
|
+
{...props}
|
|
26
|
+
field={props.fields.example}
|
|
27
|
+
fieldActions={props.actions.fields.example}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const ConnectedFormTextarea = connect(
|
|
32
|
+
mapStateToProps,
|
|
33
|
+
mapDispatchToProps
|
|
34
|
+
)(FormWrapper);
|
|
35
|
+
|
|
36
|
+
export default {
|
|
37
|
+
title: "Atoms/form-layouts/FormTextarea",
|
|
38
|
+
component: ConnectedFormTextarea,
|
|
39
|
+
tags: ["!autodocs"],
|
|
40
|
+
parameters: {
|
|
41
|
+
layout: "centered",
|
|
42
|
+
controls: { expanded: true }
|
|
43
|
+
},
|
|
44
|
+
args: {
|
|
45
|
+
labelTextWhenNoError: "",
|
|
46
|
+
errorMessages: errorMessages,
|
|
47
|
+
helperModal: undefined,
|
|
48
|
+
showErrors: undefined,
|
|
49
|
+
themeValues: {},
|
|
50
|
+
customHeight: undefined,
|
|
51
|
+
extraStyles: undefined,
|
|
52
|
+
removeFromValue: undefined,
|
|
53
|
+
dataQa: "form-textarea-qa",
|
|
54
|
+
isRequired: false,
|
|
55
|
+
resize: "vertical",
|
|
56
|
+
rows: 5,
|
|
57
|
+
cols: undefined,
|
|
58
|
+
placeholder: "",
|
|
59
|
+
maxLength: undefined
|
|
60
|
+
},
|
|
61
|
+
argTypes: {
|
|
62
|
+
extraStyles: { type: "string" },
|
|
63
|
+
fieldActions: { type: "object" },
|
|
64
|
+
field: { type: "object" },
|
|
65
|
+
isRequired: {
|
|
66
|
+
description: "adds the `required` attribute to the textarea element",
|
|
67
|
+
table: {
|
|
68
|
+
type: { summary: "boolean" },
|
|
69
|
+
defaultValue: { summary: false }
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
customHeight: {
|
|
73
|
+
description: "sets a height to the textarea",
|
|
74
|
+
table: {
|
|
75
|
+
type: { summary: "string" },
|
|
76
|
+
defaultValue: { summary: "120px" }
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
helperModal: {
|
|
80
|
+
description: "a function that returns a modal",
|
|
81
|
+
table: {
|
|
82
|
+
type: { summary: "object" },
|
|
83
|
+
defaultValue: { summary: undefined }
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
removeFromValue: {
|
|
87
|
+
description:
|
|
88
|
+
"regex pattern for characters to remove from the user inputted value before passing it to `fieldActions.set()`",
|
|
89
|
+
table: {
|
|
90
|
+
type: { summary: "string" },
|
|
91
|
+
defaultValue: { summary: undefined }
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
extraStyles: {
|
|
95
|
+
description: "styles applied to the underlying textarea element",
|
|
96
|
+
table: {
|
|
97
|
+
type: { summary: "string" },
|
|
98
|
+
defaultValue: { summary: undefined }
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
resize: {
|
|
102
|
+
description: "controls how the textarea can be resized",
|
|
103
|
+
control: {
|
|
104
|
+
type: "select",
|
|
105
|
+
options: ["none", "horizontal", "vertical", "both"]
|
|
106
|
+
},
|
|
107
|
+
table: {
|
|
108
|
+
type: { summary: "string" },
|
|
109
|
+
defaultValue: { summary: "vertical" }
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
rows: {
|
|
113
|
+
description: "number of visible text lines for the textarea",
|
|
114
|
+
table: {
|
|
115
|
+
type: { summary: "number" },
|
|
116
|
+
defaultValue: { summary: 5 }
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
cols: {
|
|
120
|
+
description: "visible width of the textarea",
|
|
121
|
+
table: {
|
|
122
|
+
type: { summary: "number" },
|
|
123
|
+
defaultValue: { summary: undefined }
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
placeholder: {
|
|
127
|
+
description: "placeholder text for the textarea",
|
|
128
|
+
table: {
|
|
129
|
+
type: { summary: "string" },
|
|
130
|
+
defaultValue: { summary: "" }
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
maxLength: {
|
|
134
|
+
description: "maximum number of characters allowed",
|
|
135
|
+
table: {
|
|
136
|
+
type: { summary: "number" },
|
|
137
|
+
defaultValue: { summary: undefined }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
decorators: [
|
|
142
|
+
Story => (
|
|
143
|
+
<Provider store={store}>
|
|
144
|
+
<Story />
|
|
145
|
+
</Provider>
|
|
146
|
+
)
|
|
147
|
+
]
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const Basic = args => <ConnectedFormTextarea {...args} />;
|
|
151
|
+
|
|
152
|
+
export const WithLabel = {
|
|
153
|
+
args: {
|
|
154
|
+
labelTextWhenNoError: "Description"
|
|
155
|
+
},
|
|
156
|
+
render: args => <ConnectedFormTextarea {...args} />
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const WithPlaceholder = {
|
|
160
|
+
args: {
|
|
161
|
+
labelTextWhenNoError: "Comments",
|
|
162
|
+
placeholder: "Enter your comments here..."
|
|
163
|
+
},
|
|
164
|
+
render: args => <ConnectedFormTextarea {...args} />
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const WithCustomHeight = {
|
|
168
|
+
args: {
|
|
169
|
+
labelTextWhenNoError: "Message",
|
|
170
|
+
customHeight: "200px"
|
|
171
|
+
},
|
|
172
|
+
render: args => <ConnectedFormTextarea {...args} />
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const Required = {
|
|
176
|
+
args: {
|
|
177
|
+
labelTextWhenNoError: "Required Field",
|
|
178
|
+
isRequired: true
|
|
179
|
+
},
|
|
180
|
+
render: args => <ConnectedFormTextarea {...args} />
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const NoResize = {
|
|
184
|
+
args: {
|
|
185
|
+
labelTextWhenNoError: "Fixed Size",
|
|
186
|
+
resize: "none"
|
|
187
|
+
},
|
|
188
|
+
render: args => <ConnectedFormTextarea {...args} />
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const HorizontalResize = {
|
|
192
|
+
args: {
|
|
193
|
+
labelTextWhenNoError: "Horizontal Resize",
|
|
194
|
+
resize: "horizontal"
|
|
195
|
+
},
|
|
196
|
+
render: args => <ConnectedFormTextarea {...args} />
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const BothResize = {
|
|
200
|
+
args: {
|
|
201
|
+
labelTextWhenNoError: "Both Directions",
|
|
202
|
+
resize: "both"
|
|
203
|
+
},
|
|
204
|
+
render: args => <ConnectedFormTextarea {...args} />
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const WithMaxLength = {
|
|
208
|
+
args: {
|
|
209
|
+
labelTextWhenNoError: "Limited Text",
|
|
210
|
+
maxLength: 100,
|
|
211
|
+
placeholder: "Maximum 100 characters"
|
|
212
|
+
},
|
|
213
|
+
render: args => <ConnectedFormTextarea {...args} />
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export const CustomRows = {
|
|
217
|
+
args: {
|
|
218
|
+
labelTextWhenNoError: "Many Rows",
|
|
219
|
+
rows: 10
|
|
220
|
+
},
|
|
221
|
+
render: args => <ConnectedFormTextarea {...args} />
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const Disabled = {
|
|
225
|
+
args: {
|
|
226
|
+
disabled: true
|
|
227
|
+
},
|
|
228
|
+
render: args => <ConnectedFormTextarea {...args} />
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const TextareaWithModal = props => {
|
|
232
|
+
const [isOpen, toggleOpen] = useState(false);
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<ConnectedFormTextarea
|
|
236
|
+
{...props}
|
|
237
|
+
helperModal={() => (
|
|
238
|
+
<Modal
|
|
239
|
+
modalOpen={isOpen}
|
|
240
|
+
hideModal={() => toggleOpen(false)}
|
|
241
|
+
showModal={() => toggleOpen(true)}
|
|
242
|
+
modalHeaderText="Help with this field"
|
|
243
|
+
modalBodyText="This textarea is for entering detailed information. You can use multiple lines and resize as needed."
|
|
244
|
+
defaultWrapper={false}
|
|
245
|
+
onlyCloseButton={true}
|
|
246
|
+
initialFocusSelector=""
|
|
247
|
+
>
|
|
248
|
+
<div onClick={() => toggleOpen(true)} role="button">
|
|
249
|
+
Help!
|
|
250
|
+
</div>
|
|
251
|
+
</Modal>
|
|
252
|
+
)}
|
|
253
|
+
/>
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export const WithHelperModal = {
|
|
258
|
+
args: {
|
|
259
|
+
labelTextWhenNoError: "Description"
|
|
260
|
+
},
|
|
261
|
+
argTypes: {
|
|
262
|
+
helperModal: { type: "function" }
|
|
263
|
+
},
|
|
264
|
+
render: args => <TextareaWithModal {...args} />
|
|
265
|
+
};
|
|
@@ -35,3 +35,31 @@ export interface FormInputProps {
|
|
|
35
35
|
|
|
36
36
|
export const FormInput: React.FC<Expand<FormInputProps> &
|
|
37
37
|
React.HTMLAttributes<HTMLElement>>;
|
|
38
|
+
|
|
39
|
+
export interface FormTextareaProps {
|
|
40
|
+
extraStyles?: string;
|
|
41
|
+
field?: Field;
|
|
42
|
+
fieldActions?: FieldActions;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
errorMessages?: ErrorMessageDictionary;
|
|
45
|
+
helperModal?: boolean;
|
|
46
|
+
labelTextWhenNoError?: string;
|
|
47
|
+
showErrors?: boolean;
|
|
48
|
+
themeValues?: object;
|
|
49
|
+
customHeight?: string;
|
|
50
|
+
removeFromValue?: RegExp;
|
|
51
|
+
dataQa?: string | null;
|
|
52
|
+
isRequired?: boolean;
|
|
53
|
+
errorFieldExtraStyles?: string;
|
|
54
|
+
showFieldErrorRow?: boolean;
|
|
55
|
+
labelTextVariant?: string;
|
|
56
|
+
errorTextVariant?: string;
|
|
57
|
+
resize?: "none" | "horizontal" | "vertical" | "both";
|
|
58
|
+
rows?: number;
|
|
59
|
+
cols?: number;
|
|
60
|
+
placeholder?: string;
|
|
61
|
+
maxLength?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const FormTextarea: React.FC<Expand<FormTextareaProps> &
|
|
65
|
+
React.HTMLAttributes<HTMLTextAreaElement>>;
|
|
@@ -3,11 +3,13 @@ import FormInputRow from "./FormInputRow";
|
|
|
3
3
|
import FormInputColumn from "./FormInputColumn";
|
|
4
4
|
import FormContainer from "./FormContainer";
|
|
5
5
|
import FormFooterPanel from "./FormFooterPanel";
|
|
6
|
+
import FormTextarea from "./FormTextarea";
|
|
6
7
|
|
|
7
8
|
export {
|
|
8
9
|
FormInput,
|
|
9
10
|
FormInputRow,
|
|
10
11
|
FormInputColumn,
|
|
11
12
|
FormContainer,
|
|
12
|
-
FormFooterPanel
|
|
13
|
+
FormFooterPanel,
|
|
14
|
+
FormTextarea
|
|
13
15
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, Fragment } from "react";
|
|
1
|
+
import React, { useState, Fragment, useEffect } from "react";
|
|
2
2
|
import { Stack, Box, Cluster } from "../../atoms/layouts";
|
|
3
3
|
import { themeComponent } from "../../../util/themeUtils";
|
|
4
4
|
import { fallbackValues } from "./Tabs.theme";
|
|
@@ -6,11 +6,20 @@ import Tab from "../../atoms/tab";
|
|
|
6
6
|
|
|
7
7
|
const HORIZONTAL = "horizontal";
|
|
8
8
|
|
|
9
|
-
const Tabs = ({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
const Tabs = ({ tabsConfig, tabsDisplayMode = HORIZONTAL, ...props }) => {
|
|
10
|
+
const [activeTab, toggleActiveTab] = useState(
|
|
11
|
+
tabsConfig.tabs[0]?.label || null
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const currentTabExists = tabsConfig.tabs.some(
|
|
16
|
+
tab => tab.label === activeTab
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
if (!activeTab || !currentTabExists) {
|
|
20
|
+
toggleActiveTab(tabsConfig.tabs[0]?.label || null);
|
|
21
|
+
}
|
|
22
|
+
}, [tabsConfig, activeTab]);
|
|
14
23
|
|
|
15
24
|
const createTabs = (tabConfig, activeTab) => {
|
|
16
25
|
return tabConfig.tabs.map(tab => {
|
|
@@ -25,7 +34,7 @@ const Tabs = ({
|
|
|
25
34
|
});
|
|
26
35
|
};
|
|
27
36
|
|
|
28
|
-
const
|
|
37
|
+
const showHorizontal = (tabsConfig, activeTab) => {
|
|
29
38
|
return (
|
|
30
39
|
<Cluster justify={"space-around"}>
|
|
31
40
|
{createTabs(tabsConfig, activeTab)}
|
|
@@ -38,10 +47,10 @@ const Tabs = ({
|
|
|
38
47
|
};
|
|
39
48
|
|
|
40
49
|
return (
|
|
41
|
-
<Box className="tabs">
|
|
50
|
+
<Box className="tabs" {...props}>
|
|
42
51
|
<Box className="tab-list">
|
|
43
52
|
{tabsDisplayMode == HORIZONTAL
|
|
44
|
-
?
|
|
53
|
+
? showHorizontal(tabsConfig, activeTab)
|
|
45
54
|
: showVertical(tabsConfig, activeTab)}
|
|
46
55
|
</Box>
|
|
47
56
|
<Box className="tab-content">
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Expand from "../../../util/expand";
|
|
3
|
+
|
|
4
|
+
export interface TabConfig {
|
|
5
|
+
label: string;
|
|
6
|
+
content: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TabsConfigObject {
|
|
10
|
+
tabs: TabConfig[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TabsProps {
|
|
14
|
+
tabsConfig: TabsConfigObject;
|
|
15
|
+
tabsDisplayMode?: "horizontal" | "vertical";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export declare const Tabs: React.FC<Expand<TabsProps> &
|
|
19
|
+
React.HTMLAttributes<HTMLElement>>;
|
|
20
|
+
|
|
21
|
+
export default Tabs;
|