@thecb/components 9.4.0-beta.0 → 9.5.0-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": "9.4.0-beta.0",
3
+ "version": "9.5.0-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",
@@ -38,7 +38,6 @@ const SearchableSelect = ({
38
38
  selectItem(value);
39
39
  }
40
40
  };
41
-
42
41
  return (
43
42
  <Box
44
43
  padding="1rem"
@@ -61,25 +60,20 @@ const SearchableSelect = ({
61
60
  extraStyles={`overflow-y: scroll; max-height: 250px;`}
62
61
  >
63
62
  <Stack>
64
- {itemList
65
- .sort((a, b) =>
66
- a.name.toLowerCase().localeCompare(b.name.toLowerCase())
67
- )
68
- .map(value => (
69
- <Checkbox
70
- key={value.name}
71
- title={value.name}
72
- name={value.name}
73
- checked={
74
- selectedItems?.find(item => item?.name === value?.name) ??
75
- false
76
- }
77
- onChange={() => handleSelect(value)}
78
- textExtraStyles={`margin: 0;`}
79
- disabled={disabled}
80
- extraStyles={`margin: 0.5rem;`}
81
- />
82
- ))}
63
+ {itemList.map(value => (
64
+ <Checkbox
65
+ key={value.name}
66
+ title={value.name}
67
+ name={value.name}
68
+ checked={
69
+ selectedItems?.find(item => item?.name === value?.name) ?? false
70
+ }
71
+ onChange={() => handleSelect(value)}
72
+ textExtraStyles={`margin: 0;`}
73
+ disabled={disabled}
74
+ extraStyles={`margin: 0.5rem;`}
75
+ />
76
+ ))}
83
77
  </Stack>
84
78
  </Box>
85
79
  </Box>
@@ -14,8 +14,8 @@ const { mapStateToProps, mapDispatchToProps, reducer } = createFormState({
14
14
 
15
15
  const items = [
16
16
  { name: "Foo", value: "foo-value" },
17
- { name: "Baz", value: "baz-value" },
18
- { name: "Bar", value: "bar-value" }
17
+ { name: "Bar", value: "bar-value" },
18
+ { name: "Baz", value: "baz-value" }
19
19
  ];
20
20
 
21
21
  const FormWrapper = props => {
@@ -0,0 +1,207 @@
1
+ import React, { Fragment } from "react";
2
+ import styled from "styled-components";
3
+ import { themeComponent } from "../../../util/themeUtils";
4
+ import { fallbackValues } from "./InnerRadioSection.theme";
5
+ import { AnimatePresence } from "framer-motion";
6
+ import RadioButton from "./radio-button/RadioButton";
7
+ import { Box, Cluster, Stack, Motion } from "../../atoms/layouts";
8
+ import { createIdFromString, noop } from "../../../util/general";
9
+ import Text from "../../atoms/text";
10
+ import { CHARADE_GREY } from "../../../constants/colors";
11
+
12
+ const idString = section =>
13
+ typeof section.title === "string"
14
+ ? createIdFromString(section.title)
15
+ : section.id;
16
+
17
+ const InnerRadioSection = ({
18
+ themeValues,
19
+ isMobile,
20
+ supportsTouch,
21
+ section,
22
+ sectionIndex,
23
+ openSection = "",
24
+ toggleOpenSection,
25
+ staggeredAnimation = false,
26
+ initiallyOpen = true,
27
+ openHeight = "auto",
28
+ ariaDescribedBy,
29
+ focused,
30
+ setFocused,
31
+ sectionRefs,
32
+ ariaLabelledBy,
33
+ onKeyDown = noop
34
+ }) => {
35
+ const wrapper = {
36
+ open: {
37
+ height: openHeight,
38
+ opacity: 1,
39
+ transition: {
40
+ duration: 0.3,
41
+ ease: [0.04, 0.62, 0.23, 0.98],
42
+ staggerChildren: staggeredAnimation ? 0.15 : 0
43
+ }
44
+ },
45
+ closed: {
46
+ height: 0,
47
+ opacity: 0,
48
+ transition: {
49
+ duration: 0.3,
50
+ ease: [0.04, 0.62, 0.23, 0.98],
51
+ staggerChildren: staggeredAnimation ? 0.15 : 0,
52
+ staggerDirection: -1
53
+ }
54
+ }
55
+ };
56
+
57
+ const borderStyles = `
58
+ border-width: 0 0 1px 0;
59
+ border-color: ${themeValues.borderColor};
60
+ border-style: solid;
61
+ border-radius: 0px;
62
+ transform-origin: 100% 0;
63
+
64
+ &:last-child {
65
+ border-width: 0;
66
+ }
67
+ `;
68
+
69
+ const RightIcon = styled.img`
70
+ height: ${({ isMobile }) => (isMobile ? "14px" : "18px")};
71
+ width: ${({ isMobile }) => (isMobile ? "22px" : "28px")};
72
+ ${({ fade }) => fade && "opacity: 0.4;"}
73
+ transition: opacity 0.3s ease;
74
+ `;
75
+
76
+ return (
77
+ <Motion
78
+ tabIndex={section.hideRadioButton || section.disabled ? "-1" : "0"}
79
+ ref={sectionRefs.current[sectionIndex]}
80
+ onBlur={() => !section.disabled && setFocused(null)}
81
+ onFocus={() => !section.disabled && setFocused(section.id)}
82
+ onKeyDown={onKeyDown}
83
+ hoverStyles={themeValues.focusStyles}
84
+ animate={openSection === section.id ? "open" : "closed"}
85
+ initial={initiallyOpen ? "open" : "closed"}
86
+ key={`item-${section.id}`}
87
+ extraStyles={borderStyles}
88
+ role="radio"
89
+ aria-checked={openSection === section.id}
90
+ aria-disabled={section.disabled}
91
+ aria-required={section.required}
92
+ aria-labelledby={ariaLabelledBy}
93
+ aria-describedby={ariaDescribedBy}
94
+ >
95
+ <Stack childGap="0">
96
+ <Box
97
+ padding={section.hideRadioButton ? "1.5rem" : "1.25rem 1.5rem"}
98
+ background={
99
+ section.disabled
100
+ ? themeValues.headingDisabledColor
101
+ : themeValues.headingBackgroundColor
102
+ }
103
+ onClick={
104
+ (isMobile && supportsTouch) || section.disabled
105
+ ? noop
106
+ : () => toggleOpenSection(section.id)
107
+ }
108
+ onTouchEnd={
109
+ isMobile && supportsTouch && !section.disabled
110
+ ? () => toggleOpenSection(section.id)
111
+ : noop
112
+ }
113
+ key={`header-${section.id}`}
114
+ borderSize="0px"
115
+ borderColor={themeValues.borderColor}
116
+ borderWidthOverride={
117
+ openSection === section.id && !!section.content
118
+ ? `0px 0px 1px 0px`
119
+ : ``
120
+ }
121
+ extraStyles={!section.disabled ? "cursor: pointer;" : ""}
122
+ dataQa={section.dataQa ? section.dataQa : section.id}
123
+ >
124
+ <Cluster justify="space-between" align="center" childGap="1px" nowrap>
125
+ <Cluster justify="flex-start" align="center" nowrap>
126
+ {!section.hideRadioButton && (
127
+ <Box padding="0">
128
+ <RadioButton
129
+ id={`radio-input-${idString(section)}`}
130
+ name={idString(section)}
131
+ ariaDescribedBy={ariaDescribedBy}
132
+ radioOn={openSection === section.id}
133
+ radioFocused={focused === section.id}
134
+ toggleRadio={
135
+ section.disabled
136
+ ? noop
137
+ : () => toggleOpenSection(section.id)
138
+ }
139
+ tabIndex="-1"
140
+ isRequired={section.required}
141
+ />
142
+ </Box>
143
+ )}
144
+ {section.titleIcon && (
145
+ <Cluster align="center">{section.titleIcon}</Cluster>
146
+ )}
147
+ <Box padding={section.titleIcon ? "0 0 0 8px" : "0"}>
148
+ <Text
149
+ as="label"
150
+ htmlFor={`radio-input-${idString(section)}`}
151
+ color={CHARADE_GREY}
152
+ >
153
+ {section.title}
154
+ </Text>
155
+ </Box>
156
+ </Cluster>
157
+ {section.rightIcons && (
158
+ <Cluster
159
+ id={`right-icons-${idString(section)}`}
160
+ childGap="0.5rem"
161
+ aria-label={section.rightIconsLabel || null}
162
+ role={section.rightIconsRole || null}
163
+ >
164
+ {section.rightIcons.map(icon => (
165
+ <RightIcon
166
+ src={icon.img}
167
+ key={icon.img}
168
+ fade={!icon.enabled}
169
+ isMobile={isMobile}
170
+ alt={icon.altText}
171
+ aria-disabled={!icon.enabled}
172
+ />
173
+ ))}
174
+ </Cluster>
175
+ )}
176
+ {section.rightTitleContent && (
177
+ <Fragment>{section.rightTitleContent}</Fragment>
178
+ )}
179
+ </Cluster>
180
+ </Box>
181
+ <AnimatePresence initial={false}>
182
+ {openSection === section.id && (
183
+ <Motion
184
+ key={`content-${section.id}`}
185
+ padding="0"
186
+ background={themeValues.bodyBackgroundColor}
187
+ layoutTransition
188
+ initial="closed"
189
+ animate="open"
190
+ exit="closed"
191
+ variants={wrapper}
192
+ extraStyles={`transform-origin: 100% 0;`}
193
+ >
194
+ {section.content}
195
+ </Motion>
196
+ )}
197
+ </AnimatePresence>
198
+ </Stack>
199
+ </Motion>
200
+ );
201
+ };
202
+
203
+ export default themeComponent(
204
+ InnerRadioSection,
205
+ "InnerRadioSection",
206
+ fallbackValues
207
+ );
@@ -0,0 +1,15 @@
1
+ import { WHITE, GREY_CHATEAU, ATHENS_GREY } from "../../../constants/colors";
2
+
3
+ const headingBackgroundColor = `${WHITE}`;
4
+ const headingDisabledColor = `${ATHENS_GREY}`;
5
+ const bodyBackgroundColor = "#eeeeee";
6
+ const borderColor = `${GREY_CHATEAU}`;
7
+ const focusStyles = `outline: none;`;
8
+
9
+ export const fallbackValues = {
10
+ headingBackgroundColor,
11
+ headingDisabledColor,
12
+ bodyBackgroundColor,
13
+ borderColor,
14
+ focusStyles
15
+ };
@@ -1,13 +1,10 @@
1
1
  import React, { createRef, Fragment, useRef, useState } from "react";
2
- import styled from "styled-components";
3
2
  import { themeComponent } from "../../../util/themeUtils";
4
3
  import { fallbackValues } from "./RadioSection.theme";
5
- import { AnimatePresence } from "framer-motion";
6
- import RadioButton from "./radio-button/RadioButton";
7
- import { Box, Cluster, Stack, Motion } from "../../atoms/layouts";
8
- import { createIdFromString, noop, wrapIndex } from "../../../util/general";
9
- import Text from "../../atoms/text";
10
- import { CHARADE_GREY } from "../../../constants/colors";
4
+ import SolidDivider from "../../atoms/solid-divider/SolidDivider";
5
+ import { Box, Stack } from "../../atoms/layouts";
6
+ import { createIdFromString, wrapIndex } from "../../../util/general";
7
+ import { MANATEE_GREY } from "../../../constants/colors";
11
8
  import {
12
9
  ARROW_DOWN,
13
10
  ARROW_LEFT,
@@ -16,27 +13,29 @@ import {
16
13
  ENTER,
17
14
  SPACEBAR
18
15
  } from "../../../constants/keyboard";
19
- /*
20
- Takes an array of section objects, each object should look like:
21
- {
22
- title: <React Component(s)>,
23
- id: <String> "identifier of section",
24
- disabled: boolean, (displays section and grayed out radio but disables interaction)
25
- hideRadioButton: boolean, (keeps section displayed but hides radio and disables open/close function),
26
- hidden: boolean, (hides section entirely)
27
- dataQa: string,
28
- content: <React Component(s)> e.g.: <Box><Stack>cool content stuff</Stack></Box> (any collection of components will work),
29
- rightTitleContent: <React Component(s)> (rendered on the very right of the title section, use to supplement "rightIcons" with text, as in expired CC status, or render other custom content)
30
- }
31
-
32
- Also takes an "openSection" which should equal the id of the section that should be open
33
- And a toggleOpenSection. RadioSection will call this function with the id of the section
34
- that it is in. It is up to the user to store the open section value in state up from the component
35
- using a useState() hook, or reducer.
36
-
37
- The section itself comes with some motion to open/close. To add more motion to the content,
38
- wrap your content with a Motion layout primitive and provide appropriate props.
39
-
16
+ import InnerRadioSection from "./InnerRadioSection";
17
+ /**
18
+ - The RadioSection component takes either a flat array (via the 'sections'
19
+ prop) of section objects or a multidimensional array (via the 'groupedSections' prop) of section objects. Note that if using a multidimensional array, the nesting cannot exceed 2 levels deep.
20
+ - Each 'section' object should look like:
21
+ {
22
+ title: <React Component(s)>,
23
+ id: <String> "identifier of section",
24
+ disabled: boolean, (displays section and grayed out radio but disables interaction)
25
+ hideRadioButton: boolean, (keeps section displayed but hides radio and disables open/close function),
26
+ hidden: boolean, (hides section entirely)
27
+ dataQa: string,
28
+ content: <React Component(s)> e.g.: <Box><Stack>cool content stuff</Stack></Box> (any collection of components will work),
29
+ rightTitleContent: <React Component(s)> (rendered on the very right of the title section, use to supplement "rightIcons" with text, as in expired CC status, or render other custom content)
30
+ }
31
+ - It also takes an "openSection" which should equal the id of the section that
32
+ should be open, along with "toggleOpenSection"
33
+ - RadioSection will call "toggleOpenSection" with the id of the section
34
+ that it is in.
35
+ - It is up to the engineer to store the open section value in state up from the
36
+ component using a useState() hook or a reducer.
37
+ - The section itself comes with some motion to open/close. To add more motion
38
+ to the content, wrap your content with a Motion layout primitive and provide appropriate props.
40
39
  */
41
40
 
42
41
  const idString = section =>
@@ -57,9 +56,11 @@ const RadioSection = ({
57
56
  containerStyles = "",
58
57
  ariaDescribedBy,
59
58
  isSectionRequired = false,
59
+ groupedSections,
60
60
  ...rest
61
61
  }) => {
62
62
  const [focused, setFocused] = useState(null);
63
+
63
64
  const sectionRefs = useRef(
64
65
  [...Array(sections.length)].map(() => createRef())
65
66
  );
@@ -93,47 +94,6 @@ const RadioSection = ({
93
94
  }
94
95
  };
95
96
 
96
- const wrapper = {
97
- open: {
98
- height: openHeight,
99
- opacity: 1,
100
- transition: {
101
- duration: 0.3,
102
- ease: [0.04, 0.62, 0.23, 0.98],
103
- staggerChildren: staggeredAnimation ? 0.15 : 0
104
- }
105
- },
106
- closed: {
107
- height: 0,
108
- opacity: 0,
109
- transition: {
110
- duration: 0.3,
111
- ease: [0.04, 0.62, 0.23, 0.98],
112
- staggerChildren: staggeredAnimation ? 0.15 : 0,
113
- staggerDirection: -1
114
- }
115
- }
116
- };
117
-
118
- const borderStyles = `
119
- border-width: 0 0 1px 0;
120
- border-color: ${themeValues.borderColor};
121
- border-style: solid;
122
- border-radius: 0px;
123
- transform-origin: 100% 0;
124
-
125
- &:last-child {
126
- border-width: 0;
127
- }
128
- `;
129
-
130
- const RightIcon = styled.img`
131
- height: ${({ isMobile }) => (isMobile ? "14px" : "18px")};
132
- width: ${({ isMobile }) => (isMobile ? "22px" : "28px")};
133
- ${({ fade }) => fade && "opacity: 0.4;"}
134
- transition: opacity 0.3s ease;
135
- `;
136
-
137
97
  return (
138
98
  <Box
139
99
  padding="1px"
@@ -147,144 +107,58 @@ const RadioSection = ({
147
107
  aria-required={isSectionRequired}
148
108
  {...rest}
149
109
  >
150
- {sections
151
- .filter(section => !section.hidden)
152
- .map((section, i) => (
153
- <Motion
154
- tabIndex={
155
- section.hideRadioButton || section.disabled ? "-1" : "0"
156
- }
157
- ref={sectionRefs.current[i]}
158
- onBlur={() => !section.disabled && setFocused(null)}
159
- onFocus={() => !section.disabled && setFocused(section.id)}
160
- onKeyDown={e =>
161
- !section.disabled && handleKeyDown(section.id, e, i)
162
- }
163
- hoverStyles={themeValues.focusStyles}
164
- animate={openSection === section.id ? "open" : "closed"}
165
- initial={initiallyOpen ? "open" : "closed"}
166
- key={`item-${section.id}`}
167
- extraStyles={borderStyles}
168
- role="radio"
169
- aria-checked={openSection === section.id}
170
- aria-disabled={section.disabled}
171
- aria-required={section.required}
172
- aria-labelledby={section.id}
173
- aria-describedby={`right-icons-${idString(section)}`}
174
- >
175
- <Stack childGap="0">
176
- <Box
177
- padding={
178
- section.hideRadioButton ? "1.5rem" : "1.25rem 1.5rem"
179
- }
180
- background={
181
- section.disabled
182
- ? themeValues.headingDisabledColor
183
- : themeValues.headingBackgroundColor
110
+ {!!sections &&
111
+ sections
112
+ .filter(section => !section.hidden)
113
+ .map((section, i) => (
114
+ <Fragment key={`radio-section-${sections.indexOf(section)}`}>
115
+ <InnerRadioSection
116
+ sectionIndex={i}
117
+ section={section}
118
+ sectionRefs={sectionRefs}
119
+ focused={focused}
120
+ setFocused={setFocused}
121
+ openHeight={openHeight}
122
+ openSection={openSection}
123
+ toggleOpenSection={toggleOpenSection}
124
+ onKeyDown={e =>
125
+ !section.disabled && handleKeyDown(section.id, e, i)
184
126
  }
185
- onClick={
186
- (isMobile && supportsTouch) || section.disabled
187
- ? noop
188
- : () => toggleOpenSection(section.id)
189
- }
190
- onTouchEnd={
191
- isMobile && supportsTouch && !section.disabled
192
- ? () => toggleOpenSection(section.id)
193
- : noop
194
- }
195
- key={`header-${section.id}`}
196
- borderSize="0px"
197
- borderColor={themeValues.borderColor}
198
- borderWidthOverride={
199
- openSection === section.id && !!section.content
200
- ? `0px 0px 1px 0px`
201
- : ``
202
- }
203
- extraStyles={!section.disabled ? "cursor: pointer;" : ""}
204
- dataQa={section.dataQa ? section.dataQa : section.id}
127
+ ariaLabelledBy={section.id}
128
+ ariaDescribedBy={`right-icons-${idString(section)}`}
129
+ />
130
+ </Fragment>
131
+ ))}
132
+ {!!groupedSections &&
133
+ groupedSections.map(sectionGroup =>
134
+ sectionGroup
135
+ .filter(unfilteredSection => !unfilteredSection.hidden)
136
+ .map((section, i) => (
137
+ <Fragment
138
+ key={`radio-section-${groupedSections.indexOf(
139
+ sectionGroup
140
+ )}-${sectionGroup.indexOf(section)}}`}
205
141
  >
206
- <Cluster
207
- justify="space-between"
208
- align="center"
209
- childGap="1px"
210
- nowrap
211
- >
212
- <Cluster justify="flex-start" align="center" nowrap>
213
- {!section.hideRadioButton && (
214
- <Box padding="0">
215
- <RadioButton
216
- id={`radio-input-${idString(section)}`}
217
- name={idString(section)}
218
- ariaDescribedBy={ariaDescribedBy}
219
- radioOn={openSection === section.id}
220
- radioFocused={focused === section.id}
221
- toggleRadio={
222
- section.disabled
223
- ? noop
224
- : () => toggleOpenSection(section.id)
225
- }
226
- tabIndex="-1"
227
- isRequired={section.required}
228
- />
229
- </Box>
230
- )}
231
- {section.titleIcon && (
232
- <Cluster align="center">{section.titleIcon}</Cluster>
233
- )}
234
- <Box padding={section.titleIcon ? "0 0 0 8px" : "0"}>
235
- <Text
236
- as="label"
237
- htmlFor={`radio-input-${idString(section)}`}
238
- color={CHARADE_GREY}
239
- >
240
- {section.title}
241
- </Text>
242
- </Box>
243
- </Cluster>
244
- {section.rightIcons && (
245
- <Cluster
246
- id={`right-icons-${idString(section)}`}
247
- childGap="0.5rem"
248
- aria-label={section.rightIconsLabel || null}
249
- role={section.rightIconsRole || null}
250
- >
251
- {section.rightIcons.map(icon => (
252
- <RightIcon
253
- src={icon.img}
254
- key={icon.img}
255
- fade={!icon.enabled}
256
- isMobile={isMobile}
257
- alt={icon.altText}
258
- aria-disabled={!icon.enabled}
259
- />
260
- ))}
261
- </Cluster>
262
- )}
263
- {section.rightTitleContent && (
264
- <Fragment>{section.rightTitleContent}</Fragment>
142
+ <InnerRadioSection
143
+ sectionIndex={i}
144
+ section={section}
145
+ sectionRefs={sectionRefs}
146
+ focused={focused}
147
+ setFocused={setFocused}
148
+ openHeight={openHeight}
149
+ ariaLabelledBy={section.id}
150
+ ariaDescribedBy={`right-icons-${idString(section)}`}
151
+ openSection={openSection}
152
+ toggleOpenSection={toggleOpenSection}
153
+ />
154
+ {sectionGroup.indexOf(section) === sectionGroup.length - 1 &&
155
+ groupedSections.indexOf(sectionGroup) !==
156
+ groupedSections.length - 1 && (
157
+ <SolidDivider borderSize="2px" color={MANATEE_GREY} />
265
158
  )}
266
- </Cluster>
267
- </Box>
268
- <AnimatePresence initial={false}>
269
- {openSection === section.id && (
270
- <Motion
271
- key={`content-${section.id}`}
272
- padding="0"
273
- background={themeValues.bodyBackgroundColor}
274
- layoutTransition
275
- initial="closed"
276
- animate="open"
277
- exit="closed"
278
- variants={wrapper}
279
- extraStyles={`transform-origin: 100% 0;`}
280
- >
281
- {section.content}
282
- </Motion>
283
- )}
284
- </AnimatePresence>
285
- </Stack>
286
- </Motion>
287
- ))}
159
+ </Fragment>
160
+ ))
161
+ )}
288
162
  </Stack>
289
163
  </Box>
290
164
  );