@lumx/react 3.3.0 → 3.3.1-alpha.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
@@ -7,8 +7,8 @@
7
7
  },
8
8
  "dependencies": {
9
9
  "@juggle/resize-observer": "^3.2.0",
10
- "@lumx/core": "^3.3.0",
11
- "@lumx/icons": "^3.3.0",
10
+ "@lumx/core": "^3.3.1-alpha.0",
11
+ "@lumx/icons": "^3.3.1-alpha.0",
12
12
  "@popperjs/core": "^2.5.4",
13
13
  "body-scroll-lock": "^3.1.5",
14
14
  "classnames": "^2.2.6",
@@ -113,5 +113,5 @@
113
113
  "build:storybook": "cd storybook && ./build"
114
114
  },
115
115
  "sideEffects": false,
116
- "version": "3.3.0"
116
+ "version": "3.3.1-alpha.0"
117
117
  }
@@ -146,6 +146,8 @@ export type Typography = TypographyInterface | TypographyCustom;
146
146
  export const AspectRatio = {
147
147
  /** Intrinsic content ratio. */
148
148
  original: 'original',
149
+ /** Ratio 3:1 */
150
+ panoramic: 'panoramic',
149
151
  /** Ratio 16:9 */
150
152
  wide: 'wide',
151
153
  /** Ratio 3:2 */
@@ -173,16 +173,18 @@ const _InnerPopover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, ref
173
173
  * unless specifically requested not to.
174
174
  */
175
175
  if (isFocusedWithin.current && focusAnchorOnClose) {
176
- let elementToFocus = parentElement?.current;
177
- if (!elementToFocus && anchorRef?.current) {
178
- // Focus the first focusable element in anchor.
179
- elementToFocus = getFirstAndLastFocusable(anchorRef.current).first;
176
+ if (parentElement?.current) {
177
+ parentElement?.current.focus();
180
178
  }
181
- if (!elementToFocus) {
179
+
180
+ const firstFocusable = anchorRef?.current && getFirstAndLastFocusable(anchorRef?.current).first;
181
+ if (firstFocusable) {
182
+ // Focus the first focusable element in anchor.
183
+ firstFocusable.focus();
184
+ } else {
182
185
  // Fallback on the anchor element.
183
- elementToFocus = anchorRef?.current;
186
+ anchorRef?.current?.focus();
184
187
  }
185
- elementToFocus?.focus({ preventScroll: true });
186
188
  }
187
189
 
188
190
  onClose();
@@ -1,20 +1,9 @@
1
1
  /* istanbul ignore file */
2
2
  import { mdiTram } from '@lumx/icons/';
3
- import {
4
- Chip,
5
- Dialog,
6
- List,
7
- ListDivider,
8
- ListItem,
9
- ListSubheader,
10
- SelectMultiple,
11
- Size,
12
- TextField,
13
- Toolbar,
14
- } from '@lumx/react';
3
+ import { Chip, List, ListItem, SelectMultiple, Size } from '@lumx/react';
15
4
  import { useBooleanState } from '@lumx/react/hooks/useBooleanState';
16
5
  import noop from 'lodash/noop';
17
- import React, { MouseEventHandler, SyntheticEvent, useRef, useState } from 'react';
6
+ import React, { MouseEventHandler, SyntheticEvent, useState } from 'react';
18
7
  import { SelectVariant } from './constants';
19
8
 
20
9
  export default { title: 'LumX components/select/Select Multiple' };
@@ -236,95 +225,3 @@ export const ChipsCustomSelectMultiple = ({ theme }: any) => {
236
225
  </SelectMultiple>
237
226
  );
238
227
  };
239
-
240
- /**
241
- * Test select focus trap (focus is contained inside the dialog then inside the select dropdown)
242
- */
243
- export const SelectWithinADialog = ({ theme }: any) => {
244
- const searchFieldRef = useRef(null);
245
-
246
- const [searchText, setSearchText] = useState<string>();
247
- const [values, setValues] = useState<string[]>([]);
248
- const [isOpen, closeSelect, , toggleSelect] = useBooleanState(false);
249
-
250
- const clearSelected = (event: SyntheticEvent, value: string) => {
251
- event.stopPropagation();
252
- setValues(value ? values.filter((val) => val !== value) : []);
253
- };
254
-
255
- const selectItem = (item: string) => () => {
256
- if (values.includes(item)) {
257
- return;
258
- }
259
-
260
- closeSelect();
261
- setValues([...values, item]);
262
- };
263
-
264
- const filteredChoices =
265
- searchText && searchText.length > 0 ? CHOICES.filter((choice) => choice.includes(searchText)) : CHOICES;
266
-
267
- return (
268
- <>
269
- <Dialog isOpen>
270
- <header>
271
- <Toolbar label={<span className="lumx-typography-title">Dialog header</span>} />
272
- </header>
273
- <div className="lumx-spacing-padding-horizontal-huge lumx-spacing-padding-bottom-huge">
274
- {/* Testing hidden input do not count in th focus trap*/}
275
- <input hidden type="file" />
276
- <input type="hidden" />
277
-
278
- <div className="lumx-spacing-margin-bottom-huge">The select should capture the focus on open.</div>
279
-
280
- <SelectMultiple
281
- isOpen={isOpen}
282
- value={values}
283
- onClear={clearSelected}
284
- clearButtonProps={{ label: 'Clear' }}
285
- label={LABEL}
286
- placeholder={PLACEHOLDER}
287
- theme={theme}
288
- onInputClick={toggleSelect}
289
- onDropdownClose={closeSelect}
290
- icon={mdiTram}
291
- focusElement={searchFieldRef}
292
- >
293
- <List isClickable>
294
- <>
295
- <ListSubheader>
296
- <TextField
297
- clearButtonProps={{ label: 'Clear' }}
298
- placeholder="Search"
299
- role="searchbox"
300
- inputRef={searchFieldRef}
301
- onChange={setSearchText}
302
- value={searchText}
303
- />
304
- </ListSubheader>
305
- <ListDivider role="presentation" />
306
- </>
307
-
308
- {filteredChoices.length > 0
309
- ? filteredChoices.map((choice) => (
310
- <ListItem
311
- isSelected={values.includes(choice)}
312
- key={choice}
313
- onItemSelected={selectItem(choice)}
314
- size={Size.tiny}
315
- >
316
- {choice}
317
- </ListItem>
318
- ))
319
- : [
320
- <ListItem key={0} size={Size.tiny}>
321
- No data
322
- </ListItem>,
323
- ]}
324
- </List>
325
- </SelectMultiple>
326
- </div>
327
- </Dialog>
328
- </>
329
- );
330
- };
@@ -1,15 +1,15 @@
1
- import classNames from 'classnames';
2
1
  import React, { Ref, useCallback, useMemo, useRef } from 'react';
2
+
3
+ import classNames from 'classnames';
3
4
  import { uid } from 'uid';
4
5
 
5
- import { Placement } from '@lumx/react';
6
6
  import { Kind, Theme } from '@lumx/react/components';
7
7
  import { Dropdown } from '@lumx/react/components/dropdown/Dropdown';
8
8
  import { InputHelper } from '@lumx/react/components/input-helper/InputHelper';
9
- import { useFocusTrap } from '@lumx/react/hooks/useFocusTrap';
10
- import { useListenFocus } from '@lumx/react/hooks/useListenFocus';
11
9
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
12
10
  import { mergeRefs } from '@lumx/react/utils/mergeRefs';
11
+ import { useListenFocus } from '@lumx/react/hooks/useListenFocus';
12
+ import { Placement } from '@lumx/react';
13
13
 
14
14
  import { CoreSelectProps, SelectVariant } from './constants';
15
15
 
@@ -30,7 +30,6 @@ export const WithSelectContext = (
30
30
  {
31
31
  children,
32
32
  className,
33
- focusElement,
34
33
  isMultiple,
35
34
  closeOnClick = !isMultiple,
36
35
  disabled,
@@ -59,7 +58,6 @@ export const WithSelectContext = (
59
58
  const selectId = useMemo(() => id || `select-${uid()}`, [id]);
60
59
  const anchorRef = useRef<HTMLElement>(null);
61
60
  const selectRef = useRef<HTMLDivElement>(null);
62
- const dropdownRef = useRef<HTMLDivElement>(null);
63
61
  const isFocus = useListenFocus(anchorRef);
64
62
 
65
63
  const handleKeyboardNav = useCallback(
@@ -79,9 +77,6 @@ export const WithSelectContext = (
79
77
  anchorRef?.current?.blur();
80
78
  };
81
79
 
82
- // Handle focus trap.
83
- useFocusTrap(isOpen && dropdownRef.current, focusElement?.current);
84
-
85
80
  return (
86
81
  <div
87
82
  ref={mergeRefs(ref, selectRef)}
@@ -130,7 +125,6 @@ export const WithSelectContext = (
130
125
  placement={Placement.BOTTOM_START}
131
126
  onClose={onClose}
132
127
  onInfiniteScroll={onInfiniteScroll}
133
- ref={dropdownRef}
134
128
  >
135
129
  {children}
136
130
  </Dropdown>
@@ -12,7 +12,6 @@ export default {
12
12
  clearButtonProps: { control: false },
13
13
  chips: { control: false },
14
14
  afterElement: { control: false },
15
- onClear: { action: true },
16
15
  },
17
16
  decorators: [withValueOnChange({})],
18
17
  };
@@ -5,14 +5,8 @@ import camelCase from 'lodash/camelCase';
5
5
  import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
6
6
  import { getBasicClass } from '@lumx/react/utils/className';
7
7
 
8
- import { render, screen } from '@testing-library/react';
9
- import {
10
- getByClassName,
11
- getByTagName,
12
- queryAllByClassName,
13
- queryByClassName,
14
- queryByTagName,
15
- } from '@lumx/react/testing/utils/queries';
8
+ import { render } from '@testing-library/react';
9
+ import { getByClassName, getByTagName, queryAllByClassName, queryByTagName } from '@lumx/react/testing/utils/queries';
16
10
  import partition from 'lodash/partition';
17
11
  import userEvent from '@testing-library/user-event';
18
12
 
@@ -33,11 +27,9 @@ const setup = (propsOverride: Partial<TextFieldProps> = {}) => {
33
27
  | HTMLInputElement;
34
28
  const helpers = queryAllByClassName(container, 'lumx-text-field__helper');
35
29
  const [[helper], [error]] = partition(helpers, (h) => !h.className.includes('lumx-input-helper--color-red'));
36
- const clearButton = queryByClassName(container, 'lumx-text-field__input-clear');
37
30
 
38
31
  return {
39
32
  props,
40
- clearButton,
41
33
  container,
42
34
  element,
43
35
  inputNative,
@@ -114,27 +106,22 @@ describe(`<${TextField.displayName}>`, () => {
114
106
  });
115
107
 
116
108
  it('should have helper text', () => {
117
- const { helper, inputNative } = setup({
109
+ const { helper } = setup({
118
110
  helper: 'helper',
119
111
  label: 'test',
120
112
  placeholder: 'test',
121
113
  });
122
-
123
114
  expect(helper).toHaveTextContent('helper');
124
- expect(inputNative).toHaveAttribute('aria-describedby');
125
115
  });
126
116
 
127
117
  it('should have error text', () => {
128
- const { error, inputNative } = setup({
118
+ const { error } = setup({
129
119
  error: 'error',
130
120
  hasError: true,
131
121
  label: 'test',
132
122
  placeholder: 'test',
133
123
  });
134
-
135
124
  expect(error).toHaveTextContent('error');
136
- expect(inputNative).toHaveAttribute('aria-invalid', 'true');
137
- expect(inputNative).toHaveAttribute('aria-describedby');
138
125
  });
139
126
 
140
127
  it('should not have error text', () => {
@@ -173,41 +160,6 @@ describe(`<${TextField.displayName}>`, () => {
173
160
 
174
161
  expect(onChange).toHaveBeenCalledWith('a', 'name', expect.objectContaining({}));
175
162
  });
176
-
177
- it('should trigger `onChange` with empty value when text field is cleared', async () => {
178
- const onChange = jest.fn();
179
- const { clearButton } = setup({
180
- value: 'initial value',
181
- name: 'name',
182
- clearButtonProps: { label: 'Clear' },
183
- onChange,
184
- });
185
-
186
- expect(clearButton).toBeInTheDocument();
187
-
188
- await userEvent.click(clearButton as HTMLElement);
189
-
190
- expect(onChange).toHaveBeenCalledWith('');
191
- });
192
-
193
- it('should trigger `onChange` with empty value and `onClear` when text field is cleared', async () => {
194
- const onChange = jest.fn();
195
- const onClear = jest.fn();
196
- const { clearButton } = setup({
197
- value: 'initial value',
198
- name: 'name',
199
- clearButtonProps: { label: 'Clear' },
200
- onChange,
201
- onClear,
202
- });
203
-
204
- expect(clearButton).toBeInTheDocument();
205
-
206
- await userEvent.click(clearButton as HTMLElement);
207
-
208
- expect(onChange).toHaveBeenCalledWith('');
209
- expect(onClear).toHaveBeenCalled();
210
- });
211
163
  });
212
164
 
213
165
  // Common tests suite.
@@ -61,8 +61,6 @@ export interface TextFieldProps extends GenericProps, HasTheme {
61
61
  onBlur?(event: React.FocusEvent): void;
62
62
  /** On change callback. */
63
63
  onChange(value: string, name?: string, event?: SyntheticEvent): void;
64
- /** On clear callback. */
65
- onClear?(event?: SyntheticEvent): void;
66
64
  /** On focus callback. */
67
65
  onFocus?(event: React.FocusEvent): void;
68
66
  }
@@ -155,8 +153,6 @@ interface InputNativeProps {
155
153
  onChange(value: string, name?: string, event?: SyntheticEvent): void;
156
154
  onFocus?(value: React.FocusEvent): void;
157
155
  onBlur?(value: React.FocusEvent): void;
158
- hasError?: boolean;
159
- describedById?: string;
160
156
  }
161
157
 
162
158
  const renderInputNative: React.FC<InputNativeProps> = (props) => {
@@ -176,8 +172,6 @@ const renderInputNative: React.FC<InputNativeProps> = (props) => {
176
172
  recomputeNumberOfRows,
177
173
  type,
178
174
  name,
179
- hasError,
180
- describedById,
181
175
  ...forwardedProps
182
176
  } = props;
183
177
  // eslint-disable-next-line react-hooks/rules-of-hooks
@@ -220,8 +214,6 @@ const renderInputNative: React.FC<InputNativeProps> = (props) => {
220
214
  onFocus: onTextFieldFocus,
221
215
  onBlur: onTextFieldBlur,
222
216
  onChange: handleChange,
223
- 'aria-invalid': hasError ? 'true' : undefined,
224
- 'aria-describedby': describedById,
225
217
  ref: mergeRefs(inputRef as any, ref) as any,
226
218
  };
227
219
  if (multiline) {
@@ -262,7 +254,6 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
262
254
  name,
263
255
  onBlur,
264
256
  onChange,
265
- onClear,
266
257
  onFocus,
267
258
  placeholder,
268
259
  textFieldRef,
@@ -273,17 +264,6 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
273
264
  ...forwardedProps
274
265
  } = props;
275
266
  const textFieldId = useMemo(() => id || `text-field-${uid()}`, [id]);
276
- /**
277
- * Generate unique ids for both the helper and error texts, in order to
278
- * later on add them to the input native as aria-describedby. If both the error and the helper are present,
279
- * we want to first use the most important one, which is the errorId. That way, screen readers will read first
280
- * the error and then the helper
281
- */
282
- const helperId = helper ? `text-field-helper-${uid()}` : undefined;
283
- const errorId = error ? `text-field-error-${uid()}` : undefined;
284
- const describedByIds = [errorId, helperId].filter(Boolean);
285
- const describedById = describedByIds.length === 0 ? undefined : describedByIds.join(' ');
286
-
287
267
  const [isFocus, setFocus] = useState(false);
288
268
  const { rows, recomputeNumberOfRows } = useComputeNumberOfRows(multiline ? minimumRows || DEFAULT_MIN_ROWS : 0);
289
269
  const valueLength = (value || '').length;
@@ -295,16 +275,12 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
295
275
  * and remove focus from the clear button.
296
276
  * @param evt On clear event.
297
277
  */
298
- const handleClear = (evt: React.ChangeEvent) => {
278
+ const onClear = (evt: React.ChangeEvent) => {
299
279
  evt.nativeEvent.preventDefault();
300
280
  evt.nativeEvent.stopPropagation();
301
281
  (evt.currentTarget as HTMLElement).blur();
302
282
 
303
283
  onChange('');
304
-
305
- if (onClear) {
306
- onClear(evt);
307
- }
308
284
  };
309
285
 
310
286
  return (
@@ -383,8 +359,6 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
383
359
  type,
384
360
  value,
385
361
  name,
386
- hasError,
387
- describedById,
388
362
  ...forwardedProps,
389
363
  })}
390
364
  </div>
@@ -409,8 +383,6 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
409
383
  type,
410
384
  value,
411
385
  name,
412
- hasError,
413
- describedById,
414
386
  ...forwardedProps,
415
387
  })}
416
388
  </div>
@@ -433,7 +405,7 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
433
405
  emphasis={Emphasis.low}
434
406
  size={Size.s}
435
407
  theme={theme}
436
- onClick={handleClear}
408
+ onClick={onClear}
437
409
  type="button"
438
410
  />
439
411
  )}
@@ -442,13 +414,13 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
442
414
  </div>
443
415
 
444
416
  {hasError && error && (
445
- <InputHelper className={`${CLASSNAME}__helper`} kind={Kind.error} theme={theme} id={errorId}>
417
+ <InputHelper className={`${CLASSNAME}__helper`} kind={Kind.error} theme={theme}>
446
418
  {error}
447
419
  </InputHelper>
448
420
  )}
449
421
 
450
422
  {helper && (
451
- <InputHelper className={`${CLASSNAME}__helper`} theme={theme} id={helperId}>
423
+ <InputHelper className={`${CLASSNAME}__helper`} theme={theme}>
452
424
  {helper}
453
425
  </InputHelper>
454
426
  )}
@@ -72,10 +72,10 @@ export function useFocusTrap(focusZoneElement: HTMLElement | Falsy, focusElement
72
72
  // SETUP:
73
73
  if (focusElement && focusZoneElement.contains(focusElement)) {
74
74
  // Focus the given element.
75
- focusElement.focus({ preventScroll: true });
75
+ focusElement.focus();
76
76
  } else {
77
77
  // Focus the first focusable element in the zone.
78
- getFirstAndLastFocusable(focusZoneElement).first?.focus({ preventScroll: true });
78
+ getFirstAndLastFocusable(focusZoneElement).first?.focus();
79
79
  }
80
80
  FOCUS_TRAPS.register(focusTrap);
81
81