@coinbase/cds-web 9.26.1 → 9.27.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/CHANGELOG.md +21 -0
- package/dts/alpha/select/Select.d.ts.map +1 -1
- package/dts/alpha/select/useTypeahead.d.ts +45 -0
- package/dts/alpha/select/useTypeahead.d.ts.map +1 -0
- package/dts/controls/HelperText.d.ts.map +1 -1
- package/esm/alpha/select/Select.js +17 -33
- package/esm/alpha/select/useTypeahead.js +121 -0
- package/esm/controls/HelperText.js +35 -31
- package/package.json +4 -4
- package/esm/controls/HelperText.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
## 9.27.0 (2026-09-18)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- Feat: add native-style typeahead keyboard selection to the web `Select (Alpha)`, supporting a multi-character search buffer, repeated-key cycling, and matching both when the listbox is open and closed.
|
|
6
|
+
|
|
7
|
+
### 🩹 Fixes
|
|
8
|
+
|
|
9
|
+
- Fix: constrain the MCP server's `get-cds-doc` tool to files inside its bundled `mcp-docs` directory. Routes that resolve outside of it are now reported as not found.
|
|
10
|
+
|
|
11
|
+
## 9.26.2 (2026-09-11)
|
|
12
|
+
|
|
13
|
+
### 🩹 Fixes
|
|
14
|
+
|
|
15
|
+
- Fix: vertically align the helper-text error icon with the text in web and mobile inputs.
|
|
16
|
+
- Fix: dismiss the Android soft keyboard when the mobile `TextInput` unmounts while focused.
|
|
17
|
+
|
|
18
|
+
### 🧱 Updated Dependencies
|
|
19
|
+
|
|
20
|
+
- Updated illustrations to 4.48.1
|
|
21
|
+
|
|
1
22
|
## 9.26.1 (2026-09-03)
|
|
2
23
|
|
|
3
24
|
### 🩹 Fixes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Select.d.ts","sourceRoot":"","sources":["../../../src/alpha/select/Select.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Select.d.ts","sourceRoot":"","sources":["../../../src/alpha/select/Select.tsx"],"names":[],"mappings":"AACA,OAAO,EAAiC,KAAK,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAgB/F,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,SAAS,CAAC;AAIjB,YAAY,EACV,eAAe,EACf,sBAAsB,EACtB,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,EACnB,mCAAmC,EACnC,+BAA+B,EAC/B,YAAY,EACZ,qBAAqB,EACrB,oBAAoB,EACpB,iBAAiB,EACjB,0BAA0B,EAC1B,yBAAyB,EACzB,sBAAsB,EACtB,iBAAiB,EACjB,WAAW,EACX,UAAU,EACV,UAAU,GACX,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAE9C,eAAO,MAAM,yBAAyB,EAAE,mBAAmB,CAAC,oBAAoB,CAG/E,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,WAAW,GACjC,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG;IACtC,IAAI,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;CACjC,CAAC;AAEJ,KAAK,eAAe,GAAG,CACrB,IAAI,SAAS,UAAU,GAAG,QAAQ,EAClC,iBAAiB,SAAS,MAAM,GAAG,MAAM,EAEzC,KAAK,EAAE,WAAW,CAAC,IAAI,EAAE,iBAAiB,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;CAAE,KACzE,KAAK,CAAC,YAAY,CAAC;AAySxB,eAAO,MAAM,MAAM,EAAiB,eAAe,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export declare const TYPEAHEAD_RESET_MS = 500;
|
|
2
|
+
export declare function isPrintableTypeaheadKey(key: string): boolean;
|
|
3
|
+
export declare function isTypeaheadKeyEvent(event: React.KeyboardEvent | KeyboardEvent): boolean;
|
|
4
|
+
export declare function normalizeOptionText(text: string | null | undefined): string;
|
|
5
|
+
export declare function getTypeaheadMatchIndex(
|
|
6
|
+
labels: string[],
|
|
7
|
+
search: string,
|
|
8
|
+
currentIndex: number,
|
|
9
|
+
): number;
|
|
10
|
+
type ElementRef = {
|
|
11
|
+
current: HTMLElement | null;
|
|
12
|
+
};
|
|
13
|
+
export type UseTypeaheadOptions = {
|
|
14
|
+
/** Whether the items container is visible (open). */
|
|
15
|
+
areItemsVisible: boolean;
|
|
16
|
+
/** Setter used to reveal items when typing while hidden. */
|
|
17
|
+
setAreItemsVisible: (visible: boolean) => void;
|
|
18
|
+
/** Ref to the trigger element. */
|
|
19
|
+
triggerRef: ElementRef;
|
|
20
|
+
/** Ref to the container element that hosts the items. */
|
|
21
|
+
itemsContainerRef: ElementRef;
|
|
22
|
+
/** Role used to query item elements within the container. */
|
|
23
|
+
itemRole: string;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
readOnly?: boolean;
|
|
26
|
+
};
|
|
27
|
+
export type UseTypeaheadResult = {
|
|
28
|
+
/** Attach to the control's `onKeyDown`; handles the closed-state type-to-open path. */
|
|
29
|
+
onControlKeyDown: (event: React.KeyboardEvent) => void;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Native `<select>`-style typeahead for the Select (Alpha). Printable keys build a short-lived
|
|
33
|
+
* buffer that focuses the matching option, both while closed (type-to-open) and open.
|
|
34
|
+
*/
|
|
35
|
+
export declare function useTypeahead({
|
|
36
|
+
areItemsVisible,
|
|
37
|
+
setAreItemsVisible,
|
|
38
|
+
triggerRef,
|
|
39
|
+
itemsContainerRef,
|
|
40
|
+
itemRole,
|
|
41
|
+
disabled,
|
|
42
|
+
readOnly,
|
|
43
|
+
}: UseTypeaheadOptions): UseTypeaheadResult;
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=useTypeahead.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTypeahead.d.ts","sourceRoot":"","sources":["../../../src/alpha/select/useTypeahead.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAItC,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE5D;AAID,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,CAAC,aAAa,GAAG,aAAa,GAAG,OAAO,CAGvF;AAGD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAE3E;AAID,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EAAE,EAChB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GACnB,MAAM,CAgBR;AAED,KAAK,UAAU,GAAG;IAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;CAAE,CAAC;AAElD,MAAM,MAAM,mBAAmB,GAAG;IAChC,qDAAqD;IACrD,eAAe,EAAE,OAAO,CAAC;IACzB,4DAA4D;IAC5D,kBAAkB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/C,kCAAkC;IAClC,UAAU,EAAE,UAAU,CAAC;IACvB,yDAAyD;IACzD,iBAAiB,EAAE,UAAU,CAAC;IAC9B,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,uFAAuF;IACvF,gBAAgB,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,aAAa,KAAK,IAAI,CAAC;CACxD,CAAC;AAEF;;;GAGG;AACH,wBAAgB,YAAY,CAAC,EAC3B,eAAe,EACf,kBAAkB,EAClB,UAAU,EACV,iBAAiB,EACjB,QAAQ,EACR,QAAQ,EACR,QAAQ,GACT,EAAE,mBAAmB,GAAG,kBAAkB,CA+F1C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HelperText.d.ts","sourceRoot":"","sources":["../../src/controls/HelperText.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"HelperText.d.ts","sourceRoot":"","sources":["../../src/controls/HelperText.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwB,MAAM,OAAO,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAKjE,OAAO,EAAQ,KAAK,kBAAkB,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEnF,MAAM,MAAM,eAAe,GAAG;IAC5B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC;IACxB,sDAAsD;IACtD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,6CAA6C;IAC7C,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,iCAAiC;IACjC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+EAA+E;IAC/E,MAAM,CAAC,EAAE;QACP,wBAAwB;QACxB,IAAI,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;QAC3B,yBAAyB;QACzB,IAAI,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;KAC5B,CAAC;IACF,6EAA6E;IAC7E,UAAU,CAAC,EAAE;QACX,wBAAwB;QACxB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,yBAAyB;QACzB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH,GAAG,SAAS,CAAC,kBAAkB,CAAC,CAAC;AAElC,eAAO,MAAM,UAAU,6CA8DrB,CAAC"}
|
|
@@ -3,7 +3,7 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
|
|
|
3
3
|
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
|
|
4
4
|
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
|
|
5
5
|
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
6
|
-
import { forwardRef, memo,
|
|
6
|
+
import { forwardRef, memo, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
7
7
|
import { autoUpdate, flip, useFloating } from '@floating-ui/react-dom';
|
|
8
8
|
import { cx } from '../../cx';
|
|
9
9
|
import { useClickOutside } from '../../hooks/useClickOutside';
|
|
@@ -19,6 +19,7 @@ import { DefaultSelectEmptyDropdownContents } from './DefaultSelectEmptyDropdown
|
|
|
19
19
|
import { DefaultSelectOption } from './DefaultSelectOption';
|
|
20
20
|
import { DefaultSelectOptionGroup } from './DefaultSelectOptionGroup';
|
|
21
21
|
import { defaultSelectSize } from './types';
|
|
22
|
+
import { useTypeahead } from './useTypeahead';
|
|
22
23
|
|
|
23
24
|
// Re-export all types for backward compatibility
|
|
24
25
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -29,6 +30,7 @@ export const defaultAccessibilityRoles = {
|
|
|
29
30
|
option: 'option'
|
|
30
31
|
};
|
|
31
32
|
const SelectBase = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_props, ref) => {
|
|
33
|
+
var _accessibilityRoles$o;
|
|
32
34
|
const mergedProps = useComponentConfig('Select', _props);
|
|
33
35
|
const {
|
|
34
36
|
value,
|
|
@@ -85,9 +87,7 @@ const SelectBase = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_props, ref) => {
|
|
|
85
87
|
testID
|
|
86
88
|
} = mergedProps;
|
|
87
89
|
const hasMounted = useHasMounted();
|
|
88
|
-
//
|
|
89
|
-
// the translation: only the smallest control size renders a compact dropdown. The control
|
|
90
|
-
// still receives the raw `compact` because it needs it for legacy label placement.
|
|
90
|
+
// Dropdown density is binary, so only the smallest size renders compact.
|
|
91
91
|
const dropdownCompact = (size !== null && size !== void 0 ? size : compact ? 's' : defaultSelectSize) === 's';
|
|
92
92
|
const [openInternal, setOpenInternal] = useState(defaultOpen !== null && defaultOpen !== void 0 ? defaultOpen : false);
|
|
93
93
|
const open = openProp !== null && openProp !== void 0 ? openProp : openInternal;
|
|
@@ -106,34 +106,18 @@ const SelectBase = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_props, ref) => {
|
|
|
106
106
|
ref: refs.floating,
|
|
107
107
|
excludeRefs: [refs.reference]
|
|
108
108
|
});
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (!open || !pendingTypeAheadKeyRef.current) return;
|
|
122
|
-
const key = pendingTypeAheadKeyRef.current;
|
|
123
|
-
pendingTypeAheadKeyRef.current = null;
|
|
124
|
-
const floatingEl = refs.floating.current;
|
|
125
|
-
if (!floatingEl) return;
|
|
126
|
-
const optionRole = (_accessibilityRoles$o = accessibilityRoles === null || accessibilityRoles === void 0 ? void 0 : accessibilityRoles.option) !== null && _accessibilityRoles$o !== void 0 ? _accessibilityRoles$o : 'option';
|
|
127
|
-
const options = floatingEl.querySelectorAll("[role=\"".concat(optionRole, "\"]"));
|
|
128
|
-
const matchingOption = Array.from(options).find(option => {
|
|
129
|
-
var _option$textContent, _firstLetterMatch$;
|
|
130
|
-
const firstLetterMatch = (_option$textContent = option.textContent) === null || _option$textContent === void 0 ? void 0 : _option$textContent.match(/[a-z]/i);
|
|
131
|
-
return (firstLetterMatch === null || firstLetterMatch === void 0 || (_firstLetterMatch$ = firstLetterMatch[0]) === null || _firstLetterMatch$ === void 0 ? void 0 : _firstLetterMatch$.toLowerCase()) === key;
|
|
132
|
-
});
|
|
133
|
-
if (matchingOption) {
|
|
134
|
-
matchingOption.focus();
|
|
135
|
-
}
|
|
136
|
-
}, [open, refs.floating, accessibilityRoles === null || accessibilityRoles === void 0 ? void 0 : accessibilityRoles.option]);
|
|
109
|
+
const optionRole = (_accessibilityRoles$o = accessibilityRoles === null || accessibilityRoles === void 0 ? void 0 : accessibilityRoles.option) !== null && _accessibilityRoles$o !== void 0 ? _accessibilityRoles$o : 'option';
|
|
110
|
+
const {
|
|
111
|
+
onControlKeyDown
|
|
112
|
+
} = useTypeahead({
|
|
113
|
+
areItemsVisible: open,
|
|
114
|
+
setAreItemsVisible: setOpen,
|
|
115
|
+
triggerRef: refs.reference,
|
|
116
|
+
itemsContainerRef: refs.floating,
|
|
117
|
+
itemRole: optionRole,
|
|
118
|
+
disabled,
|
|
119
|
+
readOnly
|
|
120
|
+
});
|
|
137
121
|
const rootStyles = useMemo(() => _objectSpread(_objectSpread({}, style), styles === null || styles === void 0 ? void 0 : styles.root), [style, styles === null || styles === void 0 ? void 0 : styles.root]);
|
|
138
122
|
const controlStyles = useMemo(() => ({
|
|
139
123
|
controlStartNode: styles === null || styles === void 0 ? void 0 : styles.controlStartNode,
|
|
@@ -212,7 +196,7 @@ const SelectBase = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_props, ref) => {
|
|
|
212
196
|
labelVariant: labelVariant,
|
|
213
197
|
maxSelectedOptionsToShow: maxSelectedOptionsToShow,
|
|
214
198
|
onChange: onChange,
|
|
215
|
-
onKeyDown:
|
|
199
|
+
onKeyDown: onControlKeyDown,
|
|
216
200
|
open: open,
|
|
217
201
|
options: options,
|
|
218
202
|
placeholder: placeholder,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { getBrowserGlobals } from '../../utils/browser';
|
|
3
|
+
|
|
4
|
+
// ~500ms buffer reset, matching native `<select>`.
|
|
5
|
+
export const TYPEAHEAD_RESET_MS = 500;
|
|
6
|
+
const printableTypeaheadKeyRegex = /^[a-z0-9]$/i;
|
|
7
|
+
export function isPrintableTypeaheadKey(key) {
|
|
8
|
+
return printableTypeaheadKeyRegex.test(key);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Union accepts both React's synthetic event (control handler) and the native event (window listener).
|
|
12
|
+
// Bare printable key, no modifier (shared by closed and open paths).
|
|
13
|
+
export function isTypeaheadKeyEvent(event) {
|
|
14
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return false;
|
|
15
|
+
return isPrintableTypeaheadKey(event.key);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Strip leading non-alphanumerics (e.g. icon/checkbox glyphs) so prefixes match.
|
|
19
|
+
export function normalizeOptionText(text) {
|
|
20
|
+
return (text !== null && text !== void 0 ? text : '').toLowerCase().replace(/^[^a-z0-9]+/, '');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Native `<select>` semantics: a single (or repeated) char cycles options by first letter;
|
|
24
|
+
// a multi-char buffer keeps the focused option while it still prefix-matches.
|
|
25
|
+
export function getTypeaheadMatchIndex(labels, search, currentIndex) {
|
|
26
|
+
const total = labels.length;
|
|
27
|
+
if (!search || total === 0) return -1;
|
|
28
|
+
const isRepeatedChar = search.length > 1 && [...search].every(char => char === search[0]);
|
|
29
|
+
const query = isRepeatedChar ? search[0] : search;
|
|
30
|
+
const startOffset = query.length === 1 ? 1 : 0;
|
|
31
|
+
const startFrom = currentIndex < 0 ? 0 : currentIndex + startOffset;
|
|
32
|
+
for (let i = 0; i < total; i++) {
|
|
33
|
+
const index = (startFrom + i) % total;
|
|
34
|
+
if (labels[index].startsWith(query)) return index;
|
|
35
|
+
}
|
|
36
|
+
return -1;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Native `<select>`-style typeahead for the Select (Alpha). Printable keys build a short-lived
|
|
40
|
+
* buffer that focuses the matching option, both while closed (type-to-open) and open.
|
|
41
|
+
*/
|
|
42
|
+
export function useTypeahead(_ref) {
|
|
43
|
+
let {
|
|
44
|
+
areItemsVisible,
|
|
45
|
+
setAreItemsVisible,
|
|
46
|
+
triggerRef,
|
|
47
|
+
itemsContainerRef,
|
|
48
|
+
itemRole,
|
|
49
|
+
disabled,
|
|
50
|
+
readOnly
|
|
51
|
+
} = _ref;
|
|
52
|
+
const typeaheadBufferRef = useRef('');
|
|
53
|
+
const typeaheadResetTimeoutRef = useRef(null);
|
|
54
|
+
const pendingTypeaheadRef = useRef(false);
|
|
55
|
+
const appendToTypeaheadBuffer = useCallback(key => {
|
|
56
|
+
typeaheadBufferRef.current += key.toLowerCase();
|
|
57
|
+
if (typeaheadResetTimeoutRef.current) clearTimeout(typeaheadResetTimeoutRef.current);
|
|
58
|
+
typeaheadResetTimeoutRef.current = setTimeout(() => {
|
|
59
|
+
typeaheadBufferRef.current = '';
|
|
60
|
+
}, TYPEAHEAD_RESET_MS);
|
|
61
|
+
}, []);
|
|
62
|
+
const focusTypeaheadMatch = useCallback(() => {
|
|
63
|
+
var _getBrowserGlobals;
|
|
64
|
+
const search = typeaheadBufferRef.current;
|
|
65
|
+
if (!search) return;
|
|
66
|
+
const containerEl = itemsContainerRef.current;
|
|
67
|
+
if (!containerEl) return;
|
|
68
|
+
const optionElements = Array.from(containerEl.querySelectorAll("[role=\"".concat(itemRole, "\"]"))).filter(option => !option.disabled && option.getAttribute('aria-disabled') !== 'true');
|
|
69
|
+
if (optionElements.length === 0) return;
|
|
70
|
+
const labels = optionElements.map(option => normalizeOptionText(option.textContent));
|
|
71
|
+
const activeElement = (_getBrowserGlobals = getBrowserGlobals()) === null || _getBrowserGlobals === void 0 ? void 0 : _getBrowserGlobals.document.activeElement;
|
|
72
|
+
const currentIndex = activeElement ? optionElements.indexOf(activeElement) : -1;
|
|
73
|
+
const matchIndex = getTypeaheadMatchIndex(labels, search, currentIndex);
|
|
74
|
+
if (matchIndex >= 0) optionElements[matchIndex].focus();
|
|
75
|
+
}, [itemsContainerRef, itemRole]);
|
|
76
|
+
const onControlKeyDown = useCallback(event => {
|
|
77
|
+
// When items are visible, the window listener owns typeahead.
|
|
78
|
+
if (disabled || readOnly || areItemsVisible || !isTypeaheadKeyEvent(event)) return;
|
|
79
|
+
appendToTypeaheadBuffer(event.key);
|
|
80
|
+
pendingTypeaheadRef.current = true;
|
|
81
|
+
setAreItemsVisible(true);
|
|
82
|
+
}, [disabled, readOnly, areItemsVisible, setAreItemsVisible, appendToTypeaheadBuffer]);
|
|
83
|
+
|
|
84
|
+
// After a type-to-open, focus the match once the dropdown (and its options) have rendered.
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!areItemsVisible || !pendingTypeaheadRef.current) return;
|
|
87
|
+
pendingTypeaheadRef.current = false;
|
|
88
|
+
focusTypeaheadMatch();
|
|
89
|
+
}, [areItemsVisible, focusTypeaheadMatch]);
|
|
90
|
+
|
|
91
|
+
// Window listener needed: focus moves into the portaled dropdown, past the control's handler.
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!areItemsVisible || disabled || readOnly) return;
|
|
94
|
+
const globals = getBrowserGlobals();
|
|
95
|
+
if (!globals) return;
|
|
96
|
+
const {
|
|
97
|
+
window: browserWindow,
|
|
98
|
+
document: browserDocument
|
|
99
|
+
} = globals;
|
|
100
|
+
const handleWindowKeyDown = event => {
|
|
101
|
+
if (!isTypeaheadKeyEvent(event)) return;
|
|
102
|
+
const triggerElement = triggerRef.current;
|
|
103
|
+
const containerElement = itemsContainerRef.current;
|
|
104
|
+
const activeElement = browserDocument.activeElement;
|
|
105
|
+
const withinSelect = !!triggerElement && triggerElement.contains(activeElement) || !!containerElement && containerElement.contains(activeElement);
|
|
106
|
+
if (!withinSelect) return;
|
|
107
|
+
appendToTypeaheadBuffer(event.key);
|
|
108
|
+
focusTypeaheadMatch();
|
|
109
|
+
};
|
|
110
|
+
browserWindow.addEventListener('keydown', handleWindowKeyDown);
|
|
111
|
+
// A reset timeout is only ever pending during an open interaction, so clearing it here also
|
|
112
|
+
// covers unmount (no dedicated cleanup-only effect needed).
|
|
113
|
+
return () => {
|
|
114
|
+
browserWindow.removeEventListener('keydown', handleWindowKeyDown);
|
|
115
|
+
if (typeaheadResetTimeoutRef.current) clearTimeout(typeaheadResetTimeoutRef.current);
|
|
116
|
+
};
|
|
117
|
+
}, [areItemsVisible, disabled, readOnly, triggerRef, itemsContainerRef, appendToTypeaheadBuffer, focusTypeaheadMatch]);
|
|
118
|
+
return {
|
|
119
|
+
onControlKeyDown
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const _excluded = ["color", "
|
|
1
|
+
const _excluded = ["color", "errorIconAccessibilityLabel", "errorIconTestID", "styles", "classNames"];
|
|
2
2
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
3
3
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
4
4
|
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
|
|
@@ -6,60 +6,64 @@ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol"
|
|
|
6
6
|
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
7
7
|
function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }
|
|
8
8
|
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
|
|
9
|
-
import React, { memo } from 'react';
|
|
9
|
+
import React, { memo, useMemo } from 'react';
|
|
10
10
|
import { cx } from '../cx';
|
|
11
11
|
import { Icon } from '../icons/Icon';
|
|
12
|
-
import {
|
|
12
|
+
import { HStack } from '../layout/HStack';
|
|
13
13
|
import { Text } from '../typography/Text';
|
|
14
14
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
15
|
-
const iconCss = "iconCss-i1ej5caw";
|
|
16
15
|
export const HelperText = /*#__PURE__*/memo(function HelperTex(_ref) {
|
|
17
16
|
let {
|
|
18
17
|
color,
|
|
19
|
-
id,
|
|
20
18
|
errorIconAccessibilityLabel,
|
|
21
19
|
errorIconTestID,
|
|
22
|
-
children,
|
|
23
|
-
dangerouslySetColor,
|
|
24
|
-
textAlign = 'start',
|
|
25
|
-
style,
|
|
26
20
|
styles,
|
|
27
|
-
className,
|
|
28
21
|
classNames
|
|
29
22
|
} = _ref,
|
|
30
23
|
props = _objectWithoutProperties(_ref, _excluded);
|
|
31
|
-
const rootStyle = _objectSpread(_objectSpread({}, style), styles === null || styles === void 0 ? void 0 : styles.root);
|
|
24
|
+
const rootStyle = _objectSpread(_objectSpread({}, props.style), styles === null || styles === void 0 ? void 0 : styles.root);
|
|
32
25
|
// TODO: when we actually remove dangerouslySetColor:
|
|
33
26
|
// when migrating from dangerouslySetColor to style.color,
|
|
34
27
|
// root style/className color will not automatically style the error icon like dangerouslySetColor.
|
|
35
28
|
// Consumers must set both styles.root and styles.icon (or classNames equivalents).
|
|
36
29
|
// We need to have a migrator handle this or document in future migration guide.
|
|
37
|
-
const iconStyle =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
children: [color === 'fgNegative' && /*#__PURE__*/_jsx(Box, {
|
|
49
|
-
as: "span",
|
|
50
|
-
className: iconCss,
|
|
51
|
-
children: /*#__PURE__*/_jsx(Icon, {
|
|
30
|
+
const iconStyle = useMemo(() => _objectSpread({
|
|
31
|
+
// Sit on the first line of label2, matching nested-text alignment.
|
|
32
|
+
marginTop: 'calc((var(--lineHeight-label2) - var(--iconSize-xs)) / 2)'
|
|
33
|
+
}, styles === null || styles === void 0 ? void 0 : styles.icon), [styles === null || styles === void 0 ? void 0 : styles.icon]);
|
|
34
|
+
const justifyContent = props.textAlign === 'end' ? 'flex-end' : props.textAlign === 'center' ? 'center' : 'flex-start';
|
|
35
|
+
if (color === 'fgNegative') {
|
|
36
|
+
return /*#__PURE__*/_jsxs(HStack, {
|
|
37
|
+
alignItems: "flex-start",
|
|
38
|
+
gap: 0.5,
|
|
39
|
+
justifyContent: justifyContent,
|
|
40
|
+
children: [/*#__PURE__*/_jsx(Icon, {
|
|
52
41
|
active: true,
|
|
53
42
|
accessibilityLabel: errorIconAccessibilityLabel,
|
|
54
43
|
className: classNames === null || classNames === void 0 ? void 0 : classNames.icon,
|
|
55
44
|
color: "fgNegative",
|
|
56
|
-
dangerouslySetColor: dangerouslySetColor,
|
|
45
|
+
dangerouslySetColor: props.dangerouslySetColor,
|
|
57
46
|
name: "info",
|
|
58
47
|
size: "xs",
|
|
59
48
|
style: iconStyle,
|
|
60
49
|
testID: errorIconTestID
|
|
61
|
-
})
|
|
62
|
-
|
|
50
|
+
}), /*#__PURE__*/_jsx(Text, _objectSpread(_objectSpread({
|
|
51
|
+
color: color,
|
|
52
|
+
display: "block",
|
|
53
|
+
font: "label2"
|
|
54
|
+
}, props), {}, {
|
|
55
|
+
className: cx(props.className, classNames === null || classNames === void 0 ? void 0 : classNames.root),
|
|
56
|
+
flexShrink: 1,
|
|
57
|
+
style: rootStyle
|
|
58
|
+
}))]
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return /*#__PURE__*/_jsx(Text, _objectSpread(_objectSpread({
|
|
62
|
+
color: color,
|
|
63
|
+
display: "block",
|
|
64
|
+
font: "label2"
|
|
65
|
+
}, props), {}, {
|
|
66
|
+
className: cx(props.className, classNames === null || classNames === void 0 ? void 0 : classNames.root),
|
|
67
|
+
style: rootStyle
|
|
63
68
|
}));
|
|
64
|
-
});
|
|
65
|
-
import "./HelperText.css";
|
|
69
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coinbase/cds-web",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.27.0",
|
|
4
4
|
"description": "Coinbase Design System - Web",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -227,9 +227,9 @@
|
|
|
227
227
|
"react-dom": "^18.0.0 || ~19.1.2"
|
|
228
228
|
},
|
|
229
229
|
"dependencies": {
|
|
230
|
-
"@coinbase/cds-common": "^9.
|
|
231
|
-
"@coinbase/cds-icons": "^5.
|
|
232
|
-
"@coinbase/cds-illustrations": "^4.48.
|
|
230
|
+
"@coinbase/cds-common": "^9.27.0",
|
|
231
|
+
"@coinbase/cds-icons": "^5.25.0",
|
|
232
|
+
"@coinbase/cds-illustrations": "^4.48.1",
|
|
233
233
|
"@coinbase/cds-lottie-files": "^3.3.4",
|
|
234
234
|
"@coinbase/cds-utils": "^2.3.5",
|
|
235
235
|
"@floating-ui/react-dom": "^2.1.1",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@layer cds{.iconCss-i1ej5caw{display:inline-block;padding-inline-end:var(--space-0_5);}}
|