@astryxdesign/core 0.2.0-canary.ea693ac → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +2 -6
- package/dist/Button/Button.d.ts +1 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +2 -4
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +2 -2
- package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.js +4 -19
- package/dist/CodeBlock/CodeBlock.js +2 -2
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +1 -10
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +11 -8
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +1 -3
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.js +1 -1
- package/dist/Field/FieldLabel.d.ts +1 -2
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +3 -7
- package/dist/Item/Item.d.ts +3 -16
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +6 -31
- package/dist/List/ListItem.d.ts +1 -11
- package/dist/List/ListItem.d.ts.map +1 -1
- package/dist/List/ListItem.js +0 -2
- package/dist/MetadataList/MetadataList.d.ts +1 -1
- package/dist/MetadataList/MetadataList.d.ts.map +1 -1
- package/dist/MetadataList/MetadataList.js +2 -4
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +2 -13
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +2 -3
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +0 -5
- package/dist/Skeleton/Skeleton.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.js +2 -2
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +4 -15
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +1 -1
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +9 -61
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +1 -3
- package/dist/Token/Token.d.ts +1 -4
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +43 -68
- package/dist/Typeahead/BaseTypeahead.d.ts +0 -9
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +0 -2
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +1 -8
- package/dist/astryx.css +3 -16
- package/dist/astryx.umd.js +48 -48
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/useKeyboardHint.d.ts +1 -1
- package/dist/hooks/useKeyboardHint.d.ts.map +1 -1
- package/dist/hooks/useKeyboardHint.js +2 -4
- package/locales/en.json +0 -32
- package/locales/pseudo.json +0 -24
- package/package.json +1 -1
- package/src/AvatarGroup/AvatarGroup.test.tsx +1 -21
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +2 -4
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +1 -1
- package/src/Button/Button.test.tsx +0 -16
- package/src/Button/Button.tsx +2 -4
- package/src/CheckboxInput/CheckboxInput.test.tsx +0 -23
- package/src/CheckboxInput/CheckboxInput.tsx +2 -17
- package/src/CheckboxList/CheckboxList.test.tsx +3 -94
- package/src/CheckboxList/CheckboxListItem.tsx +10 -17
- package/src/CodeBlock/CodeBlock.test.tsx +0 -17
- package/src/CodeBlock/CodeBlock.tsx +2 -2
- package/src/ContextMenu/ContextMenu.test.tsx +2 -17
- package/src/ContextMenu/ContextMenu.tsx +1 -10
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +12 -4
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +10 -9
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +2 -20
- package/src/Field/FieldLabel.test.tsx +0 -25
- package/src/Field/FieldLabel.tsx +2 -11
- package/src/Item/Item.doc.mjs +0 -5
- package/src/Item/Item.test.tsx +0 -88
- package/src/Item/Item.tsx +11 -57
- package/src/List/ListItem.doc.mjs +0 -12
- package/src/List/ListItem.tsx +0 -13
- package/src/MetadataList/MetadataList.test.tsx +0 -24
- package/src/MetadataList/MetadataList.tsx +2 -6
- package/src/MultiSelector/MultiSelector.test.tsx +0 -76
- package/src/MultiSelector/MultiSelector.tsx +2 -22
- package/src/RadioList/RadioList.test.tsx +4 -26
- package/src/RadioList/RadioListItem.tsx +1 -8
- package/src/SegmentedControl/SegmentedControl.test.tsx +0 -28
- package/src/SegmentedControl/SegmentedControlItem.tsx +2 -20
- package/src/Selector/Selector.test.tsx +0 -22
- package/src/Selector/Selector.tsx +1 -8
- package/src/Skeleton/Skeleton.test.tsx +0 -16
- package/src/Skeleton/Skeleton.tsx +1 -13
- package/src/Switch/Switch.test.tsx +0 -37
- package/src/Switch/Switch.tsx +3 -52
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +0 -19
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +1 -3
- package/src/Toast/ToastViewport.test.tsx +8 -218
- package/src/Toast/ToastViewport.tsx +12 -71
- package/src/ToggleButton/ToggleButton.test.tsx +0 -24
- package/src/ToggleButton/ToggleButton.tsx +1 -19
- package/src/Token/Token.test.tsx +0 -145
- package/src/Token/Token.tsx +33 -73
- package/src/Typeahead/BaseTypeahead.tsx +0 -12
- package/src/Typeahead/Typeahead.test.tsx +0 -62
- package/src/Typeahead/Typeahead.tsx +0 -6
- package/src/hooks/useKeyboardHint.test.tsx +0 -22
- package/src/hooks/useKeyboardHint.tsx +2 -6
- package/dist/Token/TokenLink.d.ts +0 -62
- package/dist/Token/TokenLink.d.ts.map +0 -1
- package/dist/Token/TokenLink.js +0 -76
- package/src/Token/TokenLink.tsx +0 -107
- package/src/__tests__/forcedColors.ts +0 -60
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file MetadataList.tsx
|
|
7
|
-
* @input Uses React, ReactNode, StyleXStyles, theme tokens, MetadataListContext
|
|
7
|
+
* @input Uses React, ReactNode, StyleXStyles, theme tokens, MetadataListContext
|
|
8
8
|
* @output Exports MetadataList component, MetadataListProps, MetadataListColumns types
|
|
9
9
|
* @position Core implementation; consumed by index.ts, tested by MetadataList.test.tsx
|
|
10
10
|
*
|
|
@@ -31,7 +31,6 @@ import {
|
|
|
31
31
|
import type {BaseProps} from '../BaseProps';
|
|
32
32
|
import {mergeProps} from '../utils';
|
|
33
33
|
import {themeProps} from '../utils/themeProps';
|
|
34
|
-
import {useTranslator} from '../i18n';
|
|
35
34
|
|
|
36
35
|
// =============================================================================
|
|
37
36
|
// Types
|
|
@@ -198,7 +197,6 @@ export function MetadataList({
|
|
|
198
197
|
const labelConfig = label ?? (isMultiColumn ? LABEL_TOP : LABEL_START);
|
|
199
198
|
const [isShowAll, setIsShowAll] = useState(false);
|
|
200
199
|
const contentId = useId();
|
|
201
|
-
const t = useTranslator();
|
|
202
200
|
|
|
203
201
|
const contextValue = useMemo(
|
|
204
202
|
() => ({
|
|
@@ -285,9 +283,7 @@ export function MetadataList({
|
|
|
285
283
|
aria-expanded={isShowAll}
|
|
286
284
|
onClick={() => setIsShowAll(prev => !prev)}
|
|
287
285
|
{...stylex.props(styles.toggleButton)}>
|
|
288
|
-
{isShowAll
|
|
289
|
-
? t('@astryx.metadataList.showLess')
|
|
290
|
-
: t('@astryx.metadataList.showMore')}
|
|
286
|
+
{isShowAll ? 'Show less' : 'Show more'}
|
|
291
287
|
</button>
|
|
292
288
|
)}
|
|
293
289
|
</div>
|
|
@@ -510,59 +510,6 @@ describe('MultiSelector', () => {
|
|
|
510
510
|
expect(options[0]).toHaveTextContent('Select all');
|
|
511
511
|
});
|
|
512
512
|
|
|
513
|
-
it('select-all accessible name reflects none/partial/all selection', async () => {
|
|
514
|
-
const user = userEvent.setup();
|
|
515
|
-
const options = [
|
|
516
|
-
{value: 'apple', label: 'Apple'},
|
|
517
|
-
{value: 'banana', label: 'Banana'},
|
|
518
|
-
];
|
|
519
|
-
const {rerender} = render(
|
|
520
|
-
<MultiSelector
|
|
521
|
-
label="Fruit"
|
|
522
|
-
options={options}
|
|
523
|
-
value={[]}
|
|
524
|
-
onChange={() => {}}
|
|
525
|
-
hasSelectAll
|
|
526
|
-
/>,
|
|
527
|
-
);
|
|
528
|
-
|
|
529
|
-
await user.click(screen.getByRole('combobox'));
|
|
530
|
-
|
|
531
|
-
// None selected: plain name, not selected
|
|
532
|
-
let selectAll = screen.getAllByRole('option', h)[0];
|
|
533
|
-
expect(selectAll).not.toHaveAccessibleName(/partially selected/);
|
|
534
|
-
expect(selectAll).toHaveAttribute('aria-selected', 'false');
|
|
535
|
-
|
|
536
|
-
// Partial: aria-selected="mixed" is invalid on role="option", so the
|
|
537
|
-
// indeterminate state must be conveyed through the accessible name.
|
|
538
|
-
rerender(
|
|
539
|
-
<MultiSelector
|
|
540
|
-
label="Fruit"
|
|
541
|
-
options={options}
|
|
542
|
-
value={['apple']}
|
|
543
|
-
onChange={() => {}}
|
|
544
|
-
hasSelectAll
|
|
545
|
-
/>,
|
|
546
|
-
);
|
|
547
|
-
selectAll = screen.getAllByRole('option', h)[0];
|
|
548
|
-
expect(selectAll).toHaveAccessibleName('Select all, partially selected');
|
|
549
|
-
expect(selectAll).toHaveAttribute('aria-selected', 'false');
|
|
550
|
-
|
|
551
|
-
// All selected: plain name again, selected
|
|
552
|
-
rerender(
|
|
553
|
-
<MultiSelector
|
|
554
|
-
label="Fruit"
|
|
555
|
-
options={options}
|
|
556
|
-
value={['apple', 'banana']}
|
|
557
|
-
onChange={() => {}}
|
|
558
|
-
hasSelectAll
|
|
559
|
-
/>,
|
|
560
|
-
);
|
|
561
|
-
selectAll = screen.getAllByRole('option', h)[0];
|
|
562
|
-
expect(selectAll).not.toHaveAccessibleName(/partially selected/);
|
|
563
|
-
expect(selectAll).toHaveAttribute('aria-selected', 'true');
|
|
564
|
-
});
|
|
565
|
-
|
|
566
513
|
it('select-all toggles via keyboard Enter', async () => {
|
|
567
514
|
const user = userEvent.setup();
|
|
568
515
|
const onChange = vi.fn();
|
|
@@ -767,29 +714,6 @@ describe('MultiSelector', () => {
|
|
|
767
714
|
).toBeInTheDocument();
|
|
768
715
|
});
|
|
769
716
|
|
|
770
|
-
it('empty-state message is not exposed as a listbox child', async () => {
|
|
771
|
-
const user = userEvent.setup();
|
|
772
|
-
render(
|
|
773
|
-
<MultiSelector
|
|
774
|
-
label="Fruit"
|
|
775
|
-
options={defaultOptions}
|
|
776
|
-
value={[]}
|
|
777
|
-
onChange={() => {}}
|
|
778
|
-
hasSearch
|
|
779
|
-
/>,
|
|
780
|
-
);
|
|
781
|
-
|
|
782
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
783
|
-
await user.type(screen.getByRole('combobox', h), 'xyz');
|
|
784
|
-
|
|
785
|
-
// role="listbox" only permits option/group children — the visual
|
|
786
|
-
// empty-state message must be presentational (it is announced through
|
|
787
|
-
// the result-count live region instead).
|
|
788
|
-
const listbox = screen.getByRole('listbox', h);
|
|
789
|
-
const empty = within(listbox).getByText('No results found');
|
|
790
|
-
expect(empty).toHaveAttribute('role', 'presentation');
|
|
791
|
-
});
|
|
792
|
-
|
|
793
717
|
describe('result announcements', () => {
|
|
794
718
|
it('announces the match count politely while searching', async () => {
|
|
795
719
|
const user = userEvent.setup();
|
|
@@ -1129,11 +1129,6 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1129
1129
|
? allEnabledSelected
|
|
1130
1130
|
: optimisticValue.includes(item.value);
|
|
1131
1131
|
const checkboxValue = isSelectAll ? selectAllState : isSelected;
|
|
1132
|
-
// aria-selected="mixed" is invalid on role="option", and the tri-state
|
|
1133
|
-
// checkbox is inert/decorative, so the indeterminate state must be
|
|
1134
|
-
// conveyed through the option's accessible name (WCAG 4.1.2).
|
|
1135
|
-
const isPartiallySelected =
|
|
1136
|
-
isSelectAll && selectAllState === 'indeterminate';
|
|
1137
1132
|
|
|
1138
1133
|
return (
|
|
1139
1134
|
<div
|
|
@@ -1141,13 +1136,6 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1141
1136
|
id={getItemId(flatIndex)}
|
|
1142
1137
|
role="option"
|
|
1143
1138
|
aria-selected={isSelected}
|
|
1144
|
-
aria-label={
|
|
1145
|
-
isPartiallySelected
|
|
1146
|
-
? t('@astryx.multiSelector.selectAllPartiallySelected', {
|
|
1147
|
-
label: selectAllLabel,
|
|
1148
|
-
})
|
|
1149
|
-
: undefined
|
|
1150
|
-
}
|
|
1151
1139
|
aria-disabled={item.disabled}
|
|
1152
1140
|
onClick={() => {
|
|
1153
1141
|
if (!item.disabled) {
|
|
@@ -1192,12 +1180,10 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1192
1180
|
optimisticValue,
|
|
1193
1181
|
allEnabledSelected,
|
|
1194
1182
|
selectAllState,
|
|
1195
|
-
selectAllLabel,
|
|
1196
1183
|
getItemId,
|
|
1197
1184
|
handleNavigableToggle,
|
|
1198
1185
|
onItemMouseEnter,
|
|
1199
1186
|
size,
|
|
1200
|
-
t,
|
|
1201
1187
|
],
|
|
1202
1188
|
);
|
|
1203
1189
|
|
|
@@ -1225,16 +1211,10 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1225
1211
|
cursor = 1;
|
|
1226
1212
|
}
|
|
1227
1213
|
|
|
1228
|
-
// Empty state — no real items to show
|
|
1229
|
-
// message out of the listbox's accessibility tree (role="listbox" only
|
|
1230
|
-
// permits option/group children); the no-results outcome is announced
|
|
1231
|
-
// via the result-count live region instead.
|
|
1214
|
+
// Empty state — no real items to show
|
|
1232
1215
|
if (realItemCount === 0) {
|
|
1233
1216
|
elements.push(
|
|
1234
|
-
<div
|
|
1235
|
-
key="empty"
|
|
1236
|
-
role="presentation"
|
|
1237
|
-
{...stylex.props(styles.emptyState)}>
|
|
1217
|
+
<div key="empty" {...stylex.props(styles.emptyState)}>
|
|
1238
1218
|
No results found
|
|
1239
1219
|
</div>,
|
|
1240
1220
|
);
|
|
@@ -14,7 +14,6 @@ import {render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
15
|
import {RadioList} from './RadioList';
|
|
16
16
|
import {RadioListItem} from './RadioListItem';
|
|
17
|
-
import {getForcedColorsRules} from '../__tests__/forcedColors';
|
|
18
17
|
|
|
19
18
|
// Mock showPopover/hidePopover (not implemented in jsdom) so the tooltip layer
|
|
20
19
|
// reflects its open state via a `popover-open` attribute the tests can assert.
|
|
@@ -581,11 +580,7 @@ describe('RadioList', () => {
|
|
|
581
580
|
it('submits the selected value under htmlName', () => {
|
|
582
581
|
const {container} = render(
|
|
583
582
|
<form>
|
|
584
|
-
<RadioList
|
|
585
|
-
label="Preference"
|
|
586
|
-
htmlName="pref"
|
|
587
|
-
value="b"
|
|
588
|
-
onChange={() => {}}>
|
|
583
|
+
<RadioList label="Preference" htmlName="pref" value="b" onChange={() => {}}>
|
|
589
584
|
<RadioListItem label="Option A" value="a" />
|
|
590
585
|
<RadioListItem label="Option B" value="b" />
|
|
591
586
|
</RadioList>
|
|
@@ -604,14 +599,13 @@ describe('RadioList', () => {
|
|
|
604
599
|
value="a"
|
|
605
600
|
onChange={() => {}}
|
|
606
601
|
isDisabled
|
|
607
|
-
disabledMessage="Locked"
|
|
602
|
+
disabledMessage="Locked"
|
|
603
|
+
>
|
|
608
604
|
<RadioListItem label="Option A" value="a" />
|
|
609
605
|
</RadioList>
|
|
610
606
|
</form>,
|
|
611
607
|
);
|
|
612
|
-
expect([
|
|
613
|
-
...new FormData(container.querySelector('form')!).keys(),
|
|
614
|
-
]).toEqual([]);
|
|
608
|
+
expect([...new FormData(container.querySelector('form')!).keys()]).toEqual([]);
|
|
615
609
|
});
|
|
616
610
|
|
|
617
611
|
it('keeps working as an isolated group when htmlName is omitted', () => {
|
|
@@ -648,19 +642,3 @@ describe('RadioList', () => {
|
|
|
648
642
|
});
|
|
649
643
|
});
|
|
650
644
|
});
|
|
651
|
-
|
|
652
|
-
// jsdom cannot emulate forced-colors rendering, so this asserts that the
|
|
653
|
-
// compiled output includes the forced-colors rule; visual behavior needs
|
|
654
|
-
// manual verification under Windows High Contrast.
|
|
655
|
-
describe('forced colors (WCAG 1.4.11)', () => {
|
|
656
|
-
it('compiles a forced-colors fill so the selected dot survives Windows High Contrast', () => {
|
|
657
|
-
render(
|
|
658
|
-
<RadioList label="Preference" value="a" onChange={() => {}}>
|
|
659
|
-
<RadioListItem label="Option A" value="a" />
|
|
660
|
-
</RadioList>,
|
|
661
|
-
);
|
|
662
|
-
// The painted inner dot would be stripped to Canvas (invisible), making
|
|
663
|
-
// checked and unchecked radios identical; CanvasText keeps it perceivable.
|
|
664
|
-
expect(getForcedColorsRules()).toContain('background-color: canvastext;');
|
|
665
|
-
});
|
|
666
|
-
});
|
|
@@ -119,14 +119,7 @@ const styles = stylex.create({
|
|
|
119
119
|
},
|
|
120
120
|
innerDot: {
|
|
121
121
|
borderRadius: '50%',
|
|
122
|
-
backgroundColor:
|
|
123
|
-
default: colorVars['--color-on-accent'],
|
|
124
|
-
// Forced colors (Windows High Contrast) strips painted backgrounds,
|
|
125
|
-
// which would make the selected dot invisible — checked and unchecked
|
|
126
|
-
// radios would look identical. CanvasText keeps the dot perceivable on
|
|
127
|
-
// the Canvas circle fill (WCAG 1.4.11).
|
|
128
|
-
'@media (forced-colors: active)': 'CanvasText',
|
|
129
|
-
},
|
|
122
|
+
backgroundColor: colorVars['--color-on-accent'],
|
|
130
123
|
},
|
|
131
124
|
labelDisabled: {
|
|
132
125
|
color: colorVars['--color-text-disabled'],
|
|
@@ -14,10 +14,6 @@ import {render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
15
|
import {SegmentedControl} from './SegmentedControl';
|
|
16
16
|
import {SegmentedControlItem} from './SegmentedControlItem';
|
|
17
|
-
import {
|
|
18
|
-
getAllInjectedCss,
|
|
19
|
-
getForcedColorsRules,
|
|
20
|
-
} from '../__tests__/forcedColors';
|
|
21
17
|
|
|
22
18
|
// Mock showPopover/hidePopover (not implemented in jsdom) so the tooltip layer
|
|
23
19
|
// reflects its open state via a `popover-open` attribute the tests can assert.
|
|
@@ -784,27 +780,3 @@ describe('SegmentedControl container handler forwarding', () => {
|
|
|
784
780
|
expect(onChange).not.toHaveBeenCalled();
|
|
785
781
|
});
|
|
786
782
|
});
|
|
787
|
-
|
|
788
|
-
// jsdom cannot emulate forced-colors rendering, so these assert that the
|
|
789
|
-
// compiled output includes the forced-colors rules; visual behavior needs
|
|
790
|
-
// manual verification under Windows High Contrast.
|
|
791
|
-
describe('forced colors (WCAG 1.4.11)', () => {
|
|
792
|
-
it('compiles forced-colors overrides so the selected segment survives Windows High Contrast', () => {
|
|
793
|
-
render(
|
|
794
|
-
<SegmentedControl value="grid" onChange={() => {}} label="View mode">
|
|
795
|
-
<SegmentedControlItem value="grid" label="Grid" />
|
|
796
|
-
<SegmentedControlItem value="list" label="List" />
|
|
797
|
-
</SegmentedControl>,
|
|
798
|
-
);
|
|
799
|
-
const css = getForcedColorsRules();
|
|
800
|
-
// The painted surface fill and shadow are stripped; Highlight/
|
|
801
|
-
// HighlightText marks the selected segment.
|
|
802
|
-
expect(css).toContain('background-color: highlight;');
|
|
803
|
-
expect(css).toContain('color: highlighttext;');
|
|
804
|
-
// The segment is a <button>; without opting out of UA remapping it keeps
|
|
805
|
-
// the native ButtonFace surface and ignores the Highlight fill, leaving
|
|
806
|
-
// HighlightText text on a white surface. forced-color-adjust: none makes
|
|
807
|
-
// both render as authored.
|
|
808
|
-
expect(getAllInjectedCss()).toContain('forced-color-adjust: none;');
|
|
809
|
-
});
|
|
810
|
-
});
|
|
@@ -103,27 +103,9 @@ const styles = stylex.create({
|
|
|
103
103
|
},
|
|
104
104
|
},
|
|
105
105
|
selected: {
|
|
106
|
-
|
|
107
|
-
// and box shadow, which would leave the selected segment with no state
|
|
108
|
-
// indication beyond font weight. Highlight/HighlightText is the platform
|
|
109
|
-
// convention for a selected item (WCAG 1.4.11).
|
|
110
|
-
//
|
|
111
|
-
// forced-color-adjust must be `none` here: the segment is a <button>, and
|
|
112
|
-
// the UA keeps native form-control colors (ButtonFace surface) for it under
|
|
113
|
-
// forced colors, ignoring the authored Highlight fill — the label kept its
|
|
114
|
-
// HighlightText color, giving white text on a white surface. Opting the
|
|
115
|
-
// selected segment out of UA remapping makes both the Highlight surface and
|
|
116
|
-
// the HighlightText label render as authored, restoring figure-ground.
|
|
117
|
-
forcedColorAdjust: 'none',
|
|
118
|
-
color: {
|
|
119
|
-
default: colorVars['--color-text-primary'],
|
|
120
|
-
'@media (forced-colors: active)': 'HighlightText',
|
|
121
|
-
},
|
|
106
|
+
color: colorVars['--color-text-primary'],
|
|
122
107
|
fontWeight: fontWeightVars['--font-weight-semibold'],
|
|
123
|
-
backgroundColor:
|
|
124
|
-
default: colorVars['--color-background-surface'],
|
|
125
|
-
'@media (forced-colors: active)': 'Highlight',
|
|
126
|
-
},
|
|
108
|
+
backgroundColor: colorVars['--color-background-surface'],
|
|
127
109
|
boxShadow: shadowVars['--shadow-low'],
|
|
128
110
|
},
|
|
129
111
|
disabled: {
|
|
@@ -613,28 +613,6 @@ describe('Selector', () => {
|
|
|
613
613
|
expect(within(listbox).getByText('No results found')).toBeInTheDocument();
|
|
614
614
|
});
|
|
615
615
|
|
|
616
|
-
it('empty-state message is not exposed as a listbox child', async () => {
|
|
617
|
-
const user = userEvent.setup();
|
|
618
|
-
render(
|
|
619
|
-
<Selector
|
|
620
|
-
label="Fruit"
|
|
621
|
-
options={OPTIONS}
|
|
622
|
-
value="Apple"
|
|
623
|
-
onChange={() => {}}
|
|
624
|
-
hasSearch
|
|
625
|
-
/>,
|
|
626
|
-
);
|
|
627
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
628
|
-
await user.type(screen.getByRole('combobox', h), 'xyz');
|
|
629
|
-
|
|
630
|
-
// role="listbox" only permits option/group children — the visual
|
|
631
|
-
// empty-state message must be presentational (it is announced through
|
|
632
|
-
// the result-count live region instead).
|
|
633
|
-
const listbox = screen.getByRole('listbox', h);
|
|
634
|
-
const empty = within(listbox).getByText('No results found');
|
|
635
|
-
expect(empty).toHaveAttribute('role', 'presentation');
|
|
636
|
-
});
|
|
637
|
-
|
|
638
616
|
it('calls onChange when selecting a filtered option', async () => {
|
|
639
617
|
const user = userEvent.setup();
|
|
640
618
|
const onChange = vi.fn();
|
|
@@ -963,15 +963,8 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
963
963
|
|
|
964
964
|
// Nothing matched across every group/option: show the empty state.
|
|
965
965
|
if (isSearching && filteredItems.length === 0) {
|
|
966
|
-
// role="presentation" keeps the message out of the listbox's
|
|
967
|
-
// accessibility tree (role="listbox" only permits option/group
|
|
968
|
-
// children); the no-results outcome is announced via the
|
|
969
|
-
// result-count live region instead.
|
|
970
966
|
return [
|
|
971
|
-
<div
|
|
972
|
-
key="empty"
|
|
973
|
-
role="presentation"
|
|
974
|
-
{...stylex.props(styles.emptyState)}>
|
|
967
|
+
<div key="empty" {...stylex.props(styles.emptyState)}>
|
|
975
968
|
No results found
|
|
976
969
|
</div>,
|
|
977
970
|
];
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import {describe, it, expect} from 'vitest';
|
|
4
4
|
import {render, screen} from '@testing-library/react';
|
|
5
5
|
import {Skeleton} from './Skeleton';
|
|
6
|
-
import {getForcedColorsRules} from '../__tests__/forcedColors';
|
|
7
6
|
|
|
8
7
|
describe('Skeleton', () => {
|
|
9
8
|
it('renders a placeholder element', () => {
|
|
@@ -24,18 +23,3 @@ describe('Skeleton', () => {
|
|
|
24
23
|
expect(screen.getByTestId('sk')).toHaveAttribute('aria-hidden', 'false');
|
|
25
24
|
});
|
|
26
25
|
});
|
|
27
|
-
|
|
28
|
-
// jsdom cannot emulate forced-colors rendering, so these assert that the
|
|
29
|
-
// compiled output includes the forced-colors rules; visual behavior needs
|
|
30
|
-
// manual verification under Windows High Contrast.
|
|
31
|
-
describe('forced colors (WCAG 1.4.11)', () => {
|
|
32
|
-
it('compiles forced-colors overrides so the placeholder stays visible under Windows High Contrast', () => {
|
|
33
|
-
render(<Skeleton width={200} height={20} data-testid="sk" />);
|
|
34
|
-
const css = getForcedColorsRules();
|
|
35
|
-
// The painted fill is stripped to Canvas (invisible); GrayText is a
|
|
36
|
-
// system color, so it survives forcing.
|
|
37
|
-
expect(css).toContain('background-color: graytext;');
|
|
38
|
-
// The resting 0.25 opacity is lifted so the static placeholder reads.
|
|
39
|
-
expect(css).toContain('opacity: 1;');
|
|
40
|
-
});
|
|
41
|
-
});
|
|
@@ -55,20 +55,8 @@ const styles = stylex.create({
|
|
|
55
55
|
backgroundColor: {
|
|
56
56
|
default: colorVars['--color-skeleton'],
|
|
57
57
|
'@media (prefers-contrast: more)': `color-mix(in srgb, ${colorVars['--color-skeleton']}, ${colorVars['--color-text-primary']} 30%)`,
|
|
58
|
-
// Forced colors (Windows High Contrast) strips painted backgrounds,
|
|
59
|
-
// which would make the placeholder invisible. GrayText is a system
|
|
60
|
-
// color, so it survives forcing and keeps the placeholder visible
|
|
61
|
-
// (WCAG 1.4.11). Listed after prefers-contrast so it wins when both
|
|
62
|
-
// media features are active.
|
|
63
|
-
'@media (forced-colors: active)': 'GrayText',
|
|
64
|
-
},
|
|
65
|
-
opacity: {
|
|
66
|
-
default: 0.25,
|
|
67
|
-
// The resting 0.25 opacity would render GrayText nearly invisible on
|
|
68
|
-
// Canvas; full opacity keeps the static placeholder perceivable (the
|
|
69
|
-
// fade animation still pulses when motion is allowed).
|
|
70
|
-
'@media (forced-colors: active)': 1,
|
|
71
58
|
},
|
|
59
|
+
opacity: 0.25,
|
|
72
60
|
},
|
|
73
61
|
animate: {
|
|
74
62
|
animationDirection: 'alternate',
|
|
@@ -13,10 +13,6 @@ import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
|
|
|
13
13
|
import {render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
15
|
import {Switch} from './Switch';
|
|
16
|
-
import {
|
|
17
|
-
getAllInjectedCss,
|
|
18
|
-
getForcedColorsRules,
|
|
19
|
-
} from '../__tests__/forcedColors';
|
|
20
16
|
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
21
17
|
|
|
22
18
|
afterEach(() => {
|
|
@@ -733,36 +729,3 @@ describe('Switch', () => {
|
|
|
733
729
|
});
|
|
734
730
|
});
|
|
735
731
|
});
|
|
736
|
-
|
|
737
|
-
// jsdom cannot emulate forced-colors rendering, so these assert that the
|
|
738
|
-
// compiled output includes the forced-colors rules; visual behavior needs
|
|
739
|
-
// manual verification under Windows High Contrast.
|
|
740
|
-
describe('forced colors (WCAG 1.4.11)', () => {
|
|
741
|
-
it('compiles forced-colors overrides so on/off state survives Windows High Contrast', () => {
|
|
742
|
-
render(<Switch label="Notifications" value={true} onChange={() => {}} />);
|
|
743
|
-
const css = getForcedColorsRules();
|
|
744
|
-
// Track outline (backgrounds are stripped; the border keeps the bounds).
|
|
745
|
-
expect(css).toContain('border-color: canvastext;');
|
|
746
|
-
// Off track stays empty; on track uses the selection color.
|
|
747
|
-
expect(css).toContain('background-color: canvas;');
|
|
748
|
-
expect(css).toContain('background-color: highlight;');
|
|
749
|
-
// Thumb fill per state.
|
|
750
|
-
expect(css).toContain('background-color: canvastext;');
|
|
751
|
-
expect(css).toContain('background-color: highlighttext;');
|
|
752
|
-
// Disabled affordance (opacity dimming does not survive forcing).
|
|
753
|
-
expect(css).toContain('border-color: graytext;');
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
it('gates the hover tint out of forced colors so the thumb stays visible on hover', () => {
|
|
757
|
-
render(<Switch label="Notifications" value={true} onChange={() => {}} />);
|
|
758
|
-
// The ancestor-hover tint is a non-system color-mix whose rule outranks the
|
|
759
|
-
// plain forced-colors track rule. It is gated behind `forced-colors: none`
|
|
760
|
-
// so it cannot reassert on hover and flatten the Highlight track to white
|
|
761
|
-
// under the HighlightText thumb (white-on-white).
|
|
762
|
-
expect(getAllInjectedCss()).toContain(
|
|
763
|
-
'(hover: hover) and (forced-colors: none)',
|
|
764
|
-
);
|
|
765
|
-
// And the tint never leaks into the forced-colors output.
|
|
766
|
-
expect(getForcedColorsRules()).not.toContain('color-mix');
|
|
767
|
-
});
|
|
768
|
-
});
|
package/src/Switch/Switch.tsx
CHANGED
|
@@ -173,21 +173,6 @@ const styles = stylex.create({
|
|
|
173
173
|
},
|
|
174
174
|
transitionTimingFunction: easeVars['--ease-standard'],
|
|
175
175
|
boxSizing: 'border-box',
|
|
176
|
-
// Forced colors (Windows High Contrast) strips painted backgrounds, which
|
|
177
|
-
// would leave the track invisible. A system-color border keeps the
|
|
178
|
-
// control's bounds perceivable (WCAG 1.4.11).
|
|
179
|
-
borderWidth: {
|
|
180
|
-
default: 0,
|
|
181
|
-
'@media (forced-colors: active)': '1px',
|
|
182
|
-
},
|
|
183
|
-
borderStyle: {
|
|
184
|
-
default: 'none',
|
|
185
|
-
'@media (forced-colors: active)': 'solid',
|
|
186
|
-
},
|
|
187
|
-
borderColor: {
|
|
188
|
-
default: null,
|
|
189
|
-
'@media (forced-colors: active)': 'CanvasText',
|
|
190
|
-
},
|
|
191
176
|
},
|
|
192
177
|
trackFocus: {
|
|
193
178
|
outline: {
|
|
@@ -204,39 +189,21 @@ const styles = stylex.create({
|
|
|
204
189
|
trackOff: {
|
|
205
190
|
backgroundColor: {
|
|
206
191
|
default: colorVars['--color-background-gray'],
|
|
207
|
-
// Off = empty (Canvas) track; on = Highlight track, so the two states
|
|
208
|
-
// stay distinguishable under forced colors.
|
|
209
|
-
'@media (forced-colors: active)': 'Canvas',
|
|
210
|
-
// The ancestor-hover tint is a non-system color-mix, and its rule
|
|
211
|
-
// outranks the plain forced-colors rule above. Left ungated it would
|
|
212
|
-
// reassert on hover under forced colors, where the UA flattens the
|
|
213
|
-
// color-mix back to Canvas — so the HighlightText thumb would sit on a
|
|
214
|
-
// white track (white-on-white). Gating on `forced-colors: none` keeps
|
|
215
|
-
// the tint out of forced colors and lets the system-color track stand.
|
|
216
192
|
[stylex.when.ancestor(':hover', switchScope)]: {
|
|
217
|
-
'@media (hover: hover)
|
|
193
|
+
'@media (hover: hover)': `color-mix(in srgb, ${colorVars['--color-background-gray']}, ${colorVars['--color-tint-hover']} 5%)`,
|
|
218
194
|
},
|
|
219
195
|
},
|
|
220
196
|
},
|
|
221
197
|
trackOn: {
|
|
222
198
|
backgroundColor: {
|
|
223
199
|
default: colorVars['--color-accent'],
|
|
224
|
-
'@media (forced-colors: active)': 'Highlight',
|
|
225
|
-
// See trackOff: gate the hover tint out of forced colors so it cannot
|
|
226
|
-
// flatten the Highlight track to white under the HighlightText thumb.
|
|
227
200
|
[stylex.when.ancestor(':hover', switchScope)]: {
|
|
228
|
-
'@media (hover: hover)
|
|
201
|
+
'@media (hover: hover)': `color-mix(in srgb, ${colorVars['--color-accent']}, ${colorVars['--color-tint-hover']} 15%)`,
|
|
229
202
|
},
|
|
230
203
|
},
|
|
231
204
|
},
|
|
232
205
|
trackDisabled: {
|
|
233
206
|
opacity: 0.5,
|
|
234
|
-
// Opacity dimming does not survive forced colors; GrayText is the
|
|
235
|
-
// platform's disabled affordance there.
|
|
236
|
-
borderColor: {
|
|
237
|
-
default: null,
|
|
238
|
-
'@media (forced-colors: active)': 'GrayText',
|
|
239
|
-
},
|
|
240
207
|
},
|
|
241
208
|
trackDisabledOff: {
|
|
242
209
|
backgroundColor: colorVars['--color-background-gray'],
|
|
@@ -246,6 +213,7 @@ const styles = stylex.create({
|
|
|
246
213
|
alignItems: 'center',
|
|
247
214
|
justifyContent: 'center',
|
|
248
215
|
borderRadius: radiusVars['--radius-full'],
|
|
216
|
+
backgroundColor: colorVars['--color-background-surface'],
|
|
249
217
|
transitionProperty: 'transform, width, height',
|
|
250
218
|
transitionDuration: {
|
|
251
219
|
default: durationVars['--duration-fast'],
|
|
@@ -253,22 +221,6 @@ const styles = stylex.create({
|
|
|
253
221
|
},
|
|
254
222
|
transitionTimingFunction: easeVars['--ease-standard'],
|
|
255
223
|
},
|
|
256
|
-
// The thumb fill lives on the on/off styles (not the shared thumb style)
|
|
257
|
-
// because forced colors needs a per-state system color: CanvasText on the
|
|
258
|
-
// empty off track, HighlightText on the Highlight on track. Sizing stays in
|
|
259
|
-
// thumbOffSizeStyles/thumbOnSizeStyles; only the fill is state-dependent.
|
|
260
|
-
thumbOff: {
|
|
261
|
-
backgroundColor: {
|
|
262
|
-
default: colorVars['--color-background-surface'],
|
|
263
|
-
'@media (forced-colors: active)': 'CanvasText',
|
|
264
|
-
},
|
|
265
|
-
},
|
|
266
|
-
thumbOn: {
|
|
267
|
-
backgroundColor: {
|
|
268
|
-
default: colorVars['--color-background-surface'],
|
|
269
|
-
'@media (forced-colors: active)': 'HighlightText',
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
224
|
labelWrapper: {
|
|
273
225
|
display: 'flex',
|
|
274
226
|
flexDirection: 'column',
|
|
@@ -585,7 +537,6 @@ export function Switch({
|
|
|
585
537
|
stylex.props(
|
|
586
538
|
styles.thumb,
|
|
587
539
|
isOn ? thumbOnSizeStyles[size] : thumbOffSizeStyles[size],
|
|
588
|
-
isOn ? styles.thumbOn : styles.thumbOff,
|
|
589
540
|
),
|
|
590
541
|
)}>
|
|
591
542
|
{isBusy && <Spinner size="sm" />}
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
useTableRowExpansion,
|
|
10
10
|
useTableRowExpansionState,
|
|
11
11
|
} from './useTableRowExpansion';
|
|
12
|
-
import {InternationalizationProvider} from '../../../i18n';
|
|
13
12
|
|
|
14
13
|
// popover mock for context-menu tests
|
|
15
14
|
beforeEach(() => {
|
|
@@ -119,24 +118,6 @@ describe('useTableRowExpansion', () => {
|
|
|
119
118
|
expect(items.length).toBeGreaterThan(0);
|
|
120
119
|
});
|
|
121
120
|
|
|
122
|
-
it('localizes the context-menu action label through the i18n catalog', () => {
|
|
123
|
-
render(
|
|
124
|
-
<InternationalizationProvider
|
|
125
|
-
locale="fr"
|
|
126
|
-
overrides={{
|
|
127
|
-
fr: {'@astryx.tableRowExpansion.expandRow': 'Développer la ligne'},
|
|
128
|
-
}}>
|
|
129
|
-
<Harness />
|
|
130
|
-
</InternationalizationProvider>,
|
|
131
|
-
);
|
|
132
|
-
fireEvent.contextMenu(screen.getByText('Folder A'));
|
|
133
|
-
const items = screen.getAllByRole('menuitem', {
|
|
134
|
-
name: /Développer la ligne/,
|
|
135
|
-
hidden: true,
|
|
136
|
-
});
|
|
137
|
-
expect(items.length).toBeGreaterThan(0);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
121
|
it('does not show chevron for leaf nodes', () => {
|
|
141
122
|
render(<Harness />);
|
|
142
123
|
// Leaf C has no children
|
|
@@ -576,9 +576,7 @@ export function useTableRowExpansion<T extends Record<string, unknown>>(
|
|
|
576
576
|
{
|
|
577
577
|
id: 'row-expansion-toggle',
|
|
578
578
|
group: 'row-expansion',
|
|
579
|
-
label: isExpanded
|
|
580
|
-
? t('@astryx.tableRowExpansion.collapseRow')
|
|
581
|
-
: t('@astryx.tableRowExpansion.expandRow'),
|
|
579
|
+
label: isExpanded ? 'Collapse row' : 'Expand row',
|
|
582
580
|
icon: (
|
|
583
581
|
<span {...stylex.props(rtlStyles.mirror)}>
|
|
584
582
|
<Icon
|