@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.
|
|
11
|
-
"@lumx/icons": "^3.2.
|
|
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.
|
|
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
|
-
|
|
177
|
-
|
|
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
|
-
|
|
184
|
-
}
|
|
179
|
+
elementToFocus = getFirstAndLastFocusable(anchorRef.current).first;
|
|
180
|
+
}
|
|
181
|
+
if (!elementToFocus) {
|
|
185
182
|
// Fallback on the anchor element.
|
|
186
|
-
anchorRef?.current
|
|
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 {
|
|
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
|
|