@thecb/components 9.5.0-beta.9 → 9.6.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.5.0-beta.9",
3
+ "version": "9.6.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",
@@ -144,7 +144,7 @@ const ButtonWithAction = forwardRef(
144
144
  {contentOverride ? (
145
145
  children
146
146
  ) : (
147
- <Center intrinsic>
147
+ <Center intrinsic as="span">
148
148
  {isLoading ? (
149
149
  <Spinner color={loadingColor} isMobile={isMobile} />
150
150
  ) : (
@@ -39,7 +39,8 @@ const EditableList = ({
39
39
  as = "p",
40
40
  listPadding = "0rem 0rem 1.5rem 0rem",
41
41
  qaPrefix,
42
- ariaLabel
42
+ ariaLabel,
43
+ editItemAriaRole = ""
43
44
  }) => {
44
45
  const addText = `Add a${
45
46
  itemName[0].match(/[aieouAIEOU]/) ? "n" : ""
@@ -151,6 +152,7 @@ const EditableList = ({
151
152
  action={() => editItem(item.id)}
152
153
  extraStyles={`min-width: 0;`}
153
154
  aria-label={`Edit ${ariaLabel || itemName}`}
155
+ role={editItemAriaRole}
154
156
  />
155
157
  </Box>
156
158
  )}
@@ -0,0 +1,31 @@
1
+ import React, { useState } from "react";
2
+ import { boolean } from "@storybook/addon-knobs";
3
+
4
+ import EditableList from "./EditableList";
5
+ import page from "../../../../.storybook/page";
6
+
7
+ const story = page({
8
+ title: "Components|Molecules/EditableList",
9
+ Component: EditableList
10
+ });
11
+ export default story;
12
+
13
+ export const editableList = () => {
14
+ return (
15
+ <EditableList
16
+ as="h2"
17
+ canRemove={false}
18
+ title="Contact first name"
19
+ titleWeight="600"
20
+ editItem={() => console.log("edit click")}
21
+ editItemAriaRole="link"
22
+ itemName="Contact first name"
23
+ listPadding="0"
24
+ renderItem={contactFirstName => contactFirstName}
25
+ maxItems={1}
26
+ items={["Ronald"]}
27
+ qaPrefix="Contact first name"
28
+ ariaLabel="Contact first name"
29
+ />
30
+ );
31
+ };
@@ -22,8 +22,7 @@ const PartialAmountForm = ({
22
22
  clearOnDismount,
23
23
  fields,
24
24
  actions,
25
- showErrors = false,
26
- dataQa
25
+ showErrors = false
27
26
  }) => {
28
27
  if (clearOnDismount) {
29
28
  useEffect(() => () => actions.form.clear(), []);
@@ -55,12 +54,7 @@ const PartialAmountForm = ({
55
54
 
56
55
  const lineItemsNew = Array.isArray(lineItems) ? lineItems : [];
57
56
  return (
58
- <FormContainer
59
- variant={variant}
60
- role="form"
61
- aria-label="Other amount"
62
- data-qa={dataQa}
63
- >
57
+ <FormContainer variant={variant} role="form" aria-label="Other amount">
64
58
  <Text variant="p">
65
59
  Pay a different amount. The total balance will still be due on the same
66
60
  due date.
@@ -1,9 +1,13 @@
1
1
  import React, { createRef, Fragment, useRef, useState } from "react";
2
+ import styled from "styled-components";
2
3
  import { themeComponent } from "../../../util/themeUtils";
3
4
  import { fallbackValues } from "./RadioSection.theme";
4
- import SolidDivider from "../../atoms/solid-divider/SolidDivider";
5
- import { Box, Stack } from "../../atoms/layouts";
6
- import { createIdFromString, wrapIndex } from "../../../util/general";
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";
7
11
  import {
8
12
  ARROW_DOWN,
9
13
  ARROW_LEFT,
@@ -12,31 +16,27 @@ import {
12
16
  ENTER,
13
17
  SPACEBAR
14
18
  } from "../../../constants/keyboard";
15
- import InnerRadioSection from "./InnerRadioSection";
16
- import { MANATEE_GREY } from "../../../constants/colors";
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.
17
39
 
18
- /**
19
- - The RadioSection component takes either a flat array (via the 'sections'
20
- 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.
21
- - Each 'section' object should look like:
22
- {
23
- title: <React Component(s)>,
24
- id: <String> "identifier of section",
25
- disabled: boolean, (displays section and grayed out radio but disables interaction)
26
- hideRadioButton: boolean, (keeps section displayed but hides radio and disables open/close function),
27
- hidden: boolean, (hides section entirely)
28
- dataQa: string,
29
- content: <React Component(s)> e.g.: <Box><Stack>cool content stuff</Stack></Box> (any collection of components will work),
30
- 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)
31
- }
32
- - It also takes an "openSection" which should equal the id of the section that
33
- should be open, along with "toggleOpenSection"
34
- - RadioSection will call "toggleOpenSection" with the id of the section
35
- that it is in.
36
- - It is up to the engineer to store the open section value in state up from the
37
- component using a useState() hook or a reducer.
38
- - The section itself comes with some motion to open/close. To add more motion
39
- to the content, wrap your content with a Motion layout primitive and provide appropriate props.
40
40
  */
41
41
 
42
42
  const idString = section =>
@@ -57,11 +57,9 @@ const RadioSection = ({
57
57
  containerStyles = "",
58
58
  ariaDescribedBy,
59
59
  isSectionRequired = false,
60
- groupedSections,
61
60
  ...rest
62
61
  }) => {
63
62
  const [focused, setFocused] = useState(null);
64
-
65
63
  const sectionRefs = useRef(
66
64
  [...Array(sections.length)].map(() => createRef())
67
65
  );
@@ -95,73 +93,198 @@ const RadioSection = ({
95
93
  }
96
94
  };
97
95
 
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
+
98
137
  return (
99
138
  <Box
100
139
  padding="1px"
101
140
  border={`1px solid ${themeValues.borderColor}`}
102
141
  borderRadius="4px"
103
142
  extraStyles={containerStyles}
104
- role="radiogroup"
105
- aria-required={isSectionRequired}
106
- {...rest}
107
143
  >
108
- <Stack childGap="0">
109
- {!!sections &&
110
- sections
111
- .filter(section => !section.hidden)
112
- .map((section, i) => (
113
- <Fragment key={`radio-section-${i}`}>
114
- <InnerRadioSection
115
- themeValues={themeValues}
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)
144
+ <Stack
145
+ childGap="0"
146
+ role="radiogroup"
147
+ aria-required={isSectionRequired}
148
+ {...rest}
149
+ >
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
184
+ }
185
+ onClick={
186
+ (isMobile && supportsTouch) || section.disabled
187
+ ? noop
188
+ : () => toggleOpenSection(section.id)
126
189
  }
127
- ariaLabelledBy={section.id}
128
- ariaDescribedBy={`right-icons-${idString(section)}`}
129
- isLastGroupedItemInSection={false}
130
- />
131
- </Fragment>
132
- ))}
133
- {!!groupedSections &&
134
- groupedSections.map((sectionGroup, sectionGroupIndex) =>
135
- sectionGroup
136
- .filter(unfilteredSection => !unfilteredSection.hidden)
137
- .map((section, sectionIndex) => (
138
- <Fragment key={`key-${sectionGroupIndex}-${sectionIndex}`}>
139
- <InnerRadioSection
140
- themeValues={themeValues}
141
- sectionIndex={`${sectionGroupIndex}-${sectionIndex}`}
142
- section={section}
143
- sectionRefs={sectionRefs}
144
- focused={focused}
145
- setFocused={setFocused}
146
- openHeight={openHeight}
147
- ariaLabelledBy={section.id}
148
- ariaDescribedBy={`right-icons-${idString(section)}`}
149
- openSection={openSection}
150
- toggleOpenSection={toggleOpenSection}
151
- isLastGroupedItemInSection={
152
- sectionIndex === sectionGroup.length - 1
153
- }
154
- />
155
- {sectionIndex === sectionGroup.length - 1 &&
156
- sectionGroupIndex !== groupedSections.length - 1 && (
157
- <SolidDivider
158
- borderSize="1px"
159
- borderColor={MANATEE_GREY}
160
- />
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}
205
+ >
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>
161
265
  )}
162
- </Fragment>
163
- ))
164
- )}
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
+ ))}
165
288
  </Stack>
166
289
  </Box>
167
290
  );
@@ -1,9 +1,8 @@
1
1
  import React, { useState } from "react";
2
- import { boolean } from "@storybook/addon-knobs";
2
+ import { text, boolean } from "@storybook/addon-knobs";
3
3
 
4
4
  import RadioSection from "./RadioSection";
5
5
  import page from "../../../../.storybook/page";
6
- import { Box } from "../../atoms/layouts";
7
6
 
8
7
  const story = page({
9
8
  title: "Components|Molecules/RadioSection",
@@ -44,106 +43,30 @@ const cardIconsLabel = `Accepting ${cardIcons
44
43
  ? ` and ${cardIcon.altText}.`
45
44
  : ` ` + cardIcon.altText
46
45
  )}`;
47
- const groupedSections = [
48
- [
49
- {
50
- id: "new-card-section",
51
- title: "Group 1: New Card",
52
- content: <p>The form to add a credit card would go here.</p>,
53
- rightIconsLabel: cardIconsLabel,
54
- rightIcons: cardIcons,
55
- required: true,
56
- dataQa: "New Card"
57
- },
58
- {
59
- id: "new-bank-section",
60
- title: "Group 1: New Bank Account",
61
- content: <p>The form to add a credit card would go here.</p>,
62
- required: true
63
- },
64
-
65
- {
66
- id: "bar4",
67
- title: "Group 1: Another One",
68
- content: <div>Content for another section</div>,
69
- required: true
70
- }
71
- ],
72
- [
73
- {
74
- id: "bar",
75
- title: "Group 2: Bar",
76
- content: <div>Content 1</div>,
77
- required: true
78
- }
79
- ],
80
- [
81
- {
82
- id: "bar2",
83
- title: "Group 3: Bar",
84
- content: <div>Content 1</div>,
85
- required: true
86
- },
87
- {
88
- id: "baz",
89
- title: "Group 3: Baz",
90
- content: <div>Content 1</div>,
91
- required: true
92
- }
93
- ]
94
- ];
95
46
  const sections = [
96
47
  {
97
- id: "new-card-section-2",
98
- title: "Section 1: New Card",
99
- content: <Box>The form to add a credit card would go here.</Box>,
48
+ id: "new-card-section",
49
+ title: "New Card",
50
+ content: <p>The form to add a credit card would go here.</p>,
100
51
  rightIconsLabel: cardIconsLabel,
101
52
  rightIcons: cardIcons,
102
53
  required: true
103
54
  },
104
- {
105
- id: "bar3",
106
- title: "Section 1: Bar",
107
- content: <div>Content 1</div>,
108
- required: true
109
- },
110
- { id: "baz2", title: "Section 1: Baz", content: <div>Content 2</div> }
55
+ { id: "bar", title: "Bar", content: <div>Content 1</div>, required: true },
56
+ { id: "baz", title: "Baz", content: <div>Content 2</div> }
111
57
  ];
112
58
 
113
59
  export const radioSection = () => {
114
60
  const [openSection, setOpenSection] = useState("");
115
- const [openGroupedSection, setOpenGroupedSection] = useState("");
116
61
  return (
117
- <>
118
- <Box padding="0 1rem" extraStyles="text-align: center;">
119
- <p>
120
- Using <b>sections</b>
121
- </p>
122
- </Box>
123
- <RadioSection
124
- isMobile={boolean("isMobile", false, "props")}
125
- supportsTouch={boolean("isMobile", false, "props")}
126
- toggleOpenSection={setOpenSection}
127
- openSection={openSection}
128
- staggeredAnimation={boolean("staggeredAnimation", false, "props")}
129
- sections={sections}
130
- isSectionRequired={true}
131
- />
132
- <Box />
133
- <Box padding="0 1rem" extraStyles="text-align: center;">
134
- <p>
135
- Using <b>groupedSections</b>
136
- </p>
137
- </Box>
138
- <RadioSection
139
- isMobile={boolean("isMobile", false, "props")}
140
- supportsTouch={boolean("isMobile", false, "props")}
141
- toggleOpenSection={setOpenGroupedSection}
142
- openSection={openGroupedSection}
143
- staggeredAnimation={boolean("staggeredAnimation", false, "props")}
144
- groupedSections={groupedSections}
145
- isSectionRequired={true}
146
- />
147
- </>
62
+ <RadioSection
63
+ isMobile={boolean("isMobile", false, "props")}
64
+ supportsTouch={boolean("isMobile", false, "props")}
65
+ toggleOpenSection={setOpenSection}
66
+ openSection={openSection}
67
+ staggeredAnimation={boolean("staggeredAnimation", false, "props")}
68
+ sections={sections}
69
+ isSectionRequired={true}
70
+ />
148
71
  );
149
72
  };
@@ -1,9 +1,9 @@
1
- import { WHITE, GHOST_GREY, ATHENS_GREY } from "../../../constants/colors";
1
+ import { WHITE, GREY_CHATEAU, ATHENS_GREY } from "../../../constants/colors";
2
2
 
3
3
  const headingBackgroundColor = `${WHITE}`;
4
4
  const headingDisabledColor = `${ATHENS_GREY}`;
5
5
  const bodyBackgroundColor = "#eeeeee";
6
- const borderColor = `${GHOST_GREY}`;
6
+ const borderColor = `${GREY_CHATEAU}`;
7
7
  const focusStyles = `outline: none;`;
8
8
 
9
9
  export const fallbackValues = {