@dbcdk/react-components 0.0.44 → 0.0.46
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/dist/components/datetime-picker/DateTimePicker.js +1 -1
- package/dist/components/datetime-picker/DateTimePicker.module.css +9 -0
- package/dist/components/filter-field/FilterField.d.ts +2 -1
- package/dist/components/filter-field/FilterField.js +8 -7
- package/dist/components/filter-field/FilterField.module.css +30 -10
- package/dist/components/forms/input/Input.d.ts +11 -0
- package/dist/components/forms/input/Input.js +12 -9
- package/dist/components/forms/input/Input.module.css +182 -30
- package/dist/components/forms/input-container/InputContainer.js +2 -2
- package/dist/components/forms/input-container/InputContainer.module.css +8 -51
- package/dist/components/forms/multi-select/MultiSelect.js +59 -140
- package/dist/components/forms/text-area/Textarea.module.css +2 -0
- package/dist/components/forms/typeahead/Typeahead.d.ts +34 -0
- package/dist/components/forms/typeahead/Typeahead.js +340 -0
- package/dist/components/menu/Menu.d.ts +6 -5
- package/dist/components/menu/Menu.js +14 -7
- package/dist/components/menu/Menu.module.css +15 -9
- package/dist/components/overlay/modal/Modal.js +9 -4
- package/dist/hooks/useListNavigation.d.ts +24 -0
- package/dist/hooks/useListNavigation.js +234 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -10,8 +10,10 @@ const isInteractiveEl = (el) => React.isValidElement(el) &&
|
|
|
10
10
|
(typeof el.type === 'string' ? el.type === 'a' || el.type === 'button' : true);
|
|
11
11
|
function applyMenuItemPropsToElement(child, opts) {
|
|
12
12
|
var _a, _b, _c, _d;
|
|
13
|
-
const { active, disabled, role, tabIndex = -1, className } = opts;
|
|
14
|
-
const childClass = [styles.item, active ? styles.active : ''
|
|
13
|
+
const { active, selected, disabled, role, tabIndex = -1, className } = opts;
|
|
14
|
+
const childClass = [styles.item, active ? styles.active : '', selected ? styles.selected : '']
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.join(' ');
|
|
15
17
|
const nextImmediate = React.cloneElement(child, {
|
|
16
18
|
className: [child.props.className, styles.interactiveChild, className]
|
|
17
19
|
.filter(Boolean)
|
|
@@ -22,7 +24,7 @@ function applyMenuItemPropsToElement(child, opts) {
|
|
|
22
24
|
return React.cloneElement(child, {
|
|
23
25
|
role: (_a = child.props.role) !== null && _a !== void 0 ? _a : role,
|
|
24
26
|
tabIndex: (_b = child.props.tabIndex) !== null && _b !== void 0 ? _b : tabIndex,
|
|
25
|
-
'aria-selected':
|
|
27
|
+
'aria-selected': selected || undefined,
|
|
26
28
|
'aria-disabled': disabled || undefined,
|
|
27
29
|
className: [child.props.className, styles.interactive, childClass, className]
|
|
28
30
|
.filter(Boolean)
|
|
@@ -34,7 +36,7 @@ function applyMenuItemPropsToElement(child, opts) {
|
|
|
34
36
|
return React.cloneElement(nextImmediate, {
|
|
35
37
|
role: (_c = nextImmediate.props.role) !== null && _c !== void 0 ? _c : role,
|
|
36
38
|
tabIndex: (_d = nextImmediate.props.tabIndex) !== null && _d !== void 0 ? _d : tabIndex,
|
|
37
|
-
'aria-selected':
|
|
39
|
+
'aria-selected': selected || undefined,
|
|
38
40
|
'aria-disabled': disabled || undefined,
|
|
39
41
|
className: [nextImmediate.props.className, styles.interactive, childClass]
|
|
40
42
|
.filter(Boolean)
|
|
@@ -42,16 +44,21 @@ function applyMenuItemPropsToElement(child, opts) {
|
|
|
42
44
|
disabled,
|
|
43
45
|
});
|
|
44
46
|
}
|
|
45
|
-
const MenuItem = React.forwardRef(({ children, active, disabled, className, itemRole, ...liProps }, ref) => {
|
|
47
|
+
const MenuItem = React.forwardRef(({ children, active, selected, disabled, className, itemRole, ...liProps }, ref) => {
|
|
46
48
|
// If caller sets itemRole prop, use it; otherwise attempt to inherit from parent Menu via data attr.
|
|
47
49
|
// (We can’t reliably read parent props here without context; simplest is: caller passes itemRole on Menu.Item when needed.)
|
|
48
50
|
const resolvedRole = itemRole !== null && itemRole !== void 0 ? itemRole : 'menuitem';
|
|
49
51
|
if (isInteractiveEl(children)) {
|
|
50
52
|
const child = children;
|
|
51
|
-
return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: applyMenuItemPropsToElement(child, { active, disabled, role: resolvedRole }) }));
|
|
53
|
+
return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: applyMenuItemPropsToElement(child, { active, selected, disabled, role: resolvedRole }) }));
|
|
52
54
|
}
|
|
53
55
|
// Fallback: wrap non-interactive children in a <button>
|
|
54
|
-
return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: _jsx("button", { role: resolvedRole, tabIndex: -1, "aria-selected":
|
|
56
|
+
return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: _jsx("button", { role: resolvedRole, tabIndex: -1, "aria-selected": selected || undefined, "aria-disabled": disabled || undefined, className: [
|
|
57
|
+
styles.interactive,
|
|
58
|
+
styles.item,
|
|
59
|
+
active ? styles.active : '',
|
|
60
|
+
selected ? styles.selected : '',
|
|
61
|
+
]
|
|
55
62
|
.filter(Boolean)
|
|
56
63
|
.join(' '), type: "button", disabled: disabled, children: children }) }));
|
|
57
64
|
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* Menu.module.css */
|
|
2
1
|
.container {
|
|
3
2
|
list-style: none;
|
|
4
3
|
margin: 0;
|
|
@@ -11,11 +10,11 @@
|
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
.container > li + li {
|
|
14
|
-
margin-block-start: var(--spacing-
|
|
13
|
+
margin-block-start: var(--spacing-xxs);
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
.row {
|
|
18
|
-
display:
|
|
17
|
+
display: block;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
/* Applied to actual interactive elements (button/a/custom that forwards className) */
|
|
@@ -29,7 +28,7 @@
|
|
|
29
28
|
text-decoration: none;
|
|
30
29
|
|
|
31
30
|
/* choose your density */
|
|
32
|
-
padding-block: var(--spacing-
|
|
31
|
+
padding-block: var(--spacing-xxs);
|
|
33
32
|
padding-inline: var(--spacing-md);
|
|
34
33
|
|
|
35
34
|
background: transparent;
|
|
@@ -70,7 +69,7 @@
|
|
|
70
69
|
display: flex;
|
|
71
70
|
align-items: flex-start;
|
|
72
71
|
inline-size: 100%;
|
|
73
|
-
padding-block:
|
|
72
|
+
padding-block: 2px;
|
|
74
73
|
padding-inline: var(--spacing-md);
|
|
75
74
|
border-radius: var(--border-radius-sm);
|
|
76
75
|
}
|
|
@@ -90,8 +89,8 @@
|
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
/* Hover: support both cases (interactive element, or wrapper child) */
|
|
93
|
-
.interactive:hover,
|
|
94
|
-
.row:hover > .interactiveChild {
|
|
92
|
+
.interactive:hover:not(.selected),
|
|
93
|
+
.row:hover > .interactiveChild:not(.selected) {
|
|
95
94
|
background-color: var(--color-bg-hover-subtle);
|
|
96
95
|
}
|
|
97
96
|
|
|
@@ -107,10 +106,17 @@
|
|
|
107
106
|
box-shadow: var(--focus-ring);
|
|
108
107
|
}
|
|
109
108
|
|
|
110
|
-
/*
|
|
109
|
+
/* Keyboard active/current item */
|
|
111
110
|
.active,
|
|
111
|
+
.interactive.active,
|
|
112
|
+
.row > .interactiveChild.active {
|
|
113
|
+
background-color: var(--color-bg-hover-subtle);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Selected item */
|
|
117
|
+
.selected,
|
|
112
118
|
.interactive[aria-selected='true'],
|
|
113
|
-
.row > .interactiveChild.
|
|
119
|
+
.row > .interactiveChild.selected,
|
|
114
120
|
.row > .interactiveChild[aria-selected='true'] {
|
|
115
121
|
background-color: var(--color-bg-selected);
|
|
116
122
|
color: var(--color-fg-default);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { X } from 'lucide-react';
|
|
4
|
-
import { useEffect, useId, useRef } from 'react';
|
|
4
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
5
|
+
import { createPortal } from 'react-dom';
|
|
5
6
|
import { Button } from '../../../components/button/Button';
|
|
6
7
|
import { Headline } from '../../../components/headline/Headline';
|
|
7
8
|
import styles from './Modal.module.css';
|
|
@@ -9,11 +10,15 @@ export function Modal({ isOpen, onRequestClose, header, content, children, prima
|
|
|
9
10
|
const titleId = useId();
|
|
10
11
|
const dialogRef = useRef(null);
|
|
11
12
|
const lastActiveElementRef = useRef(null);
|
|
13
|
+
const [mounted, setMounted] = useState(false);
|
|
12
14
|
// Always call the latest onRequestClose without re-running effects
|
|
13
15
|
const onRequestCloseRef = useRef(onRequestClose);
|
|
14
16
|
useEffect(() => {
|
|
15
17
|
onRequestCloseRef.current = onRequestClose;
|
|
16
18
|
}, [onRequestClose]);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setMounted(true);
|
|
21
|
+
}, []);
|
|
17
22
|
// Track open transition so we only autofocus once per open
|
|
18
23
|
const wasOpenRef = useRef(false);
|
|
19
24
|
useEffect(() => {
|
|
@@ -72,7 +77,7 @@ export function Modal({ isOpen, onRequestClose, header, content, children, prima
|
|
|
72
77
|
}
|
|
73
78
|
};
|
|
74
79
|
}, [isOpen]);
|
|
75
|
-
if (!isOpen)
|
|
80
|
+
if (!isOpen || !mounted)
|
|
76
81
|
return null;
|
|
77
82
|
const handleOverlayClick = () => {
|
|
78
83
|
if (closeOnOverlayClick) {
|
|
@@ -86,7 +91,7 @@ export function Modal({ isOpen, onRequestClose, header, content, children, prima
|
|
|
86
91
|
const shouldRenderFooter = Boolean(primaryAction || resolvedSecondaryAction);
|
|
87
92
|
const body = children !== null && children !== void 0 ? children : content;
|
|
88
93
|
const resolvedWidth = typeof width === 'number' ? `${width}px` : typeof width === 'string' ? width : undefined;
|
|
89
|
-
return (_jsx("div", { className: styles.overlay, onClick: handleOverlayClick, children: _jsxs("div", { "data-cy": dataCy, ref: dialogRef, className: `${styles.modal} ${disableContentSpacing ? '' : styles.contentSpacing}`, style: resolvedWidth
|
|
94
|
+
return createPortal(_jsx("div", { className: styles.overlay, onClick: handleOverlayClick, children: _jsxs("div", { "data-cy": dataCy, ref: dialogRef, className: `${styles.modal} ${disableContentSpacing ? '' : styles.contentSpacing}`, style: resolvedWidth
|
|
90
95
|
? { ['--modal-width']: resolvedWidth }
|
|
91
|
-
: undefined, onClick: stopPropagation, role: "dialog", "aria-modal": "true", "aria-labelledby": header ? titleId : undefined, tabIndex: -1, children: [_jsxs("div", { className: styles.header, children: [header && (_jsx(Headline, { severity: severity, size: 3, disableMargin: true, children: header })), _jsx(Button, { type: "button", variant: "inline", onClick: () => onRequestCloseRef.current(), "aria-label": "Luk", shape: "round", icon: _jsx(X, {}) })] }), _jsx("div", { className: styles.body, children: body }), shouldRenderFooter && (_jsxs("div", { className: styles.footer, children: [resolvedSecondaryAction && (_jsxs(Button, { type: "button", onClick: resolvedSecondaryAction.onClick, disabled: isLoading, children: [resolvedSecondaryAction.icon && (_jsx("span", { className: styles.icon, children: resolvedSecondaryAction.icon })), _jsx("span", { children: resolvedSecondaryAction.label })] })), primaryAction && (_jsxs(Button, { type: "button", variant: primaryAction.severity || 'primary', onClick: primaryAction.onClick, disabled: primaryAction.disabled || isLoading, loading: isLoading, children: [primaryAction.icon && _jsx("span", { className: styles.icon, children: primaryAction.icon }), _jsx("span", { children: primaryAction.label })] }))] }))] }) }));
|
|
96
|
+
: undefined, onClick: stopPropagation, role: "dialog", "aria-modal": "true", "aria-labelledby": header ? titleId : undefined, tabIndex: -1, children: [_jsxs("div", { className: styles.header, children: [header && (_jsx(Headline, { severity: severity, size: 3, disableMargin: true, children: header })), _jsx(Button, { type: "button", variant: "inline", onClick: () => onRequestCloseRef.current(), "aria-label": "Luk", shape: "round", icon: _jsx(X, {}) })] }), _jsx("div", { className: styles.body, children: body }), shouldRenderFooter && (_jsxs("div", { className: styles.footer, children: [resolvedSecondaryAction && (_jsxs(Button, { type: "button", onClick: resolvedSecondaryAction.onClick, disabled: isLoading, children: [resolvedSecondaryAction.icon && (_jsx("span", { className: styles.icon, children: resolvedSecondaryAction.icon })), _jsx("span", { children: resolvedSecondaryAction.label })] })), primaryAction && (_jsxs(Button, { type: "button", variant: primaryAction.severity || 'primary', onClick: primaryAction.onClick, disabled: primaryAction.disabled || isLoading, loading: isLoading, children: [primaryAction.icon && _jsx("span", { className: styles.icon, children: primaryAction.icon }), _jsx("span", { children: primaryAction.label })] }))] }))] }) }), document.body);
|
|
92
97
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction } from 'react';
|
|
2
|
+
import type { KeyboardEvent, RefObject } from 'react';
|
|
3
|
+
interface UseListNavigationProps<T> {
|
|
4
|
+
options: T[];
|
|
5
|
+
getLabel: (option: T) => string;
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
onOpenChange: (open: boolean) => void;
|
|
8
|
+
searchInputRef?: RefObject<HTMLInputElement | null>;
|
|
9
|
+
searchable?: boolean;
|
|
10
|
+
focusActiveOptionOnOpen?: boolean;
|
|
11
|
+
typeaheadTimeoutMs?: number;
|
|
12
|
+
getInitialActiveIndex?: (options: T[]) => number;
|
|
13
|
+
}
|
|
14
|
+
interface UseListNavigationResult {
|
|
15
|
+
activeIndex: number;
|
|
16
|
+
setActiveIndex: Dispatch<SetStateAction<number>>;
|
|
17
|
+
optionRefs: RefObject<(HTMLButtonElement | null)[]>;
|
|
18
|
+
resetActiveIndex: () => void;
|
|
19
|
+
clearTypeahead: () => void;
|
|
20
|
+
handleKeyDown: (e: KeyboardEvent) => void;
|
|
21
|
+
focusActiveOption: () => void;
|
|
22
|
+
}
|
|
23
|
+
export declare function useListNavigation<T>({ options, getLabel, isOpen, onOpenChange, searchInputRef, searchable, focusActiveOptionOnOpen, typeaheadTimeoutMs, getInitialActiveIndex, }: UseListNavigationProps<T>): UseListNavigationResult;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
export function useListNavigation({ options, getLabel, isOpen, onOpenChange, searchInputRef, searchable = false, focusActiveOptionOnOpen = true, typeaheadTimeoutMs = 500, getInitialActiveIndex, }) {
|
|
3
|
+
const optionRefs = useRef([]);
|
|
4
|
+
const typeaheadRef = useRef('');
|
|
5
|
+
const typeaheadTimeoutRef = useRef(null);
|
|
6
|
+
const normalizedLabels = useMemo(() => options.map(option => getLabel(option).trim().toLocaleLowerCase()), [options, getLabel]);
|
|
7
|
+
const getDefaultInitialIndex = useCallback((items) => {
|
|
8
|
+
if (items.length === 0)
|
|
9
|
+
return -1;
|
|
10
|
+
if (getInitialActiveIndex) {
|
|
11
|
+
const nextIndex = getInitialActiveIndex(items);
|
|
12
|
+
if (nextIndex < 0)
|
|
13
|
+
return -1;
|
|
14
|
+
return Math.min(nextIndex, items.length - 1);
|
|
15
|
+
}
|
|
16
|
+
return searchable ? -1 : 0;
|
|
17
|
+
}, [getInitialActiveIndex, searchable]);
|
|
18
|
+
const [activeIndex, setActiveIndex] = useState(() => getDefaultInitialIndex(options));
|
|
19
|
+
const clearTypeahead = useCallback(() => {
|
|
20
|
+
typeaheadRef.current = '';
|
|
21
|
+
if (typeaheadTimeoutRef.current) {
|
|
22
|
+
clearTimeout(typeaheadTimeoutRef.current);
|
|
23
|
+
typeaheadTimeoutRef.current = null;
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
26
|
+
const focusActiveOption = useCallback(() => {
|
|
27
|
+
var _a;
|
|
28
|
+
if (activeIndex < 0 || options.length === 0)
|
|
29
|
+
return;
|
|
30
|
+
(_a = optionRefs.current[activeIndex]) === null || _a === void 0 ? void 0 : _a.focus();
|
|
31
|
+
}, [activeIndex, options.length]);
|
|
32
|
+
const resetActiveIndex = useCallback(() => {
|
|
33
|
+
setActiveIndex(getDefaultInitialIndex(options));
|
|
34
|
+
}, [getDefaultInitialIndex, options]);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
return () => {
|
|
37
|
+
if (typeaheadTimeoutRef.current)
|
|
38
|
+
clearTimeout(typeaheadTimeoutRef.current);
|
|
39
|
+
};
|
|
40
|
+
}, []);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setActiveIndex(current => {
|
|
43
|
+
if (options.length === 0)
|
|
44
|
+
return -1;
|
|
45
|
+
if (current < 0)
|
|
46
|
+
return current;
|
|
47
|
+
return Math.min(current, options.length - 1);
|
|
48
|
+
});
|
|
49
|
+
}, [options]);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
optionRefs.current = optionRefs.current.slice(0, options.length);
|
|
52
|
+
}, [options.length]);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!isOpen || !focusActiveOptionOnOpen)
|
|
55
|
+
return;
|
|
56
|
+
if (searchable && document.activeElement === (searchInputRef === null || searchInputRef === void 0 ? void 0 : searchInputRef.current))
|
|
57
|
+
return;
|
|
58
|
+
focusActiveOption();
|
|
59
|
+
}, [activeIndex, focusActiveOption, focusActiveOptionOnOpen, isOpen, searchable, searchInputRef]);
|
|
60
|
+
const moveNext = useCallback(() => {
|
|
61
|
+
if (options.length === 0)
|
|
62
|
+
return;
|
|
63
|
+
setActiveIndex(index => {
|
|
64
|
+
if (index < 0)
|
|
65
|
+
return 0;
|
|
66
|
+
return Math.min(index + 1, options.length - 1);
|
|
67
|
+
});
|
|
68
|
+
}, [options.length]);
|
|
69
|
+
const movePrev = useCallback(() => {
|
|
70
|
+
if (options.length === 0)
|
|
71
|
+
return;
|
|
72
|
+
setActiveIndex(index => {
|
|
73
|
+
if (index < 0)
|
|
74
|
+
return options.length - 1;
|
|
75
|
+
return Math.max(index - 1, 0);
|
|
76
|
+
});
|
|
77
|
+
}, [options.length]);
|
|
78
|
+
const moveFirst = useCallback(() => {
|
|
79
|
+
if (options.length === 0)
|
|
80
|
+
return;
|
|
81
|
+
setActiveIndex(0);
|
|
82
|
+
}, [options.length]);
|
|
83
|
+
const moveLast = useCallback(() => {
|
|
84
|
+
if (options.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
setActiveIndex(options.length - 1);
|
|
87
|
+
}, [options.length]);
|
|
88
|
+
const findTypeaheadMatch = useCallback((query, startIndex) => {
|
|
89
|
+
if (!query || options.length === 0)
|
|
90
|
+
return -1;
|
|
91
|
+
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
92
|
+
if (!normalizedQuery)
|
|
93
|
+
return -1;
|
|
94
|
+
const safeStartIndex = startIndex < 0 ? -1 : startIndex;
|
|
95
|
+
for (let step = 1; step <= options.length; step += 1) {
|
|
96
|
+
const index = (safeStartIndex + step + options.length) % options.length;
|
|
97
|
+
const label = normalizedLabels[index];
|
|
98
|
+
if (label === null || label === void 0 ? void 0 : label.startsWith(normalizedQuery))
|
|
99
|
+
return index;
|
|
100
|
+
}
|
|
101
|
+
return -1;
|
|
102
|
+
}, [normalizedLabels, options.length]);
|
|
103
|
+
const handleTypeahead = useCallback((key) => {
|
|
104
|
+
const nextBuffer = `${typeaheadRef.current}${key.toLocaleLowerCase()}`;
|
|
105
|
+
const repeatedChar = new Set(nextBuffer).size === 1;
|
|
106
|
+
let nextIndex = findTypeaheadMatch(nextBuffer, activeIndex);
|
|
107
|
+
let appliedBuffer = nextBuffer;
|
|
108
|
+
if (nextIndex < 0 && repeatedChar) {
|
|
109
|
+
appliedBuffer = key.toLocaleLowerCase();
|
|
110
|
+
nextIndex = findTypeaheadMatch(appliedBuffer, activeIndex);
|
|
111
|
+
}
|
|
112
|
+
if (typeaheadTimeoutRef.current)
|
|
113
|
+
clearTimeout(typeaheadTimeoutRef.current);
|
|
114
|
+
typeaheadRef.current = appliedBuffer;
|
|
115
|
+
typeaheadTimeoutRef.current = setTimeout(() => {
|
|
116
|
+
typeaheadRef.current = '';
|
|
117
|
+
typeaheadTimeoutRef.current = null;
|
|
118
|
+
}, typeaheadTimeoutMs);
|
|
119
|
+
if (nextIndex < 0)
|
|
120
|
+
return;
|
|
121
|
+
setActiveIndex(nextIndex);
|
|
122
|
+
if (!isOpen)
|
|
123
|
+
onOpenChange(true);
|
|
124
|
+
}, [activeIndex, findTypeaheadMatch, isOpen, onOpenChange, typeaheadTimeoutMs]);
|
|
125
|
+
const handleKeyDown = useCallback((e) => {
|
|
126
|
+
var _a;
|
|
127
|
+
const isSearchInputTarget = searchable && e.target === (searchInputRef === null || searchInputRef === void 0 ? void 0 : searchInputRef.current);
|
|
128
|
+
if (isSearchInputTarget) {
|
|
129
|
+
switch (e.key) {
|
|
130
|
+
case 'ArrowDown':
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
if (!isOpen) {
|
|
133
|
+
onOpenChange(true);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
moveNext();
|
|
137
|
+
return;
|
|
138
|
+
case 'ArrowUp':
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
if (!isOpen) {
|
|
141
|
+
onOpenChange(true);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
movePrev();
|
|
145
|
+
return;
|
|
146
|
+
case 'Home':
|
|
147
|
+
if (!isOpen)
|
|
148
|
+
return;
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
moveFirst();
|
|
151
|
+
return;
|
|
152
|
+
case 'End':
|
|
153
|
+
if (!isOpen)
|
|
154
|
+
return;
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
moveLast();
|
|
157
|
+
return;
|
|
158
|
+
case 'Escape':
|
|
159
|
+
e.preventDefault();
|
|
160
|
+
onOpenChange(false);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (e.key.length === 1 && !e.altKey && !e.ctrlKey && !e.metaKey && !/\s/.test(e.key)) {
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
handleTypeahead(e.key);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
switch (e.key) {
|
|
171
|
+
case 'ArrowDown':
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
if (!isOpen) {
|
|
174
|
+
onOpenChange(true);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
moveNext();
|
|
178
|
+
break;
|
|
179
|
+
case 'ArrowUp':
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
if (!isOpen) {
|
|
182
|
+
onOpenChange(true);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (searchable &&
|
|
186
|
+
activeIndex === 0 &&
|
|
187
|
+
optionRefs.current[activeIndex] === document.activeElement) {
|
|
188
|
+
(_a = searchInputRef === null || searchInputRef === void 0 ? void 0 : searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
189
|
+
setActiveIndex(-1);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
movePrev();
|
|
193
|
+
break;
|
|
194
|
+
case 'Home':
|
|
195
|
+
if (!isOpen)
|
|
196
|
+
return;
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
moveFirst();
|
|
199
|
+
break;
|
|
200
|
+
case 'End':
|
|
201
|
+
if (!isOpen)
|
|
202
|
+
return;
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
moveLast();
|
|
205
|
+
break;
|
|
206
|
+
case 'Escape':
|
|
207
|
+
if (!isOpen)
|
|
208
|
+
return;
|
|
209
|
+
e.preventDefault();
|
|
210
|
+
onOpenChange(false);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}, [
|
|
214
|
+
activeIndex,
|
|
215
|
+
handleTypeahead,
|
|
216
|
+
isOpen,
|
|
217
|
+
moveFirst,
|
|
218
|
+
moveLast,
|
|
219
|
+
moveNext,
|
|
220
|
+
movePrev,
|
|
221
|
+
onOpenChange,
|
|
222
|
+
searchInputRef,
|
|
223
|
+
searchable,
|
|
224
|
+
]);
|
|
225
|
+
return {
|
|
226
|
+
activeIndex,
|
|
227
|
+
setActiveIndex,
|
|
228
|
+
optionRefs,
|
|
229
|
+
resetActiveIndex,
|
|
230
|
+
clearTypeahead,
|
|
231
|
+
handleKeyDown,
|
|
232
|
+
focusActiveOption,
|
|
233
|
+
};
|
|
234
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -67,3 +67,4 @@ export * from './components/interval-select/IntervalSelect';
|
|
|
67
67
|
export * from './components/accordion/Accordion';
|
|
68
68
|
export * from './components/state-page/StatePage';
|
|
69
69
|
export * from './components/sticky-footer-layout/StickyFooterLayout';
|
|
70
|
+
export * from './components/forms/typeahead/Typeahead';
|
package/dist/index.js
CHANGED
|
@@ -67,3 +67,4 @@ export * from './components/interval-select/IntervalSelect';
|
|
|
67
67
|
export * from './components/accordion/Accordion';
|
|
68
68
|
export * from './components/state-page/StatePage';
|
|
69
69
|
export * from './components/sticky-footer-layout/StickyFooterLayout';
|
|
70
|
+
export * from './components/forms/typeahead/Typeahead';
|