@astryxdesign/core 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
- package/dist/AspectRatio/AspectRatio.js +1 -2
- package/dist/AspectRatio/index.d.ts.map +1 -1
- package/dist/AspectRatio/index.js +1 -2
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +1 -2
- package/dist/Badge/index.d.ts.map +1 -1
- package/dist/Badge/index.js +1 -2
- package/dist/Banner/Banner.d.ts +3 -0
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +17 -3
- package/dist/Blockquote/Blockquote.d.ts.map +1 -1
- package/dist/Blockquote/Blockquote.js +1 -2
- package/dist/Blockquote/index.d.ts.map +1 -1
- package/dist/Blockquote/index.js +1 -2
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +2 -2
- package/dist/Card/Card.d.ts.map +1 -1
- package/dist/Card/Card.js +1 -2
- package/dist/Card/index.d.ts.map +1 -1
- package/dist/Card/index.js +1 -2
- package/dist/Center/Center.d.ts.map +1 -1
- package/dist/Center/Center.js +1 -2
- package/dist/Center/index.d.ts.map +1 -1
- package/dist/Center/index.js +1 -2
- package/dist/Code/Code.d.ts.map +1 -1
- package/dist/Code/Code.js +1 -2
- package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPalette.js +5 -0
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +7 -8
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +23 -10
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +44 -16
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +32 -13
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +10 -3
- package/dist/Field/PanelSearchInput.d.ts +81 -0
- package/dist/Field/PanelSearchInput.d.ts.map +1 -0
- package/dist/Field/PanelSearchInput.js +151 -0
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +21 -10
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +1 -2
- package/dist/Grid/index.d.ts.map +1 -1
- package/dist/Grid/index.js +1 -2
- package/dist/Icon/globalIconRegistry.d.ts +5 -4
- package/dist/Icon/globalIconRegistry.d.ts.map +1 -1
- package/dist/Icon/globalIconRegistry.js +5 -4
- package/dist/Indicator/index.d.ts +1 -1
- package/dist/Indicator/index.d.ts.map +1 -1
- package/dist/Indicator/types.d.ts +10 -0
- package/dist/Indicator/types.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +24 -50
- package/dist/MultiSelector/MultiSelector.d.ts +8 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +134 -102
- package/dist/Popover/usePopover.d.ts +15 -0
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +14 -2
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +2 -3
- package/dist/Section/Section.d.ts.map +1 -1
- package/dist/Section/Section.js +1 -2
- package/dist/Section/index.d.ts.map +1 -1
- package/dist/Section/index.js +1 -2
- package/dist/Selector/Selector.d.ts +10 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +135 -95
- package/dist/Skeleton/Skeleton.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.js +1 -2
- package/dist/Skeleton/index.d.ts.map +1 -1
- package/dist/Skeleton/index.js +1 -2
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +2 -6
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +3 -3
- package/dist/Table/TableRow.d.ts +1 -1
- package/dist/Table/TableRow.d.ts.map +1 -1
- package/dist/Table/TableRow.js +4 -2
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +7 -2
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +14 -3
- package/dist/Timestamp/formatInstant.d.ts +11 -1
- package/dist/Timestamp/formatInstant.d.ts.map +1 -1
- package/dist/Timestamp/formatInstant.js +3 -1
- package/dist/VisuallyHidden/VisuallyHidden.d.ts.map +1 -1
- package/dist/VisuallyHidden/VisuallyHidden.js +1 -2
- package/dist/VisuallyHidden/index.d.ts.map +1 -1
- package/dist/VisuallyHidden/index.js +1 -2
- package/dist/astryx.css +18 -17
- package/dist/astryx.umd.js +45 -45
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +17 -4
- package/dist/theme/defineTheme.d.ts +2 -2
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +2 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/tokens.stylex.d.ts +12 -0
- package/dist/theme/tokens.stylex.d.ts.map +1 -1
- package/dist/theme/tokens.stylex.js +23 -0
- package/dist/utils/focusOutline.stylex.d.ts +34 -19
- package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
- package/dist/utils/focusOutline.stylex.js +46 -33
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +5 -1
- package/dist/utils/interactionModality.d.ts +34 -0
- package/dist/utils/interactionModality.d.ts.map +1 -0
- package/dist/utils/interactionModality.js +70 -0
- package/locales/en.json +40 -0
- package/locales/pseudo.json +30 -0
- package/package.json +2 -2
- package/src/AppShell/AppShell.doc.mjs +3 -0
- package/src/AspectRatio/AspectRatio.tsx +0 -2
- package/src/AspectRatio/index.ts +0 -2
- package/src/Badge/Badge.tsx +0 -2
- package/src/Badge/index.ts +0 -2
- package/src/Banner/Banner.test.tsx +58 -1
- package/src/Banner/Banner.tsx +24 -5
- package/src/Blockquote/Blockquote.tsx +0 -2
- package/src/Blockquote/index.ts +0 -2
- package/src/Button/Button.doc.mjs +2 -2
- package/src/Button/Button.tsx +5 -4
- package/src/Card/Card.tsx +0 -2
- package/src/Card/index.ts +0 -2
- package/src/Center/Center.tsx +0 -2
- package/src/Center/index.ts +0 -2
- package/src/CheckboxInput/CheckboxInput.test.tsx +8 -3
- package/src/Code/Code.tsx +0 -2
- package/src/CommandPalette/CommandPalette.test.tsx +54 -0
- package/src/CommandPalette/CommandPalette.tsx +5 -0
- package/src/ComplexSelector/ComplexSelector.test.tsx +15 -6
- package/src/ComplexSelector/ComplexSelector.tsx +6 -10
- package/src/DateInput/DateInput.test.tsx +19 -0
- package/src/DateInput/DateInput.tsx +3 -6
- package/src/DateRangeInput/DateRangeInput.tsx +3 -9
- package/src/DateTimeInput/DateTimeInput.test.tsx +79 -1
- package/src/DateTimeInput/DateTimeInput.tsx +23 -9
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +2 -1
- package/src/Field/PanelSearchInput.tsx +271 -0
- package/src/FileInput/FileInput.tsx +21 -8
- package/src/Grid/Grid.tsx +0 -2
- package/src/Grid/index.ts +0 -2
- package/src/Icon/globalIconRegistry.tsx +5 -4
- package/src/Indicator/Indicator.doc.mjs +1 -1
- package/src/Indicator/index.ts +1 -0
- package/src/Indicator/types.ts +11 -0
- package/src/Lightbox/Lightbox.tsx +2 -11
- package/src/MultiSelector/MultiSelector.doc.mjs +9 -0
- package/src/MultiSelector/MultiSelector.test.tsx +135 -5
- package/src/MultiSelector/MultiSelector.tsx +170 -127
- package/src/Popover/Popover.doc.mjs +2 -0
- package/src/Popover/usePopover.tsx +37 -4
- package/src/ProgressBar/ProgressBar.tsx +2 -8
- package/src/Section/Section.tsx +0 -2
- package/src/Section/index.ts +0 -2
- package/src/Selector/Selector.doc.mjs +10 -0
- package/src/Selector/Selector.test.tsx +386 -2
- package/src/Selector/Selector.tsx +182 -122
- package/src/Skeleton/Skeleton.tsx +0 -2
- package/src/Skeleton/index.ts +0 -2
- package/src/Slider/Slider.tsx +2 -11
- package/src/Switch/Switch.tsx +10 -2
- package/src/Table/Table.test.tsx +31 -0
- package/src/Table/TableRow.tsx +6 -0
- package/src/TimeInput/TimeInput.test.tsx +54 -0
- package/src/TimeInput/TimeInput.tsx +17 -2
- package/src/Timestamp/Timestamp.test.tsx +91 -14
- package/src/Timestamp/Timestamp.tsx +14 -2
- package/src/Timestamp/formatInstant.ts +16 -1
- package/src/TopNav/TopNavMegaMenu.doc.mjs +5 -0
- package/src/TreeList/TreeList.test.tsx +3 -2
- package/src/VisuallyHidden/VisuallyHidden.tsx +0 -2
- package/src/VisuallyHidden/index.ts +0 -2
- package/src/hooks/useInputStatusIcon.tsx +8 -8
- package/src/serverSafeComponents.test.ts +665 -0
- package/src/theme/defineTheme.ts +3 -0
- package/src/theme/generateThemeRules.test.ts +17 -0
- package/src/theme/index.ts +2 -0
- package/src/theme/tokens.stylex.ts +18 -0
- package/src/theme/tokens.test.ts +37 -0
- package/src/utils/focusOutline.stylex.ts +37 -23
- package/src/utils/index.ts +4 -0
- package/src/utils/interactionModality.ts +74 -0
|
@@ -33,6 +33,7 @@ import {usePopover} from '../Popover/usePopover';
|
|
|
33
33
|
import {useTooltip} from '../Tooltip';
|
|
34
34
|
import {Icon, renderIconSlot, type IconType} from '../Icon';
|
|
35
35
|
import {useIndicator} from '../Indicator';
|
|
36
|
+
import type {IndicatorPosition} from '../Indicator';
|
|
36
37
|
import type {IconName} from '../Icon';
|
|
37
38
|
import {
|
|
38
39
|
Field,
|
|
@@ -46,7 +47,7 @@ import {Divider} from '../Divider';
|
|
|
46
47
|
import {layerAnimations} from '../Layer/layerAnimations.stylex';
|
|
47
48
|
import type {LayerPlacement} from '../Layer/useLayer';
|
|
48
49
|
import {Spinner} from '../Spinner';
|
|
49
|
-
import {
|
|
50
|
+
import {PanelSearchInput} from '../Field/PanelSearchInput';
|
|
50
51
|
import {useAnnounce} from '../hooks/useAnnounce';
|
|
51
52
|
import {
|
|
52
53
|
colorVars,
|
|
@@ -76,6 +77,7 @@ import {useSize} from '../SizeContext/SizeContext';
|
|
|
76
77
|
import type {BaseProps} from '../BaseProps';
|
|
77
78
|
import type {SizeValue} from '../utils/types';
|
|
78
79
|
import {themeProps} from '../utils/themeProps';
|
|
80
|
+
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
79
81
|
import {stableClassName} from '../naming';
|
|
80
82
|
import {groupStyles} from '../InputGroup/groupStyles';
|
|
81
83
|
import {useInputGroup} from '../InputGroup/InputGroupContext';
|
|
@@ -179,14 +181,6 @@ const styles = stylex.create({
|
|
|
179
181
|
':focus-within': 'none',
|
|
180
182
|
},
|
|
181
183
|
fontWeight: fontWeightVars['--font-weight-medium'],
|
|
182
|
-
outline: {
|
|
183
|
-
default: 'none',
|
|
184
|
-
':has(:focus-visible)': `2px solid ${colorVars['--color-accent']}`,
|
|
185
|
-
},
|
|
186
|
-
outlineOffset: {
|
|
187
|
-
default: '0',
|
|
188
|
-
':has(:focus-visible)': '3px',
|
|
189
|
-
},
|
|
190
184
|
transitionProperty:
|
|
191
185
|
'background-image, background-color, color, opacity, transform',
|
|
192
186
|
transform: {
|
|
@@ -215,11 +209,6 @@ const styles = stylex.create({
|
|
|
215
209
|
color: 'inherit',
|
|
216
210
|
cursor: 'pointer',
|
|
217
211
|
borderRadius: radiusVars['--radius-element'],
|
|
218
|
-
outline: {
|
|
219
|
-
default: 'none',
|
|
220
|
-
':focus-visible': `${borderVars['--border-width']} solid ${colorVars['--color-accent']}`,
|
|
221
|
-
},
|
|
222
|
-
outlineOffset: 1,
|
|
223
212
|
},
|
|
224
213
|
|
|
225
214
|
// Dropdown container
|
|
@@ -237,6 +226,11 @@ const styles = stylex.create({
|
|
|
237
226
|
// pixel in the menu; the borderless ghost variant needs no correction.
|
|
238
227
|
paddingInline: `calc(${spacingVars['--spacing-1']} + ${borderVars['--border-width']})`,
|
|
239
228
|
},
|
|
229
|
+
// Same correction for the search row's gutter, so the search field and the
|
|
230
|
+
// option rows share one left edge.
|
|
231
|
+
searchRowInput: {
|
|
232
|
+
paddingInline: `calc(${spacingVars['--spacing-1']} + ${borderVars['--border-width']})`,
|
|
233
|
+
},
|
|
240
234
|
dropdownHidden: {
|
|
241
235
|
opacity: 0,
|
|
242
236
|
transition: 'none',
|
|
@@ -246,15 +240,6 @@ const styles = stylex.create({
|
|
|
246
240
|
popover: {
|
|
247
241
|
minWidth: 'anchor-size(width)',
|
|
248
242
|
},
|
|
249
|
-
// Search field. The inner TextInput owns the border, focus ring, magnifier
|
|
250
|
-
// (startIcon), and clear button (hasClear); this wrapper only supplies the
|
|
251
|
-
// dropdown's inline/block padding around it.
|
|
252
|
-
searchWrapper: {
|
|
253
|
-
display: 'flex',
|
|
254
|
-
alignItems: 'center',
|
|
255
|
-
paddingInline: spacingVars['--spacing-2'],
|
|
256
|
-
paddingBlock: spacingVars['--spacing-1'],
|
|
257
|
-
},
|
|
258
243
|
|
|
259
244
|
// Empty state
|
|
260
245
|
emptyState: {
|
|
@@ -265,9 +250,19 @@ const styles = stylex.create({
|
|
|
265
250
|
fontSize: typeScaleVars['--text-label-size'],
|
|
266
251
|
},
|
|
267
252
|
|
|
268
|
-
// Section
|
|
269
|
-
|
|
270
|
-
|
|
253
|
+
// Section heading. Plain secondary text, no rules — the same treatment
|
|
254
|
+
// DropdownMenu and CommandPaletteGroup already use for a group heading in a
|
|
255
|
+
// panel list. A labeled Divider (line–text–line) reads as a separator, and
|
|
256
|
+
// next to the search row's own divider it stacked two rules a few pixels
|
|
257
|
+
// apart.
|
|
258
|
+
sectionHeading: {
|
|
259
|
+
paddingBlock: spacingVars['--spacing-1'],
|
|
260
|
+
paddingInline: spacingVars['--spacing-2'],
|
|
261
|
+
fontFamily: typographyVars['--font-family-body'],
|
|
262
|
+
fontSize: typeScaleVars['--text-supporting-size'],
|
|
263
|
+
lineHeight: typeScaleVars['--text-supporting-leading'],
|
|
264
|
+
color: colorVars['--color-text-secondary'],
|
|
265
|
+
userSelect: 'none',
|
|
271
266
|
},
|
|
272
267
|
|
|
273
268
|
// Divider
|
|
@@ -301,6 +296,19 @@ const styles = stylex.create({
|
|
|
301
296
|
flex: 1,
|
|
302
297
|
minWidth: 0,
|
|
303
298
|
},
|
|
299
|
+
// The mark's column, reserved on every row and at either position, so a row
|
|
300
|
+
// occupies the same geometry whether or not it is the chosen one — the
|
|
301
|
+
// default check draws nothing when unchecked, and without the column a list
|
|
302
|
+
// would indent (or truncate) its chosen row differently from the rest.
|
|
303
|
+
// `minWidth` rather than `width`: a theme can replace `check` with a larger
|
|
304
|
+
// indicator (a radio is 20px at `sm`), and the column has to grow with it.
|
|
305
|
+
itemMarkColumn: {
|
|
306
|
+
display: 'inline-flex',
|
|
307
|
+
alignItems: 'center',
|
|
308
|
+
justifyContent: 'center',
|
|
309
|
+
flexShrink: 0,
|
|
310
|
+
minWidth: '1rem',
|
|
311
|
+
},
|
|
304
312
|
itemCheckmark: {
|
|
305
313
|
flexShrink: 0,
|
|
306
314
|
width: 16,
|
|
@@ -518,6 +526,16 @@ interface SelectorPropsBase<
|
|
|
518
526
|
*/
|
|
519
527
|
renderOption?: (option: SelectorOptionData) => ReactNode;
|
|
520
528
|
|
|
529
|
+
/**
|
|
530
|
+
* Which edge of the option row carries the selected mark. `start` reserves a
|
|
531
|
+
* mark column ahead of every label so they stay aligned, the way a native
|
|
532
|
+
* menu does; `end` is the house convention shared with Typeahead and
|
|
533
|
+
* CommandPalette.
|
|
534
|
+
*
|
|
535
|
+
* @default 'end'
|
|
536
|
+
*/
|
|
537
|
+
indicatorPosition?: IndicatorPosition;
|
|
538
|
+
|
|
521
539
|
/**
|
|
522
540
|
* Whether to show a search input for filtering options.
|
|
523
541
|
* @default false
|
|
@@ -668,6 +686,7 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
668
686
|
startIcon,
|
|
669
687
|
htmlName,
|
|
670
688
|
renderOption,
|
|
689
|
+
indicatorPosition = 'end',
|
|
671
690
|
hasSearch = false,
|
|
672
691
|
searchPlaceholder: searchPlaceholderFromProps,
|
|
673
692
|
placement,
|
|
@@ -706,7 +725,7 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
706
725
|
const inputGroup = useInputGroup();
|
|
707
726
|
|
|
708
727
|
const [searchQuery, setSearchQuery] = useState('');
|
|
709
|
-
// A typed query shows
|
|
728
|
+
// A typed query shows the search row's clear (✕) button, which becomes
|
|
710
729
|
// the next tab stop after the search input.
|
|
711
730
|
const hasQuery = searchQuery.length > 0;
|
|
712
731
|
|
|
@@ -797,6 +816,9 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
797
816
|
// The popup's own role="listbox" is the exposed semantics; the trigger
|
|
798
817
|
// keeps DOM focus, so wrapping it in a modal dialog would misrepresent it.
|
|
799
818
|
role: 'none',
|
|
819
|
+
// The theme target belongs on the SURFACE that paints the popup, which
|
|
820
|
+
// `usePopover` owns — not on the scrolling list inside it.
|
|
821
|
+
surfaceTarget: 'selector-popup',
|
|
800
822
|
});
|
|
801
823
|
|
|
802
824
|
// Open dropdown on mount when isDefaultOpen is true
|
|
@@ -988,11 +1010,40 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
988
1010
|
return null;
|
|
989
1011
|
}
|
|
990
1012
|
return (
|
|
991
|
-
<
|
|
992
|
-
{
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1013
|
+
<PanelSearchInput
|
|
1014
|
+
ref={searchRef}
|
|
1015
|
+
id={searchId}
|
|
1016
|
+
// The search row is the panel's header: a magnifier, a borderless
|
|
1017
|
+
// input, and the shared clear (✕) button. It deliberately does NOT
|
|
1018
|
+
// render a bordered TextInput — the popup is already a bordered
|
|
1019
|
+
// surface, and a field inside it drew a second box within that box.
|
|
1020
|
+
label={t('@astryx.selector.searchOptions')}
|
|
1021
|
+
// Same accessible name the TextInput's built-in clear produced
|
|
1022
|
+
// ("Clear Search options"), so the affordance keeps its name while its
|
|
1023
|
+
// chrome changes.
|
|
1024
|
+
clearLabel={t('@astryx.textInput.clearLabel', {
|
|
1025
|
+
label: t('@astryx.selector.searchOptions'),
|
|
1026
|
+
})}
|
|
1027
|
+
{...themeProps('selector-search')}
|
|
1028
|
+
xstyle={variant !== 'ghost' && styles.searchRowInput}
|
|
1029
|
+
// When hasSearch is set, focus moves into this input on open, so it —
|
|
1030
|
+
// not the trigger — must be the combobox that reports the highlighted
|
|
1031
|
+
// option via aria-activedescendant (comboboxes-4). A bare searchbox
|
|
1032
|
+
// left the highlight silent to screen readers.
|
|
1033
|
+
role="combobox"
|
|
1034
|
+
aria-expanded={popover.isOpen}
|
|
1035
|
+
aria-controls={listboxId}
|
|
1036
|
+
aria-autocomplete="list"
|
|
1037
|
+
aria-activedescendant={
|
|
1038
|
+
popover.isOpen && highlightedIndex >= 0
|
|
1039
|
+
? getItemId(highlightedIndex)
|
|
1040
|
+
: undefined
|
|
1041
|
+
}
|
|
1042
|
+
value={searchQuery}
|
|
1043
|
+
onValueChange={handleSearchChange}
|
|
1044
|
+
onContainerKeyDown={e => {
|
|
1045
|
+
// The clear (✕) button lives inside the row, after the input in DOM
|
|
1046
|
+
// order. When it is focused and the user tabs forward there is
|
|
996
1047
|
// nothing else in the popup, so dismiss it (Shift+Tab returns to the
|
|
997
1048
|
// input natively). Key events originating on the input are handled on
|
|
998
1049
|
// the input below; ignore them here so we don't double-dismiss.
|
|
@@ -1002,65 +1053,32 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1002
1053
|
if (e.key === 'Tab' && !e.shiftKey) {
|
|
1003
1054
|
onKeyDown(e);
|
|
1004
1055
|
}
|
|
1005
|
-
}}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
width="100%"
|
|
1022
|
-
// When hasSearch is set, focus moves into this input on open, so it —
|
|
1023
|
-
// not the trigger — must be the combobox that reports the highlighted
|
|
1024
|
-
// option via aria-activedescendant (comboboxes-4). A bare searchbox
|
|
1025
|
-
// left the highlight silent to screen readers. role + aria-* pass
|
|
1026
|
-
// through to the underlying <input> via BaseProps.
|
|
1027
|
-
role="combobox"
|
|
1028
|
-
aria-expanded={popover.isOpen}
|
|
1029
|
-
aria-controls={listboxId}
|
|
1030
|
-
aria-autocomplete="list"
|
|
1031
|
-
aria-activedescendant={
|
|
1032
|
-
popover.isOpen && highlightedIndex >= 0
|
|
1033
|
-
? getItemId(highlightedIndex)
|
|
1034
|
-
: undefined
|
|
1056
|
+
}}
|
|
1057
|
+
onKeyDown={e => {
|
|
1058
|
+
// Arrow keys navigate options; Enter selects; Escape closes.
|
|
1059
|
+
// Home/End are left to the input for caret movement (APG editable
|
|
1060
|
+
// combobox); PageUp/PageDown are the sanctioned substitute for
|
|
1061
|
+
// jumping to the first/last option.
|
|
1062
|
+
if (
|
|
1063
|
+
e.key === 'ArrowDown' ||
|
|
1064
|
+
e.key === 'ArrowUp' ||
|
|
1065
|
+
e.key === 'PageUp' ||
|
|
1066
|
+
e.key === 'PageDown' ||
|
|
1067
|
+
e.key === 'Enter' ||
|
|
1068
|
+
e.key === 'Escape'
|
|
1069
|
+
) {
|
|
1070
|
+
onKeyDown(e);
|
|
1071
|
+
return;
|
|
1035
1072
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
e.key === 'ArrowUp' ||
|
|
1046
|
-
e.key === 'PageUp' ||
|
|
1047
|
-
e.key === 'PageDown' ||
|
|
1048
|
-
e.key === 'Enter' ||
|
|
1049
|
-
e.key === 'Escape'
|
|
1050
|
-
) {
|
|
1051
|
-
onKeyDown(e);
|
|
1052
|
-
return;
|
|
1053
|
-
}
|
|
1054
|
-
// Tab: when a query is showing the clear (✕) button, forward-tab
|
|
1055
|
-
// moves focus to it (keeping the popup open) so the affordance is
|
|
1056
|
-
// keyboard-reachable. Every other Tab dismisses the popup as usual.
|
|
1057
|
-
if (e.key === 'Tab' && (e.shiftKey || !hasQuery)) {
|
|
1058
|
-
onKeyDown(e);
|
|
1059
|
-
}
|
|
1060
|
-
}}
|
|
1061
|
-
placeholder={searchPlaceholder}
|
|
1062
|
-
/>
|
|
1063
|
-
</div>
|
|
1073
|
+
// Tab: when a query is showing the clear (✕) button, forward-tab
|
|
1074
|
+
// moves focus to it (keeping the popup open) so the affordance is
|
|
1075
|
+
// keyboard-reachable. Every other Tab dismisses the popup as usual.
|
|
1076
|
+
if (e.key === 'Tab' && (e.shiftKey || !hasQuery)) {
|
|
1077
|
+
onKeyDown(e);
|
|
1078
|
+
}
|
|
1079
|
+
}}
|
|
1080
|
+
placeholder={searchPlaceholder}
|
|
1081
|
+
/>
|
|
1064
1082
|
);
|
|
1065
1083
|
}, [
|
|
1066
1084
|
hasSearch,
|
|
@@ -1074,6 +1092,7 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1074
1092
|
popover.isOpen,
|
|
1075
1093
|
highlightedIndex,
|
|
1076
1094
|
getItemId,
|
|
1095
|
+
variant,
|
|
1077
1096
|
t,
|
|
1078
1097
|
]);
|
|
1079
1098
|
|
|
@@ -1088,6 +1107,45 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1088
1107
|
const isHighlighted = flatIndex === highlightedIndex;
|
|
1089
1108
|
const isSelected = item.value === normalizedValue;
|
|
1090
1109
|
|
|
1110
|
+
/*
|
|
1111
|
+
* Rendered UNCONDITIONALLY, with the state passed down: the default
|
|
1112
|
+
* check draws nothing when unchecked, but a theme that replaces the
|
|
1113
|
+
* `check` indicator with a radio needs the unselected state to draw
|
|
1114
|
+
* its empty circle. `{isSelected && …}` would make that impossible.
|
|
1115
|
+
*
|
|
1116
|
+
* `selector-check` stays the stable target for the mark's position
|
|
1117
|
+
* in the row; the indicator owns what the mark looks like.
|
|
1118
|
+
*/
|
|
1119
|
+
const mark = (
|
|
1120
|
+
<span {...stylex.props(styles.itemMarkColumn)}>
|
|
1121
|
+
<SelectionMark
|
|
1122
|
+
state={isSelected ? 'checked' : 'unchecked'}
|
|
1123
|
+
size="sm"
|
|
1124
|
+
isDisabled={item.disabled ?? false}
|
|
1125
|
+
{...themeProps('selector-check')}
|
|
1126
|
+
/>
|
|
1127
|
+
</span>
|
|
1128
|
+
);
|
|
1129
|
+
|
|
1130
|
+
const optionContent = (
|
|
1131
|
+
<span {...stylex.props(styles.itemContent)}>
|
|
1132
|
+
{renderOption ? renderOption(item) : <DefaultOption option={item} />}
|
|
1133
|
+
</span>
|
|
1134
|
+
);
|
|
1135
|
+
|
|
1136
|
+
const content =
|
|
1137
|
+
indicatorPosition === 'start' ? (
|
|
1138
|
+
<>
|
|
1139
|
+
{mark}
|
|
1140
|
+
{optionContent}
|
|
1141
|
+
</>
|
|
1142
|
+
) : (
|
|
1143
|
+
<>
|
|
1144
|
+
{optionContent}
|
|
1145
|
+
{mark}
|
|
1146
|
+
</>
|
|
1147
|
+
);
|
|
1148
|
+
|
|
1091
1149
|
return (
|
|
1092
1150
|
<div
|
|
1093
1151
|
key={item.value}
|
|
@@ -1104,33 +1162,13 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1104
1162
|
isSelected && styles.itemSelected,
|
|
1105
1163
|
item.disabled && styles.itemDisabled,
|
|
1106
1164
|
)}>
|
|
1107
|
-
|
|
1108
|
-
{renderOption ? (
|
|
1109
|
-
renderOption(item)
|
|
1110
|
-
) : (
|
|
1111
|
-
<DefaultOption option={item} />
|
|
1112
|
-
)}
|
|
1113
|
-
</span>
|
|
1114
|
-
{/*
|
|
1115
|
-
* Rendered UNCONDITIONALLY, with the state passed down: the default
|
|
1116
|
-
* check draws nothing when unchecked, but a theme that replaces the
|
|
1117
|
-
* `check` indicator with a radio needs the unselected state to draw
|
|
1118
|
-
* its empty circle. `{isSelected && …}` would make that impossible.
|
|
1119
|
-
*
|
|
1120
|
-
* `selector-check` stays the stable target for the mark's position
|
|
1121
|
-
* in the row; the indicator owns what the mark looks like.
|
|
1122
|
-
*/}
|
|
1123
|
-
<SelectionMark
|
|
1124
|
-
state={isSelected ? 'checked' : 'unchecked'}
|
|
1125
|
-
size="sm"
|
|
1126
|
-
isDisabled={item.disabled ?? false}
|
|
1127
|
-
{...themeProps('selector-check')}
|
|
1128
|
-
/>
|
|
1165
|
+
{content}
|
|
1129
1166
|
</div>
|
|
1130
1167
|
);
|
|
1131
1168
|
},
|
|
1132
1169
|
[
|
|
1133
1170
|
renderOption,
|
|
1171
|
+
indicatorPosition,
|
|
1134
1172
|
highlightedIndex,
|
|
1135
1173
|
size,
|
|
1136
1174
|
normalizedValue,
|
|
@@ -1192,17 +1230,23 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1192
1230
|
if (sectionItems.length === 0) {
|
|
1193
1231
|
continue;
|
|
1194
1232
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
xstyle={styles.sectionDivider}
|
|
1201
|
-
/>,
|
|
1202
|
-
);
|
|
1203
|
-
}
|
|
1233
|
+
// The heading lives INSIDE the group and is aria-hidden: the group
|
|
1234
|
+
// already carries the title as its accessible name, so exposing the
|
|
1235
|
+
// text again would announce it twice. This also keeps role="listbox"'s
|
|
1236
|
+
// children to option/group only — the old labeled Divider sat in the
|
|
1237
|
+
// listbox as a stray role="separator".
|
|
1204
1238
|
elements.push(
|
|
1205
1239
|
<div key={`section-${i}`} role="group" aria-label={option.title}>
|
|
1240
|
+
{option.title && (
|
|
1241
|
+
<div
|
|
1242
|
+
aria-hidden="true"
|
|
1243
|
+
{...mergeProps(
|
|
1244
|
+
themeProps('selector-section-heading'),
|
|
1245
|
+
stylex.props(styles.sectionHeading),
|
|
1246
|
+
)}>
|
|
1247
|
+
{option.title}
|
|
1248
|
+
</div>
|
|
1249
|
+
)}
|
|
1206
1250
|
{sectionItems}
|
|
1207
1251
|
</div>,
|
|
1208
1252
|
);
|
|
@@ -1251,6 +1295,7 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1251
1295
|
styles.triggerContainer,
|
|
1252
1296
|
sizeStyles[size],
|
|
1253
1297
|
variant === 'ghost' && styles.triggerGhost,
|
|
1298
|
+
variant === 'ghost' && focusOutlineStyles.focusWithin,
|
|
1254
1299
|
isDisabled && inputWrapperStyles.disabled,
|
|
1255
1300
|
variant === 'ghost' && isDisabled && styles.triggerGhostDisabled,
|
|
1256
1301
|
!selectedItem && styles.triggerPlaceholder,
|
|
@@ -1338,7 +1383,10 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1338
1383
|
aria-label={t(STATUS_BUTTON_LABEL_KEY[status.type])}
|
|
1339
1384
|
aria-describedby={statusTooltip.describedBy}
|
|
1340
1385
|
onClick={e => e.stopPropagation()}
|
|
1341
|
-
{...stylex.props(
|
|
1386
|
+
{...stylex.props(
|
|
1387
|
+
focusOutlineStyles.focusVisible,
|
|
1388
|
+
styles.statusButton,
|
|
1389
|
+
)}>
|
|
1342
1390
|
<Icon
|
|
1343
1391
|
icon={STATUS_ICON_MAP[status.type]}
|
|
1344
1392
|
size="sm"
|
|
@@ -1383,6 +1431,12 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1383
1431
|
hasSearch ? (
|
|
1384
1432
|
<div>
|
|
1385
1433
|
{renderSearch()}
|
|
1434
|
+
{/*
|
|
1435
|
+
Separates the header from the options and spans the panel: the
|
|
1436
|
+
search row and the listbox each hold their own inline padding,
|
|
1437
|
+
the line does not, so it reads as the panel's own edge.
|
|
1438
|
+
*/}
|
|
1439
|
+
<Divider />
|
|
1386
1440
|
<div
|
|
1387
1441
|
ref={listboxRef}
|
|
1388
1442
|
id={listboxId}
|
|
@@ -1412,6 +1466,12 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1412
1466
|
{
|
|
1413
1467
|
placement: popoverPlacement,
|
|
1414
1468
|
alignment: 'start',
|
|
1469
|
+
// The system's standard menu clearance, except in overlay mode:
|
|
1470
|
+
// there the measured negative margin owns the block geometry and
|
|
1471
|
+
// the menu is meant to sit on the trigger, not clear it.
|
|
1472
|
+
offset: shouldOverlaySelectedItem
|
|
1473
|
+
? undefined
|
|
1474
|
+
: spacingVars['--spacing-1'],
|
|
1415
1475
|
xstyle: [styles.popover, layerAnimations[popoverPlacement]],
|
|
1416
1476
|
style: popoverOffsetStyle,
|
|
1417
1477
|
},
|
package/src/Skeleton/index.ts
CHANGED
package/src/Slider/Slider.tsx
CHANGED
|
@@ -41,6 +41,7 @@ import {useTooltip} from '../Tooltip';
|
|
|
41
41
|
import {VisuallyHidden} from '../VisuallyHidden';
|
|
42
42
|
import type {InputStatus} from '../Field/types';
|
|
43
43
|
import {mergeProps, mergeRefs, rtlStyles} from '../utils';
|
|
44
|
+
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
44
45
|
import {isRtlElement} from '../hooks/isRtlElement';
|
|
45
46
|
import type {BaseProps} from '../BaseProps';
|
|
46
47
|
import type {SizeValue} from '../utils/types';
|
|
@@ -253,16 +254,6 @@ const styles = stylex.create({
|
|
|
253
254
|
},
|
|
254
255
|
},
|
|
255
256
|
},
|
|
256
|
-
thumbFocusVisible: {
|
|
257
|
-
outline: {
|
|
258
|
-
default: 'none',
|
|
259
|
-
':focus-visible': `2px solid ${colorVars['--color-accent']}`,
|
|
260
|
-
},
|
|
261
|
-
outlineOffset: {
|
|
262
|
-
default: '0',
|
|
263
|
-
':focus-visible': '2px',
|
|
264
|
-
},
|
|
265
|
-
},
|
|
266
257
|
thumbDisabled: {
|
|
267
258
|
backgroundColor: colorVars['--color-background-muted'],
|
|
268
259
|
cursor: 'not-allowed',
|
|
@@ -792,7 +783,7 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
792
783
|
? styles.thumbHorizontal
|
|
793
784
|
: rtlStyles.centerInline('50%'),
|
|
794
785
|
!isDisabled && styles.thumbHover,
|
|
795
|
-
!isDisabled &&
|
|
786
|
+
!isDisabled && focusOutlineStyles.focusVisible,
|
|
796
787
|
isDisabled && styles.thumbDisabled,
|
|
797
788
|
),
|
|
798
789
|
undefined,
|
package/src/Switch/Switch.tsx
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
easeVars,
|
|
34
34
|
typographyVars,
|
|
35
35
|
typeScaleVars,
|
|
36
|
+
focusVars,
|
|
36
37
|
} from '../theme/tokens.stylex';
|
|
37
38
|
import {FieldLabel} from '../Field/FieldLabel';
|
|
38
39
|
import {FieldStatus} from '../FieldStatus/FieldStatus';
|
|
@@ -189,15 +190,22 @@ const styles = stylex.create({
|
|
|
189
190
|
'@media (forced-colors: active)': 'CanvasText',
|
|
190
191
|
},
|
|
191
192
|
},
|
|
193
|
+
// The one ring in the system not drawn by focusOutlineStyles. The focusable
|
|
194
|
+
// input is a sibling of the track, so the condition has to reach the shared
|
|
195
|
+
// scope marker — and a marker cannot be shared across components without
|
|
196
|
+
// leaking focus state from an outer one, so it cannot live in the utility.
|
|
197
|
+
// StyleX also cannot inline a constant imported from another module, so the
|
|
198
|
+
// values are read from the tokens the utility reads.
|
|
192
199
|
trackFocus: {
|
|
193
200
|
outline: {
|
|
194
201
|
default: 'none',
|
|
195
202
|
[stylex.when.ancestor(':has(:focus-visible)', switchScope)]:
|
|
196
|
-
|
|
203
|
+
`${focusVars['--focus-outline-width']} ${focusVars['--focus-outline-style']} ${focusVars['--focus-outline-color']}`,
|
|
197
204
|
},
|
|
198
205
|
outlineOffset: {
|
|
199
206
|
default: null,
|
|
200
|
-
[stylex.when.ancestor(':has(:focus-visible)', switchScope)]:
|
|
207
|
+
[stylex.when.ancestor(':has(:focus-visible)', switchScope)]:
|
|
208
|
+
focusVars['--focus-outline-offset'],
|
|
201
209
|
},
|
|
202
210
|
},
|
|
203
211
|
// State-dependent colors with ancestor hover behavior
|
package/src/Table/Table.test.tsx
CHANGED
|
@@ -496,6 +496,37 @@ describe('BaseTable', () => {
|
|
|
496
496
|
});
|
|
497
497
|
});
|
|
498
498
|
|
|
499
|
+
describe('TableRow styling props', () => {
|
|
500
|
+
it('applies className and style to the row inside a Table', () => {
|
|
501
|
+
render(
|
|
502
|
+
<Table>
|
|
503
|
+
<tbody>
|
|
504
|
+
<TableRow className="custom-row" style={{opacity: 0.9}}>
|
|
505
|
+
<TableCell>Cell</TableCell>
|
|
506
|
+
</TableRow>
|
|
507
|
+
</tbody>
|
|
508
|
+
</Table>,
|
|
509
|
+
);
|
|
510
|
+
const row = screen.getByRole('row');
|
|
511
|
+
expect(row.className).toContain('custom-row');
|
|
512
|
+
expect(row.className).toContain('astryx-table-row');
|
|
513
|
+
expect(row.style.opacity).toBe('0.9');
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('applies className to a standalone row (no table context)', () => {
|
|
517
|
+
render(
|
|
518
|
+
<table>
|
|
519
|
+
<tbody>
|
|
520
|
+
<TableRow className="custom-row">
|
|
521
|
+
<td>Cell</td>
|
|
522
|
+
</TableRow>
|
|
523
|
+
</tbody>
|
|
524
|
+
</table>,
|
|
525
|
+
);
|
|
526
|
+
expect(screen.getByRole('row').className).toContain('custom-row');
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
499
530
|
describe('plugin pipeline', () => {
|
|
500
531
|
it('applies transformTable plugin', () => {
|
|
501
532
|
const plugin: TablePlugin<User> = {
|
package/src/Table/TableRow.tsx
CHANGED
|
@@ -120,6 +120,8 @@ export function TableRow({
|
|
|
120
120
|
xstyle,
|
|
121
121
|
ref,
|
|
122
122
|
isHeaderRow = false,
|
|
123
|
+
className: incomingClassName,
|
|
124
|
+
style: incomingStyle,
|
|
123
125
|
...props
|
|
124
126
|
}: TableRowProps) {
|
|
125
127
|
const ctx = use(TableContext);
|
|
@@ -132,6 +134,8 @@ export function TableRow({
|
|
|
132
134
|
{...mergeProps(
|
|
133
135
|
themeProps('table-row'),
|
|
134
136
|
stylex.props(tableRowMarker, xstyle),
|
|
137
|
+
incomingClassName,
|
|
138
|
+
incomingStyle,
|
|
135
139
|
)}>
|
|
136
140
|
{children}
|
|
137
141
|
</tr>
|
|
@@ -169,6 +173,8 @@ export function TableRow({
|
|
|
169
173
|
{...mergeProps(
|
|
170
174
|
themeProps('table-row'),
|
|
171
175
|
stylex.props(tableRowMarker, ...rowStyles),
|
|
176
|
+
incomingClassName,
|
|
177
|
+
incomingStyle,
|
|
172
178
|
)}>
|
|
173
179
|
{children}
|
|
174
180
|
</tr>
|