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

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.22",
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,18 @@ const Dropdown = ({
93
95
  maxHeight,
94
96
  widthFitOptions = false,
95
97
  disabled,
96
- hasTitles = false
98
+ hasTitles = false,
99
+ autoEraseTypeAhead = true,
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);
109
+ const [inputChangedByAutofill, setInputChangedByAutofill] = useState(false);
102
110
 
103
111
  if (optionsState !== options) {
104
112
  setOptionsState(options);
@@ -118,16 +126,17 @@ const Dropdown = ({
118
126
  value ? options.find(option => option.value === value)?.text : placeholder;
119
127
 
120
128
  const onKeyDown = e => {
129
+ console.log("current input value top of keyDown", inputValue);
121
130
  const { key, keyCode } = e;
122
131
  const focus = document.activeElement;
123
132
  console.log("dropdown value is", value);
124
- console.log("focus is", focus);
125
- console.log("option refs are", optionRefs.current);
126
133
  const optionEl = optionRefs.current.find(ref => ref.current === focus);
127
- console.log("option el is", optionEl);
128
134
  switch (key) {
129
135
  case "ArrowDown":
130
136
  e.preventDefault();
137
+ if (!isOpen) {
138
+ onClick();
139
+ }
131
140
  if (optionEl) {
132
141
  if (optionEl.current.nextElementSibling) {
133
142
  optionEl.current.nextElementSibling.focus();
@@ -159,24 +168,48 @@ const Dropdown = ({
159
168
  break;
160
169
  case "Backspace" || "Delete":
161
170
  e.preventDefault();
162
- console.log("input value is", inputValue);
163
- console.log("new input value will be", inputValue.slice(0, -1));
164
171
  setInputValue(inputValue.slice(0, -1));
165
172
  break;
173
+ case "Home":
174
+ e.preventDefault();
175
+ optionRefs.current[0].current.focus();
176
+ break;
177
+ case "End":
178
+ e.preventDefault();
179
+ console.log("option refs current", optionRefs.current);
180
+ optionRefs.current.at(-1).current.focus();
181
+ break;
182
+ case "Escape":
183
+ if (isOpen) {
184
+ onClick();
185
+ }
186
+ break;
166
187
  }
167
188
  if ((keyCode > 64 && keyCode < 91) || keyCode == 32 || keyCode == 189) {
168
189
  e.preventDefault();
190
+ console.log("current input value inside keydown if", inputValue);
169
191
  setInputValue(inputValue + key);
170
192
  }
171
193
  };
172
194
 
195
+ const handleItemSelection = (evt, choice, i) => {
196
+ if (disabledValues.includes(choice.value)) {
197
+ evt.preventDefault();
198
+ } else {
199
+ setSelectedRef(optionRefs.current[i]);
200
+ onSelect(choice.value);
201
+ if (isOpen) {
202
+ onClick();
203
+ }
204
+ }
205
+ };
206
+
173
207
  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) {
208
+ if (isOpen && selectedRef !== undefined && selectedRef.current !== null) {
209
+ // WAI-ARIA requires the selected option to receive focus
210
+ selectedRef.current.focus();
211
+ } else if (isOpen && optionRefs.current[0].current) {
212
+ // If no selected option, first option receives focus
180
213
  optionRefs.current[0].current.focus();
181
214
  }
182
215
  clearTimeout(timer);
@@ -184,8 +217,10 @@ const Dropdown = ({
184
217
  }, [isOpen]);
185
218
 
186
219
  useEffect(() => {
187
- clearTimeout(timer);
188
- setTimer(setTimeout(() => setInputValue(""), 2000));
220
+ if (autoEraseTypeAhead) {
221
+ clearTimeout(timer);
222
+ setTimer(setTimeout(() => setInputValue(""), 3000));
223
+ }
189
224
  setFilteredOptions(
190
225
  options.filter(
191
226
  option =>
@@ -197,11 +232,12 @@ const Dropdown = ({
197
232
 
198
233
  useEffect(() => {
199
234
  if (
200
- !isOpen &&
235
+ (!isOpen || inputChangedByAutofill) &&
201
236
  filteredOptions[0] &&
202
237
  !disabledValues.includes(filteredOptions[0].value) &&
203
238
  filteredOptions[0].text != placeholder
204
239
  ) {
240
+ setInputChangedByAutofill(false);
205
241
  onSelect(filteredOptions[0].value);
206
242
  }
207
243
  if (optionRefs.current[0].current) {
@@ -213,26 +249,31 @@ const Dropdown = ({
213
249
 
214
250
  return (
215
251
  <Box
216
- onKeyDown={onKeyDown}
217
- onClick={onClick}
218
252
  padding="0"
253
+ background={isOpen ? themeValues.hoverColor : WHITE}
254
+ extraStyles={`position: relative;`}
255
+ minWidth="100%"
256
+ onClick={() => {
257
+ if (!isOpen) {
258
+ onClick();
259
+ }
260
+ }}
261
+ onKeyDown={onKeyDown}
219
262
  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
263
  >
230
264
  <Box
231
- as="button"
265
+ as="input"
266
+ aria-multiline="false"
267
+ aria-autocomplete="list"
268
+ aria-controls={`${ariaLabelledby}_listbox`}
269
+ aria-activedescendant="focused_option"
270
+ aria-owns={`${ariaLabelledby}_listbox`}
271
+ aria-haspopup="listbox"
272
+ aria-labelledby={ariaLabelledby}
273
+ aria-expanded={isOpen}
274
+ autocomplete={autocompleteValue}
232
275
  background={isOpen ? themeValues.hoverColor : WHITE}
233
- width="100%"
234
- padding="12px"
235
- hoverStyles={`background-color: ${themeValues.hoverColor};`}
276
+ borderRadius="2px"
236
277
  borderSize="1px"
237
278
  borderColor={
238
279
  isError
@@ -241,78 +282,95 @@ const Dropdown = ({
241
282
  ? themeValues.selectedColor
242
283
  : GREY_CHATEAU
243
284
  }
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;
285
+ extraStyles={
286
+ disabled &&
287
+ `color: #6e727e;
268
288
  background-color: #f7f7f7;
269
289
  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)
290
+ }
291
+ hoverStyles={`background-color: ${themeValues.hoverColor};`}
292
+ isOpen={isOpen}
293
+ minHeight="48px"
294
+ minWidth="100%"
295
+ name={autocompleteValue}
296
+ onChange={e => {
297
+ console.log("current input value onChange", inputValue);
298
+ console.log("input change event", e.target);
299
+ console.log("input change event value", e.target.value);
300
+ // support autofill and copy/paste
301
+ if (e.target.value !== inputValue) {
302
+ setInputValue(e.target.value);
303
+ setInputChangedByAutofill(true);
304
+ }
305
+ }}
306
+ padding="12px"
307
+ placeholder={getSelection()}
308
+ role="combobox"
309
+ themeValues={themeValues}
310
+ title={hasTitles ? getSelection() : null}
311
+ type="text"
312
+ tabIndex={0}
313
+ value={inputValue}
314
+ width="100%"
315
+ dataQa={placeholder}
316
+ />
317
+ <IconWrapper open={isOpen} onClick={onClick}>
318
+ <DropdownIcon />
319
+ </IconWrapper>
320
+ <Fragment>
321
+ {isOpen ? (
322
+ <DropdownContentWrapper
323
+ maxHeight={maxHeight}
324
+ open={isOpen}
325
+ ref={dropdownRef}
326
+ widthFitOptions={widthFitOptions}
327
+ tabIndex={0}
328
+ role="listbox"
329
+ id={`${ariaLabelledby}_listbox`}
330
+ >
331
+ <Stack childGap="0" as="ul">
332
+ {filteredOptions.map((choice, i) => {
333
+ if (
334
+ choice.value === value &&
335
+ selectedRef !== optionRefs.current[i]
336
+ ) {
337
+ setSelectedRef(optionRefs.current[i]);
299
338
  }
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;
339
+ return (
340
+ <DropdownItemWrapper
341
+ id={
342
+ focusedRef === optionRefs.current[i]
343
+ ? "focused_option"
344
+ : choice.value
345
+ }
346
+ key={choice.value}
347
+ ref={optionRefs.current[i]}
348
+ tabIndex={-1}
349
+ onClick={e => handleItemSelection(e, choice, i)}
350
+ onKeyDown={e => {
351
+ if (e.keyCode === 13) {
352
+ handleItemSelection(e, choice, i);
353
+ }
354
+ }}
355
+ selected={choice.value === value}
356
+ aria-selected={choice.value === value}
357
+ disabled={disabledValues.includes(choice.value)}
358
+ data-qa={choice.text}
359
+ themeValues={themeValues}
360
+ title={hasTitles ? choice.text : null}
361
+ role="option"
362
+ onFocus={() => setFocusedRef(optionRefs.current[i])}
363
+ >
364
+ <Text
365
+ variant="p"
366
+ color={
367
+ choice.value === value
368
+ ? WHITE
369
+ : disabledValues.includes(choice.value)
370
+ ? STORM_GREY
371
+ : MINESHAFT_GREY
372
+ }
373
+ extraStyles={`padding-left: 16px;
316
374
  cursor: ${
317
375
  disabledValues.includes(choice.value)
318
376
  ? "default"
@@ -321,16 +379,18 @@ const Dropdown = ({
321
379
  white-space: nowrap;
322
380
  overflow: hidden;
323
381
  text-overflow: ellipsis;`}
324
- >
325
- {choice.text}
326
- </Text>
327
- </DropdownItemWrapper>
328
- ))}
329
- </Stack>
330
- </DropdownContentWrapper>
331
- ) : (
332
- <Fragment />
333
- )}
382
+ >
383
+ {choice.text}
384
+ </Text>
385
+ </DropdownItemWrapper>
386
+ );
387
+ })}
388
+ </Stack>
389
+ </DropdownContentWrapper>
390
+ ) : (
391
+ <Fragment />
392
+ )}
393
+ </Fragment>
334
394
  </Box>
335
395
  );
336
396
  };
@@ -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
  };