@thecb/components 6.0.0-beta.2 → 6.0.0-beta.20

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": "6.0.0-beta.2",
3
+ "version": "6.0.0-beta.20",
4
4
  "description": "Common lib for CityBase react components",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.esm.js",
@@ -78,6 +78,7 @@
78
78
  },
79
79
  "dependencies": {
80
80
  "@babel/runtime": "^7.15.4",
81
+ "core-js": "^3.22.5",
81
82
  "formatted-input": "^1.0.0",
82
83
  "framer-motion": "^1.11.0",
83
84
  "numeral": "^2.0.6",
@@ -19,6 +19,7 @@ const CountryDropdown = ({
19
19
  errorMessages={errorMessages}
20
20
  showErrors={showErrors}
21
21
  onChange={onChange}
22
+ autocompleteValue="country-name"
22
23
  />
23
24
  );
24
25
  export default CountryDropdown;
@@ -4,6 +4,8 @@ import Text from "../text";
4
4
  import { noop } from "../../../util/general";
5
5
  import DropdownIcon from "./DropdownIcon";
6
6
  import styled from "styled-components";
7
+ // support for Array.prototype.at() in older browsers
8
+ import "core-js/proposals/relative-indexing-method";
7
9
 
8
10
  import {
9
11
  WHITE,
@@ -16,11 +18,14 @@ import { fallbackValues } from "./Dropdown.theme";
16
18
  import { themeComponent } from "../../../util/themeUtils";
17
19
 
18
20
  const IconWrapper = styled.div`
21
+ position: absolute;
19
22
  display: flex;
20
23
  flex-direction: column;
21
24
  justify-content: center;
22
25
  transition: transform 0.3s ease;
23
- ${({ open }) => (open ? "transform: rotate(-180deg)" : "")}
26
+ ${({ open }) => (open ? "transform: rotate(-180deg)" : "")};
27
+ top: 20px;
28
+ right: 12px;
24
29
  `;
25
30
 
26
31
  const DropdownContentWrapper = styled.div`
@@ -39,9 +44,13 @@ const DropdownContentWrapper = styled.div`
39
44
  &:focus {
40
45
  outline: none;
41
46
  }
47
+
48
+ ul {
49
+ padding-left: 0;
50
+ }
42
51
  `;
43
52
 
44
- const DropdownItemWrapper = styled.div`
53
+ const DropdownItemWrapper = styled.li`
45
54
  background-color: ${({ selected, themeValues }) =>
46
55
  selected ? themeValues.selectedColor : WHITE};
47
56
  text-align: start;
@@ -51,6 +60,7 @@ const DropdownItemWrapper = styled.div`
51
60
  padding: 1rem;
52
61
  box-sizing: border-box;
53
62
  width: 100%;
63
+ list-style: none;
54
64
  cursor: ${({ disabled }) => (disabled ? "default" : "pointer")};
55
65
 
56
66
  &:hover {
@@ -72,14 +82,6 @@ const DropdownItemWrapper = styled.div`
72
82
  }
73
83
  `;
74
84
 
75
- const SearchInput = styled.input`
76
- border: none;
77
- background-color: ${({ themeValues }) =>
78
- themeValues.hoverColor && themeValues.hoverColor};
79
- font-size: 16px;
80
- height: 24px;
81
- `;
82
-
83
85
  const Dropdown = ({
84
86
  placeholder,
85
87
  options,
@@ -93,12 +95,17 @@ const Dropdown = ({
93
95
  maxHeight,
94
96
  widthFitOptions = false,
95
97
  disabled,
96
- hasTitles = false
98
+ hasTitles = false,
99
+ autoEraseTypeAhead = true, // legacy behavior as of 05/22
100
+ ariaLabelledby,
101
+ autocompleteValue = "" // autofill item for browsers, like country-name or address-level1 for state
97
102
  }) => {
98
103
  const [inputValue, setInputValue] = useState("");
99
104
  const [optionsState, setOptionsState] = useState([]);
100
105
  const [filteredOptions, setFilteredOptions] = useState([]);
101
106
  const [optionsChanged, setOptionsChanged] = useState(true);
107
+ const [selectedRef, setSelectedRef] = useState(undefined);
108
+ const [focusedRef, setFocusedRef] = useState(undefined);
102
109
 
103
110
  if (optionsState !== options) {
104
111
  setOptionsState(options);
@@ -118,16 +125,17 @@ const Dropdown = ({
118
125
  value ? options.find(option => option.value === value)?.text : placeholder;
119
126
 
120
127
  const onKeyDown = e => {
128
+ console.log("current input value top of keyDown", inputValue);
121
129
  const { key, keyCode } = e;
122
130
  const focus = document.activeElement;
123
131
  console.log("dropdown value is", value);
124
- console.log("focus is", focus);
125
- console.log("option refs are", optionRefs.current);
126
132
  const optionEl = optionRefs.current.find(ref => ref.current === focus);
127
- console.log("option el is", optionEl);
128
133
  switch (key) {
129
134
  case "ArrowDown":
130
135
  e.preventDefault();
136
+ if (!isOpen) {
137
+ onClick();
138
+ }
131
139
  if (optionEl) {
132
140
  if (optionEl.current.nextElementSibling) {
133
141
  optionEl.current.nextElementSibling.focus();
@@ -159,24 +167,48 @@ const Dropdown = ({
159
167
  break;
160
168
  case "Backspace" || "Delete":
161
169
  e.preventDefault();
162
- console.log("input value is", inputValue);
163
- console.log("new input value will be", inputValue.slice(0, -1));
164
170
  setInputValue(inputValue.slice(0, -1));
165
171
  break;
172
+ case "Home":
173
+ e.preventDefault();
174
+ optionRefs.current[0].current.focus();
175
+ break;
176
+ case "End":
177
+ e.preventDefault();
178
+ console.log("option refs current", optionRefs.current);
179
+ optionRefs.current.at(-1).current.focus();
180
+ break;
181
+ case "Escape":
182
+ if (isOpen) {
183
+ onClick();
184
+ }
185
+ break;
166
186
  }
167
187
  if ((keyCode > 64 && keyCode < 91) || keyCode == 32 || keyCode == 189) {
168
188
  e.preventDefault();
189
+ console.log("current input value inside keydown if", inputValue);
169
190
  setInputValue(inputValue + key);
170
191
  }
171
192
  };
172
193
 
194
+ const handleItemSelection = (evt, choice, i) => {
195
+ if (disabledValues.includes(choice.value)) {
196
+ evt.preventDefault();
197
+ } else {
198
+ setSelectedRef(optionRefs.current[i]);
199
+ onSelect(choice.value);
200
+ if (isOpen) {
201
+ onClick();
202
+ }
203
+ }
204
+ };
205
+
173
206
  useEffect(() => {
174
- console.log(
175
- "option refs in isopen useffect",
176
- optionRefs.current[0].current
177
- );
178
- console.log("value in isopen useffect", value);
179
- if (isOpen && optionRefs.current[0].current) {
207
+ if (isOpen && selectedRef !== undefined && selectedRef.current !== null) {
208
+ // WAI-ARIA requires the selected option to receive focus
209
+ selectedRef.current.focus();
210
+ } else if (isOpen && optionRefs.current[0].current) {
211
+ // If no selected option, first option receives focus
180
212
  optionRefs.current[0].current.focus();
181
213
  }
182
214
  clearTimeout(timer);
@@ -184,8 +216,10 @@ const Dropdown = ({
184
216
  }, [isOpen]);
185
217
 
186
218
  useEffect(() => {
187
- clearTimeout(timer);
188
- setTimer(setTimeout(() => setInputValue(""), 2000));
219
+ if (autoEraseTypeAhead) {
220
+ clearTimeout(timer);
221
+ setTimer(setTimeout(() => setInputValue(""), 3000));
222
+ }
189
223
  setFilteredOptions(
190
224
  options.filter(
191
225
  option =>
@@ -213,26 +247,31 @@ const Dropdown = ({
213
247
 
214
248
  return (
215
249
  <Box
216
- onKeyDown={onKeyDown}
217
- onClick={onClick}
218
250
  padding="0"
251
+ background={isOpen ? themeValues.hoverColor : WHITE}
252
+ extraStyles={`position: relative;`}
253
+ minWidth="100%"
254
+ onClick={() => {
255
+ if (!isOpen) {
256
+ onClick();
257
+ }
258
+ }}
259
+ onKeyDown={onKeyDown}
219
260
  width="100%"
220
- hoverStyles={`background-color: ${themeValues.hoverColor};`}
221
- aria-expanded={isOpen}
222
- extraStyles={
223
- disabled &&
224
- `color: #6e727e;
225
- background-color: #f7f7f7;
226
- pointer-events: none;`
227
- }
228
- title={hasTitles ? getSelection() : null}
229
261
  >
230
262
  <Box
231
- as="button"
263
+ as="input"
264
+ aria-multiline="false"
265
+ aria-autocomplete="list"
266
+ aria-controls={`${ariaLabelledby}_listbox`}
267
+ aria-activedescendant="focused_option"
268
+ aria-owns={`${ariaLabelledby}_listbox`}
269
+ aria-haspopup="listbox"
270
+ aria-labelledby={ariaLabelledby}
271
+ aria-expanded={isOpen}
272
+ autocomplete={autocompleteValue}
232
273
  background={isOpen ? themeValues.hoverColor : WHITE}
233
- width="100%"
234
- padding="12px"
235
- hoverStyles={`background-color: ${themeValues.hoverColor};`}
274
+ borderRadius="2px"
236
275
  borderSize="1px"
237
276
  borderColor={
238
277
  isError
@@ -241,78 +280,101 @@ const Dropdown = ({
241
280
  ? themeValues.selectedColor
242
281
  : GREY_CHATEAU
243
282
  }
244
- borderRadius="2px"
245
- tabIndex={0}
246
- dataQa={placeholder}
247
- extraStyles={`height: 48px;
248
- ${disabled &&
249
- `color: #6e727e;
250
- background-color: #f7f7f7;
251
- pointer-events: none;`}
252
- `}
253
- >
254
- <Stack direction="row" bottomItem={2}>
255
- {isOpen ? (
256
- <SearchInput
257
- aria-label={inputValue || "Dropdown awaiting search value"}
258
- value={inputValue}
259
- onChange={noop}
260
- themeValues={themeValues}
261
- />
262
- ) : (
263
- <Text
264
- variant="p"
265
- extraStyles={
266
- disabled &&
267
- `color: #6e727e;
283
+ extraStyles={
284
+ disabled &&
285
+ `color: #6e727e;
268
286
  background-color: #f7f7f7;
269
287
  pointer-events: none;`
270
- }
271
- >
272
- {getSelection()}
273
- </Text>
274
- )}
275
- <IconWrapper open={isOpen}>
276
- <DropdownIcon />
277
- </IconWrapper>
278
- </Stack>
279
- </Box>
280
- {isOpen ? (
281
- <DropdownContentWrapper
282
- maxHeight={maxHeight}
283
- open={isOpen}
284
- ref={dropdownRef}
285
- widthFitOptions={widthFitOptions}
286
- tabIndex={0}
287
- >
288
- <Stack childGap="0">
289
- {filteredOptions.map((choice, i) => (
290
- <DropdownItemWrapper
291
- key={choice.value}
292
- ref={optionRefs.current[i]}
293
- as="button"
294
- tabIndex={-1}
295
- onClick={
296
- disabledValues.includes(choice.value)
297
- ? evt => evt.preventDefault()
298
- : () => onSelect(choice.value)
288
+ }
289
+ hoverStyles={`background-color: ${themeValues.hoverColor};`}
290
+ isOpen={isOpen}
291
+ minHeight="48px"
292
+ minWidth="100%"
293
+ name={autocompleteValue}
294
+ onFocus={() => {
295
+ /*
296
+ if (!isOpen) {
297
+ onClick();
298
+ }
299
+ */
300
+ }}
301
+ onChange={e => {
302
+ console.log("current input value onChange", inputValue);
303
+ console.log("input change event", e.target);
304
+ console.log("input change event value", e.target.value);
305
+ // support autofill and copy/paste
306
+ if (e.tarvet.value !== inputValue) {
307
+ setInputValue(e.target.value);
308
+ }
309
+ }}
310
+ padding="12px"
311
+ placeholder={getSelection()}
312
+ role="combobox"
313
+ themeValues={themeValues}
314
+ title={hasTitles ? getSelection() : null}
315
+ type="text"
316
+ tabIndex={0}
317
+ value={inputValue}
318
+ width="100%"
319
+ dataQa={placeholder}
320
+ />
321
+ <IconWrapper open={isOpen} onClick={onClick}>
322
+ <DropdownIcon />
323
+ </IconWrapper>
324
+ <Fragment>
325
+ {isOpen ? (
326
+ <DropdownContentWrapper
327
+ maxHeight={maxHeight}
328
+ open={isOpen}
329
+ ref={dropdownRef}
330
+ widthFitOptions={widthFitOptions}
331
+ tabIndex={0}
332
+ role="listbox"
333
+ id={`${ariaLabelledby}_listbox`}
334
+ >
335
+ <Stack childGap="0" as="ul">
336
+ {filteredOptions.map((choice, i) => {
337
+ if (
338
+ choice.value === value &&
339
+ selectedRef !== optionRefs.current[i]
340
+ ) {
341
+ setSelectedRef(optionRefs.current[i]);
299
342
  }
300
- selected={choice.value === value}
301
- disabled={disabledValues.includes(choice.value)}
302
- data-qa={choice.text}
303
- themeValues={themeValues}
304
- title={hasTitles ? choice.text : null}
305
- >
306
- <Text
307
- variant="p"
308
- color={
309
- choice.value === value
310
- ? WHITE
311
- : disabledValues.includes(choice.value)
312
- ? STORM_GREY
313
- : MINESHAFT_GREY
314
- }
315
- extraStyles={`padding-left: 16px;
343
+ return (
344
+ <DropdownItemWrapper
345
+ id={
346
+ focusedRef === optionRefs.current[i]
347
+ ? "focused_option"
348
+ : choice.value
349
+ }
350
+ key={choice.value}
351
+ ref={optionRefs.current[i]}
352
+ tabIndex={-1}
353
+ onClick={e => handleItemSelection(e, choice, i)}
354
+ onKeyDown={e => {
355
+ if (e.keyCode === 13) {
356
+ handleItemSelection(e, choice, i);
357
+ }
358
+ }}
359
+ selected={choice.value === value}
360
+ aria-selected={choice.value === value}
361
+ disabled={disabledValues.includes(choice.value)}
362
+ data-qa={choice.text}
363
+ themeValues={themeValues}
364
+ title={hasTitles ? choice.text : null}
365
+ role="option"
366
+ onFocus={() => setFocusedRef(optionRefs.current[i])}
367
+ >
368
+ <Text
369
+ variant="p"
370
+ color={
371
+ choice.value === value
372
+ ? WHITE
373
+ : disabledValues.includes(choice.value)
374
+ ? STORM_GREY
375
+ : MINESHAFT_GREY
376
+ }
377
+ extraStyles={`padding-left: 16px;
316
378
  cursor: ${
317
379
  disabledValues.includes(choice.value)
318
380
  ? "default"
@@ -321,16 +383,18 @@ const Dropdown = ({
321
383
  white-space: nowrap;
322
384
  overflow: hidden;
323
385
  text-overflow: ellipsis;`}
324
- >
325
- {choice.text}
326
- </Text>
327
- </DropdownItemWrapper>
328
- ))}
329
- </Stack>
330
- </DropdownContentWrapper>
331
- ) : (
332
- <Fragment />
333
- )}
386
+ >
387
+ {choice.text}
388
+ </Text>
389
+ </DropdownItemWrapper>
390
+ );
391
+ })}
392
+ </Stack>
393
+ </DropdownContentWrapper>
394
+ ) : (
395
+ <Fragment />
396
+ )}
397
+ </Fragment>
334
398
  </Box>
335
399
  );
336
400
  };
@@ -19,7 +19,8 @@ const FormSelect = ({
19
19
  disabledValues,
20
20
  disabled,
21
21
  themeValues,
22
- hasTitles = false
22
+ hasTitles = false,
23
+ autocompleteValue // autofill item for browsers, like country-name or address-level1 for state
23
24
  }) => {
24
25
  const [open, setOpen] = useState(false);
25
26
  const dropdownRef = useRef(null);
@@ -58,7 +59,7 @@ const FormSelect = ({
58
59
  </Cluster>
59
60
  </Box>
60
61
  <Dropdown
61
- aria-labelledby={labelTextWhenNoError.replace(/\s+/g, "-")}
62
+ ariaLabelledby={labelTextWhenNoError.replace(/\s+/g, "-")}
62
63
  maxHeight={dropdownMaxHeight}
63
64
  hasTitles={hasTitles}
64
65
  placeholder={options[0] ? options[0].text : ""}
@@ -74,6 +75,7 @@ const FormSelect = ({
74
75
  }
75
76
  onClick={() => setOpen(!open)}
76
77
  disabled={disabled}
78
+ autocompleteValue={autocompleteValue}
77
79
  />
78
80
  <Stack direction="row" justify="space-between">
79
81
  {(field.hasErrors && field.dirty) || (field.hasErrors && showErrors) ? (
@@ -80,7 +80,7 @@ const Box = ({
80
80
  onTouchEnd={onTouchEnd}
81
81
  {...rest}
82
82
  >
83
- {safeChildren(children, <Fragment />)}
83
+ {children && safeChildren(children, <Fragment />)}
84
84
  </BoxWrapper>
85
85
  );
86
86
 
@@ -22,6 +22,7 @@ const FormStateDropdown = ({
22
22
  labelTextWhenNoError={labelTextWhenNoError}
23
23
  errorMessages={errorMessages}
24
24
  showErrors={showErrors}
25
+ autocompleteValue="address-level1"
25
26
  />
26
27
  );
27
28
  };