@astryxdesign/core 0.7.1 → 0.7.2-canary.07f97c6
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/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +38 -14
- package/dist/Drawer/Drawer.d.ts +191 -0
- package/dist/Drawer/Drawer.d.ts.map +1 -0
- package/dist/Drawer/Drawer.js +382 -0
- package/dist/Drawer/DrawerHeader.d.ts +85 -0
- package/dist/Drawer/DrawerHeader.d.ts.map +1 -0
- package/dist/Drawer/DrawerHeader.js +134 -0
- package/dist/Drawer/index.d.ts +11 -0
- package/dist/Drawer/index.d.ts.map +1 -0
- package/dist/Drawer/index.js +11 -0
- package/dist/Drawer/useDrawerDialogPresence.d.ts +42 -0
- package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -0
- package/dist/Drawer/useDrawerDialogPresence.js +236 -0
- package/dist/DropdownMenu/DropdownMenu.js +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +4 -4
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +10 -2
- package/dist/Layer/layerViewportInset.stylex.d.ts +6 -2
- package/dist/Layer/layerViewportInset.stylex.d.ts.map +1 -1
- package/dist/Layer/layerViewportInset.stylex.js +6 -2
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +13 -7
- package/dist/Layer/useLayerDismissal.d.ts +1 -1
- package/dist/Layer/useLayerDismissal.js +1 -1
- package/dist/Layout/container.stylex.d.ts +5 -5
- package/dist/Layout/container.stylex.js +4 -4
- package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -1
- package/dist/Markdown/plugins/frontmatter.js +7 -3
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +22 -8
- package/dist/MultiSelector/OptionActions.a11y.chromium.spec.js +100 -2
- package/dist/MultiSelector/hooks.d.ts.map +1 -1
- package/dist/MultiSelector/hooks.js +3 -1
- package/dist/Pagination/Pagination.d.ts +8 -5
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +21 -8
- package/dist/Popover/Popover.js +2 -2
- package/dist/PowerSearch/PowerSearch.js +1 -1
- package/dist/Selector/hooks.d.ts.map +1 -1
- package/dist/Selector/hooks.js +6 -2
- package/dist/Switch/Switch.d.ts +1 -1
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +13 -1
- package/dist/TabList/TabMenu.d.ts +2 -2
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +6 -3
- package/dist/Text/useTruncation.d.ts +7 -5
- package/dist/Text/useTruncation.d.ts.map +1 -1
- package/dist/Text/useTruncation.js +16 -5
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +11 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +26 -0
- package/dist/astryx.css +21 -10
- package/dist/hooks/useEntryAnimation.d.ts +2 -1
- package/dist/hooks/useEntryAnimation.d.ts.map +1 -1
- package/dist/hooks/useEntryAnimation.js +14 -12
- package/dist/hooks/useIsHydrating.d.ts +6 -0
- package/dist/hooks/useIsHydrating.d.ts.map +1 -0
- package/dist/hooks/useIsHydrating.js +30 -0
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +112 -1
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +112 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +0 -3
- package/dist/utils/focusOutline.stylex.d.ts +4 -0
- package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
- package/dist/utils/focusOutline.stylex.js +4 -0
- package/dist/utils/sharedMutationObserver.d.ts +21 -0
- package/dist/utils/sharedMutationObserver.d.ts.map +1 -0
- package/dist/utils/sharedMutationObserver.js +69 -0
- package/locales/en.json +444 -0
- package/locales/pseudo.json +333 -0
- package/package.json +9 -4
- package/src/Button/Button.doc.mjs +2 -2
- package/src/Button/Button.test.tsx +155 -9
- package/src/Button/Button.tsx +39 -16
- package/src/Button/__tests__/Button.a11y.known-failures.ts +1 -62
- package/src/Button/__tests__/Button.a11y.states.ts +0 -10
- package/src/Drawer/Drawer.doc.mjs +472 -0
- package/src/Drawer/Drawer.spec.md +357 -0
- package/src/Drawer/Drawer.test.tsx +1300 -0
- package/src/Drawer/Drawer.tsx +613 -0
- package/src/Drawer/DrawerDirection.test.tsx +127 -0
- package/src/Drawer/DrawerHeader.doc.mjs +293 -0
- package/src/Drawer/DrawerHeader.test.tsx +102 -0
- package/src/Drawer/DrawerHeader.tsx +235 -0
- package/src/Drawer/__tests__/DrawerAudit.a11y.chromium.spec.ts +401 -0
- package/src/Drawer/__tests__/DrawerHeaderAudit.a11y.chromium.spec.ts +297 -0
- package/src/Drawer/index.ts +13 -0
- package/src/Drawer/useDrawerDialogPresence.ts +284 -0
- package/src/DropdownMenu/DropdownMenu.tsx +2 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +2 -2
- package/src/HoverCard/HoverCard.test.tsx +58 -0
- package/src/HoverCard/useHoverCard.tsx +10 -2
- package/src/IconButton/IconButton.doc.mjs +2 -2
- package/src/IconButton/IconButton.test.tsx +3 -1
- package/src/Layer/layerViewportInset.source-build.test.mjs +90 -0
- package/src/Layer/layerViewportInset.stylex.ts +6 -2
- package/src/Layer/layerViewportInset.test.ts +1 -0
- package/src/Layer/useLayer.tsx +24 -13
- package/src/Layer/useLayerDismissal.ts +1 -1
- package/src/Layout/container.stylex.ts +6 -6
- package/src/List/ListItem.doc.mjs +8 -5
- package/src/Markdown/plugins/frontmatter.test.tsx +25 -0
- package/src/Markdown/plugins/frontmatter.ts +7 -3
- package/src/MultiSelector/MultiSelector.test.tsx +159 -0
- package/src/MultiSelector/MultiSelector.tsx +40 -7
- package/src/MultiSelector/OptionActions.a11y.chromium.spec.ts +112 -2
- package/src/MultiSelector/hooks.ts +3 -1
- package/src/MultiSelector/useMultiCombobox.doc.mjs +267 -0
- package/src/Pagination/Pagination.doc.mjs +7 -7
- package/src/Pagination/Pagination.spec.md +9 -9
- package/src/Pagination/Pagination.test.tsx +242 -0
- package/src/Pagination/Pagination.tsx +27 -11
- package/src/Popover/Popover.test.tsx +2 -2
- package/src/Popover/Popover.tsx +4 -4
- package/src/PowerSearch/PowerSearch.test.tsx +85 -0
- package/src/PowerSearch/PowerSearch.tsx +2 -2
- package/src/Selector/hooks.ts +6 -2
- package/src/Selector/useCombobox.doc.mjs +237 -0
- package/src/Switch/Switch.test.tsx +49 -0
- package/src/Switch/Switch.tsx +11 -0
- package/src/TabList/TabList.test.tsx +67 -0
- package/src/TabList/TabMenu.doc.mjs +4 -3
- package/src/TabList/TabMenu.tsx +7 -4
- package/src/Text/useTruncation.test.tsx +69 -0
- package/src/Text/useTruncation.ts +16 -5
- package/src/Tokenizer/Tokenizer.test.tsx +54 -1
- package/src/Tooltip/Tooltip.test.tsx +31 -0
- package/src/Tooltip/useTooltip.test.tsx +13 -0
- package/src/Tooltip/useTooltip.tsx +11 -0
- package/src/Typeahead/BaseTypeahead.tsx +26 -0
- package/src/Typeahead/Typeahead.test.tsx +260 -2
- package/src/hooks/useEntryAnimation.doc.mjs +4 -4
- package/src/hooks/useEntryAnimation.ssr.test.tsx +143 -0
- package/src/hooks/useEntryAnimation.ts +15 -16
- package/src/hooks/useIsHydrating.test.tsx +70 -0
- package/src/hooks/useIsHydrating.ts +31 -0
- package/src/i18n/InternationalizationProvider.doc.mjs +8 -0
- package/src/i18n/InternationalizationProvider.spec.md +252 -0
- package/src/i18n/__tests__/InternationalizationProvider.test.tsx +128 -0
- package/src/i18n/generated-locales/en.generated.ts +121 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +121 -0
- package/src/i18n/useTranslator.test.tsx +37 -0
- package/src/index.ts +1 -0
- package/src/theme/derivedVarRegistry.test.ts +0 -3
- package/src/theme/derivedVarRegistry.ts +0 -3
- package/src/utils/focusOutline.stylex.ts +4 -0
- package/src/utils/sharedMutationObserver.test.ts +53 -0
- package/src/utils/sharedMutationObserver.ts +78 -0
|
@@ -345,6 +345,14 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
|
|
|
345
345
|
hideLayer();
|
|
346
346
|
}, [clearTimeouts, hideLayer]);
|
|
347
347
|
|
|
348
|
+
// The imperative hide. A card closed while the pointer rests on it unmounts
|
|
349
|
+
// without a mouseleave, so the content-hover latch is cleared here; left set,
|
|
350
|
+
// it would keep the trigger's next hover-open from closing on leave.
|
|
351
|
+
const hide = useCallback(() => {
|
|
352
|
+
isHoveringContentRef.current = false;
|
|
353
|
+
hideLayer();
|
|
354
|
+
}, [hideLayer]);
|
|
355
|
+
|
|
348
356
|
const {
|
|
349
357
|
isTouchPointerRef,
|
|
350
358
|
isTouchInteraction,
|
|
@@ -508,7 +516,7 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
|
|
|
508
516
|
? null
|
|
509
517
|
: document.getElementById(layerId);
|
|
510
518
|
const hadFocus = card?.contains(document.activeElement) ?? false;
|
|
511
|
-
|
|
519
|
+
hide();
|
|
512
520
|
if (hadFocus) {
|
|
513
521
|
isEscapeDismissingRef.current = true;
|
|
514
522
|
triggerRef.current?.focus();
|
|
@@ -720,6 +728,6 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
|
|
|
720
728
|
isOpen: isLayerOpen,
|
|
721
729
|
renderHoverCard,
|
|
722
730
|
show: showLayer,
|
|
723
|
-
hide
|
|
731
|
+
hide,
|
|
724
732
|
};
|
|
725
733
|
}
|
|
@@ -46,7 +46,7 @@ export const docs = {
|
|
|
46
46
|
{
|
|
47
47
|
name: 'isLoading',
|
|
48
48
|
type: 'boolean',
|
|
49
|
-
description: 'Shows a loading spinner and
|
|
49
|
+
description: 'Shows a loading spinner and blocks re-activation while staying focusable (aria-busy + aria-disabled, not native disabled).',
|
|
50
50
|
default: 'false',
|
|
51
51
|
},
|
|
52
52
|
{
|
|
@@ -141,7 +141,7 @@ export const docsDense = {
|
|
|
141
141
|
variant: 'visual style variant',
|
|
142
142
|
size: 'size variant',
|
|
143
143
|
elevation: 'resting shadow depth: none|low|med|high; raise for a floating action button (FAB)',
|
|
144
|
-
isLoading: 'shows loading spinner
|
|
144
|
+
isLoading: 'shows loading spinner, blocks re-activation but stays focusable (aria-disabled, not native disabled)',
|
|
145
145
|
isDisabled: 'disables button',
|
|
146
146
|
tooltip: 'tooltip text shown on hover',
|
|
147
147
|
onClick: 'standard click handler',
|
|
@@ -124,7 +124,9 @@ describe('IconButton', () => {
|
|
|
124
124
|
it('shows loading state', () => {
|
|
125
125
|
render(<IconButton label="Save" icon={<span>💾</span>} isLoading />);
|
|
126
126
|
const button = screen.getByRole('button');
|
|
127
|
-
|
|
127
|
+
// Busy is aria-disabled (focusable), not native disabled — see #4871.
|
|
128
|
+
expect(button).not.toBeDisabled();
|
|
129
|
+
expect(button).toHaveAttribute('aria-disabled', 'true');
|
|
128
130
|
});
|
|
129
131
|
|
|
130
132
|
it('forwards ref correctly', () => {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file layerViewportInset.source-build.test.mjs
|
|
5
|
+
* @input Uses Babel, the core StyleX build config, the StyleX rule processor,
|
|
6
|
+
* and the sources that cap or inset anchored layers
|
|
7
|
+
* @output Verifies the shipped stylesheet carries the dynamic-viewport,
|
|
8
|
+
* safe-area gutter and caps (spec:AST-059 FR1–FR3), not only the fallbacks
|
|
9
|
+
* @position Regression test for the compiled layer viewport inset
|
|
10
|
+
*
|
|
11
|
+
* `stylex.firstThatWorks(a, b)` turns two `var()` arguments into one nested
|
|
12
|
+
* `var(a, b)`. A `defineConsts` reference compiles to such a `var()`, and the
|
|
13
|
+
* rule processor inlines a constant only where its `var()` stands alone, so a
|
|
14
|
+
* pair of bare constant references shipped as `var(--<hash>, <fallback>)`
|
|
15
|
+
* with `--<hash>` defined nowhere: every engine took the fallback.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'node:fs/promises';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import {fileURLToPath} from 'node:url';
|
|
21
|
+
import {transformAsync} from '@babel/core';
|
|
22
|
+
import stylexBabelPlugin from '@stylexjs/babel-plugin';
|
|
23
|
+
import {describe, expect, it} from 'vitest';
|
|
24
|
+
|
|
25
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const SRC = path.resolve(__dirname, '..');
|
|
27
|
+
const CORE_ROOT = path.resolve(SRC, '..');
|
|
28
|
+
|
|
29
|
+
const SOURCES = [
|
|
30
|
+
'Layer/layerViewportInset.stylex.ts',
|
|
31
|
+
'Layer/useLayer.tsx',
|
|
32
|
+
'Popover/Popover.tsx',
|
|
33
|
+
'DropdownMenu/DropdownMenu.tsx',
|
|
34
|
+
'DropdownMenu/DropdownMenuSubMenu.tsx',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
async function compileStylesheet() {
|
|
38
|
+
const rules = [];
|
|
39
|
+
for (const relative of SOURCES) {
|
|
40
|
+
const filename = path.join(SRC, relative);
|
|
41
|
+
const result = await transformAsync(await fs.readFile(filename, 'utf8'), {
|
|
42
|
+
babelrc: false,
|
|
43
|
+
configFile: path.join(CORE_ROOT, 'babel.config.json'),
|
|
44
|
+
filename,
|
|
45
|
+
});
|
|
46
|
+
rules.push(...(result?.metadata?.stylex ?? []));
|
|
47
|
+
}
|
|
48
|
+
return stylexBabelPlugin.processStylexRules(rules, false);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Every declaration of `property` in the compiled sheet. */
|
|
52
|
+
function declarations(css, property) {
|
|
53
|
+
return [...css.matchAll(new RegExp(`[{;]${property}:([^;}]+)`, 'g'))].map(
|
|
54
|
+
match => match[1],
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('layer viewport inset stylesheet (spec:AST-059)', () => {
|
|
59
|
+
it('leaves no constant reference unresolved behind a fallback', async () => {
|
|
60
|
+
const css = await compileStylesheet();
|
|
61
|
+
const defined = new Set(
|
|
62
|
+
[...css.matchAll(/(--[\w-]+)\s*:/g)].map(match => match[1]),
|
|
63
|
+
);
|
|
64
|
+
const undefinedReferences = [
|
|
65
|
+
...css.matchAll(/var\((--x[a-z0-9]+),/g),
|
|
66
|
+
].flatMap(match => (defined.has(match[1]) ? [] : [match[0]]));
|
|
67
|
+
// Covers the caps and the four alignment-axis gutter margins.
|
|
68
|
+
expect(undefinedReferences).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('caps both axes against the dynamic viewport and the safe-area gutters', async () => {
|
|
72
|
+
const css = await compileStylesheet();
|
|
73
|
+
const blockCaps = declarations(css, 'max-height');
|
|
74
|
+
const inlineCaps = declarations(css, 'max-width');
|
|
75
|
+
|
|
76
|
+
expect(blockCaps).toContainEqual(
|
|
77
|
+
expect.stringMatching(/^calc\(calc\(100dvb - .*safe-area-inset-top/),
|
|
78
|
+
);
|
|
79
|
+
expect(inlineCaps).toContainEqual(
|
|
80
|
+
expect.stringMatching(/^calc\(calc\(100vi - .*safe-area-inset-left/),
|
|
81
|
+
);
|
|
82
|
+
// The older-engine fallbacks still ship, ahead of the preferred value.
|
|
83
|
+
expect(blockCaps).toContainEqual(
|
|
84
|
+
expect.stringMatching(/^calc\(calc\(100vh - /),
|
|
85
|
+
);
|
|
86
|
+
expect(inlineCaps).toContainEqual(
|
|
87
|
+
expect.stringMatching(/^calc\(calc\(100vw - /),
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -56,8 +56,12 @@ const GUTTER_BLOCK = `calc(max(${SPACING}, env(safe-area-inset-top, 0px), env(sa
|
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* The viewport gutter and the caps derived from it. Every value has an
|
|
59
|
-
* `env()`-less fallback for engines without safe-area support; pair them
|
|
60
|
-
* `stylex.firstThatWorks(cap
|
|
59
|
+
* `env()`-less fallback for engines without safe-area support; pair them as
|
|
60
|
+
* `stylex.firstThatWorks(`calc(${cap})`, `calc(${capFallback})`)`. Pass
|
|
61
|
+
* each inside a `calc()` (or another function), never as a bare reference: a
|
|
62
|
+
* constant reference compiles to a `var()`, and `firstThatWorks` folds two
|
|
63
|
+
* `var()` arguments into one `var(cap, capFallback)` whose constant is never
|
|
64
|
+
* inlined, so every engine would take the fallback.
|
|
61
65
|
*/
|
|
62
66
|
export const layerViewportInset = stylex.defineConsts({
|
|
63
67
|
/** Gutter from the block-start viewport edge. */
|
package/src/Layer/useLayer.tsx
CHANGED
|
@@ -102,15 +102,17 @@ const styles = stylex.create({
|
|
|
102
102
|
// The cap is on the layer box itself: content wider than the viewport
|
|
103
103
|
// overflows inside the layer, where the composing component decides
|
|
104
104
|
// whether it scrolls or clips; the box never leaves the viewport.
|
|
105
|
+
// Each constant is wrapped in `calc()` so both declarations ship (see
|
|
106
|
+
// layerViewportInset).
|
|
105
107
|
viewportFit: {
|
|
106
108
|
boxSizing: 'border-box',
|
|
107
109
|
maxInlineSize: stylex.firstThatWorks(
|
|
108
|
-
layerViewportInset.maxInlineSize
|
|
109
|
-
layerViewportInset.maxInlineSizeFallback
|
|
110
|
+
`calc(${layerViewportInset.maxInlineSize})`,
|
|
111
|
+
`calc(${layerViewportInset.maxInlineSizeFallback})`,
|
|
110
112
|
),
|
|
111
113
|
maxBlockSize: stylex.firstThatWorks(
|
|
112
|
-
layerViewportInset.maxBlockSize
|
|
113
|
-
layerViewportInset.maxBlockSizeFallback
|
|
114
|
+
`calc(${layerViewportInset.maxBlockSize})`,
|
|
115
|
+
`calc(${layerViewportInset.maxBlockSizeFallback})`,
|
|
114
116
|
),
|
|
115
117
|
},
|
|
116
118
|
// Alignment-axis gutter (spec:AST-059 FR1): a margin on the far viewport
|
|
@@ -120,26 +122,26 @@ const styles = stylex.create({
|
|
|
120
122
|
// (see layerSlideRules.ts).
|
|
121
123
|
gutterInlineEnd: {
|
|
122
124
|
marginInlineEnd: stylex.firstThatWorks(
|
|
123
|
-
layerViewportInset.gutterInline
|
|
124
|
-
layerViewportInset.gutterInlineFallback
|
|
125
|
+
`calc(${layerViewportInset.gutterInline})`,
|
|
126
|
+
`calc(${layerViewportInset.gutterInlineFallback})`,
|
|
125
127
|
),
|
|
126
128
|
},
|
|
127
129
|
gutterInlineStart: {
|
|
128
130
|
marginInlineStart: stylex.firstThatWorks(
|
|
129
|
-
layerViewportInset.gutterInline
|
|
130
|
-
layerViewportInset.gutterInlineFallback
|
|
131
|
+
`calc(${layerViewportInset.gutterInline})`,
|
|
132
|
+
`calc(${layerViewportInset.gutterInlineFallback})`,
|
|
131
133
|
),
|
|
132
134
|
},
|
|
133
135
|
gutterBlockEnd: {
|
|
134
136
|
marginBlockEnd: stylex.firstThatWorks(
|
|
135
|
-
layerViewportInset.gutterBlock
|
|
136
|
-
layerViewportInset.gutterBlockFallback
|
|
137
|
+
`calc(${layerViewportInset.gutterBlock})`,
|
|
138
|
+
`calc(${layerViewportInset.gutterBlockFallback})`,
|
|
137
139
|
),
|
|
138
140
|
},
|
|
139
141
|
gutterBlockStart: {
|
|
140
142
|
marginBlockStart: stylex.firstThatWorks(
|
|
141
|
-
layerViewportInset.gutterBlock
|
|
142
|
-
layerViewportInset.gutterBlockFallback
|
|
143
|
+
`calc(${layerViewportInset.gutterBlock})`,
|
|
144
|
+
`calc(${layerViewportInset.gutterBlockFallback})`,
|
|
143
145
|
),
|
|
144
146
|
},
|
|
145
147
|
});
|
|
@@ -760,7 +762,16 @@ function useLayerImplementation(
|
|
|
760
762
|
// hosted away from its trigger then still takes its sequential focus
|
|
761
763
|
// order (and its popover nesting) from the trigger rather than from
|
|
762
764
|
// its own DOM position. Browsers without the option ignore it.
|
|
763
|
-
|
|
765
|
+
// `source` is declared as an `HTMLElement`. A caller-rendered icon
|
|
766
|
+
// trigger is an `SVGElement`, whose dictionary conversion throws
|
|
767
|
+
// before the popover opens. Omitting the option for it still anchors
|
|
768
|
+
// the layer, because positioning reads the anchor-name pair, and only
|
|
769
|
+
// gives up the invoker's focus order.
|
|
770
|
+
const trigger =
|
|
771
|
+
triggerRef.current instanceof HTMLElement
|
|
772
|
+
? triggerRef.current
|
|
773
|
+
: undefined;
|
|
774
|
+
popover.showPopover({source: trigger});
|
|
764
775
|
});
|
|
765
776
|
} else {
|
|
766
777
|
popover.style.display = 'block';
|
|
@@ -98,7 +98,7 @@ export interface UseLayerDismissalReturn {
|
|
|
98
98
|
* routes each Escape press to the top-most REGISTERED layer, so one press
|
|
99
99
|
* dismisses exactly one of them. Dialog (and what is built on it, including
|
|
100
100
|
* CommandPalette), Popover and the menus built on it, ContextMenu, Tooltip,
|
|
101
|
-
* HoverCard, Lightbox, MobileNav, BottomSheet, BottomSheetSwitcher and
|
|
101
|
+
* HoverCard, Lightbox, MobileNav, BottomSheet, BottomSheetSwitcher and
|
|
102
102
|
* `Drawer` register today.
|
|
103
103
|
*
|
|
104
104
|
* `DropdownMenuSubMenu` and `PowerSearchEditPopover` still run their own Escape
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* @file container.stylex.ts
|
|
5
5
|
* @input Uses @stylexjs/stylex, spacing from theme
|
|
6
6
|
* @output StyleX utility for layout container styling
|
|
7
|
-
* @position Layout utility; used by Card, Section, Dialog, BottomSheet, and
|
|
8
|
-
*
|
|
7
|
+
* @position Layout utility; used by Card, Section, Dialog, BottomSheet, and
|
|
8
|
+
* Drawer
|
|
9
9
|
*
|
|
10
10
|
* ## Public API for themes
|
|
11
11
|
*
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* --astryx-card-padding-block-end
|
|
20
20
|
*
|
|
21
21
|
* Read order per level: `var(--astryx-…, <next level>)`, terminating at
|
|
22
|
-
* `--spacing-4`. Same pattern for section, dialog, bottom-sheet, and
|
|
22
|
+
* `--spacing-4`. Same pattern for section, dialog, bottom-sheet, and
|
|
23
23
|
* drawer.
|
|
24
24
|
*
|
|
25
25
|
* ```ts
|
|
@@ -150,7 +150,7 @@ const bottomSheetInlineEnd = `var(--astryx-bottom-sheet-padding-inline-end, ${bo
|
|
|
150
150
|
const bottomSheetBlockStart = `var(--astryx-bottom-sheet-padding-block-start, ${bottomSheetShorthand})`;
|
|
151
151
|
const bottomSheetBlockEnd = `var(--astryx-bottom-sheet-padding-block-end, ${bottomSheetShorthand})`;
|
|
152
152
|
|
|
153
|
-
// Drawer
|
|
153
|
+
// Drawer padding chains: --astryx-* then the next specificity level,
|
|
154
154
|
// terminating at --spacing-4 like Dialog. Built as chained const strings (no
|
|
155
155
|
// function calls) so StyleX can statically analyze them; see naming.ts for the
|
|
156
156
|
// prefix policy.
|
|
@@ -522,8 +522,8 @@ export interface ContainerOptions {
|
|
|
522
522
|
* This allows themes to override container padding via component-specific
|
|
523
523
|
* public CSS custom properties without touching internal vars.
|
|
524
524
|
*
|
|
525
|
-
* Used by Card, Section, Dialog, BottomSheet, and
|
|
526
|
-
*
|
|
525
|
+
* Used by Card, Section, Dialog, BottomSheet, and Drawer when no explicit
|
|
526
|
+
* padding prop is provided.
|
|
527
527
|
* @default undefined (uses explicit spacing token values)
|
|
528
528
|
*/
|
|
529
529
|
useThemeDefault?: ContainerComponent;
|
|
@@ -12,8 +12,9 @@ export const docs = {
|
|
|
12
12
|
props: [
|
|
13
13
|
{
|
|
14
14
|
name: 'label',
|
|
15
|
-
type: '
|
|
16
|
-
description:
|
|
15
|
+
type: 'ReactNode',
|
|
16
|
+
description:
|
|
17
|
+
'Primary content. A plain string gets single-line truncation automatically; a ReactNode renders as given, so child components control their own text behavior.',
|
|
17
18
|
required: true,
|
|
18
19
|
},
|
|
19
20
|
{
|
|
@@ -123,8 +124,9 @@ export const docsZh = {
|
|
|
123
124
|
props: [
|
|
124
125
|
{
|
|
125
126
|
name: 'label',
|
|
126
|
-
type: '
|
|
127
|
-
description:
|
|
127
|
+
type: 'ReactNode',
|
|
128
|
+
description:
|
|
129
|
+
'主要内容。纯字符串会自动应用单行截断;ReactNode 按原样渲染,由子组件自行控制文本行为。',
|
|
128
130
|
required: true,
|
|
129
131
|
},
|
|
130
132
|
{
|
|
@@ -205,7 +207,8 @@ export const docsDense = {
|
|
|
205
207
|
description:
|
|
206
208
|
'List item w/ label, description, start/end content slots, interactive patterns.',
|
|
207
209
|
propDescriptions: {
|
|
208
|
-
label:
|
|
210
|
+
label:
|
|
211
|
+
'Primary content. String auto-truncates; ReactNode renders as given.',
|
|
209
212
|
description: 'Secondary text below label.',
|
|
210
213
|
startContent: 'Content before label area (e.g. icon, avatar).',
|
|
211
214
|
endContent: 'Content after label area (e.g. badge, chevron).',
|
|
@@ -117,6 +117,31 @@ describe('createMarkdownFrontmatter', () => {
|
|
|
117
117
|
).not.toThrow();
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
it('accepts the decoded fields record as metadata (identity parse)', () => {
|
|
121
|
+
const fields = createMarkdownFrontmatter({
|
|
122
|
+
name: 'fields',
|
|
123
|
+
parse: record => record,
|
|
124
|
+
});
|
|
125
|
+
const source = '---\ntitle: Release notes\n__proto__: kept\n---\n# Shipped';
|
|
126
|
+
|
|
127
|
+
const parsed = fields.parse(source);
|
|
128
|
+
expect(parsed).toEqual({
|
|
129
|
+
status: 'match',
|
|
130
|
+
contentStart: source.indexOf('# Shipped'),
|
|
131
|
+
metadata: {title: 'Release notes', ['__proto__']: 'kept'},
|
|
132
|
+
});
|
|
133
|
+
if (parsed.status !== 'match') {
|
|
134
|
+
throw new Error('expected a frontmatter match');
|
|
135
|
+
}
|
|
136
|
+
// A field named like a prototype key stays an ordinary own field.
|
|
137
|
+
expect(Object.hasOwn(parsed.metadata, '__proto__')).toBe(true);
|
|
138
|
+
expect(Object.getPrototypeOf(parsed.metadata)).toBe(Object.prototype);
|
|
139
|
+
|
|
140
|
+
const root = parseMarkdownAst(source, {plugins: [fields.plugin]});
|
|
141
|
+
expect(fields.getMetadata(root)).toEqual(parsed.metadata);
|
|
142
|
+
expect(JSON.stringify(root)).not.toContain('title: Release notes');
|
|
143
|
+
});
|
|
144
|
+
|
|
120
145
|
it('returns a transform plugin that composes in the ordered pipeline', () => {
|
|
121
146
|
expectTypeOf<MarkdownTransform>().toBeFunction();
|
|
122
147
|
const root = parseMarkdownAst(SOURCE, {plugins: [documentMetadata.plugin]});
|
|
@@ -104,18 +104,22 @@ function readFrontmatter(source: string):
|
|
|
104
104
|
if (close == null) {
|
|
105
105
|
return {status: 'defer'};
|
|
106
106
|
}
|
|
107
|
-
const
|
|
107
|
+
const entries = new Map<string, string>();
|
|
108
108
|
for (const line of body.slice(0, close.index).split(/\r?\n/)) {
|
|
109
109
|
if (line.trim() === '') {
|
|
110
110
|
continue;
|
|
111
111
|
}
|
|
112
112
|
const separator = line.indexOf(':');
|
|
113
113
|
const key = line.slice(0, separator).trim();
|
|
114
|
-
if (separator <= 0 || key === '' ||
|
|
114
|
+
if (separator <= 0 || key === '' || entries.has(key)) {
|
|
115
115
|
return {status: 'none'};
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
entries.set(key, line.slice(separator + 1).trim());
|
|
118
118
|
}
|
|
119
|
+
// A plain object, so a parser that returns the record unchanged passes the
|
|
120
|
+
// JSON-like metadata check. `fromEntries` defines every key as an own data
|
|
121
|
+
// property, so a field named `__proto__` stays a field.
|
|
122
|
+
const fields: Record<string, string> = Object.fromEntries(entries);
|
|
119
123
|
let end = openingLength + close.index + close[0].length;
|
|
120
124
|
if (source.startsWith('\r\n', end)) {
|
|
121
125
|
end += 2;
|
|
@@ -35,6 +35,7 @@ import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
|
35
35
|
import {selectorPresentationStyles} from '../Selector/selectorPresentation.stylex';
|
|
36
36
|
import {groupStyles} from '../InputGroup/groupStyles';
|
|
37
37
|
import {InputGroup, InputGroupText} from '../InputGroup';
|
|
38
|
+
import {focusVars} from '../theme/tokens.stylex';
|
|
38
39
|
|
|
39
40
|
function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
|
|
40
41
|
const {prose, component} = generateThemeCSS(theme);
|
|
@@ -4083,6 +4084,164 @@ describe('MultiSelector option actions (grid)', () => {
|
|
|
4083
4084
|
expect(onChange).not.toHaveBeenCalled();
|
|
4084
4085
|
});
|
|
4085
4086
|
|
|
4087
|
+
it('draws the standard focus ring on the active action cell, from the focus tokens', async () => {
|
|
4088
|
+
// jsdom resolves no custom property, so the ring is read from StyleX
|
|
4089
|
+
// classes: a class name hashes one property and its value, and the same
|
|
4090
|
+
// declaration yields the same class wherever it is written.
|
|
4091
|
+
const ring = stylex.create({
|
|
4092
|
+
active: {
|
|
4093
|
+
outlineWidth: focusVars['--focus-outline-width'],
|
|
4094
|
+
outlineStyle: focusVars['--focus-outline-style'],
|
|
4095
|
+
outlineColor: focusVars['--focus-outline-color'],
|
|
4096
|
+
outlineOffset: focusVars['--focus-outline-offset'],
|
|
4097
|
+
},
|
|
4098
|
+
});
|
|
4099
|
+
const ringClasses = stylex
|
|
4100
|
+
.props(ring.active)
|
|
4101
|
+
.className!.split(' ')
|
|
4102
|
+
.filter(token => /^x[a-z0-9]+$/.test(token));
|
|
4103
|
+
expect(ringClasses).toHaveLength(4);
|
|
4104
|
+
|
|
4105
|
+
const user = userEvent.setup();
|
|
4106
|
+
render(
|
|
4107
|
+
<MultiSelector
|
|
4108
|
+
label="Labels"
|
|
4109
|
+
options={OPTIONS}
|
|
4110
|
+
value={[]}
|
|
4111
|
+
onChange={() => {}}
|
|
4112
|
+
/>,
|
|
4113
|
+
);
|
|
4114
|
+
const trigger = screen.getByRole('combobox', {name: 'Labels'});
|
|
4115
|
+
await user.click(trigger);
|
|
4116
|
+
const bug = screen.getByRole('row', {name: 'Bug', ...h});
|
|
4117
|
+
const actionCell = bug.children[1] as HTMLElement;
|
|
4118
|
+
for (const token of ringClasses) {
|
|
4119
|
+
expect(actionCell).not.toHaveClass(token);
|
|
4120
|
+
}
|
|
4121
|
+
fireEvent.keyDown(trigger, {key: 'ArrowRight'});
|
|
4122
|
+
expect(trigger).toHaveAttribute('aria-activedescendant', actionCell.id);
|
|
4123
|
+
for (const token of ringClasses) {
|
|
4124
|
+
expect(actionCell).toHaveClass(token);
|
|
4125
|
+
}
|
|
4126
|
+
});
|
|
4127
|
+
|
|
4128
|
+
it('draws no focus ring on an action cell highlighted after pointer input, until a key moves the highlight', async () => {
|
|
4129
|
+
const ring = stylex.create({
|
|
4130
|
+
active: {
|
|
4131
|
+
outlineWidth: focusVars['--focus-outline-width'],
|
|
4132
|
+
outlineStyle: focusVars['--focus-outline-style'],
|
|
4133
|
+
outlineColor: focusVars['--focus-outline-color'],
|
|
4134
|
+
outlineOffset: focusVars['--focus-outline-offset'],
|
|
4135
|
+
},
|
|
4136
|
+
});
|
|
4137
|
+
const ringClasses = stylex
|
|
4138
|
+
.props(ring.active)
|
|
4139
|
+
.className!.split(' ')
|
|
4140
|
+
.filter(token => /^x[a-z0-9]+$/.test(token));
|
|
4141
|
+
expect(ringClasses).toHaveLength(4);
|
|
4142
|
+
|
|
4143
|
+
const user = userEvent.setup();
|
|
4144
|
+
render(
|
|
4145
|
+
<MultiSelector
|
|
4146
|
+
label="Labels"
|
|
4147
|
+
options={OPTIONS}
|
|
4148
|
+
value={[]}
|
|
4149
|
+
onChange={() => {}}
|
|
4150
|
+
/>,
|
|
4151
|
+
);
|
|
4152
|
+
const trigger = screen.getByRole('combobox', {name: 'Labels'});
|
|
4153
|
+
await user.click(trigger);
|
|
4154
|
+
const bug = screen.getByRole('row', {name: 'Bug', ...h});
|
|
4155
|
+
const actionCell = bug.children[1] as HTMLElement;
|
|
4156
|
+
fireEvent.mouseEnter(actionCell);
|
|
4157
|
+
// The hover moved the highlight onto the action cell, without the ring.
|
|
4158
|
+
expect(trigger).toHaveAttribute('aria-activedescendant', actionCell.id);
|
|
4159
|
+
for (const token of ringClasses) {
|
|
4160
|
+
expect(actionCell).not.toHaveClass(token);
|
|
4161
|
+
}
|
|
4162
|
+
// Keyboard navigation back onto the cell draws it.
|
|
4163
|
+
fireEvent.keyDown(trigger, {key: 'ArrowLeft'});
|
|
4164
|
+
fireEvent.keyDown(trigger, {key: 'ArrowRight'});
|
|
4165
|
+
expect(trigger).toHaveAttribute('aria-activedescendant', actionCell.id);
|
|
4166
|
+
for (const token of ringClasses) {
|
|
4167
|
+
expect(actionCell).toHaveClass(token);
|
|
4168
|
+
}
|
|
4169
|
+
});
|
|
4170
|
+
|
|
4171
|
+
describe('ring under mixed input', () => {
|
|
4172
|
+
const ringClasses = () => {
|
|
4173
|
+
const ring = stylex.create({
|
|
4174
|
+
active: {
|
|
4175
|
+
outlineWidth: focusVars['--focus-outline-width'],
|
|
4176
|
+
outlineStyle: focusVars['--focus-outline-style'],
|
|
4177
|
+
outlineColor: focusVars['--focus-outline-color'],
|
|
4178
|
+
outlineOffset: focusVars['--focus-outline-offset'],
|
|
4179
|
+
},
|
|
4180
|
+
});
|
|
4181
|
+
return stylex
|
|
4182
|
+
.props(ring.active)
|
|
4183
|
+
.className!.split(' ')
|
|
4184
|
+
.filter(token => /^x[a-z0-9]+$/.test(token));
|
|
4185
|
+
};
|
|
4186
|
+
const hasRing = (cell: HTMLElement) =>
|
|
4187
|
+
ringClasses().every(token => cell.classList.contains(token));
|
|
4188
|
+
|
|
4189
|
+
async function openWithKeyboardOnBugAction() {
|
|
4190
|
+
const user = userEvent.setup();
|
|
4191
|
+
render(
|
|
4192
|
+
<MultiSelector
|
|
4193
|
+
label="Labels"
|
|
4194
|
+
options={OPTIONS}
|
|
4195
|
+
value={[]}
|
|
4196
|
+
onChange={() => {}}
|
|
4197
|
+
/>,
|
|
4198
|
+
);
|
|
4199
|
+
const trigger = screen.getByRole('combobox', {name: 'Labels'});
|
|
4200
|
+
await user.click(trigger);
|
|
4201
|
+
const row = (name: string) => screen.getByRole('row', {name, ...h});
|
|
4202
|
+
fireEvent.keyDown(trigger, {key: 'ArrowRight'});
|
|
4203
|
+
const bugAction = row('Bug').children[1] as HTMLElement;
|
|
4204
|
+
expect(trigger).toHaveAttribute('aria-activedescendant', bugAction.id);
|
|
4205
|
+
expect(hasRing(bugAction)).toBe(true);
|
|
4206
|
+
return {trigger, row, bugAction};
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
it('keeps the ring when the pointer crosses a disabled option the highlight ignores', async () => {
|
|
4210
|
+
const {trigger, row, bugAction} = await openWithKeyboardOnBugAction();
|
|
4211
|
+
fireEvent.mouseEnter(row('Docs').children[0]);
|
|
4212
|
+
expect(trigger).toHaveAttribute('aria-activedescendant', bugAction.id);
|
|
4213
|
+
expect(hasRing(bugAction)).toBe(true);
|
|
4214
|
+
});
|
|
4215
|
+
|
|
4216
|
+
it('follows the keyboard modality when a hover moves the highlight to another action', async () => {
|
|
4217
|
+
const {trigger, row} = await openWithKeyboardOnBugAction();
|
|
4218
|
+
const docsAction = row('Docs').children[1] as HTMLElement;
|
|
4219
|
+
fireEvent.mouseEnter(docsAction);
|
|
4220
|
+
expect(trigger).toHaveAttribute('aria-activedescendant', docsAction.id);
|
|
4221
|
+
expect(hasRing(docsAction)).toBe(true);
|
|
4222
|
+
});
|
|
4223
|
+
|
|
4224
|
+
it('draws no ring for a modifier chord after pointer input', async () => {
|
|
4225
|
+
const user = userEvent.setup();
|
|
4226
|
+
render(
|
|
4227
|
+
<MultiSelector
|
|
4228
|
+
label="Labels"
|
|
4229
|
+
options={OPTIONS}
|
|
4230
|
+
value={[]}
|
|
4231
|
+
onChange={() => {}}
|
|
4232
|
+
/>,
|
|
4233
|
+
);
|
|
4234
|
+
const trigger = screen.getByRole('combobox', {name: 'Labels'});
|
|
4235
|
+
await user.click(trigger);
|
|
4236
|
+
const bugAction = screen.getByRole('row', {name: 'Bug', ...h})
|
|
4237
|
+
.children[1] as HTMLElement;
|
|
4238
|
+
fireEvent.mouseEnter(bugAction);
|
|
4239
|
+
expect(hasRing(bugAction)).toBe(false);
|
|
4240
|
+
fireEvent.keyDown(trigger, {key: 'ArrowRight', ctrlKey: true});
|
|
4241
|
+
expect(hasRing(bugAction)).toBe(false);
|
|
4242
|
+
});
|
|
4243
|
+
});
|
|
4244
|
+
|
|
4086
4245
|
it('the inline-end arrow reaches the action cell, Enter fires it, inline-start returns', async () => {
|
|
4087
4246
|
const user = userEvent.setup();
|
|
4088
4247
|
const onChange = vi.fn();
|
|
@@ -50,6 +50,7 @@ import type {IndicatorPosition} from '../Indicator';
|
|
|
50
50
|
import {Badge} from '../Badge';
|
|
51
51
|
import {
|
|
52
52
|
colorVars,
|
|
53
|
+
focusVars,
|
|
53
54
|
sizeVars,
|
|
54
55
|
spacingVars,
|
|
55
56
|
radiusVars,
|
|
@@ -74,7 +75,10 @@ import {
|
|
|
74
75
|
import {useMultiCombobox} from './hooks';
|
|
75
76
|
import {getInputARIA, isImeKeyEvent, mergeProps} from '../utils';
|
|
76
77
|
import {devWarn, warnOnce} from '../utils/devWarning';
|
|
77
|
-
import {
|
|
78
|
+
import {
|
|
79
|
+
getInteractionModality,
|
|
80
|
+
useInteractionModalityTracking,
|
|
81
|
+
} from '../utils/interactionModality';
|
|
78
82
|
import {useAnnounce} from '../hooks/useAnnounce';
|
|
79
83
|
import {FOCUSABLE_SELECTOR} from '../hooks/focusableSelector';
|
|
80
84
|
import {useAnnounceRenderedText} from '../hooks/useAnnounceRenderedText';
|
|
@@ -436,11 +440,15 @@ const styles = stylex.create({
|
|
|
436
440
|
flexShrink: 0,
|
|
437
441
|
borderRadius: radiusVars['--radius-element'],
|
|
438
442
|
},
|
|
443
|
+
// The standard ring, from the focus tokens themselves: inside
|
|
444
|
+
// `stylex.create` StyleX resolves an imported `defineVars` group, and a
|
|
445
|
+
// plain object such as `FOCUS_OUTLINE_PARTS` compiles to variables nothing
|
|
446
|
+
// defines.
|
|
439
447
|
actionCellActive: {
|
|
440
|
-
outlineWidth:
|
|
441
|
-
outlineStyle:
|
|
442
|
-
outlineColor:
|
|
443
|
-
outlineOffset:
|
|
448
|
+
outlineWidth: focusVars['--focus-outline-width'],
|
|
449
|
+
outlineStyle: focusVars['--focus-outline-style'],
|
|
450
|
+
outlineColor: focusVars['--focus-outline-color'],
|
|
451
|
+
outlineOffset: focusVars['--focus-outline-offset'],
|
|
444
452
|
},
|
|
445
453
|
|
|
446
454
|
// Empty state
|
|
@@ -1752,8 +1760,8 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1752
1760
|
getActionCellId,
|
|
1753
1761
|
activeDescendantId,
|
|
1754
1762
|
onTriggerClick,
|
|
1755
|
-
onKeyDown,
|
|
1756
|
-
onItemMouseEnter,
|
|
1763
|
+
onKeyDown: onComboboxKeyDown,
|
|
1764
|
+
onItemMouseEnter: onComboboxItemMouseEnter,
|
|
1757
1765
|
} = useMultiCombobox({
|
|
1758
1766
|
selectableItems: sortedItems,
|
|
1759
1767
|
isDisabled: isDisabled || isEffectivelyReadOnly,
|
|
@@ -1784,6 +1792,29 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1784
1792
|
),
|
|
1785
1793
|
});
|
|
1786
1794
|
|
|
1795
|
+
// A hover moves the highlight too, but the focus ring follows the shared
|
|
1796
|
+
// interaction modality (architecture:interaction-modality INV1): it is read
|
|
1797
|
+
// whenever a key or the pointer moves the highlight, so an action cell
|
|
1798
|
+
// highlighted after pointer input keeps the hover treatment and no ring.
|
|
1799
|
+
useInteractionModalityTracking();
|
|
1800
|
+
const [isKeyboardHighlight, setIsKeyboardHighlight] = useState(
|
|
1801
|
+
() => getInteractionModality() === 'keyboard',
|
|
1802
|
+
);
|
|
1803
|
+
const onKeyDown = useCallback(
|
|
1804
|
+
(event: React.KeyboardEvent<HTMLElement>) => {
|
|
1805
|
+
onComboboxKeyDown(event);
|
|
1806
|
+
setIsKeyboardHighlight(getInteractionModality() === 'keyboard');
|
|
1807
|
+
},
|
|
1808
|
+
[onComboboxKeyDown],
|
|
1809
|
+
);
|
|
1810
|
+
const onItemMouseEnter = useCallback(
|
|
1811
|
+
(...args: Parameters<typeof onComboboxItemMouseEnter>) => {
|
|
1812
|
+
onComboboxItemMouseEnter(...args);
|
|
1813
|
+
setIsKeyboardHighlight(getInteractionModality() === 'keyboard');
|
|
1814
|
+
},
|
|
1815
|
+
[onComboboxItemMouseEnter],
|
|
1816
|
+
);
|
|
1817
|
+
|
|
1787
1818
|
// The trigger button owns its click so a consumer `onClick`, which lands on
|
|
1788
1819
|
// the button, composes after the built-in toggle. A press elsewhere in the
|
|
1789
1820
|
// control box (padding, start icon, chevron) still opens the panel.
|
|
@@ -2117,6 +2148,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
2117
2148
|
styles.actionCell,
|
|
2118
2149
|
isHighlighted &&
|
|
2119
2150
|
highlightedCell === 'action' &&
|
|
2151
|
+
isKeyboardHighlight &&
|
|
2120
2152
|
styles.actionCellActive,
|
|
2121
2153
|
)}>
|
|
2122
2154
|
{isSelectAll || isCreate ? null : item.action}
|
|
@@ -2131,6 +2163,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
2131
2163
|
[
|
|
2132
2164
|
isGrid,
|
|
2133
2165
|
highlightedCell,
|
|
2166
|
+
isKeyboardHighlight,
|
|
2134
2167
|
getActionCellId,
|
|
2135
2168
|
renderOption,
|
|
2136
2169
|
indicatorPosition,
|