@lumx/react 2.2.19 → 2.2.21-alpha.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/esm/_internal/ButtonRoot.js.map +1 -1
- package/esm/_internal/Checkbox2.js +3 -1
- package/esm/_internal/Checkbox2.js.map +1 -1
- package/esm/_internal/ClickAwayProvider.js +90 -12
- package/esm/_internal/ClickAwayProvider.js.map +1 -1
- package/esm/_internal/DatePickerField.js +18 -11
- package/esm/_internal/DatePickerField.js.map +1 -1
- package/esm/_internal/Dialog2.js +2 -2
- package/esm/_internal/Dialog2.js.map +1 -1
- package/esm/_internal/GenericBlock.js +90 -0
- package/esm/_internal/GenericBlock.js.map +1 -0
- package/esm/_internal/Lightbox2.js +2 -2
- package/esm/_internal/Lightbox2.js.map +1 -1
- package/esm/_internal/LinkPreview.js +22 -12
- package/esm/_internal/LinkPreview.js.map +1 -1
- package/esm/_internal/Popover2.js +21 -8
- package/esm/_internal/Popover2.js.map +1 -1
- package/esm/_internal/SelectMultiple.js +16 -4
- package/esm/_internal/SelectMultiple.js.map +1 -1
- package/esm/_internal/alert-dialog.js +2 -2
- package/esm/_internal/autocomplete.js +2 -1
- package/esm/_internal/autocomplete.js.map +1 -1
- package/esm/_internal/button.js +2 -1
- package/esm/_internal/button.js.map +1 -1
- package/esm/_internal/comment-block.js +2 -1
- package/esm/_internal/comment-block.js.map +1 -1
- package/esm/_internal/date-picker.js +3 -2
- package/esm/_internal/date-picker.js.map +1 -1
- package/esm/_internal/dialog.js +2 -2
- package/esm/_internal/dropdown.js +2 -1
- package/esm/_internal/dropdown.js.map +1 -1
- package/esm/_internal/expansion-panel.js +1 -1
- package/esm/_internal/generic-block.js +12 -0
- package/esm/_internal/generic-block.js.map +1 -0
- package/esm/_internal/lightbox.js +3 -2
- package/esm/_internal/lightbox.js.map +1 -1
- package/esm/_internal/popover.js +2 -1
- package/esm/_internal/popover.js.map +1 -1
- package/esm/_internal/select.js +2 -1
- package/esm/_internal/select.js.map +1 -1
- package/esm/_internal/side-navigation.js +2 -1
- package/esm/_internal/side-navigation.js.map +1 -1
- package/esm/_internal/slideshow.js +2 -1
- package/esm/_internal/slideshow.js.map +1 -1
- package/esm/_internal/text-field.js +2 -1
- package/esm/_internal/text-field.js.map +1 -1
- package/esm/_internal/tooltip.js +2 -1
- package/esm/_internal/tooltip.js.map +1 -1
- package/esm/_internal/type.js.map +1 -1
- package/esm/_internal/useFocusTrap.js +62 -78
- package/esm/_internal/useFocusTrap.js.map +1 -1
- package/esm/index.js +3 -2
- package/esm/index.js.map +1 -1
- package/package.json +5 -5
- package/src/components/button/Button.stories.tsx +1 -0
- package/src/components/button/ButtonRoot.tsx +4 -4
- package/src/components/checkbox/Checkbox.tsx +2 -1
- package/src/components/checkbox/__snapshots__/Checkbox.test.tsx.snap +4 -0
- package/src/components/date-picker/DatePickerField.tsx +15 -16
- package/src/components/date-picker/types.ts +2 -2
- package/src/components/dialog/Dialog.stories.tsx +57 -13
- package/src/components/dialog/Dialog.tsx +1 -1
- package/src/components/dialog/__snapshots__/Dialog.test.tsx.snap +82 -14
- package/src/components/generic-block/GenericBlock.stories.tsx +149 -0
- package/src/components/generic-block/GenericBlock.test.tsx +28 -0
- package/src/components/generic-block/GenericBlock.tsx +120 -0
- package/src/components/generic-block/__snapshots__/GenericBlock.test.tsx.snap +92 -0
- package/src/components/generic-block/index.ts +1 -0
- package/src/components/lightbox/Lightbox.tsx +1 -1
- package/src/components/link-preview/LinkPreview.test.tsx +50 -55
- package/src/components/link-preview/LinkPreview.tsx +43 -16
- package/src/components/popover/Popover.tsx +20 -4
- package/src/components/select/Select.stories.tsx +2 -0
- package/src/components/select/Select.tsx +11 -1
- package/src/components/select/SelectMultiple.stories.tsx +2 -0
- package/src/components/select/SelectMultiple.tsx +11 -1
- package/src/components/select/constants.ts +2 -0
- package/src/components/table/__snapshots__/Table.test.tsx.snap +5 -0
- package/src/hooks/useCallbackOnEscape.ts +21 -13
- package/src/hooks/useFocusTrap.ts +68 -51
- package/src/index.ts +1 -0
- package/src/stories/generated/GenericBlock/Demos.stories.tsx +6 -0
- package/src/utils/focus/getFirstAndLastFocusable.test.ts +6 -0
- package/src/utils/focus/getFirstAndLastFocusable.ts +2 -2
- package/src/utils/makeListenerTowerContext.ts +32 -0
- package/src/utils/type.ts +3 -0
- package/types.d.ts +57 -9
- package/src/components/link-preview/__snapshots__/LinkPreview.test.tsx.snap +0 -51
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { detectOverflow } from '@popperjs/core';
|
|
2
|
-
import React, { forwardRef, ReactNode, RefObject, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import React, { forwardRef, ReactNode, RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
4
|
import { usePopper } from 'react-popper';
|
|
5
5
|
|
|
@@ -13,6 +13,7 @@ import { ClickAwayProvider } from '@lumx/react/utils/ClickAwayProvider';
|
|
|
13
13
|
import { Comp, GenericProps, getRootClassName, handleBasicClasses, ValueOf } from '@lumx/react/utils';
|
|
14
14
|
import { mergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
15
15
|
import { useFocusWithin } from '@lumx/react/hooks/useFocusWithin';
|
|
16
|
+
import { getFirstAndLastFocusable } from '@lumx/react/utils/focus/getFirstAndLastFocusable';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Different possible placements for the popover.
|
|
@@ -90,6 +91,8 @@ export interface PopoverProps extends GenericProps {
|
|
|
90
91
|
isOpen: boolean;
|
|
91
92
|
/** Offset placement relative to anchor. */
|
|
92
93
|
offset?: Offset;
|
|
94
|
+
/** Reference to the parent element that triggered the popover (will get back focus on close or else fallback on the anchor element). */
|
|
95
|
+
parentElement?: RefObject<HTMLElement>;
|
|
93
96
|
/** Placement relative to anchor. */
|
|
94
97
|
placement?: Placement;
|
|
95
98
|
/** Whether the popover should be rendered into a DOM node that exists outside the DOM hierarchy of the parent component. */
|
|
@@ -212,6 +215,7 @@ export const Popover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, re
|
|
|
212
215
|
isOpen,
|
|
213
216
|
offset,
|
|
214
217
|
onClose,
|
|
218
|
+
parentElement,
|
|
215
219
|
placement,
|
|
216
220
|
style,
|
|
217
221
|
usePortal,
|
|
@@ -245,7 +249,8 @@ export const Popover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, re
|
|
|
245
249
|
});
|
|
246
250
|
|
|
247
251
|
/** Action on close */
|
|
248
|
-
|
|
252
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
253
|
+
const handleClose = useCallback(() => {
|
|
249
254
|
if (!onClose) {
|
|
250
255
|
return;
|
|
251
256
|
}
|
|
@@ -256,11 +261,22 @@ export const Popover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, re
|
|
|
256
261
|
* unless specifically requested not to.
|
|
257
262
|
*/
|
|
258
263
|
if (isFocusedWithin.current && focusAnchorOnClose) {
|
|
259
|
-
|
|
264
|
+
if (parentElement?.current) {
|
|
265
|
+
parentElement?.current.focus();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const firstFocusable = anchorRef?.current && getFirstAndLastFocusable(anchorRef?.current).first;
|
|
269
|
+
if (firstFocusable) {
|
|
270
|
+
// Focus the first focusable element in anchor.
|
|
271
|
+
firstFocusable.focus();
|
|
272
|
+
} else {
|
|
273
|
+
// Fallback on the anchor element.
|
|
274
|
+
anchorRef?.current?.focus();
|
|
275
|
+
}
|
|
260
276
|
}
|
|
261
277
|
|
|
262
278
|
onClose();
|
|
263
|
-
};
|
|
279
|
+
}, [anchorRef, focusAnchorOnClose, onClose, parentElement]);
|
|
264
280
|
|
|
265
281
|
const modifiers: any = [];
|
|
266
282
|
const actualOffset: [number, number] = [offset?.along ?? 0, (offset?.away ?? 0) + (hasArrow ? ARROW_SIZE : 0)];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mdiBullhornOutline } from '@lumx/icons/';
|
|
1
2
|
import { List, ListItem, Select, Size, TextField } from '@lumx/react';
|
|
2
3
|
import { useBooleanState } from '@lumx/react/hooks/useBooleanState';
|
|
3
4
|
import { text } from '@storybook/addon-knobs';
|
|
@@ -32,6 +33,7 @@ export const SimpleSelect = ({ theme }: any) => {
|
|
|
32
33
|
theme={theme}
|
|
33
34
|
onInputClick={toggleSelect}
|
|
34
35
|
onDropdownClose={closeSelect}
|
|
36
|
+
icon={mdiBullhornOutline}
|
|
35
37
|
>
|
|
36
38
|
<List isClickable>
|
|
37
39
|
{CHOICES.length > 0
|
|
@@ -5,7 +5,7 @@ import lodashIsEmpty from 'lodash/isEmpty';
|
|
|
5
5
|
|
|
6
6
|
import { mdiAlertCircle, mdiCheckCircle, mdiCloseCircle, mdiMenuDown } from '@lumx/icons';
|
|
7
7
|
|
|
8
|
-
import { Emphasis, Size } from '@lumx/react/components';
|
|
8
|
+
import { Emphasis, Size, Theme } from '@lumx/react/components';
|
|
9
9
|
import { IconButton } from '@lumx/react/components/button/IconButton';
|
|
10
10
|
import { Chip } from '@lumx/react/components/chip/Chip';
|
|
11
11
|
import { Icon } from '@lumx/react/components/icon/Icon';
|
|
@@ -46,6 +46,7 @@ const SelectField: React.FC<SelectProps> = ({
|
|
|
46
46
|
handleKeyboardNav,
|
|
47
47
|
hasError,
|
|
48
48
|
hasInputClear,
|
|
49
|
+
icon,
|
|
49
50
|
id,
|
|
50
51
|
isDisabled,
|
|
51
52
|
isEmpty,
|
|
@@ -89,6 +90,15 @@ const SelectField: React.FC<SelectProps> = ({
|
|
|
89
90
|
aria-disabled={isDisabled || undefined}
|
|
90
91
|
{...forwardedProps}
|
|
91
92
|
>
|
|
93
|
+
{icon && (
|
|
94
|
+
<Icon
|
|
95
|
+
className={`${CLASSNAME}__input-icon`}
|
|
96
|
+
color={theme === Theme.dark ? 'light' : undefined}
|
|
97
|
+
icon={icon}
|
|
98
|
+
size={Size.xs}
|
|
99
|
+
/>
|
|
100
|
+
)}
|
|
101
|
+
|
|
92
102
|
<div
|
|
93
103
|
className={classNames([
|
|
94
104
|
`${CLASSNAME}__input-native`,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* istanbul ignore file */
|
|
2
|
+
import { mdiTram } from '@lumx/icons/';
|
|
2
3
|
import { Chip, List, ListItem, SelectMultiple, Size } from '@lumx/react';
|
|
3
4
|
import { useBooleanState } from '@lumx/react/hooks/useBooleanState';
|
|
4
5
|
import noop from 'lodash/noop';
|
|
@@ -40,6 +41,7 @@ export const DefaultSelectMultiple = ({ theme }: any) => {
|
|
|
40
41
|
theme={theme}
|
|
41
42
|
onInputClick={toggleSelect}
|
|
42
43
|
onDropdownClose={closeSelect}
|
|
44
|
+
icon={mdiTram}
|
|
43
45
|
>
|
|
44
46
|
<List isClickable>
|
|
45
47
|
{CHOICES.length > 0
|
|
@@ -4,7 +4,7 @@ import classNames from 'classnames';
|
|
|
4
4
|
|
|
5
5
|
import { mdiAlertCircle, mdiCheckCircle, mdiClose, mdiCloseCircle, mdiMenuDown } from '@lumx/icons';
|
|
6
6
|
|
|
7
|
-
import { Size } from '@lumx/react/components';
|
|
7
|
+
import { Size, Theme } from '@lumx/react/components';
|
|
8
8
|
import { Chip } from '@lumx/react/components/chip/Chip';
|
|
9
9
|
import { Icon } from '@lumx/react/components/icon/Icon';
|
|
10
10
|
import { InputLabel } from '@lumx/react/components/input-label/InputLabel';
|
|
@@ -59,6 +59,7 @@ export const SelectMultipleField: React.FC<SelectMultipleProps> = ({
|
|
|
59
59
|
anchorRef,
|
|
60
60
|
handleKeyboardNav,
|
|
61
61
|
hasError,
|
|
62
|
+
icon,
|
|
62
63
|
id,
|
|
63
64
|
isDisabled,
|
|
64
65
|
isEmpty,
|
|
@@ -102,6 +103,15 @@ export const SelectMultipleField: React.FC<SelectMultipleProps> = ({
|
|
|
102
103
|
aria-disabled={isDisabled || undefined}
|
|
103
104
|
{...forwardedProps}
|
|
104
105
|
>
|
|
106
|
+
{icon && (
|
|
107
|
+
<Icon
|
|
108
|
+
className={`${CLASSNAME}__input-icon`}
|
|
109
|
+
color={theme === Theme.dark ? 'light' : undefined}
|
|
110
|
+
icon={icon}
|
|
111
|
+
size={Size.xs}
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
|
|
105
115
|
<div className={`${CLASSNAME}__chips`}>
|
|
106
116
|
{!isEmpty &&
|
|
107
117
|
value.map((val, index) => selectedChipRender?.(val, index, onClear, isDisabled, theme))}
|
|
@@ -21,6 +21,8 @@ export interface CoreSelectProps extends GenericProps {
|
|
|
21
21
|
helper?: string;
|
|
22
22
|
/** Whether the select should close on click. */
|
|
23
23
|
closeOnClick?: boolean;
|
|
24
|
+
/** Icon (SVG path). */
|
|
25
|
+
icon?: string;
|
|
24
26
|
/** Whether the component is disabled or not. */
|
|
25
27
|
isDisabled?: boolean;
|
|
26
28
|
/** Whether the component is required or not. */
|
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
import { DOCUMENT } from '@lumx/react/constants';
|
|
2
2
|
import { Callback, onEscapePressed } from '@lumx/react/utils';
|
|
3
3
|
import { useEffect } from 'react';
|
|
4
|
+
import { Listener, makeListenerTowerContext } from '@lumx/react/utils/makeListenerTowerContext';
|
|
5
|
+
|
|
6
|
+
const LISTENERS = makeListenerTowerContext();
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
|
-
*
|
|
9
|
+
* Register a global listener on 'Escape' key pressed.
|
|
10
|
+
*
|
|
11
|
+
* If multiple listener are registered, only the last one is maintained. When a listener is unregistered, the previous
|
|
12
|
+
* one gets activated again.
|
|
7
13
|
*
|
|
8
14
|
* @param callback Callback
|
|
9
15
|
* @param closeOnEscape Disables the hook when false
|
|
10
|
-
* @param rootElement Element on which to listen the escape key
|
|
11
16
|
*/
|
|
12
|
-
export function useCallbackOnEscape(
|
|
13
|
-
callback: Callback | undefined,
|
|
14
|
-
closeOnEscape = true,
|
|
15
|
-
rootElement = DOCUMENT?.body,
|
|
16
|
-
) {
|
|
17
|
+
export function useCallbackOnEscape(callback: Callback | undefined, closeOnEscape = true) {
|
|
17
18
|
useEffect(() => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return () => rootElement.removeEventListener('keydown', onKeyDown);
|
|
19
|
+
const rootElement = DOCUMENT?.body;
|
|
20
|
+
if (!closeOnEscape || !callback || !rootElement) {
|
|
21
|
+
return undefined;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const onKeyDown = onEscapePressed(callback);
|
|
24
|
+
|
|
25
|
+
const listener: Listener = {
|
|
26
|
+
enable: () => rootElement.addEventListener('keydown', onKeyDown),
|
|
27
|
+
disable: () => rootElement.removeEventListener('keydown', onKeyDown),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
LISTENERS.register(listener);
|
|
31
|
+
return () => LISTENERS.unregister(listener);
|
|
32
|
+
}, [callback, closeOnEscape]);
|
|
25
33
|
}
|
|
@@ -2,67 +2,84 @@ import { useEffect } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { DOCUMENT } from '@lumx/react/constants';
|
|
4
4
|
import { getFirstAndLastFocusable } from '@lumx/react/utils/focus/getFirstAndLastFocusable';
|
|
5
|
+
import { Falsy } from '@lumx/react/utils';
|
|
6
|
+
import { Listener, makeListenerTowerContext } from '@lumx/react/utils/makeListenerTowerContext';
|
|
7
|
+
|
|
8
|
+
const FOCUS_TRAPS = makeListenerTowerContext();
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
11
|
+
* Trap 'Tab' focus switch inside the `focusZoneElement`.
|
|
12
|
+
*
|
|
13
|
+
* If multiple focus trap are activated, only the last one is maintained and when a focus trap closes, the previous one
|
|
14
|
+
* gets activated again.
|
|
10
15
|
*
|
|
11
16
|
* @param focusZoneElement The element in which to trap the focus.
|
|
12
|
-
* @param focusElement The element to focus when the focus trap is activated
|
|
13
|
-
*
|
|
17
|
+
* @param focusElement The element to focus when the focus trap is activated (otherwise the first focusable element
|
|
18
|
+
* will be focused).
|
|
14
19
|
*/
|
|
15
|
-
export function useFocusTrap(
|
|
16
|
-
focusZoneElement: HTMLElement | null,
|
|
17
|
-
focusElement?: HTMLElement | null,
|
|
18
|
-
rootElement = DOCUMENT?.body,
|
|
19
|
-
): void {
|
|
20
|
+
export function useFocusTrap(focusZoneElement: HTMLElement | Falsy, focusElement?: HTMLElement | null): void {
|
|
20
21
|
useEffect(() => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
// Body element can be undefined in SSR context.
|
|
23
|
+
const rootElement = DOCUMENT?.body;
|
|
24
|
+
|
|
25
|
+
if (!rootElement || !focusZoneElement) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Trap 'Tab' key down focus switch into the focus zone.
|
|
30
|
+
const trapTabFocusInFocusZone = (evt: KeyboardEvent) => {
|
|
31
|
+
const { key } = evt;
|
|
32
|
+
if (key !== 'Tab') {
|
|
33
|
+
return;
|
|
25
34
|
}
|
|
35
|
+
const focusable = getFirstAndLastFocusable(focusZoneElement);
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const focusable = getFirstAndLastFocusable(focusZoneElement);
|
|
37
|
+
// Prevent focus switch if no focusable available.
|
|
38
|
+
if (!focusable.first) {
|
|
39
|
+
evt.preventDefault();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
33
42
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
if (
|
|
44
|
+
// No previous focus
|
|
45
|
+
!document.activeElement ||
|
|
46
|
+
// Previous focus is at the end of the focus zone.
|
|
47
|
+
(!evt.shiftKey && document.activeElement === focusable.last) ||
|
|
48
|
+
// Previous focus is outside the focus zone
|
|
49
|
+
!focusZoneElement.contains(document.activeElement)
|
|
50
|
+
) {
|
|
51
|
+
focusable.first.focus();
|
|
52
|
+
evt.preventDefault();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
39
55
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
56
|
+
if (
|
|
57
|
+
// Focus order reversed
|
|
58
|
+
evt.shiftKey &&
|
|
59
|
+
// Previous focus is at the start of the focus zone.
|
|
60
|
+
document.activeElement === focusable.first
|
|
61
|
+
) {
|
|
62
|
+
focusable.last.focus();
|
|
63
|
+
evt.preventDefault();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
52
66
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
const focusTrap: Listener = {
|
|
68
|
+
enable: () => rootElement.addEventListener('keydown', trapTabFocusInFocusZone),
|
|
69
|
+
disable: () => rootElement.removeEventListener('keydown', trapTabFocusInFocusZone),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// SETUP:
|
|
73
|
+
if (focusElement && focusZoneElement.contains(focusElement)) {
|
|
74
|
+
// Focus the given element.
|
|
75
|
+
focusElement.focus();
|
|
76
|
+
} else {
|
|
77
|
+
// Focus the first focusable element in the zone.
|
|
78
|
+
getFirstAndLastFocusable(focusZoneElement).first?.focus();
|
|
65
79
|
}
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
FOCUS_TRAPS.register(focusTrap);
|
|
81
|
+
|
|
82
|
+
// TEARDOWN:
|
|
83
|
+
return () => FOCUS_TRAPS.unregister(focusTrap);
|
|
84
|
+
}, [focusElement, focusZoneElement]);
|
|
68
85
|
}
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export * from './components/dropdown';
|
|
|
15
15
|
export * from './components/expansion-panel';
|
|
16
16
|
export * from './components/flag';
|
|
17
17
|
export * from './components/flex-box';
|
|
18
|
+
export * from './components/generic-block';
|
|
18
19
|
export * from './components/grid';
|
|
19
20
|
export * from './components/icon';
|
|
20
21
|
export * from './components/image-block';
|
|
@@ -124,5 +124,11 @@ describe(getFirstAndLastFocusable.name, () => {
|
|
|
124
124
|
const focusable = getFirstAndLastFocusable(element);
|
|
125
125
|
expect(focusable.first).toMatchInlineSnapshot(`<button />`);
|
|
126
126
|
});
|
|
127
|
+
|
|
128
|
+
it('should skip hidden input', () => {
|
|
129
|
+
const element = htmlToElement(`<div><input hidden /><button /></div>`);
|
|
130
|
+
const focusable = getFirstAndLastFocusable(element);
|
|
131
|
+
expect(focusable.first).toMatchInlineSnapshot(`<button />`);
|
|
132
|
+
});
|
|
127
133
|
});
|
|
128
134
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** CSS selector listing all tabbable elements. */
|
|
2
|
-
const TABBABLE_ELEMENTS_SELECTOR = `a[href], button, textarea, input:not([type="hidden"]), [tabindex]`;
|
|
2
|
+
const TABBABLE_ELEMENTS_SELECTOR = `a[href], button, textarea, input:not([type="hidden"]):not([hidden]), [tabindex]`;
|
|
3
3
|
|
|
4
4
|
/** CSS selector matching element that are disabled (should not receive focus). */
|
|
5
|
-
const DISABLED_SELECTOR = `[tabindex="-1"], [disabled]:not([disabled="false"]), [aria-disabled]:not([aria-disabled="false"])`;
|
|
5
|
+
const DISABLED_SELECTOR = `[hidden], [tabindex="-1"], [disabled]:not([disabled="false"]), [aria-disabled]:not([aria-disabled="false"])`;
|
|
6
6
|
|
|
7
7
|
const isNotDisabled = (element: HTMLElement) => !element.matches(DISABLED_SELECTOR);
|
|
8
8
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import last from 'lodash/last';
|
|
2
|
+
import pull from 'lodash/pull';
|
|
3
|
+
|
|
4
|
+
export type Listener = { enable(): void; disable(): void };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Keep track of listeners, only the last registered listener gets activated at any point (previously registered
|
|
8
|
+
* listener are disabled).
|
|
9
|
+
* When a listener gets unregistered, the previously registered listener gets enabled again.
|
|
10
|
+
*/
|
|
11
|
+
export function makeListenerTowerContext() {
|
|
12
|
+
const LISTENERS: Listener[] = [];
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
register(listener: Listener) {
|
|
16
|
+
// Disable previous listener.
|
|
17
|
+
last(LISTENERS)?.disable();
|
|
18
|
+
// Keep track of current listener.
|
|
19
|
+
LISTENERS.push(listener);
|
|
20
|
+
// Enable current listener.
|
|
21
|
+
listener.enable();
|
|
22
|
+
},
|
|
23
|
+
unregister(listener: Listener) {
|
|
24
|
+
// Disable current listener.
|
|
25
|
+
listener.disable();
|
|
26
|
+
// Remove current listener.
|
|
27
|
+
pull(LISTENERS, listener);
|
|
28
|
+
// Enable previous listener.
|
|
29
|
+
last(LISTENERS)?.enable();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
package/src/utils/type.ts
CHANGED
package/types.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// Generated by dts-bundle-generator v5.6.0
|
|
2
2
|
|
|
3
|
-
/// <reference types="
|
|
3
|
+
/// <reference types="cheerio" />
|
|
4
|
+
/// <reference types="node" />
|
|
5
|
+
/// <reference types="prop-types" />
|
|
6
|
+
|
|
7
|
+
import * as CSS from 'csstype';
|
|
8
|
+
import * as PropTypes from 'prop-types';
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { AriaAttributes, ButtonHTMLAttributes, CSSProperties, DetailedHTMLProps, ImgHTMLAttributes, InputHTMLAttributes, Key, KeyboardEventHandler, MouseEventHandler, ReactElement, ReactNode, Ref, RefObject, SetStateAction, SyntheticEvent } from 'react';
|
|
4
11
|
|
|
5
12
|
/** Get types of the values of a record. */
|
|
6
13
|
export declare type ValueOf<T extends Record<any, any>> = T[keyof T];
|
|
@@ -18,6 +25,8 @@ export declare type Comp<P, T = HTMLElement> = {
|
|
|
18
25
|
/** Component base class name. */
|
|
19
26
|
className?: string;
|
|
20
27
|
};
|
|
28
|
+
/** Union type of all heading elements */
|
|
29
|
+
export declare type HeadingElement = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
|
|
21
30
|
/**
|
|
22
31
|
* Define a generic props types.
|
|
23
32
|
*/
|
|
@@ -476,9 +485,7 @@ export declare type HTMLButtonProps = DetailedHTMLProps<ButtonHTMLAttributes<HTM
|
|
|
476
485
|
* Button size definition.
|
|
477
486
|
*/
|
|
478
487
|
export declare type ButtonSize = Extract<Size, "s" | "m">;
|
|
479
|
-
export interface BaseButtonProps extends GenericProps {
|
|
480
|
-
/** ARIA button label. */
|
|
481
|
-
["aria-label"]?: string;
|
|
488
|
+
export interface BaseButtonProps extends GenericProps, Pick<AriaAttributes, "aria-expanded" | "aria-haspopup" | "aria-pressed" | "aria-label"> {
|
|
482
489
|
/** Color variant. */
|
|
483
490
|
color?: Color;
|
|
484
491
|
/** Emphasis variant. */
|
|
@@ -743,7 +750,7 @@ export interface DatePickerProps extends GenericProps {
|
|
|
743
750
|
/** Props to pass to the previous month button (minus those already set by the DatePickerControlled props). */
|
|
744
751
|
previousButtonProps: Pick<IconButtonProps, "label"> & Omit<IconButtonProps, "label" | "onClick" | "icon" | "emphasis">;
|
|
745
752
|
/** Reference to the <button> element corresponding to the current date or the selected date. */
|
|
746
|
-
todayOrSelectedDateRef?:
|
|
753
|
+
todayOrSelectedDateRef?: Ref<HTMLButtonElement>;
|
|
747
754
|
/** Currently selected date. */
|
|
748
755
|
value: Date | undefined;
|
|
749
756
|
/** On change callback. */
|
|
@@ -951,6 +958,8 @@ export interface PopoverProps extends GenericProps {
|
|
|
951
958
|
isOpen: boolean;
|
|
952
959
|
/** Offset placement relative to anchor. */
|
|
953
960
|
offset?: Offset;
|
|
961
|
+
/** Reference to the parent element that triggered the popover (will get back focus on close or else fallback on the anchor element). */
|
|
962
|
+
parentElement?: RefObject<HTMLElement>;
|
|
954
963
|
/** Placement relative to anchor. */
|
|
955
964
|
placement?: Placement;
|
|
956
965
|
/** Whether the popover should be rendered into a DOM node that exists outside the DOM hierarchy of the parent component. */
|
|
@@ -1124,6 +1133,41 @@ export interface FlexBoxProps extends GenericProps {
|
|
|
1124
1133
|
* @return React element.
|
|
1125
1134
|
*/
|
|
1126
1135
|
export declare const FlexBox: Comp<FlexBoxProps, HTMLDivElement>;
|
|
1136
|
+
export interface GenericBlockProps extends FlexBoxProps {
|
|
1137
|
+
/** Component to use as visual element. */
|
|
1138
|
+
figure?: ReactNode;
|
|
1139
|
+
/** Actions to set after the main content. */
|
|
1140
|
+
actions?: ReactNode;
|
|
1141
|
+
/** Main content to display */
|
|
1142
|
+
children: ReactNode;
|
|
1143
|
+
/** Orientation of the 3 sections */
|
|
1144
|
+
orientation?: FlexBoxProps["orientation"];
|
|
1145
|
+
/** Horizontal alignment. */
|
|
1146
|
+
hAlign?: FlexBoxProps["hAlign"];
|
|
1147
|
+
/** Vertical alignment. */
|
|
1148
|
+
vAlign?: FlexBoxProps["vAlign"];
|
|
1149
|
+
/**
|
|
1150
|
+
* The props to forward to the content.
|
|
1151
|
+
* By default, the content will have the same alignment as wrapper.
|
|
1152
|
+
*/
|
|
1153
|
+
contentProps?: Omit<FlexBoxProps, "children">;
|
|
1154
|
+
/** props to forward to the actions element. */
|
|
1155
|
+
actionsProps?: Omit<FlexBoxProps, "children">;
|
|
1156
|
+
/** props to forward to the figure element. */
|
|
1157
|
+
figureProps?: Omit<FlexBoxProps, "children">;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* The GenericBlock is a layout component made of 3 sections that can be
|
|
1161
|
+
* displayed either horizontally of vertically with the same gap between each section.
|
|
1162
|
+
*
|
|
1163
|
+
* The sections are:
|
|
1164
|
+
* * (Optional) `Figure` => A visual element to display before the main content.
|
|
1165
|
+
* * (Required) `Content` => The main content displayed
|
|
1166
|
+
* * (Optional) `Actions` => One or more actions to set after the element.
|
|
1167
|
+
*
|
|
1168
|
+
* @see https://www.figma.com/file/lzzrQmsfaXRaOyRfoEogPZ/DS%3A-playground?node-id=1%3A4076
|
|
1169
|
+
*/
|
|
1170
|
+
export declare const GenericBlock: Comp<GenericBlockProps, HTMLDivElement>;
|
|
1127
1171
|
export declare type GridGutterSize = Extract<Size, "regular" | "big" | "huge">;
|
|
1128
1172
|
/**
|
|
1129
1173
|
* Defines the props of the component.
|
|
@@ -1433,12 +1477,12 @@ export declare const Link: Comp<LinkProps, HTMLAnchorElement | HTMLButtonElement
|
|
|
1433
1477
|
* Defines the props of the component.
|
|
1434
1478
|
*/
|
|
1435
1479
|
export interface LinkPreviewProps extends GenericProps {
|
|
1436
|
-
/** Description
|
|
1437
|
-
description?: string
|
|
1438
|
-
__html: string;
|
|
1439
|
-
};
|
|
1480
|
+
/** Description. */
|
|
1481
|
+
description?: string;
|
|
1440
1482
|
/** Link URL. */
|
|
1441
1483
|
link: string;
|
|
1484
|
+
/** Custom react component for the link (can be used to inject react router Link). */
|
|
1485
|
+
linkAs?: "a" | any;
|
|
1442
1486
|
/** Props to pass to the link (minus those already set by the LinkPreview props). */
|
|
1443
1487
|
linkProps?: Omit<LinkProps, "color" | "colorVariant" | "href" | "target">;
|
|
1444
1488
|
/** Size variant. */
|
|
@@ -1449,6 +1493,8 @@ export interface LinkPreviewProps extends GenericProps {
|
|
|
1449
1493
|
thumbnailProps?: ThumbnailProps;
|
|
1450
1494
|
/** Title. */
|
|
1451
1495
|
title?: string;
|
|
1496
|
+
/** Customize the title heading tag. */
|
|
1497
|
+
titleHeading?: HeadingElement;
|
|
1452
1498
|
}
|
|
1453
1499
|
/**
|
|
1454
1500
|
* LinkPreview component.
|
|
@@ -1858,6 +1904,8 @@ export interface CoreSelectProps extends GenericProps {
|
|
|
1858
1904
|
helper?: string;
|
|
1859
1905
|
/** Whether the select should close on click. */
|
|
1860
1906
|
closeOnClick?: boolean;
|
|
1907
|
+
/** Icon (SVG path). */
|
|
1908
|
+
icon?: string;
|
|
1861
1909
|
/** Whether the component is disabled or not. */
|
|
1862
1910
|
isDisabled?: boolean;
|
|
1863
1911
|
/** Whether the component is required or not. */
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
-
|
|
3
|
-
exports[`<LinkPreview> Snapshots and structure should render correctly 1`] = `
|
|
4
|
-
<div
|
|
5
|
-
className="lumx-link-preview lumx-link-preview--size-regular lumx-link-preview--theme-light"
|
|
6
|
-
>
|
|
7
|
-
<div
|
|
8
|
-
className="lumx-link-preview__wrapper"
|
|
9
|
-
>
|
|
10
|
-
<div
|
|
11
|
-
className="lumx-link-preview__container"
|
|
12
|
-
>
|
|
13
|
-
<div
|
|
14
|
-
className="lumx-link-preview__link"
|
|
15
|
-
>
|
|
16
|
-
<Link
|
|
17
|
-
className="lumx-link-preview__link"
|
|
18
|
-
color="primary"
|
|
19
|
-
colorVariant="N"
|
|
20
|
-
target="_blank"
|
|
21
|
-
/>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
</div>
|
|
25
|
-
</div>
|
|
26
|
-
`;
|
|
27
|
-
|
|
28
|
-
exports[`<LinkPreview> Snapshots and structure should render correctly 2`] = `
|
|
29
|
-
<div
|
|
30
|
-
className="lumx-link-preview lumx-link-preview--size-regular lumx-link-preview--theme-light"
|
|
31
|
-
>
|
|
32
|
-
<div
|
|
33
|
-
className="lumx-link-preview__wrapper"
|
|
34
|
-
>
|
|
35
|
-
<div
|
|
36
|
-
className="lumx-link-preview__container"
|
|
37
|
-
>
|
|
38
|
-
<div
|
|
39
|
-
className="lumx-link-preview__link"
|
|
40
|
-
>
|
|
41
|
-
<Link
|
|
42
|
-
className="lumx-link-preview__link"
|
|
43
|
-
color="primary"
|
|
44
|
-
colorVariant="N"
|
|
45
|
-
target="_blank"
|
|
46
|
-
/>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
`;
|