@astryxdesign/core 0.4.2 → 0.4.3-canary.0a9618e
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 +45 -0
- package/README.md +79 -45
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +3 -19
- package/dist/Banner/Banner.d.ts +9 -2
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +84 -33
- package/dist/Banner/index.d.ts +14 -12
- package/dist/Banner/index.d.ts.map +1 -1
- package/dist/Banner/index.js +10 -8
- package/dist/BottomSheet/BottomSheet.d.ts +66 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheet.js +381 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts +57 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetPanel.js +395 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +61 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.js +410 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts +21 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.js +13 -0
- package/dist/BottomSheet/index.d.ts +11 -0
- package/dist/BottomSheet/index.d.ts.map +1 -0
- package/dist/BottomSheet/index.js +12 -0
- package/dist/BottomSheet/snapOffsets.d.ts +72 -0
- package/dist/BottomSheet/snapOffsets.d.ts.map +1 -0
- package/dist/BottomSheet/snapOffsets.js +130 -0
- package/dist/BottomSheet/useMobileKeyboard.d.ts +25 -0
- package/dist/BottomSheet/useMobileKeyboard.d.ts.map +1 -0
- package/dist/BottomSheet/useMobileKeyboard.js +384 -0
- package/dist/BottomSheet/useSheetGestures.d.ts +170 -0
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
- package/dist/BottomSheet/useSheetGestures.js +881 -0
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +3 -4
- package/dist/Chat/ChatTokenizedText.js +1 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts +38 -4
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +98 -31
- package/dist/ComplexSelector/index.d.ts +2 -2
- package/dist/ComplexSelector/index.d.ts.map +1 -1
- package/dist/ComplexSelector/index.js +1 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +4 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +9 -1
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +31 -1
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +2 -1
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +55 -12
- package/dist/MobileNav/MobileNav.d.ts +12 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +102 -30
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +10 -1
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +9 -1
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +4 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
- package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
- package/dist/PowerSearch/formatFilterValue.js +2 -4
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +10 -1
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +5 -1
- package/dist/Table/columnUtils.d.ts.map +1 -1
- package/dist/Table/columnUtils.js +4 -1
- package/dist/TextArea/TextArea.d.ts +6 -3
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +17 -6
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +8 -1
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +4 -1
- package/dist/TreeList/TreeList.js +2 -1
- package/dist/Typeahead/BaseTypeahead.js +1 -1
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +10 -1
- package/dist/astryx.css +25 -2
- package/dist/hooks/index.d.ts +8 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +8 -1
- package/dist/hooks/useFocusTrap.d.ts +0 -11
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +1 -11
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/defineTheme.d.ts +30 -7
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +36 -47
- package/dist/theme/expandColorScale.d.ts +17 -2
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +63 -33
- package/dist/theme/mergeComponents.d.ts +20 -0
- package/dist/theme/mergeComponents.d.ts.map +1 -0
- package/dist/theme/mergeComponents.js +56 -0
- package/dist/theme/onMediaTokens.d.ts +6 -1
- package/dist/theme/onMediaTokens.d.ts.map +1 -1
- package/dist/theme/onMediaTokens.js +11 -3
- package/dist/utils/characters.d.ts +27 -0
- package/dist/utils/characters.d.ts.map +1 -0
- package/dist/utils/characters.js +83 -0
- package/dist/utils/ime.d.ts +5 -0
- package/dist/utils/ime.d.ts.map +1 -0
- package/dist/utils/ime.js +63 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -1
- package/package.json +9 -8
- package/src/Avatar/Avatar.test.tsx +6 -1
- package/src/Avatar/Avatar.tsx +3 -21
- package/src/Banner/Banner.doc.mjs +12 -10
- package/src/Banner/Banner.test.tsx +108 -0
- package/src/Banner/Banner.tsx +124 -38
- package/src/Banner/index.ts +15 -13
- package/src/BottomSheet/BottomSheet.doc.mjs +266 -0
- package/src/BottomSheet/BottomSheet.test.tsx +1865 -0
- package/src/BottomSheet/BottomSheet.tsx +539 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +240 -0
- package/src/BottomSheet/BottomSheetPanel.tsx +542 -0
- package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +219 -0
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +850 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +628 -0
- package/src/BottomSheet/BottomSheetSwitcherContext.ts +51 -0
- package/src/BottomSheet/index.ts +15 -0
- package/src/BottomSheet/snapOffsets.test.ts +187 -0
- package/src/BottomSheet/snapOffsets.ts +159 -0
- package/src/BottomSheet/useMobileKeyboard.ts +566 -0
- package/src/BottomSheet/useSheetGestures.test.ts +833 -0
- package/src/BottomSheet/useSheetGestures.ts +1333 -0
- package/src/Chat/ChatComposerInput.tsx +3 -4
- package/src/Chat/ChatTokenizedText.tsx +1 -1
- package/src/ComplexSelector/ComplexSelector.doc.mjs +55 -6
- package/src/ComplexSelector/ComplexSelector.test.tsx +197 -6
- package/src/ComplexSelector/ComplexSelector.tsx +156 -28
- package/src/ComplexSelector/index.ts +3 -1
- package/src/ContextMenu/ContextMenu.tsx +4 -2
- package/src/DateInput/DateInput.test.tsx +21 -0
- package/src/DateInput/DateInput.tsx +9 -1
- package/src/DateTimeInput/DateTimeInput.doc.mjs +4 -0
- package/src/DateTimeInput/DateTimeInput.test.tsx +137 -0
- package/src/DateTimeInput/DateTimeInput.tsx +39 -2
- package/src/Dialog/Dialog.tsx +2 -1
- package/src/Markdown/parser.test.ts +53 -0
- package/src/Markdown/parser.ts +53 -12
- package/src/MobileNav/MobileNav.tsx +128 -37
- package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +426 -0
- package/src/MobileNav/MobileNavCloseTiming.test.ts +58 -0
- package/src/MobileNav/MobileNavCloseVisibility.test.tsx +172 -0
- package/src/MultiSelector/MultiSelector.test.tsx +31 -0
- package/src/MultiSelector/MultiSelector.tsx +10 -1
- package/src/NumberInput/NumberInput.test.tsx +18 -0
- package/src/NumberInput/NumberInput.tsx +9 -1
- package/src/PowerSearch/PowerSearch.test.tsx +48 -3
- package/src/PowerSearch/PowerSearch.tsx +4 -1
- package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
- package/src/PowerSearch/formatFilterValue.test.ts +22 -0
- package/src/PowerSearch/formatFilterValue.ts +2 -4
- package/src/Selector/Selector.test.tsx +33 -0
- package/src/Selector/Selector.tsx +10 -1
- package/src/Switch/Switch.test.tsx +16 -0
- package/src/Switch/Switch.tsx +7 -0
- package/src/Table/Table.test.tsx +6 -0
- package/src/Table/columnUtils.ts +3 -1
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextArea/TextArea.test.tsx +72 -0
- package/src/TextArea/TextArea.tsx +26 -8
- package/src/TimeInput/TimeInput.test.tsx +23 -0
- package/src/TimeInput/TimeInput.tsx +8 -0
- package/src/Tooltip/useTooltip.tsx +4 -1
- package/src/TreeList/TreeList.doc.mjs +2 -2
- package/src/TreeList/TreeList.tsx +1 -1
- package/src/Typeahead/BaseTypeahead.tsx +1 -1
- package/src/Typeahead/Typeahead.test.tsx +35 -0
- package/src/Typeahead/Typeahead.tsx +10 -1
- package/src/__tests__/stubMatchMedia.test.ts +79 -0
- package/src/__tests__/stubMatchMedia.ts +95 -0
- package/src/hooks/index.ts +9 -5
- package/src/hooks/useFocusTrap.ts +1 -14
- package/src/index.ts +1 -0
- package/src/theme/defineTheme.test.ts +127 -0
- package/src/theme/defineTheme.ts +70 -55
- package/src/theme/expandColorScale.test.ts +138 -0
- package/src/theme/expandColorScale.ts +89 -44
- package/src/theme/extensibleAxes.test.ts +365 -0
- package/src/theme/mergeComponents.ts +59 -0
- package/src/theme/onMediaTokens.ts +9 -2
- package/src/utils/characters.test.ts +141 -0
- package/src/utils/characters.ts +88 -0
- package/src/utils/ime.test.ts +32 -0
- package/src/utils/ime.ts +69 -0
- package/src/utils/index.ts +4 -0
- package/dist/astryx.umd.js +0 -147
- package/dist/astryx.umd.js.map +0 -7
|
@@ -71,7 +71,7 @@ import {
|
|
|
71
71
|
getSelectableOptions,
|
|
72
72
|
} from '../Selector/utils';
|
|
73
73
|
import {useMultiCombobox} from './hooks';
|
|
74
|
-
import {getInputARIA, mergeProps} from '../utils';
|
|
74
|
+
import {getInputARIA, isImeKeyEvent, mergeProps} from '../utils';
|
|
75
75
|
import {useAnnounce} from '../hooks/useAnnounce';
|
|
76
76
|
import type {BaseProps} from '../BaseProps';
|
|
77
77
|
import type {SizeValue} from '../utils/types';
|
|
@@ -1182,6 +1182,15 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1182
1182
|
}
|
|
1183
1183
|
}}
|
|
1184
1184
|
onKeyDown={e => {
|
|
1185
|
+
// An in-progress IME composition uses these same keys (Enter to
|
|
1186
|
+
// commit the candidate, Escape/Arrows to navigate the candidate
|
|
1187
|
+
// window); the composing keydown fires before compositionend, so
|
|
1188
|
+
// without this guard a Korean/Japanese/Chinese user committing a
|
|
1189
|
+
// syllable with Enter would instead toggle the highlighted option.
|
|
1190
|
+
// See utils/ime.ts.
|
|
1191
|
+
if (isImeKeyEvent(e.nativeEvent)) {
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1185
1194
|
// Arrow keys navigate options; Enter toggles; Escape closes.
|
|
1186
1195
|
// Space and Home/End are left to the input (type a space / move
|
|
1187
1196
|
// the caret) per the APG editable combobox; PageUp/PageDown are
|
|
@@ -1340,6 +1340,24 @@ describe('NumberInput stepping', () => {
|
|
|
1340
1340
|
expect(onChange).toHaveBeenLastCalledWith(4);
|
|
1341
1341
|
});
|
|
1342
1342
|
|
|
1343
|
+
it('does not step or commit on a composing keydown (IME)', () => {
|
|
1344
|
+
const onChange = vi.fn();
|
|
1345
|
+
render(<NumberInput label="Amount" value={5} onChange={onChange} />);
|
|
1346
|
+
const input = screen.getByRole('spinbutton');
|
|
1347
|
+
|
|
1348
|
+
// The field is type="text", so an IME composes into it. Enter commits the
|
|
1349
|
+
// candidate and the arrows walk the candidate window; neither should reach
|
|
1350
|
+
// the stepping or commit paths. Both signals a browser may report.
|
|
1351
|
+
fireEvent.keyDown(input, {key: 'ArrowUp', isComposing: true});
|
|
1352
|
+
fireEvent.keyDown(input, {key: 'ArrowDown', keyCode: 229});
|
|
1353
|
+
fireEvent.keyDown(input, {key: 'Enter', isComposing: true});
|
|
1354
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1355
|
+
|
|
1356
|
+
// A real, non-composing ArrowUp still steps.
|
|
1357
|
+
fireEvent.keyDown(input, {key: 'ArrowUp'});
|
|
1358
|
+
expect(onChange).toHaveBeenLastCalledWith(6);
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1343
1361
|
it('lets onKeyDown cancel keyboard stepping', () => {
|
|
1344
1362
|
const onChange = vi.fn();
|
|
1345
1363
|
const onKeyDown = vi.fn((event: React.KeyboardEvent<HTMLInputElement>) =>
|
|
@@ -169,7 +169,7 @@ export type {
|
|
|
169
169
|
InputStatus as NumberInputStatus,
|
|
170
170
|
InputStatusType as NumberInputStatusType,
|
|
171
171
|
} from '../Field';
|
|
172
|
-
import {mergeProps, mergeRefs} from '../utils';
|
|
172
|
+
import {isImeKeyEvent, mergeProps, mergeRefs} from '../utils';
|
|
173
173
|
import type {BaseProps} from '../BaseProps';
|
|
174
174
|
import type {SizeValue} from '../utils/types';
|
|
175
175
|
import {themeProps} from '../utils/themeProps';
|
|
@@ -746,6 +746,14 @@ export function NumberInput({
|
|
|
746
746
|
// Handle keyboard events
|
|
747
747
|
const handleKeyDown = useCallback(
|
|
748
748
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
749
|
+
// The field is type="text" for formatted display, so an IME can compose
|
|
750
|
+
// into it: Enter commits the candidate and the arrows walk the candidate
|
|
751
|
+
// window. The composing keydown fires before compositionend, so without
|
|
752
|
+
// this guard those keystrokes would commit or step the value instead.
|
|
753
|
+
// See utils/ime.ts.
|
|
754
|
+
if (isImeKeyEvent(e.nativeEvent)) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
749
757
|
const hasModifier = e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;
|
|
750
758
|
if (!hasModifier && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
|
751
759
|
onKeyDown?.(e);
|
|
@@ -158,6 +158,41 @@ describe('PowerSearch', () => {
|
|
|
158
158
|
expect(screen.getByRole('combobox')).toHaveFocus();
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
+
describe('token value truncation (#4759)', () => {
|
|
162
|
+
it('truncates token values by characters, not code units', () => {
|
|
163
|
+
const truncConfig: PowerSearchConfig = {
|
|
164
|
+
name: 'trunc',
|
|
165
|
+
fields: [
|
|
166
|
+
{
|
|
167
|
+
key: 'status',
|
|
168
|
+
label: 'Status',
|
|
169
|
+
operators: [{key: 'is', label: 'is', value: {type: 'string'}}],
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
// 14 emoji = 28 code units but 14 user-perceived characters.
|
|
174
|
+
// adjustedMaxLength = max(15 - 'Status'.length - 'is'.length, 10) = 10,
|
|
175
|
+
// so the value truncates only past 13 characters, cutting at 10.
|
|
176
|
+
render(
|
|
177
|
+
<PowerSearch
|
|
178
|
+
config={truncConfig}
|
|
179
|
+
filters={[
|
|
180
|
+
{
|
|
181
|
+
field: 'status',
|
|
182
|
+
operator: 'is',
|
|
183
|
+
value: {type: 'string', value: '\u{1F600}'.repeat(14)},
|
|
184
|
+
},
|
|
185
|
+
]}
|
|
186
|
+
onChange={() => {}}
|
|
187
|
+
maxTokenLength={15}
|
|
188
|
+
/>,
|
|
189
|
+
);
|
|
190
|
+
expect(
|
|
191
|
+
screen.getByText('\u{1F600}'.repeat(10) + '...'),
|
|
192
|
+
).toBeInTheDocument();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
161
196
|
describe('startIcon', () => {
|
|
162
197
|
it('does not render a start icon when omitted', () => {
|
|
163
198
|
render(<PowerSearch config={config} filters={[]} onChange={() => {}} />);
|
|
@@ -425,11 +460,15 @@ describe('PowerSearch', () => {
|
|
|
425
460
|
});
|
|
426
461
|
});
|
|
427
462
|
|
|
428
|
-
|
|
429
463
|
describe('PowerSearch statusVariant forwarding', () => {
|
|
430
464
|
it('defaults to attached (status renders with data-variant="attached")', () => {
|
|
431
465
|
const {container} = render(
|
|
432
|
-
<PowerSearch
|
|
466
|
+
<PowerSearch
|
|
467
|
+
config={config}
|
|
468
|
+
filters={[]}
|
|
469
|
+
onChange={() => {}}
|
|
470
|
+
status={{type: 'error', message: 'Required'}}
|
|
471
|
+
/>,
|
|
433
472
|
);
|
|
434
473
|
expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
|
|
435
474
|
'data-variant',
|
|
@@ -439,7 +478,13 @@ describe('PowerSearch statusVariant forwarding', () => {
|
|
|
439
478
|
|
|
440
479
|
it('forwards statusVariant="detached" to the underlying Field status', () => {
|
|
441
480
|
const {container} = render(
|
|
442
|
-
<PowerSearch
|
|
481
|
+
<PowerSearch
|
|
482
|
+
config={config}
|
|
483
|
+
filters={[]}
|
|
484
|
+
onChange={() => {}}
|
|
485
|
+
status={{type: 'error', message: 'Required'}}
|
|
486
|
+
statusVariant="detached"
|
|
487
|
+
/>,
|
|
443
488
|
);
|
|
444
489
|
expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
|
|
445
490
|
'data-variant',
|
|
@@ -54,6 +54,7 @@ import {formatFilterValue} from './formatFilterValue';
|
|
|
54
54
|
import {PowerSearchEditPopover} from './PowerSearchEditPopover';
|
|
55
55
|
import {resolveOperatorLabel} from './resolveOperatorLabel';
|
|
56
56
|
import {themeProps} from '../utils/themeProps';
|
|
57
|
+
import {truncateCharacters} from '../utils/characters';
|
|
57
58
|
import {useTranslator} from '../i18n';
|
|
58
59
|
import type {
|
|
59
60
|
PowerSearchConfig,
|
|
@@ -117,7 +118,9 @@ const resultCountStyles = stylex.create({
|
|
|
117
118
|
});
|
|
118
119
|
|
|
119
120
|
function truncateString(value: string, limit: number): string {
|
|
120
|
-
|
|
121
|
+
// Same semantics as before — strings within limit + 3 pass through, longer
|
|
122
|
+
// ones cut to limit + '...' — but counted in characters, not code units.
|
|
123
|
+
return truncateCharacters(value, limit + 3, '...');
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
function getEnumLabel(values: ReadonlyArray<EnumItem>, value: string): string {
|
|
@@ -20,7 +20,7 @@ import {HStack, VStack} from '../Stack';
|
|
|
20
20
|
import {Icon} from '../Icon';
|
|
21
21
|
import {TreeList, type TreeListItemData} from '../TreeList';
|
|
22
22
|
import {useTranslator} from '../i18n';
|
|
23
|
-
import {isImeKeyEvent} from '../
|
|
23
|
+
import {isImeKeyEvent} from '../utils/ime';
|
|
24
24
|
import {spacingVars, typeScaleVars} from '../theme/tokens.stylex';
|
|
25
25
|
import {PowerSearchValueEditor} from './PowerSearchValueEditor';
|
|
26
26
|
import {resolveOperatorLabel} from './resolveOperatorLabel';
|
|
@@ -62,6 +62,28 @@ describe('formatFilterValue', () => {
|
|
|
62
62
|
'abcde',
|
|
63
63
|
);
|
|
64
64
|
});
|
|
65
|
+
|
|
66
|
+
it('truncates by characters, never splitting an emoji (#4759)', () => {
|
|
67
|
+
// Four surrogate-pair emoji: 8 code units, 4 user-perceived characters.
|
|
68
|
+
// The exact-string assertion also proves no surrogate pair was split.
|
|
69
|
+
const out = fmt(
|
|
70
|
+
{type: 'string'},
|
|
71
|
+
{type: 'string', value: '\u{1F600}'.repeat(4)},
|
|
72
|
+
3,
|
|
73
|
+
);
|
|
74
|
+
expect(out).toBe('\u{1F600}'.repeat(2) + ELLIPSIS);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('keeps multi-unit characters within maxLength intact (#4759)', () => {
|
|
78
|
+
// Three emoji are 6 code units but only 3 user-perceived characters.
|
|
79
|
+
expect(
|
|
80
|
+
fmt(
|
|
81
|
+
{type: 'string'},
|
|
82
|
+
{type: 'string', value: '\u{1F600}'.repeat(3)},
|
|
83
|
+
3,
|
|
84
|
+
),
|
|
85
|
+
).toBe('\u{1F600}'.repeat(3));
|
|
86
|
+
});
|
|
65
87
|
});
|
|
66
88
|
|
|
67
89
|
describe('integer / float', () => {
|
|
@@ -13,12 +13,10 @@
|
|
|
13
13
|
import type {OperatorValue, FilterValue, EnumItem} from './types';
|
|
14
14
|
import type {InternalConfig} from './useInternalConfig';
|
|
15
15
|
import type {TranslatorFn} from '../i18n';
|
|
16
|
+
import {truncateCharacters} from '../utils/characters';
|
|
16
17
|
|
|
17
18
|
function truncate(str: string, maxLength: number): string {
|
|
18
|
-
|
|
19
|
-
return str;
|
|
20
|
-
}
|
|
21
|
-
return str.slice(0, maxLength - 1) + '\u2026';
|
|
19
|
+
return truncateCharacters(str, maxLength);
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
function formatEnumLabel(
|
|
@@ -1000,6 +1000,39 @@ describe('Selector', () => {
|
|
|
1000
1000
|
).toBeInTheDocument();
|
|
1001
1001
|
});
|
|
1002
1002
|
|
|
1003
|
+
it('does not select the highlighted option on a composing Enter (IME)', async () => {
|
|
1004
|
+
const user = userEvent.setup();
|
|
1005
|
+
const onChange = vi.fn();
|
|
1006
|
+
render(
|
|
1007
|
+
<Selector
|
|
1008
|
+
label="Fruit"
|
|
1009
|
+
options={OPTIONS}
|
|
1010
|
+
onChange={onChange}
|
|
1011
|
+
hasSearch
|
|
1012
|
+
/>,
|
|
1013
|
+
);
|
|
1014
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1015
|
+
const search = screen.getByRole('combobox', h);
|
|
1016
|
+
// Filter to Banana and highlight it so an unguarded Enter would commit a
|
|
1017
|
+
// selection.
|
|
1018
|
+
await user.type(search, 'ban');
|
|
1019
|
+
await user.keyboard('{ArrowDown}');
|
|
1020
|
+
expect(search).toHaveAttribute('aria-activedescendant');
|
|
1021
|
+
|
|
1022
|
+
// The browser fires this composing keydown for the Enter that commits an
|
|
1023
|
+
// IME candidate (isComposing: true, or the legacy keyCode 229) before
|
|
1024
|
+
// compositionend writes the syllable. It must NOT be read as "select the
|
|
1025
|
+
// highlighted option".
|
|
1026
|
+
fireEvent.keyDown(search, {key: 'Enter', isComposing: true});
|
|
1027
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1028
|
+
fireEvent.keyDown(search, {key: 'Enter', keyCode: 229});
|
|
1029
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1030
|
+
|
|
1031
|
+
// A real, non-composing Enter still selects the highlighted option.
|
|
1032
|
+
fireEvent.keyDown(search, {key: 'Enter'});
|
|
1033
|
+
expect(onChange).toHaveBeenCalledWith('Banana');
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1003
1036
|
describe('result announcements', () => {
|
|
1004
1037
|
it('announces the match count politely while searching', async () => {
|
|
1005
1038
|
const user = userEvent.setup();
|
|
@@ -72,7 +72,7 @@ import {
|
|
|
72
72
|
import {useCombobox, useSelectedItemOffset} from './hooks';
|
|
73
73
|
import {useTypeahead} from '../hooks/useTypeahead';
|
|
74
74
|
import {SelectorOption} from './SelectorOption';
|
|
75
|
-
import {getInputARIA, mergeProps} from '../utils';
|
|
75
|
+
import {getInputARIA, isImeKeyEvent, mergeProps} from '../utils';
|
|
76
76
|
import {useSize} from '../SizeContext/SizeContext';
|
|
77
77
|
import type {BaseProps} from '../BaseProps';
|
|
78
78
|
import type {SizeValue} from '../utils/types';
|
|
@@ -1055,6 +1055,15 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1055
1055
|
}
|
|
1056
1056
|
}}
|
|
1057
1057
|
onKeyDown={e => {
|
|
1058
|
+
// An in-progress IME composition uses these same keys (Enter to
|
|
1059
|
+
// commit the candidate, Escape/Arrows to navigate the candidate
|
|
1060
|
+
// window); the composing keydown fires before compositionend, so
|
|
1061
|
+
// without this guard a Korean/Japanese/Chinese user committing a
|
|
1062
|
+
// syllable with Enter would instead select the highlighted option.
|
|
1063
|
+
// See utils/ime.ts.
|
|
1064
|
+
if (isImeKeyEvent(e.nativeEvent)) {
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1058
1067
|
// Arrow keys navigate options; Enter selects; Escape closes.
|
|
1059
1068
|
// Home/End are left to the input for caret movement (APG editable
|
|
1060
1069
|
// combobox); PageUp/PageDown are the sanctioned substitute for
|
|
@@ -262,6 +262,22 @@ describe('Switch', () => {
|
|
|
262
262
|
expect(screen.getByLabelText('Toggle row')).toBeInTheDocument();
|
|
263
263
|
});
|
|
264
264
|
|
|
265
|
+
it('drops the label gap when isLabelHidden so the row is only as wide as the track', () => {
|
|
266
|
+
const {rerender} = render(
|
|
267
|
+
<Switch
|
|
268
|
+
label="Toggle row"
|
|
269
|
+
isLabelHidden
|
|
270
|
+
value={false}
|
|
271
|
+
onChange={() => {}}
|
|
272
|
+
/>,
|
|
273
|
+
);
|
|
274
|
+
const row = screen.getByRole('switch').parentElement!.parentElement!;
|
|
275
|
+
expect(getComputedStyle(row).gap).toMatch(/^0(px)?$/);
|
|
276
|
+
|
|
277
|
+
rerender(<Switch label="Toggle row" value={false} onChange={() => {}} />);
|
|
278
|
+
expect(getComputedStyle(row).gap).not.toMatch(/^0(px)?$/);
|
|
279
|
+
});
|
|
280
|
+
|
|
265
281
|
it('keeps description linked via aria-describedby when isLabelHidden', () => {
|
|
266
282
|
render(
|
|
267
283
|
<Switch
|
package/src/Switch/Switch.tsx
CHANGED
|
@@ -135,6 +135,12 @@ const styles = stylex.create({
|
|
|
135
135
|
alignItems: 'center',
|
|
136
136
|
gap: spacingVars['--spacing-2'],
|
|
137
137
|
},
|
|
138
|
+
// A hidden label is sr-only, so its wrapper is a zero-width flex item — the
|
|
139
|
+
// row gap would still be painted beside the track, making the field box
|
|
140
|
+
// wider than the control it contains. Matches CheckboxInput.
|
|
141
|
+
containerLabelHidden: {
|
|
142
|
+
gap: 0,
|
|
143
|
+
},
|
|
138
144
|
containerSpread: {
|
|
139
145
|
justifyContent: 'space-between',
|
|
140
146
|
width: '100%',
|
|
@@ -655,6 +661,7 @@ export function Switch({
|
|
|
655
661
|
}}
|
|
656
662
|
{...stylex.props(
|
|
657
663
|
styles.container,
|
|
664
|
+
isLabelHidden && styles.containerLabelHidden,
|
|
658
665
|
labelSpacing === 'spread' && styles.containerSpread,
|
|
659
666
|
!isDisabled && switchScope,
|
|
660
667
|
)}>
|
package/src/Table/Table.test.tsx
CHANGED
|
@@ -95,6 +95,12 @@ describe('columnUtils', () => {
|
|
|
95
95
|
it('handles single character', () => {
|
|
96
96
|
expect(capitalize('a')).toBe('A');
|
|
97
97
|
});
|
|
98
|
+
|
|
99
|
+
it('uppercases an astral-plane letter without splitting it (#4759)', () => {
|
|
100
|
+
// Deseret 𐐨 (U+10428) uppercases to 𐐀 (U+10400); charAt(0) would grab
|
|
101
|
+
// half the surrogate pair and leave the string unchanged.
|
|
102
|
+
expect(capitalize('\u{10428}pple')).toBe('\u{10400}pple');
|
|
103
|
+
});
|
|
98
104
|
});
|
|
99
105
|
|
|
100
106
|
describe('generateColumns', () => {
|
package/src/Table/columnUtils.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import type {CSSProperties, ReactNode} from 'react';
|
|
15
15
|
import type {TableColumn, ProportionalWidth, PixelWidth} from './types';
|
|
16
|
+
import {firstCharacter} from '../utils/characters';
|
|
16
17
|
|
|
17
18
|
/** Default minimum width (in px) for proportional columns. */
|
|
18
19
|
export const DEFAULT_MIN_COLUMN_WIDTH = 120;
|
|
@@ -168,7 +169,8 @@ export function capitalize(str: string): string {
|
|
|
168
169
|
if (str.length === 0) {
|
|
169
170
|
return str;
|
|
170
171
|
}
|
|
171
|
-
|
|
172
|
+
const first = firstCharacter(str);
|
|
173
|
+
return first.toUpperCase() + str.slice(first.length);
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
/**
|
|
@@ -105,7 +105,7 @@ export const docs = {
|
|
|
105
105
|
name: 'maxLength',
|
|
106
106
|
type: 'number',
|
|
107
107
|
description:
|
|
108
|
-
'Maximum number of characters allowed. When set, a character counter (current/max) is displayed inside the input container, anchored to the bottom-right beneath the text. Does not enforce the limit natively; when exceeded the counter turns red and shows a warning icon (a non-color cue), and screen-reader users hear the remaining/over-limit count announced.',
|
|
108
|
+
'Maximum number of characters allowed, counted as user-perceived characters — an emoji or flag sequence counts as one. When set, a character counter (current/max) is displayed inside the input container, anchored to the bottom-right beneath the text. Does not enforce the limit natively; when exceeded the counter turns red and shows a warning icon (a non-color cue), and screen-reader users hear the remaining/over-limit count announced. Consumers validating the limit should count with characterCount (exported from the package) so enforcement matches the displayed count.',
|
|
109
109
|
},
|
|
110
110
|
{
|
|
111
111
|
name: 'status',
|
|
@@ -504,6 +504,33 @@ describe('TextArea', () => {
|
|
|
504
504
|
expect(screen.getByRole('textbox')).not.toHaveAttribute('maxlength');
|
|
505
505
|
});
|
|
506
506
|
|
|
507
|
+
it('counts user-perceived characters, not code units (#4759)', () => {
|
|
508
|
+
// Two surrogate-pair emoji: 4 code units, but 2 user-perceived characters.
|
|
509
|
+
render(
|
|
510
|
+
<TextArea
|
|
511
|
+
label="Description"
|
|
512
|
+
value={'\u{1F600}\u{1F600}'}
|
|
513
|
+
onChange={() => {}}
|
|
514
|
+
maxLength={5}
|
|
515
|
+
/>,
|
|
516
|
+
);
|
|
517
|
+
expect(screen.getByText('2/5')).toBeInTheDocument();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('measures the over-limit state in characters (#4759)', () => {
|
|
521
|
+
// Three ZWJ family emoji: 33 code units, 3 user-perceived characters.
|
|
522
|
+
const family = '\u{1F468}\u{1F469}\u{1F467}\u{1F466}';
|
|
523
|
+
render(
|
|
524
|
+
<TextArea
|
|
525
|
+
label="Description"
|
|
526
|
+
value={family.repeat(3)}
|
|
527
|
+
onChange={() => {}}
|
|
528
|
+
maxLength={2}
|
|
529
|
+
/>,
|
|
530
|
+
);
|
|
531
|
+
expect(screen.getByText('3/2')).toBeInTheDocument();
|
|
532
|
+
});
|
|
533
|
+
|
|
507
534
|
it('counter updates as user types (controlled)', async () => {
|
|
508
535
|
const user = userEvent.setup();
|
|
509
536
|
function Wrapper() {
|
|
@@ -570,6 +597,51 @@ describe('TextArea', () => {
|
|
|
570
597
|
});
|
|
571
598
|
});
|
|
572
599
|
|
|
600
|
+
it('announces zone transitions using character counts (#4759)', async () => {
|
|
601
|
+
function Wrapper() {
|
|
602
|
+
const [val, setVal] = useState('x'.repeat(7));
|
|
603
|
+
return (
|
|
604
|
+
<TextArea
|
|
605
|
+
label="Description"
|
|
606
|
+
value={val}
|
|
607
|
+
onChange={setVal}
|
|
608
|
+
maxLength={10}
|
|
609
|
+
/>
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
render(<Wrapper />);
|
|
613
|
+
// Appending two emoji makes 9 characters (11 code units): near the limit
|
|
614
|
+
// with 1 remaining. Code-unit counting would call this over the limit
|
|
615
|
+
// and announce assertively instead.
|
|
616
|
+
fireEvent.change(screen.getByRole('textbox'), {
|
|
617
|
+
target: {value: 'x'.repeat(7) + '\u{1F600}\u{1F600}'},
|
|
618
|
+
});
|
|
619
|
+
const politeRegion = () =>
|
|
620
|
+
document.querySelector('[data-astryx-live-region="polite"]');
|
|
621
|
+
const assertiveRegion = () =>
|
|
622
|
+
document.querySelector('[data-astryx-live-region="assertive"]');
|
|
623
|
+
await waitFor(() => {
|
|
624
|
+
expect(politeRegion()).toHaveTextContent('1 character remaining');
|
|
625
|
+
});
|
|
626
|
+
expect(assertiveRegion()).not.toHaveTextContent('over the limit');
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('does not flag over-limit while characters fit, even when code units exceed (#4759)', () => {
|
|
630
|
+
// Three emoji: 6 code units but 3 user-perceived characters — within a
|
|
631
|
+
// maxLength of 4, so no error state anywhere.
|
|
632
|
+
render(
|
|
633
|
+
<TextArea
|
|
634
|
+
label="Description"
|
|
635
|
+
value={'\u{1F600}'.repeat(3)}
|
|
636
|
+
onChange={() => {}}
|
|
637
|
+
maxLength={4}
|
|
638
|
+
/>,
|
|
639
|
+
);
|
|
640
|
+
const counter = screen.getByText('3/4');
|
|
641
|
+
expect(counter.querySelector('svg')).toBeNull();
|
|
642
|
+
expect(screen.getByRole('textbox')).not.toHaveAttribute('aria-invalid');
|
|
643
|
+
});
|
|
644
|
+
|
|
573
645
|
it('shows a non-color over-limit indicator icon when exceeded', () => {
|
|
574
646
|
const {container} = render(
|
|
575
647
|
<TextArea
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
useTransition,
|
|
23
23
|
useRef,
|
|
24
24
|
useCallback,
|
|
25
|
+
useMemo,
|
|
25
26
|
type ChangeEvent,
|
|
26
27
|
type ClipboardEvent,
|
|
27
28
|
type FocusEvent,
|
|
@@ -53,6 +54,7 @@ import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
|
|
|
53
54
|
import {useAnnounce} from '../hooks/useAnnounce';
|
|
54
55
|
import {useSize} from '../SizeContext/SizeContext';
|
|
55
56
|
import {themeProps} from '../utils/themeProps';
|
|
57
|
+
import {characterCount} from '../utils/characters';
|
|
56
58
|
import {useTranslator} from '../i18n';
|
|
57
59
|
|
|
58
60
|
const COUNTER_WARNING_THRESHOLD = 0.8;
|
|
@@ -312,10 +314,13 @@ export interface TextAreaProps extends Omit<
|
|
|
312
314
|
*/
|
|
313
315
|
onPaste?: (e: ClipboardEvent<HTMLTextAreaElement>) => void;
|
|
314
316
|
/**
|
|
315
|
-
* Maximum number of characters allowed
|
|
316
|
-
*
|
|
317
|
+
* Maximum number of characters allowed, counted as user-perceived
|
|
318
|
+
* characters — an emoji or flag sequence counts as one. When set,
|
|
319
|
+
* displays a character counter below the textarea.
|
|
317
320
|
* Does not enforce the limit natively — the counter shows error styling
|
|
318
|
-
* when exceeded, and the consumer can validate via onChange.
|
|
321
|
+
* when exceeded, and the consumer can validate via onChange. Validate with
|
|
322
|
+
* `characterCount` (exported from this package) rather than `value.length`
|
|
323
|
+
* so enforcement matches the displayed count.
|
|
319
324
|
*/
|
|
320
325
|
maxLength?: number;
|
|
321
326
|
/**
|
|
@@ -482,7 +487,11 @@ export function TextArea({
|
|
|
482
487
|
return;
|
|
483
488
|
}
|
|
484
489
|
const newValue = e.target.value;
|
|
485
|
-
announceCounter
|
|
490
|
+
// Guarded here, not just inside announceCounter, so textareas without a
|
|
491
|
+
// maxLength never pay for segmenting the whole value on each keystroke.
|
|
492
|
+
if (maxLength != null) {
|
|
493
|
+
announceCounter(characterCount(newValue));
|
|
494
|
+
}
|
|
486
495
|
onChange?.(newValue, e);
|
|
487
496
|
if (changeAction && !e.defaultPrevented) {
|
|
488
497
|
startTransition(async () => {
|
|
@@ -492,6 +501,15 @@ export function TextArea({
|
|
|
492
501
|
}
|
|
493
502
|
};
|
|
494
503
|
|
|
504
|
+
// Counter semantics count user-perceived characters, so an emoji or flag
|
|
505
|
+
// sequence counts as one character, not its code units.
|
|
506
|
+
// Only measured when a counter exists — segmentation is O(value length),
|
|
507
|
+
// and memoized so re-renders that keep the same value skip it entirely.
|
|
508
|
+
const valueLength = useMemo(
|
|
509
|
+
() => (maxLength != null ? characterCount(optimisticValue) : 0),
|
|
510
|
+
[maxLength, optimisticValue],
|
|
511
|
+
);
|
|
512
|
+
|
|
495
513
|
const effectivelyDisabled = isDisabled || isBusy;
|
|
496
514
|
|
|
497
515
|
// Focus textarea when clicking anywhere on the wrapper (icons, padding, etc.)
|
|
@@ -583,7 +601,7 @@ export function TextArea({
|
|
|
583
601
|
aria-required={isRequired && !isOptional ? 'true' : undefined}
|
|
584
602
|
aria-invalid={
|
|
585
603
|
status?.type === 'error' ||
|
|
586
|
-
(maxLength != null &&
|
|
604
|
+
(maxLength != null && valueLength > maxLength)
|
|
587
605
|
? 'true'
|
|
588
606
|
: undefined
|
|
589
607
|
}
|
|
@@ -614,15 +632,15 @@ export function TextArea({
|
|
|
614
632
|
id={counterID}
|
|
615
633
|
{...stylex.props(
|
|
616
634
|
styles.counter,
|
|
617
|
-
|
|
635
|
+
valueLength > maxLength && styles.counterError,
|
|
618
636
|
)}>
|
|
619
|
-
{
|
|
637
|
+
{valueLength > maxLength && (
|
|
620
638
|
// Non-color cue so the over-limit state isn't conveyed by the red
|
|
621
639
|
// color alone (WCAG 1.4.1). Decorative — the count text and the
|
|
622
640
|
// live-region announcement carry the meaning.
|
|
623
641
|
<Icon icon="warning" size="sm" />
|
|
624
642
|
)}
|
|
625
|
-
{
|
|
643
|
+
{valueLength}/{maxLength}
|
|
626
644
|
</div>
|
|
627
645
|
)}
|
|
628
646
|
</div>
|
|
@@ -42,6 +42,29 @@ describe('TimeInput', () => {
|
|
|
42
42
|
expect(screen.getByPlaceholderText('Pick a time')).toBeInTheDocument();
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
it('does not step the time on a composing ArrowUp/ArrowDown (IME)', () => {
|
|
46
|
+
const onChange = vi.fn();
|
|
47
|
+
render(
|
|
48
|
+
<TimeInput
|
|
49
|
+
label="Time"
|
|
50
|
+
value={'14:30' as ISOTimeString}
|
|
51
|
+
onChange={onChange}
|
|
52
|
+
/>,
|
|
53
|
+
);
|
|
54
|
+
const input = screen.getByLabelText('Time');
|
|
55
|
+
|
|
56
|
+
// An IME candidate window navigates with the arrows; a composing keydown
|
|
57
|
+
// (isComposing / legacy keyCode 229) must not step the time value.
|
|
58
|
+
fireEvent.keyDown(input, {key: 'ArrowUp', isComposing: true});
|
|
59
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
60
|
+
fireEvent.keyDown(input, {key: 'ArrowDown', keyCode: 229});
|
|
61
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
62
|
+
|
|
63
|
+
// A real, non-composing ArrowUp still steps the time by one minute.
|
|
64
|
+
fireEvent.keyDown(input, {key: 'ArrowUp'});
|
|
65
|
+
expect(onChange).toHaveBeenCalledWith('14:31');
|
|
66
|
+
});
|
|
67
|
+
|
|
45
68
|
it('displays formatted time in 12h format', () => {
|
|
46
69
|
render(
|
|
47
70
|
<TimeInput
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
formatDisplayTime24h,
|
|
56
56
|
formatISOTime,
|
|
57
57
|
adjustTime,
|
|
58
|
+
isImeKeyEvent,
|
|
58
59
|
isTimeInRange,
|
|
59
60
|
mergeProps,
|
|
60
61
|
mergeRefs,
|
|
@@ -530,6 +531,13 @@ export function TimeInput({
|
|
|
530
531
|
// Handle keyboard navigation on input
|
|
531
532
|
const handleInputKeyDown = useCallback(
|
|
532
533
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
534
|
+
// ArrowUp/ArrowDown step the time and preventDefault; an IME candidate
|
|
535
|
+
// window uses those same arrows to navigate candidates, so guard the
|
|
536
|
+
// composing keydown (fires before compositionend) to avoid stealing them
|
|
537
|
+
// mid-composition. See utils/ime.ts.
|
|
538
|
+
if (isImeKeyEvent(e.nativeEvent)) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
533
541
|
// Arrow-key adjustment mutates the value; block it while showing a
|
|
534
542
|
// disabled reason (the input keeps focusability via aria-disabled).
|
|
535
543
|
if (isDisabled) {
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from '../Layer/useLayer';
|
|
29
29
|
import {layerAnimations} from '../Layer/layerAnimations.stylex';
|
|
30
30
|
import {themeProps} from '../utils/themeProps';
|
|
31
|
+
import {isImeKeyEvent} from '../utils/ime';
|
|
31
32
|
import {
|
|
32
33
|
colorVars,
|
|
33
34
|
radiusVars,
|
|
@@ -454,7 +455,9 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
|
|
|
454
455
|
if (e.key !== 'Escape') {
|
|
455
456
|
return;
|
|
456
457
|
}
|
|
457
|
-
if (e
|
|
458
|
+
if (isImeKeyEvent(e)) {
|
|
459
|
+
// Ignore Escape that is committing/cancelling an IME composition;
|
|
460
|
+
// see utils/ime.ts for why.
|
|
458
461
|
return;
|
|
459
462
|
}
|
|
460
463
|
clearTimeouts();
|
|
@@ -24,7 +24,7 @@ export const docs = {
|
|
|
24
24
|
},
|
|
25
25
|
theming: {
|
|
26
26
|
targets: [
|
|
27
|
-
{className: 'astryx-tree-list', visualProps: ['density']},
|
|
27
|
+
{className: 'astryx-tree-list', visualProps: ['density', 'variant']},
|
|
28
28
|
{className: 'astryx-tree-list-item', visualProps: ['density'], states: ['selected', 'disabled']},
|
|
29
29
|
{className: 'astryx-tree-list-chevron', states: ['state']},
|
|
30
30
|
{className: 'astryx-tree-list-item-label', states: ['selected']},
|
|
@@ -104,7 +104,7 @@ export const docsZh = {
|
|
|
104
104
|
group: 'TreeList',
|
|
105
105
|
theming: {
|
|
106
106
|
targets: [
|
|
107
|
-
{className: 'astryx-tree-list', visualProps: ['density']},
|
|
107
|
+
{className: 'astryx-tree-list', visualProps: ['density', 'variant']},
|
|
108
108
|
{className: 'astryx-tree-list-item', visualProps: ['density'], states: ['selected', 'disabled']},
|
|
109
109
|
{className: 'astryx-tree-list-chevron', states: ['state']},
|
|
110
110
|
{className: 'astryx-tree-list-item-label', states: ['selected']},
|