@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
|
@@ -42,8 +42,9 @@ import {
|
|
|
42
42
|
} from '../Field';
|
|
43
43
|
import {Divider} from '../Divider';
|
|
44
44
|
import {Spinner} from '../Spinner';
|
|
45
|
-
import {
|
|
45
|
+
import {PanelSearchInput} from '../Field/PanelSearchInput';
|
|
46
46
|
import {CheckboxInput} from '../CheckboxInput';
|
|
47
|
+
import type {IndicatorPosition} from '../Indicator';
|
|
47
48
|
import {Badge} from '../Badge';
|
|
48
49
|
import {
|
|
49
50
|
colorVars,
|
|
@@ -76,6 +77,7 @@ import type {BaseProps} from '../BaseProps';
|
|
|
76
77
|
import type {SizeValue} from '../utils/types';
|
|
77
78
|
import {useSize} from '../SizeContext/SizeContext';
|
|
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';
|
|
@@ -200,14 +202,6 @@ const styles = stylex.create({
|
|
|
200
202
|
':focus-within': 'none',
|
|
201
203
|
},
|
|
202
204
|
fontWeight: fontWeightVars['--font-weight-medium'],
|
|
203
|
-
outline: {
|
|
204
|
-
default: 'none',
|
|
205
|
-
':has(:focus-visible)': `2px solid ${colorVars['--color-accent']}`,
|
|
206
|
-
},
|
|
207
|
-
outlineOffset: {
|
|
208
|
-
default: '0',
|
|
209
|
-
':has(:focus-visible)': '3px',
|
|
210
|
-
},
|
|
211
205
|
transitionProperty:
|
|
212
206
|
'background-image, background-color, color, opacity, transform',
|
|
213
207
|
transform: {
|
|
@@ -236,11 +230,6 @@ const styles = stylex.create({
|
|
|
236
230
|
color: 'inherit',
|
|
237
231
|
cursor: 'pointer',
|
|
238
232
|
borderRadius: radiusVars['--radius-element'],
|
|
239
|
-
outline: {
|
|
240
|
-
default: 'none',
|
|
241
|
-
':focus-visible': `${borderVars['--border-width']} solid ${colorVars['--color-accent']}`,
|
|
242
|
-
},
|
|
243
|
-
outlineOffset: 1,
|
|
244
233
|
},
|
|
245
234
|
|
|
246
235
|
// Dropdown container
|
|
@@ -256,16 +245,6 @@ const styles = stylex.create({
|
|
|
256
245
|
minWidth: 'anchor-size(width)',
|
|
257
246
|
},
|
|
258
247
|
|
|
259
|
-
// Search field. The inner TextInput owns the border, focus ring, magnifier
|
|
260
|
-
// (startIcon), and clear button (hasClear); this wrapper only supplies the
|
|
261
|
-
// dropdown's inline/block padding around it.
|
|
262
|
-
searchWrapper: {
|
|
263
|
-
display: 'flex',
|
|
264
|
-
alignItems: 'center',
|
|
265
|
-
paddingInline: spacingVars['--spacing-2'],
|
|
266
|
-
paddingBlock: spacingVars['--spacing-1'],
|
|
267
|
-
},
|
|
268
|
-
|
|
269
248
|
// Select-all wrapper
|
|
270
249
|
selectAllWrapper: {
|
|
271
250
|
display: 'flex',
|
|
@@ -274,9 +253,19 @@ const styles = stylex.create({
|
|
|
274
253
|
cursor: 'pointer',
|
|
275
254
|
},
|
|
276
255
|
|
|
277
|
-
// Section
|
|
278
|
-
|
|
279
|
-
|
|
256
|
+
// Section heading. Plain secondary text, no rules — the same treatment
|
|
257
|
+
// DropdownMenu and CommandPaletteGroup already use for a group heading in a
|
|
258
|
+
// panel list. A labeled Divider (line–text–line) reads as a separator, and
|
|
259
|
+
// next to the search row's own divider it stacked two rules a few pixels
|
|
260
|
+
// apart.
|
|
261
|
+
sectionHeading: {
|
|
262
|
+
paddingBlock: spacingVars['--spacing-1'],
|
|
263
|
+
paddingInline: spacingVars['--spacing-2'],
|
|
264
|
+
fontFamily: typographyVars['--font-family-body'],
|
|
265
|
+
fontSize: typeScaleVars['--text-supporting-size'],
|
|
266
|
+
lineHeight: typeScaleVars['--text-supporting-leading'],
|
|
267
|
+
color: colorVars['--color-text-secondary'],
|
|
268
|
+
userSelect: 'none',
|
|
280
269
|
},
|
|
281
270
|
|
|
282
271
|
// Divider
|
|
@@ -320,6 +309,14 @@ const styles = stylex.create({
|
|
|
320
309
|
display: 'flex',
|
|
321
310
|
flexShrink: 0,
|
|
322
311
|
},
|
|
312
|
+
// Pushed to the row's far edge rather than sitting against the label, which
|
|
313
|
+
// is what an end-positioned control means here. The row is not
|
|
314
|
+
// `space-between` (a truncating label plus a trailing control is what wants
|
|
315
|
+
// the auto margin), and `renderOption` content is not wrapped in a growing
|
|
316
|
+
// span, so the margin has to live on the checkbox itself.
|
|
317
|
+
checkboxDecorativeEnd: {
|
|
318
|
+
marginInlineStart: 'auto',
|
|
319
|
+
},
|
|
323
320
|
|
|
324
321
|
// Label text for items (rendered outside checkbox for correct click
|
|
325
322
|
// behavior). Typography is inherited from the row; this only handles
|
|
@@ -607,6 +604,13 @@ export interface MultiSelectorProps<
|
|
|
607
604
|
*/
|
|
608
605
|
renderOption?: (option: MultiSelectorOptionData) => ReactNode;
|
|
609
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Which edge of the option row carries the checkbox.
|
|
609
|
+
*
|
|
610
|
+
* @default 'start'
|
|
611
|
+
*/
|
|
612
|
+
indicatorPosition?: IndicatorPosition;
|
|
613
|
+
|
|
610
614
|
/**
|
|
611
615
|
* Whether the dropdown starts open on mount.
|
|
612
616
|
* Useful for showcases and previews.
|
|
@@ -692,6 +696,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
692
696
|
triggerDisplay = 'count',
|
|
693
697
|
maxBadges = 3,
|
|
694
698
|
renderOption,
|
|
699
|
+
indicatorPosition = 'start',
|
|
695
700
|
isDefaultOpen = false,
|
|
696
701
|
'data-testid': testId,
|
|
697
702
|
htmlName,
|
|
@@ -724,7 +729,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
724
729
|
const inputGroup = useInputGroup();
|
|
725
730
|
|
|
726
731
|
const [searchQuery, setSearchQuery] = useState('');
|
|
727
|
-
// A typed query shows
|
|
732
|
+
// A typed query shows the search row's clear (✕) button, which becomes
|
|
728
733
|
// the next tab stop after the search input.
|
|
729
734
|
const hasQuery = searchQuery.length > 0;
|
|
730
735
|
|
|
@@ -869,6 +874,9 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
869
874
|
// The popup's own role="listbox" is the exposed semantics; the trigger
|
|
870
875
|
// keeps DOM focus, so wrapping it in a modal dialog would misrepresent it.
|
|
871
876
|
role: 'none',
|
|
877
|
+
// The theme target belongs on the SURFACE that paints the popup, which
|
|
878
|
+
// `usePopover` owns — not on the scrolling list inside it.
|
|
879
|
+
surfaceTarget: 'multi-selector-popup',
|
|
872
880
|
});
|
|
873
881
|
|
|
874
882
|
// Open dropdown on mount when isDefaultOpen is true
|
|
@@ -1131,11 +1139,38 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1131
1139
|
return null;
|
|
1132
1140
|
}
|
|
1133
1141
|
return (
|
|
1134
|
-
<
|
|
1135
|
-
{
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1142
|
+
<PanelSearchInput
|
|
1143
|
+
ref={searchRef}
|
|
1144
|
+
id={searchId}
|
|
1145
|
+
// The search row is the panel's header: a magnifier, a borderless
|
|
1146
|
+
// input, and the shared clear (✕) button. It deliberately does NOT
|
|
1147
|
+
// render a bordered TextInput — the popup is already a bordered
|
|
1148
|
+
// surface, and a field inside it drew a second box within that box.
|
|
1149
|
+
label={t('@astryx.multiSelector.searchOptions')}
|
|
1150
|
+
// Same accessible name the TextInput's built-in clear produced
|
|
1151
|
+
// ("Clear Search options"), so the affordance keeps its name while its
|
|
1152
|
+
// chrome changes.
|
|
1153
|
+
clearLabel={t('@astryx.textInput.clearLabel', {
|
|
1154
|
+
label: t('@astryx.multiSelector.searchOptions'),
|
|
1155
|
+
})}
|
|
1156
|
+
{...themeProps('multi-selector-search')}
|
|
1157
|
+
// When hasSearch is set, focus moves into this input on open, so it —
|
|
1158
|
+
// not the trigger — must be the combobox reporting the highlighted
|
|
1159
|
+
// option via aria-activedescendant (comboboxes-4).
|
|
1160
|
+
role="combobox"
|
|
1161
|
+
aria-expanded={popover.isOpen}
|
|
1162
|
+
aria-controls={listboxId}
|
|
1163
|
+
aria-autocomplete="list"
|
|
1164
|
+
aria-activedescendant={
|
|
1165
|
+
popover.isOpen && highlightedIndex >= 0
|
|
1166
|
+
? getItemId(highlightedIndex)
|
|
1167
|
+
: undefined
|
|
1168
|
+
}
|
|
1169
|
+
value={searchQuery}
|
|
1170
|
+
onValueChange={handleSearchChange}
|
|
1171
|
+
onContainerKeyDown={e => {
|
|
1172
|
+
// The clear (✕) button lives inside the row, after the input in DOM
|
|
1173
|
+
// order. When it is focused and the user tabs forward there is
|
|
1139
1174
|
// nothing else in the popup, so dismiss it (Shift+Tab returns to the
|
|
1140
1175
|
// input natively). Key events originating on the input are handled on
|
|
1141
1176
|
// the input below; ignore them here so we don't double-dismiss.
|
|
@@ -1145,64 +1180,32 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1145
1180
|
if (e.key === 'Tab' && !e.shiftKey) {
|
|
1146
1181
|
onKeyDown(e);
|
|
1147
1182
|
}
|
|
1148
|
-
}}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
//
|
|
1153
|
-
//
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
width="100%"
|
|
1165
|
-
// When hasSearch is set, focus moves into this input on open, so it —
|
|
1166
|
-
// not the trigger — must be the combobox reporting the highlighted
|
|
1167
|
-
// option via aria-activedescendant (comboboxes-4). role + aria-* pass
|
|
1168
|
-
// through to the underlying <input> via BaseProps.
|
|
1169
|
-
role="combobox"
|
|
1170
|
-
aria-expanded={popover.isOpen}
|
|
1171
|
-
aria-controls={listboxId}
|
|
1172
|
-
aria-autocomplete="list"
|
|
1173
|
-
aria-activedescendant={
|
|
1174
|
-
popover.isOpen && highlightedIndex >= 0
|
|
1175
|
-
? getItemId(highlightedIndex)
|
|
1176
|
-
: undefined
|
|
1183
|
+
}}
|
|
1184
|
+
onKeyDown={e => {
|
|
1185
|
+
// Arrow keys navigate options; Enter toggles; Escape closes.
|
|
1186
|
+
// Space and Home/End are left to the input (type a space / move
|
|
1187
|
+
// the caret) per the APG editable combobox; PageUp/PageDown are
|
|
1188
|
+
// the sanctioned substitute for jumping to the first/last option.
|
|
1189
|
+
if (
|
|
1190
|
+
e.key === 'ArrowDown' ||
|
|
1191
|
+
e.key === 'ArrowUp' ||
|
|
1192
|
+
e.key === 'PageUp' ||
|
|
1193
|
+
e.key === 'PageDown' ||
|
|
1194
|
+
e.key === 'Enter' ||
|
|
1195
|
+
e.key === 'Escape'
|
|
1196
|
+
) {
|
|
1197
|
+
onKeyDown(e);
|
|
1198
|
+
return;
|
|
1177
1199
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
e.key === 'ArrowUp' ||
|
|
1188
|
-
e.key === 'PageUp' ||
|
|
1189
|
-
e.key === 'PageDown' ||
|
|
1190
|
-
e.key === 'Enter' ||
|
|
1191
|
-
e.key === 'Escape'
|
|
1192
|
-
) {
|
|
1193
|
-
onKeyDown(e);
|
|
1194
|
-
return;
|
|
1195
|
-
}
|
|
1196
|
-
// Tab: when a query is showing the clear (✕) button, forward-tab
|
|
1197
|
-
// moves focus to it (keeping the popup open) so the affordance is
|
|
1198
|
-
// keyboard-reachable. Every other Tab dismisses the popup as usual.
|
|
1199
|
-
if (e.key === 'Tab' && (e.shiftKey || !hasQuery)) {
|
|
1200
|
-
onKeyDown(e);
|
|
1201
|
-
}
|
|
1202
|
-
}}
|
|
1203
|
-
placeholder={searchPlaceholder}
|
|
1204
|
-
/>
|
|
1205
|
-
</div>
|
|
1200
|
+
// Tab: when a query is showing the clear (✕) button, forward-tab
|
|
1201
|
+
// moves focus to it (keeping the popup open) so the affordance is
|
|
1202
|
+
// keyboard-reachable. Every other Tab dismisses the popup as usual.
|
|
1203
|
+
if (e.key === 'Tab' && (e.shiftKey || !hasQuery)) {
|
|
1204
|
+
onKeyDown(e);
|
|
1205
|
+
}
|
|
1206
|
+
}}
|
|
1207
|
+
placeholder={searchPlaceholder}
|
|
1208
|
+
/>
|
|
1206
1209
|
);
|
|
1207
1210
|
}, [
|
|
1208
1211
|
hasSearch,
|
|
@@ -1234,6 +1237,24 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1234
1237
|
const isPartiallySelected =
|
|
1235
1238
|
isSelectAll && selectAllState === 'indeterminate';
|
|
1236
1239
|
|
|
1240
|
+
const checkbox = (
|
|
1241
|
+
<div
|
|
1242
|
+
inert
|
|
1243
|
+
{...stylex.props(
|
|
1244
|
+
styles.checkboxDecorative,
|
|
1245
|
+
indicatorPosition === 'end' && styles.checkboxDecorativeEnd,
|
|
1246
|
+
)}>
|
|
1247
|
+
<CheckboxInput
|
|
1248
|
+
label=""
|
|
1249
|
+
isLabelHidden
|
|
1250
|
+
value={checkboxValue}
|
|
1251
|
+
onChange={() => {}}
|
|
1252
|
+
isDisabled={item.disabled}
|
|
1253
|
+
size={size === 'lg' ? 'md' : size}
|
|
1254
|
+
/>
|
|
1255
|
+
</div>
|
|
1256
|
+
);
|
|
1257
|
+
|
|
1237
1258
|
return (
|
|
1238
1259
|
<div
|
|
1239
1260
|
key={item.value}
|
|
@@ -1273,16 +1294,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1273
1294
|
item.disabled && styles.itemDisabled,
|
|
1274
1295
|
),
|
|
1275
1296
|
)}>
|
|
1276
|
-
|
|
1277
|
-
<CheckboxInput
|
|
1278
|
-
label=""
|
|
1279
|
-
isLabelHidden
|
|
1280
|
-
value={checkboxValue}
|
|
1281
|
-
onChange={() => {}}
|
|
1282
|
-
isDisabled={item.disabled}
|
|
1283
|
-
size={size === 'lg' ? 'md' : size}
|
|
1284
|
-
/>
|
|
1285
|
-
</div>
|
|
1297
|
+
{indicatorPosition === 'start' && checkbox}
|
|
1286
1298
|
{renderOption && !isSelectAll ? (
|
|
1287
1299
|
renderOption(item)
|
|
1288
1300
|
) : (
|
|
@@ -1290,11 +1302,13 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1290
1302
|
{item.label ?? item.value}
|
|
1291
1303
|
</span>
|
|
1292
1304
|
)}
|
|
1305
|
+
{indicatorPosition === 'end' && checkbox}
|
|
1293
1306
|
</div>
|
|
1294
1307
|
);
|
|
1295
1308
|
},
|
|
1296
1309
|
[
|
|
1297
1310
|
renderOption,
|
|
1311
|
+
indicatorPosition,
|
|
1298
1312
|
highlightedIndex,
|
|
1299
1313
|
optimisticValue,
|
|
1300
1314
|
allEnabledSelected,
|
|
@@ -1320,12 +1334,11 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1320
1334
|
? sortedItems.length - 1
|
|
1321
1335
|
: sortedItems.length;
|
|
1322
1336
|
|
|
1323
|
-
// Show select-all only when there are real items to select
|
|
1337
|
+
// Show select-all only when there are real items to select. It reads as
|
|
1338
|
+
// the first row of the list, not a section of its own — no divider under
|
|
1339
|
+
// it (the checkbox column already lines it up with the options below).
|
|
1324
1340
|
if (hasSelectAll && realItemCount > 0) {
|
|
1325
1341
|
elements.push(renderItem(sortedItems[0], 0));
|
|
1326
|
-
elements.push(
|
|
1327
|
-
<Divider key="select-all-divider" xstyle={styles.divider} />,
|
|
1328
|
-
);
|
|
1329
1342
|
cursor = 1;
|
|
1330
1343
|
} else if (hasSelectAll) {
|
|
1331
1344
|
// Skip the select-all sentinel when there are no real items
|
|
@@ -1394,17 +1407,23 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1394
1407
|
sectionItems.push(renderItem(sortedItems[cursor], cursor));
|
|
1395
1408
|
cursor++;
|
|
1396
1409
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
xstyle={styles.sectionDivider}
|
|
1403
|
-
/>,
|
|
1404
|
-
);
|
|
1405
|
-
}
|
|
1410
|
+
// The heading lives INSIDE the group and is aria-hidden: the group
|
|
1411
|
+
// already carries the title as its accessible name, so exposing the
|
|
1412
|
+
// text again would announce it twice. This also keeps role="listbox"'s
|
|
1413
|
+
// children to option/group only — the old labeled Divider sat in the
|
|
1414
|
+
// listbox as a stray role="separator".
|
|
1406
1415
|
elements.push(
|
|
1407
1416
|
<div key={`section-${i}`} role="group" aria-label={option.title}>
|
|
1417
|
+
{option.title && (
|
|
1418
|
+
<div
|
|
1419
|
+
aria-hidden="true"
|
|
1420
|
+
{...mergeProps(
|
|
1421
|
+
themeProps('multi-selector-section-heading'),
|
|
1422
|
+
stylex.props(styles.sectionHeading),
|
|
1423
|
+
)}>
|
|
1424
|
+
{option.title}
|
|
1425
|
+
</div>
|
|
1426
|
+
)}
|
|
1408
1427
|
{sectionItems}
|
|
1409
1428
|
</div>,
|
|
1410
1429
|
);
|
|
@@ -1453,6 +1472,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1453
1472
|
styles.triggerContainer,
|
|
1454
1473
|
sizeStyles[size],
|
|
1455
1474
|
variant === 'ghost' && styles.triggerGhost,
|
|
1475
|
+
variant === 'ghost' && focusOutlineStyles.focusWithin,
|
|
1456
1476
|
isDisabled && inputWrapperStyles.disabled,
|
|
1457
1477
|
variant === 'ghost' && isDisabled && styles.triggerGhostDisabled,
|
|
1458
1478
|
optimisticValue.length === 0 && styles.triggerPlaceholder,
|
|
@@ -1541,7 +1561,10 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1541
1561
|
aria-label={t(STATUS_BUTTON_LABEL_KEY[status.type])}
|
|
1542
1562
|
aria-describedby={statusTooltip.describedBy}
|
|
1543
1563
|
onClick={e => e.stopPropagation()}
|
|
1544
|
-
{...stylex.props(
|
|
1564
|
+
{...stylex.props(
|
|
1565
|
+
focusOutlineStyles.focusVisible,
|
|
1566
|
+
styles.statusButton,
|
|
1567
|
+
)}>
|
|
1545
1568
|
<Icon
|
|
1546
1569
|
icon={STATUS_ICON_MAP[status.type]}
|
|
1547
1570
|
size="sm"
|
|
@@ -1583,20 +1606,40 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1583
1606
|
</div>
|
|
1584
1607
|
|
|
1585
1608
|
{popover.render(
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1609
|
+
hasSearch ? (
|
|
1610
|
+
// With a search row the panel splits: the header stays put while the
|
|
1611
|
+
// options scroll under it, so the field does not slide out of reach
|
|
1612
|
+
// in a long list. Without one the panel is a single scroll container,
|
|
1613
|
+
// exactly as before.
|
|
1614
|
+
<div>
|
|
1615
|
+
{renderSearch()}
|
|
1616
|
+
{/*
|
|
1617
|
+
Separates the header from the options and spans the panel: the
|
|
1618
|
+
search row and the option list each hold their own inline
|
|
1619
|
+
padding, the line does not, so it reads as the panel's own edge.
|
|
1620
|
+
*/}
|
|
1621
|
+
<Divider />
|
|
1622
|
+
<div {...stylex.props(styles.dropdown)}>
|
|
1623
|
+
<div
|
|
1624
|
+
id={listboxId}
|
|
1625
|
+
role="listbox"
|
|
1626
|
+
aria-multiselectable="true"
|
|
1627
|
+
aria-labelledby={triggerId}>
|
|
1628
|
+
{renderOptions()}
|
|
1629
|
+
</div>
|
|
1630
|
+
</div>
|
|
1598
1631
|
</div>
|
|
1599
|
-
|
|
1632
|
+
) : (
|
|
1633
|
+
<div {...stylex.props(styles.dropdown)}>
|
|
1634
|
+
<div
|
|
1635
|
+
id={listboxId}
|
|
1636
|
+
role="listbox"
|
|
1637
|
+
aria-multiselectable="true"
|
|
1638
|
+
aria-labelledby={triggerId}>
|
|
1639
|
+
{renderOptions()}
|
|
1640
|
+
</div>
|
|
1641
|
+
</div>
|
|
1642
|
+
),
|
|
1600
1643
|
{
|
|
1601
1644
|
placement: 'below',
|
|
1602
1645
|
alignment: 'start',
|
|
@@ -133,6 +133,7 @@ export const docs = {
|
|
|
133
133
|
theming: {
|
|
134
134
|
targets: [
|
|
135
135
|
{className: 'astryx-popover'},
|
|
136
|
+
{className: 'astryx-popover-surface'},
|
|
136
137
|
],
|
|
137
138
|
vars: [
|
|
138
139
|
{name: '--_popover-radius', description: 'Border radius of the popover', default: 'var(--radius-element)', private: true},
|
|
@@ -277,6 +278,7 @@ export const docsZh = {
|
|
|
277
278
|
theming: {
|
|
278
279
|
targets: [
|
|
279
280
|
{className: 'astryx-popover'},
|
|
281
|
+
{className: 'astryx-popover-surface'},
|
|
280
282
|
],
|
|
281
283
|
vars: [
|
|
282
284
|
{name: '--_popover-radius', description: 'Border radius of the popover', default: 'var(--radius-element)', private: true},
|
|
@@ -30,6 +30,9 @@ import {Button} from '../Button';
|
|
|
30
30
|
import {rtlStyles} from '../utils';
|
|
31
31
|
import {useTranslator} from '../i18n';
|
|
32
32
|
import {useDevWarning} from '../hooks/useDevWarning';
|
|
33
|
+
import {mergeProps} from '../utils/mergeProps';
|
|
34
|
+
import {themeProps} from '../utils/themeProps';
|
|
35
|
+
import {stableClassName} from '../naming';
|
|
33
36
|
|
|
34
37
|
const styles = stylex.create({
|
|
35
38
|
// Default popover surface — background, radius, shadow.
|
|
@@ -187,6 +190,22 @@ export interface UsePopoverOptions {
|
|
|
187
190
|
* @default true
|
|
188
191
|
*/
|
|
189
192
|
hasSurface?: boolean;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Theme-target name stamped on the popup SURFACE — the element that paints
|
|
196
|
+
* the background, radius and elevation — without the `astryx-` prefix
|
|
197
|
+
* (e.g. `'complex-selector-popup'`).
|
|
198
|
+
*
|
|
199
|
+
* The surface is created here, not by the calling component, so a component
|
|
200
|
+
* that wants its popup themeable cannot reach it on its own: a target it
|
|
201
|
+
* renders itself lands on its content INSIDE the surface, where a background
|
|
202
|
+
* or radius rule paints the wrong box. Name the surface through this option
|
|
203
|
+
* and document the class in the component's `theming.targets`.
|
|
204
|
+
*
|
|
205
|
+
* The shared `astryx-popover-surface` class is always present alongside it,
|
|
206
|
+
* so a theme can style every popup surface at once.
|
|
207
|
+
*/
|
|
208
|
+
surfaceTarget?: string;
|
|
190
209
|
}
|
|
191
210
|
|
|
192
211
|
/**
|
|
@@ -312,6 +331,7 @@ export function usePopover(options: UsePopoverOptions = {}): UsePopoverReturn {
|
|
|
312
331
|
hasEscapeDismiss = true,
|
|
313
332
|
hasAutoFocus = true,
|
|
314
333
|
hasSurface = true,
|
|
334
|
+
surfaceTarget,
|
|
315
335
|
hasCloseButton = true,
|
|
316
336
|
closeButtonLabel: closeButtonLabelFromProps,
|
|
317
337
|
dialogLabel,
|
|
@@ -406,16 +426,28 @@ export function usePopover(options: UsePopoverOptions = {}): UsePopoverReturn {
|
|
|
406
426
|
// Wrapped render function that includes surface styles and optional hidden close button
|
|
407
427
|
const render = useCallback(
|
|
408
428
|
(children: ReactNode, props?: ContextRenderProps): ReactNode => {
|
|
429
|
+
// `mergeProps` is positional — a third OBJECT argument is read as
|
|
430
|
+
// `style`, not as more props — so the surface's classes are composed
|
|
431
|
+
// into one props object before merging with the StyleX result.
|
|
432
|
+
const surfaceProps = themeProps('popover-surface');
|
|
433
|
+
const surfaceClassName =
|
|
434
|
+
surfaceTarget != null
|
|
435
|
+
? `${surfaceProps.className} ${stableClassName(surfaceTarget)}`
|
|
436
|
+
: surfaceProps.className;
|
|
437
|
+
|
|
409
438
|
return layer.render(
|
|
410
439
|
<div
|
|
411
440
|
ref={contentRef}
|
|
412
441
|
role={role === 'dialog' ? 'dialog' : undefined}
|
|
413
442
|
aria-modal={role === 'dialog' && isModal ? true : undefined}
|
|
414
443
|
aria-label={role === 'dialog' ? dialogLabel : undefined}
|
|
415
|
-
{...
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
444
|
+
{...mergeProps(
|
|
445
|
+
{...surfaceProps, className: surfaceClassName},
|
|
446
|
+
stylex.props(
|
|
447
|
+
styles.contentWrapper,
|
|
448
|
+
hasSurface && styles.surface,
|
|
449
|
+
xstyle,
|
|
450
|
+
),
|
|
419
451
|
)}>
|
|
420
452
|
{children}
|
|
421
453
|
{hasCloseButton && (
|
|
@@ -439,6 +471,7 @@ export function usePopover(options: UsePopoverOptions = {}): UsePopoverReturn {
|
|
|
439
471
|
layer,
|
|
440
472
|
hasCloseButton,
|
|
441
473
|
hasSurface,
|
|
474
|
+
surfaceTarget,
|
|
442
475
|
closeButtonLabel,
|
|
443
476
|
contentRef,
|
|
444
477
|
dialogLabel,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
typeScaleVars,
|
|
31
31
|
} from '../theme/tokens.stylex';
|
|
32
32
|
import {mergeProps} from '../utils';
|
|
33
|
+
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
33
34
|
import type {BaseProps} from '../BaseProps';
|
|
34
35
|
import {themeProps} from '../utils/themeProps';
|
|
35
36
|
import {VisuallyHidden} from '../VisuallyHidden';
|
|
@@ -282,14 +283,6 @@ const styles = stylex.create({
|
|
|
282
283
|
top: '50%',
|
|
283
284
|
width: 'var(--_progressbar-mark-width, 2px)',
|
|
284
285
|
height: 'var(--_progressbar-mark-height, 8px)',
|
|
285
|
-
outline: {
|
|
286
|
-
default: 'none',
|
|
287
|
-
':focus-visible': `2px solid ${colorVars['--color-accent']}`,
|
|
288
|
-
},
|
|
289
|
-
outlineOffset: {
|
|
290
|
-
default: '0',
|
|
291
|
-
':focus-visible': '2px',
|
|
292
|
-
},
|
|
293
286
|
transform: {
|
|
294
287
|
default: 'translate(-50%, -50%)',
|
|
295
288
|
':is([dir="rtl"] *)': 'translate(50%, -50%)',
|
|
@@ -581,6 +574,7 @@ export function ProgressBar({
|
|
|
581
574
|
placement: mark.isOnFill ? 'fill' : 'track',
|
|
582
575
|
}),
|
|
583
576
|
stylex.props(
|
|
577
|
+
focusOutlineStyles.focusVisible,
|
|
584
578
|
styles.mark,
|
|
585
579
|
mark.isOnFill
|
|
586
580
|
? markOnFillStyles[fillVariant]
|
package/src/Section/Section.tsx
CHANGED
package/src/Section/index.ts
CHANGED
|
@@ -27,10 +27,13 @@ export const docs = {
|
|
|
27
27
|
states: ['disabled'],
|
|
28
28
|
},
|
|
29
29
|
{className: 'astryx-selector-option'},
|
|
30
|
+
{className: 'astryx-selector-search'},
|
|
31
|
+
{className: 'astryx-selector-section-heading'},
|
|
30
32
|
{className: 'astryx-selector-empty-state'},
|
|
31
33
|
{className: 'astryx-selector-clear-icon', deprecatedFor: 'input-clear-icon'},
|
|
32
34
|
{className: 'astryx-selector-indicator-icon', states: ['state']},
|
|
33
35
|
{className: 'astryx-selector-check'},
|
|
36
|
+
{className: 'astryx-selector-popup'},
|
|
34
37
|
],
|
|
35
38
|
},
|
|
36
39
|
description: 'Dropdown selector for choosing from a list of options.',
|
|
@@ -156,6 +159,13 @@ export const docs = {
|
|
|
156
159
|
description:
|
|
157
160
|
'Custom render function for each selectable option in the dropdown. Use this instead of JSX children; dividers and sections are rendered by the selector.',
|
|
158
161
|
},
|
|
162
|
+
{
|
|
163
|
+
name: 'indicatorPosition',
|
|
164
|
+
type: "'start' | 'end'",
|
|
165
|
+
description:
|
|
166
|
+
'Which edge of the option row carries the selected mark. start reserves a mark column ahead of every label so they stay aligned, the way a native menu does; end is the house convention shared with Typeahead and CommandPalette.',
|
|
167
|
+
default: "'end'",
|
|
168
|
+
},
|
|
159
169
|
{
|
|
160
170
|
name: 'width',
|
|
161
171
|
type: 'SizeValue',
|