@lumx/react 3.2.0 → 3.2.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.
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.2.0",
11
- "@lumx/icons": "^3.2.0",
10
+ "@lumx/core": "^3.2.1",
11
+ "@lumx/icons": "^3.2.1",
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.2.0"
116
+ "version": "3.2.1"
117
117
  }
@@ -173,18 +173,16 @@ const _InnerPopover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, ref
173
173
  * unless specifically requested not to.
174
174
  */
175
175
  if (isFocusedWithin.current && focusAnchorOnClose) {
176
- if (parentElement?.current) {
177
- parentElement?.current.focus();
178
- }
179
-
180
- const firstFocusable = anchorRef?.current && getFirstAndLastFocusable(anchorRef?.current).first;
181
- if (firstFocusable) {
176
+ let elementToFocus = parentElement?.current;
177
+ if (!elementToFocus && anchorRef?.current) {
182
178
  // Focus the first focusable element in anchor.
183
- firstFocusable.focus();
184
- } else {
179
+ elementToFocus = getFirstAndLastFocusable(anchorRef.current).first;
180
+ }
181
+ if (!elementToFocus) {
185
182
  // Fallback on the anchor element.
186
- anchorRef?.current?.focus();
183
+ elementToFocus = anchorRef?.current;
187
184
  }
185
+ elementToFocus?.focus({ preventScroll: true });
188
186
  }
189
187
 
190
188
  onClose();
@@ -1,9 +1,20 @@
1
1
  /* istanbul ignore file */
2
2
  import { mdiTram } from '@lumx/icons/';
3
- import { Chip, List, ListItem, SelectMultiple, Size } from '@lumx/react';
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';
4
15
  import { useBooleanState } from '@lumx/react/hooks/useBooleanState';
5
16
  import noop from 'lodash/noop';
6
- import React, { MouseEventHandler, SyntheticEvent, useState } from 'react';
17
+ import React, { MouseEventHandler, SyntheticEvent, useRef, useState } from 'react';
7
18
  import { SelectVariant } from './constants';
8
19
 
9
20
  export default { title: 'LumX components/select/Select Multiple' };
@@ -225,3 +236,95 @@ export const ChipsCustomSelectMultiple = ({ theme }: any) => {
225
236
  </SelectMultiple>
226
237
  );
227
238
  };
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 React, { Ref, useCallback, useMemo, useRef } from 'react';
2
-
3
1
  import classNames from 'classnames';
2
+ import React, { Ref, useCallback, useMemo, useRef } from 'react';
4
3
  import { uid } from 'uid';
5
4
 
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';
9
11
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
10
12
  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,6 +30,7 @@ export const WithSelectContext = (
30
30
  {
31
31
  children,
32
32
  className,
33
+ focusElement,
33
34
  isMultiple,
34
35
  closeOnClick = !isMultiple,
35
36
  disabled,
@@ -58,6 +59,7 @@ export const WithSelectContext = (
58
59
  const selectId = useMemo(() => id || `select-${uid()}`, [id]);
59
60
  const anchorRef = useRef<HTMLElement>(null);
60
61
  const selectRef = useRef<HTMLDivElement>(null);
62
+ const dropdownRef = useRef<HTMLDivElement>(null);
61
63
  const isFocus = useListenFocus(anchorRef);
62
64
 
63
65
  const handleKeyboardNav = useCallback(
@@ -77,6 +79,9 @@ export const WithSelectContext = (
77
79
  anchorRef?.current?.blur();
78
80
  };
79
81
 
82
+ // Handle focus trap.
83
+ useFocusTrap(isOpen && dropdownRef.current, focusElement?.current);
84
+
80
85
  return (
81
86
  <div
82
87
  ref={mergeRefs(ref, selectRef)}
@@ -125,6 +130,7 @@ export const WithSelectContext = (
125
130
  placement={Placement.BOTTOM_START}
126
131
  onClose={onClose}
127
132
  onInfiniteScroll={onInfiniteScroll}
133
+ ref={dropdownRef}
128
134
  >
129
135
  {children}
130
136
  </Dropdown>
@@ -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();
75
+ focusElement.focus({ preventScroll: true });
76
76
  } else {
77
77
  // Focus the first focusable element in the zone.
78
- getFirstAndLastFocusable(focusZoneElement).first?.focus();
78
+ getFirstAndLastFocusable(focusZoneElement).first?.focus({ preventScroll: true });
79
79
  }
80
80
  FOCUS_TRAPS.register(focusTrap);
81
81