@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thecb/components",
3
- "version": "11.3.5",
3
+ "version": "11.4.1-beta.0",
4
4
  "description": "Common lib for CityBase react components",
5
5
  "main": "dist/index.cjs.js",
6
6
  "typings": "dist/index.d.ts",
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
- tabsConfig,
11
- tabsDisplayMode // can be either HORIZONTAL or VERTICAL
12
- }) => {
13
- const [activeTab, toggleActiveTab] = useState(tabsConfig.tabs[0].label);
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 showHorozontal = (tabsConfig, activeTab) => {
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
- ? showHorozontal(tabsConfig, activeTab)
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;