@thecb/components 9.3.1-beta.8 → 9.3.1

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.
Files changed (39) hide show
  1. package/dist/index.cjs.js +114 -143
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.esm.js +114 -144
  5. package/dist/index.esm.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/components/atoms/button-with-action/ButtonWithAction.js +76 -70
  8. package/src/components/atoms/checkbox/Checkbox.js +2 -1
  9. package/src/components/atoms/dropdown/Dropdown.js +3 -3
  10. package/src/components/atoms/icons/KebabMenuIcon.d.ts +1 -0
  11. package/src/components/atoms/icons/KebabMenuIcon.js +38 -0
  12. package/src/components/atoms/icons/TrashIcon.js +42 -40
  13. package/src/components/atoms/icons/icons.stories.js +3 -1
  14. package/src/components/atoms/icons/index.d.ts +1 -0
  15. package/src/components/atoms/icons/index.js +3 -1
  16. package/src/components/atoms/layouts/Motion.js +7 -10
  17. package/src/components/atoms/toggle-switch/ToggleSwitch.js +2 -1
  18. package/src/components/molecules/collapsible-section/CollapsibleSection.js +2 -1
  19. package/src/components/molecules/popover/Popover.js +2 -1
  20. package/src/components/molecules/popup-menu/PopupMenu.js +152 -0
  21. package/src/components/molecules/popup-menu/PopupMenu.stories.js +40 -0
  22. package/src/components/molecules/popup-menu/PopupMenu.styled.js +20 -0
  23. package/src/components/molecules/popup-menu/PopupMenu.theme.js +11 -0
  24. package/src/components/molecules/popup-menu/index.d.ts +25 -0
  25. package/src/components/molecules/popup-menu/index.js +3 -0
  26. package/src/components/molecules/popup-menu/popup-menu-item/PopupMenuItem.js +79 -0
  27. package/src/components/molecules/popup-menu/popup-menu-item/PopupMenuItem.styled.js +27 -0
  28. package/src/components/molecules/popup-menu/popup-menu-item/PopupMenuItem.theme.js +23 -0
  29. package/src/components/molecules/radio-section/RadioSection.js +177 -284
  30. package/src/components/molecules/radio-section/RadioSection.stories.js +11 -42
  31. package/src/components/molecules/radio-section/radio-button/RadioButton.js +1 -1
  32. package/src/constants/keyboard.js +7 -0
  33. package/src/util/general.js +10 -0
  34. package/dist/src/apps/checkout/pages/payment/sub-pages/payment-amount/PaymentAmount_old.js +0 -49322
  35. package/src/.DS_Store +0 -0
  36. package/src/components/.DS_Store +0 -0
  37. package/src/components/atoms/.DS_Store +0 -0
  38. package/src/components/atoms/icons/.DS_Store +0 -0
  39. /package/src/components/atoms/icons/{ExternalLinkIcon.js → ExternalLinkicon.js} +0 -0
@@ -1,14 +1,21 @@
1
- import React, { Fragment, useState } from "react";
1
+ import React, { createRef, Fragment, useRef, useState } from "react";
2
2
  import styled from "styled-components";
3
3
  import { themeComponent } from "../../../util/themeUtils";
4
4
  import { fallbackValues } from "./RadioSection.theme";
5
5
  import { AnimatePresence } from "framer-motion";
6
6
  import RadioButton from "./radio-button/RadioButton";
7
7
  import { Box, Cluster, Stack, Motion } from "../../atoms/layouts";
8
- import { createIdFromString, noop } from "../../../util/general";
8
+ import { createIdFromString, noop, wrapIndex } from "../../../util/general";
9
9
  import Text from "../../atoms/text";
10
10
  import { CHARADE_GREY } from "../../../constants/colors";
11
- import SolidDivider from "../../atoms/solid-divider/SolidDivider";
11
+ import {
12
+ ARROW_DOWN,
13
+ ARROW_LEFT,
14
+ ARROW_RIGHT,
15
+ ARROW_UP,
16
+ ENTER,
17
+ SPACEBAR
18
+ } from "../../../constants/keyboard";
12
19
  /*
13
20
  Takes an array of section objects, each object should look like:
14
21
  {
@@ -49,13 +56,41 @@ const RadioSection = ({
49
56
  openHeight = "auto",
50
57
  containerStyles = "",
51
58
  ariaDescribedBy,
52
- isSectionRequired = false
59
+ isSectionRequired = false,
60
+ ...rest
53
61
  }) => {
54
- const areSectionsGrouped = typeof sections?.[0]?.[0] !== "undefined";
55
- const handleKeyDown = (id, e) => {
56
- if (e?.keyCode === 13 || e?.keyCode === 32) {
62
+ const [focused, setFocused] = useState(null);
63
+ const sectionRefs = useRef(
64
+ [...Array(sections.length)].map(() => createRef())
65
+ );
66
+
67
+ const handleKeyDown = (id, e, i) => {
68
+ if (e.currentTarget !== e.target) {
69
+ return;
70
+ }
71
+
72
+ // Allow Enter and Space to select a section
73
+ if (e.keyCode === ENTER || e.keyCode === SPACEBAR) {
74
+ e.preventDefault();
57
75
  toggleOpenSection(id);
58
76
  }
77
+
78
+ // Allow Up and Down arrow navigation between sections
79
+ if (
80
+ e.keyCode == ARROW_UP ||
81
+ e.keyCode == ARROW_DOWN ||
82
+ e.keyCode == ARROW_LEFT ||
83
+ e.keyCode == ARROW_RIGHT
84
+ ) {
85
+ e.preventDefault();
86
+ const indexIncrement =
87
+ e.keyCode == ARROW_RIGHT || e.keyCode == ARROW_DOWN ? 1 : -1;
88
+ const nextIndex = wrapIndex(i + indexIncrement, sections.length);
89
+
90
+ sectionRefs?.current[nextIndex]?.current?.focus();
91
+ setFocused(sections[nextIndex]?.id);
92
+ toggleOpenSection(sections[nextIndex]?.id);
93
+ }
59
94
  };
60
95
 
61
96
  const wrapper = {
@@ -99,8 +134,6 @@ const RadioSection = ({
99
134
  transition: opacity 0.3s ease;
100
135
  `;
101
136
 
102
- const [focused, setFocused] = useState(null);
103
-
104
137
  return (
105
138
  <Box
106
139
  padding="1px"
@@ -108,290 +141,150 @@ const RadioSection = ({
108
141
  borderRadius="4px"
109
142
  extraStyles={containerStyles}
110
143
  >
111
- <Stack childGap="0" role="radiogroup" aria-required={isSectionRequired}>
112
- {!areSectionsGrouped &&
113
- sections
114
- .filter(section => !section.hidden)
115
- .map(section => (
116
- <Motion
117
- tabIndex={
118
- section.hideRadioButton || section.disabled ? "-1" : "0"
119
- }
120
- onKeyDown={e =>
121
- !section.disabled && handleKeyDown(section.id, e)
122
- }
123
- onFocus={() => !section.disabled && setFocused(section.id)}
124
- onBlur={() => !section.disabled && setFocused(null)}
125
- hoverStyles={themeValues.focusStyles}
126
- animate={openSection === section.id ? "open" : "closed"}
127
- initial={initiallyOpen ? "open" : "closed"}
128
- key={`item-${sections.indexOf(section)}`}
129
- extraStyles={borderStyles}
130
- role="radio"
131
- aria-checked={openSection === section.id}
132
- aria-disabled={section.disabled}
133
- aria-required={section?.required}
134
- >
135
- <Stack childGap="0">
136
- <Box
137
- padding={
138
- section.hideRadioButton ? "1.5rem" : "1.25rem 1.5rem"
139
- }
140
- background={
141
- section.disabled
142
- ? themeValues.headingDisabledColor
143
- : themeValues.headingBackgroundColor
144
- }
145
- onClick={
146
- (isMobile && supportsTouch) || section.disabled
147
- ? noop
148
- : () => toggleOpenSection(section.id)
149
- }
150
- onTouchEnd={
151
- isMobile && supportsTouch && !section.disabled
152
- ? () => toggleOpenSection(section.id)
153
- : noop
154
- }
155
- key={`header-${section.id}`}
156
- borderSize="0px"
157
- borderColor={themeValues.borderColor}
158
- borderWidthOverride={
159
- openSection === section.id && !!section.content
160
- ? `0px 0px 1px 0px`
161
- : ``
162
- }
163
- extraStyles={!section.disabled ? "cursor: pointer;" : ""}
164
- dataQa={section.dataQa ? section.dataQa : section.id}
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)
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}
205
+ >
206
+ <Cluster
207
+ justify="space-between"
208
+ align="center"
209
+ childGap="1px"
210
+ nowrap
165
211
  >
166
- <Cluster
167
- justify="space-between"
168
- align="center"
169
- childGap="1px"
170
- nowrap
171
- >
172
- <Cluster justify="flex-start" align="center" nowrap>
173
- {!section.hideRadioButton && (
174
- <Box padding="0">
175
- <RadioButton
176
- id={`radio-input-${idString(section)}`}
177
- name={idString(section)}
178
- ariaDescribedBy={ariaDescribedBy}
179
- radioOn={openSection === section.id}
180
- radioFocused={focused === section.id}
181
- toggleRadio={
182
- section.disabled
183
- ? noop
184
- : () => toggleOpenSection(section.id)
185
- }
186
- tabIndex="-1"
187
- isRequired={section?.required}
188
- />
189
- </Box>
190
- )}
191
- {section.titleIcon && (
192
- <Cluster align="center">{section.titleIcon}</Cluster>
193
- )}
194
- <Box padding={section.titleIcon ? "0 0 0 8px" : "0"}>
195
- <Text
196
- as="label"
197
- htmlFor={`radio-input-${idString(section)}`}
198
- color={CHARADE_GREY}
199
- >
200
- {section.title}
201
- </Text>
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
+ />
202
229
  </Box>
203
- </Cluster>
204
- {section.rightIcons && (
205
- <Cluster
206
- childGap="0.5rem"
207
- aria-label={section?.rightIconsLabel || null}
208
- role={section?.rightIconsRole || null}
209
- >
210
- {section.rightIcons.map(icon => (
211
- <RightIcon
212
- src={icon.img}
213
- key={icon.img}
214
- fade={!icon.enabled}
215
- isMobile={isMobile}
216
- alt={icon.altText}
217
- aria-disabled={!icon.enabled}
218
- />
219
- ))}
220
- </Cluster>
221
230
  )}
222
- {section.rightTitleContent && (
223
- <Fragment>{section.rightTitleContent}</Fragment>
231
+ {section.titleIcon && (
232
+ <Cluster align="center">{section.titleIcon}</Cluster>
224
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>
225
243
  </Cluster>
226
- </Box>
227
- <AnimatePresence initial={false}>
228
- {openSection === section.id && (
229
- <Motion
230
- key={`content-${section.id}`}
231
- padding="0"
232
- background={themeValues.bodyBackgroundColor}
233
- layoutTransition
234
- initial="closed"
235
- animate="open"
236
- exit="closed"
237
- variants={wrapper}
238
- extraStyles={`transform-origin: 100% 0;`}
239
- >
240
- {section.content}
241
- </Motion>
242
- )}
243
- </AnimatePresence>
244
- </Stack>
245
- </Motion>
246
- ))}
247
- {areSectionsGrouped &&
248
- sections.map(sectionGroup =>
249
- sectionGroup
250
- .filter(section => !section.hidden)
251
- .map(section => (
252
- <Motion
253
- tabIndex={
254
- section.hideRadioButton || section.disabled ? "-1" : "0"
255
- }
256
- onKeyDown={e =>
257
- !section.disabled && handleKeyDown(section.id, e)
258
- }
259
- onFocus={() => !section.disabled && setFocused(section.id)}
260
- onBlur={() => !section.disabled && setFocused(null)}
261
- hoverStyles={themeValues.focusStyles}
262
- animate={openSection === section.id ? "open" : "closed"}
263
- initial={initiallyOpen ? "open" : "closed"}
264
- key={`item-${sections.indexOf(
265
- sectionGroup
266
- )}-${sectionGroup.indexOf(section)}`}
267
- extraStyles={borderStyles}
268
- role="radio"
269
- aria-checked={openSection === section.id}
270
- aria-disabled={section.disabled}
271
- aria-required={section?.required}
272
- >
273
- <Stack childGap="0">
274
- <Box
275
- padding={
276
- section.hideRadioButton ? "1.5rem" : "1.25rem 1.5rem"
277
- }
278
- background={
279
- section.disabled
280
- ? themeValues.headingDisabledColor
281
- : themeValues.headingBackgroundColor
282
- }
283
- onClick={
284
- (isMobile && supportsTouch) || section.disabled
285
- ? noop
286
- : () => toggleOpenSection(section.id)
287
- }
288
- onTouchEnd={
289
- isMobile && supportsTouch && !section.disabled
290
- ? () => toggleOpenSection(section.id)
291
- : noop
292
- }
293
- key={`header-${section.id}`}
294
- borderSize="0px"
295
- borderColor={themeValues.borderColor}
296
- borderWidthOverride={
297
- openSection === section.id && !!section.content
298
- ? `0px 0px 1px 0px`
299
- : ``
300
- }
301
- extraStyles={!section.disabled ? "cursor: pointer;" : ""}
302
- dataQa={section.dataQa ? section.dataQa : section.id}
303
- >
244
+ {section.rightIcons && (
304
245
  <Cluster
305
- justify="space-between"
306
- align="center"
307
- childGap="1px"
308
- nowrap
246
+ id={`right-icons-${idString(section)}`}
247
+ childGap="0.5rem"
248
+ aria-label={section.rightIconsLabel || null}
249
+ role={section.rightIconsRole || null}
309
250
  >
310
- <Cluster justify="flex-start" align="center" nowrap>
311
- {!section.hideRadioButton && (
312
- <Box padding="0">
313
- <RadioButton
314
- id={`radio-input-${idString(section)}`}
315
- name={idString(section)}
316
- ariaDescribedBy={ariaDescribedBy}
317
- radioOn={openSection === section.id}
318
- radioFocused={focused === section.id}
319
- toggleRadio={
320
- section.disabled
321
- ? noop
322
- : () => toggleOpenSection(section.id)
323
- }
324
- tabIndex="-1"
325
- isRequired={section?.required}
326
- />
327
- </Box>
328
- )}
329
- {section.titleIcon && (
330
- <Cluster align="center">
331
- {section.titleIcon}
332
- </Cluster>
333
- )}
334
- <Box padding={section.titleIcon ? "0 0 0 8px" : "0"}>
335
- <Text
336
- as="label"
337
- htmlFor={`radio-input-${idString(section)}`}
338
- color={CHARADE_GREY}
339
- >
340
- {section.title}
341
- </Text>
342
- </Box>
343
- </Cluster>
344
- {section.rightIcons && (
345
- <Cluster
346
- childGap="0.5rem"
347
- aria-label={section?.rightIconsLabel || null}
348
- role={section?.rightIconsRole || null}
349
- >
350
- {section.rightIcons.map(icon => (
351
- <RightIcon
352
- src={icon.img}
353
- key={icon.img}
354
- fade={!icon.enabled}
355
- isMobile={isMobile}
356
- alt={icon.altText}
357
- aria-disabled={!icon.enabled}
358
- />
359
- ))}
360
- </Cluster>
361
- )}
362
- {section.rightTitleContent && (
363
- <Fragment>{section.rightTitleContent}</Fragment>
364
- )}
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
+ ))}
365
261
  </Cluster>
366
- </Box>
367
- <AnimatePresence initial={false}>
368
- {openSection === section.id && (
369
- <Motion
370
- key={`content-${section.id}`}
371
- padding="0"
372
- background={themeValues.bodyBackgroundColor}
373
- layoutTransition
374
- initial="closed"
375
- animate="open"
376
- exit="closed"
377
- variants={wrapper}
378
- extraStyles={`transform-origin: 100% 0;`}
379
- >
380
- {section.content}
381
- </Motion>
382
- )}
383
- </AnimatePresence>
384
- </Stack>
385
- {sectionGroup.indexOf(section) ===
386
- sectionGroup.length - 1 && (
387
- <SolidDivider
388
- borderSize="2px"
389
- borderColor={themeValues.borderColor}
390
- />
262
+ )}
263
+ {section.rightTitleContent && (
264
+ <Fragment>{section.rightTitleContent}</Fragment>
265
+ )}
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>
391
283
  )}
392
- </Motion>
393
- ))
394
- )}
284
+ </AnimatePresence>
285
+ </Stack>
286
+ </Motion>
287
+ ))}
395
288
  </Stack>
396
289
  </Box>
397
290
  );
@@ -1,8 +1,7 @@
1
- import React, { Fragment, useState } from "react";
1
+ import React, { useState } from "react";
2
2
  import { text, boolean } from "@storybook/addon-knobs";
3
- import SolidDivider from "../../atoms/solid-divider/SolidDivider";
3
+
4
4
  import RadioSection from "./RadioSection";
5
- import { Box } from "../../atoms/layouts";
6
5
  import page from "../../../../.storybook/page";
7
6
 
8
7
  const story = page({
@@ -57,47 +56,17 @@ const sections = [
57
56
  { id: "baz", title: "Baz", content: <div>Content 2</div> }
58
57
  ];
59
58
 
60
- const groupedSections = [
61
- [
62
- {
63
- id: "new-card-section",
64
- title: "New Card",
65
- content: <p>The form to add a credit card would go here.</p>,
66
- rightIconsLabel: cardIconsLabel,
67
- rightIcons: cardIcons,
68
- required: true
69
- }
70
- ],
71
- [
72
- { id: "bar", title: "Bar", content: <div>Content 1</div>, required: true },
73
- { id: "baz", title: "Baz", content: <div>Content 2</div> }
74
- ]
75
- ];
76
-
77
59
  export const radioSection = () => {
78
60
  const [openSection, setOpenSection] = useState("");
79
- const [openSectionGrouped, setOpenSectionGrouped] = useState(null);
80
61
  return (
81
- <Fragment>
82
- <RadioSection
83
- isMobile={boolean("isMobile", false, "props")}
84
- supportsTouch={boolean("isMobile", false, "props")}
85
- toggleOpenSection={setOpenSection}
86
- openSection={openSection}
87
- staggeredAnimation={boolean("staggeredAnimation", false, "props")}
88
- sections={sections}
89
- isSectionRequired={true}
90
- />
91
- <Box />
92
- <RadioSection
93
- isMobile={boolean("isMobile", false, "props")}
94
- supportsTouch={boolean("isMobile", false, "props")}
95
- toggleOpenSection={setOpenSectionGrouped}
96
- openSection={openSectionGrouped}
97
- staggeredAnimation={boolean("staggeredAnimation", false, "props")}
98
- sections={groupedSections}
99
- isSectionRequired={true}
100
- />
101
- </Fragment>
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
+ />
102
71
  );
103
72
  };
@@ -16,7 +16,7 @@ const RadioButton = ({
16
16
  toggleRadio,
17
17
  name,
18
18
  disabled = false,
19
- ariaDescribedBy = "",
19
+ ariaDescribedBy,
20
20
  themeValues,
21
21
  ariaLabelledBy = "",
22
22
  ariaLabel = null,
@@ -0,0 +1,7 @@
1
+ export const ARROW_LEFT = 37;
2
+ export const ARROW_UP = 38;
3
+ export const ARROW_RIGHT = 39;
4
+ export const ARROW_DOWN = 40;
5
+ export const ENTER = 13;
6
+ export const ESCAPE = 27;
7
+ export const SPACEBAR = 32;
@@ -163,3 +163,13 @@ export const titleCaseString = string => {
163
163
  export const kebabCaseString = string => {
164
164
  return string?.replaceAll(" ", "-").toLowerCase();
165
165
  };
166
+
167
+ export const wrapIndex = (index, length) => {
168
+ if (index <= -1) {
169
+ return length - 1;
170
+ } else if (index >= length) {
171
+ return 0;
172
+ } else {
173
+ return index;
174
+ }
175
+ };