@astryxdesign/core 0.2.0-canary.f306f13 → 0.2.0-canary.fc53ff7
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 +6 -2
- package/dist/Button/Button.d.ts +1 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +4 -2
- 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 +19 -4
- package/dist/CodeBlock/CodeBlock.js +2 -2
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +10 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +8 -11
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +3 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.js +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +9 -1
- package/dist/DropdownMenu/menuItemHover.d.ts +1 -0
- package/dist/DropdownMenu/menuItemHover.d.ts.map +1 -1
- package/dist/DropdownMenu/menuItemHover.js +1 -0
- package/dist/Field/FieldLabel.d.ts +2 -1
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +7 -3
- package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
- package/dist/FieldStatus/FieldStatus.js +12 -2
- package/dist/Item/Item.d.ts +16 -3
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +31 -6
- package/dist/List/ListItem.d.ts +11 -1
- package/dist/List/ListItem.d.ts.map +1 -1
- package/dist/List/ListItem.js +2 -0
- package/dist/MetadataList/MetadataList.d.ts +1 -1
- package/dist/MetadataList/MetadataList.d.ts.map +1 -1
- package/dist/MetadataList/MetadataList.js +4 -2
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +14 -3
- 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 +3 -2
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +5 -0
- 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 +15 -4
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +1 -1
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +42 -43
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +61 -9
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +3 -1
- package/dist/Token/Token.d.ts +4 -1
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +68 -43
- package/dist/Token/TokenLink.d.ts +62 -0
- package/dist/Token/TokenLink.d.ts.map +1 -0
- package/dist/Token/TokenLink.js +76 -0
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +9 -3
- package/dist/Typeahead/BaseTypeahead.d.ts +9 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +2 -0
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +8 -1
- package/dist/astryx.css +51 -8
- package/dist/astryx.umd.js +50 -50
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/containerReveal.pool.stylex.d.ts +281 -0
- package/dist/hooks/containerReveal.pool.stylex.d.ts.map +1 -0
- package/dist/hooks/containerReveal.pool.stylex.js +281 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +2 -1
- package/dist/hooks/useContainerReveal.d.ts +83 -0
- package/dist/hooks/useContainerReveal.d.ts.map +1 -0
- package/dist/hooks/useContainerReveal.js +123 -0
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +11 -0
- package/dist/hooks/useKeyboardHint.d.ts +1 -1
- package/dist/hooks/useKeyboardHint.d.ts.map +1 -1
- package/dist/hooks/useKeyboardHint.js +4 -2
- package/locales/en.json +32 -0
- package/locales/pseudo.json +24 -0
- package/package.json +1 -1
- package/src/AvatarGroup/AvatarGroup.test.tsx +21 -1
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +4 -2
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +1 -1
- package/src/Button/Button.test.tsx +16 -0
- package/src/Button/Button.tsx +4 -2
- package/src/CheckboxInput/CheckboxInput.test.tsx +23 -0
- package/src/CheckboxInput/CheckboxInput.tsx +17 -2
- package/src/CheckboxList/CheckboxList.test.tsx +94 -3
- package/src/CheckboxList/CheckboxListItem.tsx +17 -10
- package/src/CodeBlock/CodeBlock.test.tsx +17 -0
- package/src/CodeBlock/CodeBlock.tsx +2 -2
- package/src/ContextMenu/ContextMenu.test.tsx +17 -2
- package/src/ContextMenu/ContextMenu.tsx +10 -1
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +4 -12
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +9 -10
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +20 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +29 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +13 -5
- package/src/DropdownMenu/menuItemHover.ts +1 -0
- package/src/Field/Field.doc.mjs +4 -0
- package/src/Field/FieldLabel.test.tsx +25 -0
- package/src/Field/FieldLabel.tsx +11 -2
- package/src/FieldStatus/FieldStatus.doc.mjs +1 -0
- package/src/FieldStatus/FieldStatus.test.tsx +61 -0
- package/src/FieldStatus/FieldStatus.tsx +14 -3
- package/src/Item/Item.doc.mjs +5 -0
- package/src/Item/Item.test.tsx +88 -0
- package/src/Item/Item.tsx +57 -11
- package/src/List/ListItem.doc.mjs +12 -0
- package/src/List/ListItem.tsx +13 -0
- package/src/MetadataList/MetadataList.test.tsx +24 -0
- package/src/MetadataList/MetadataList.tsx +6 -2
- package/src/MultiSelector/MultiSelector.test.tsx +76 -0
- package/src/MultiSelector/MultiSelector.tsx +28 -7
- package/src/RadioList/RadioList.test.tsx +26 -4
- package/src/RadioList/RadioListItem.tsx +8 -1
- package/src/SegmentedControl/SegmentedControl.test.tsx +28 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +20 -2
- package/src/Selector/Selector.test.tsx +22 -0
- package/src/Selector/Selector.tsx +8 -1
- package/src/Skeleton/Skeleton.test.tsx +16 -0
- package/src/Skeleton/Skeleton.tsx +13 -1
- package/src/Switch/Switch.test.tsx +37 -0
- package/src/Switch/Switch.tsx +52 -3
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +19 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +3 -1
- package/src/Thumbnail/Thumbnail.tsx +18 -32
- package/src/Toast/ToastViewport.test.tsx +218 -8
- package/src/Toast/ToastViewport.tsx +71 -12
- package/src/ToggleButton/ToggleButton.test.tsx +24 -0
- package/src/ToggleButton/ToggleButton.tsx +19 -1
- package/src/Token/Token.test.tsx +145 -0
- package/src/Token/Token.tsx +73 -33
- package/src/Token/TokenLink.tsx +107 -0
- package/src/TopNav/TopNavMegaMenu.tsx +26 -2
- package/src/Typeahead/BaseTypeahead.tsx +12 -0
- package/src/Typeahead/Typeahead.test.tsx +62 -0
- package/src/Typeahead/Typeahead.tsx +6 -0
- package/src/__tests__/forcedColors.ts +60 -0
- package/src/hooks/containerReveal.pool.stylex.ts +464 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useContainerReveal.test.tsx +116 -0
- package/src/hooks/useContainerReveal.ts +180 -0
- package/src/hooks/useInputStatusIcon.test.tsx +59 -0
- package/src/hooks/useInputStatusIcon.tsx +12 -1
- package/src/hooks/useKeyboardHint.test.tsx +22 -0
- package/src/hooks/useKeyboardHint.tsx +6 -2
- package/dist/Thumbnail/thumbnail.markers.stylex.d.ts +0 -9
- package/dist/Thumbnail/thumbnail.markers.stylex.d.ts.map +0 -1
- package/dist/Thumbnail/thumbnail.markers.stylex.js +0 -14
- package/src/Thumbnail/thumbnail.markers.stylex.ts +0 -12
|
@@ -14,6 +14,7 @@ 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';
|
|
17
18
|
|
|
18
19
|
// Mock showPopover/hidePopover (not implemented in jsdom) so the tooltip layer
|
|
19
20
|
// reflects its open state via a `popover-open` attribute the tests can assert.
|
|
@@ -580,7 +581,11 @@ describe('RadioList', () => {
|
|
|
580
581
|
it('submits the selected value under htmlName', () => {
|
|
581
582
|
const {container} = render(
|
|
582
583
|
<form>
|
|
583
|
-
<RadioList
|
|
584
|
+
<RadioList
|
|
585
|
+
label="Preference"
|
|
586
|
+
htmlName="pref"
|
|
587
|
+
value="b"
|
|
588
|
+
onChange={() => {}}>
|
|
584
589
|
<RadioListItem label="Option A" value="a" />
|
|
585
590
|
<RadioListItem label="Option B" value="b" />
|
|
586
591
|
</RadioList>
|
|
@@ -599,13 +604,14 @@ describe('RadioList', () => {
|
|
|
599
604
|
value="a"
|
|
600
605
|
onChange={() => {}}
|
|
601
606
|
isDisabled
|
|
602
|
-
disabledMessage="Locked"
|
|
603
|
-
>
|
|
607
|
+
disabledMessage="Locked">
|
|
604
608
|
<RadioListItem label="Option A" value="a" />
|
|
605
609
|
</RadioList>
|
|
606
610
|
</form>,
|
|
607
611
|
);
|
|
608
|
-
expect([
|
|
612
|
+
expect([
|
|
613
|
+
...new FormData(container.querySelector('form')!).keys(),
|
|
614
|
+
]).toEqual([]);
|
|
609
615
|
});
|
|
610
616
|
|
|
611
617
|
it('keeps working as an isolated group when htmlName is omitted', () => {
|
|
@@ -642,3 +648,19 @@ describe('RadioList', () => {
|
|
|
642
648
|
});
|
|
643
649
|
});
|
|
644
650
|
});
|
|
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,7 +119,14 @@ const styles = stylex.create({
|
|
|
119
119
|
},
|
|
120
120
|
innerDot: {
|
|
121
121
|
borderRadius: '50%',
|
|
122
|
-
backgroundColor:
|
|
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
|
+
},
|
|
123
130
|
},
|
|
124
131
|
labelDisabled: {
|
|
125
132
|
color: colorVars['--color-text-disabled'],
|
|
@@ -14,6 +14,10 @@ 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';
|
|
17
21
|
|
|
18
22
|
// Mock showPopover/hidePopover (not implemented in jsdom) so the tooltip layer
|
|
19
23
|
// reflects its open state via a `popover-open` attribute the tests can assert.
|
|
@@ -780,3 +784,27 @@ describe('SegmentedControl container handler forwarding', () => {
|
|
|
780
784
|
expect(onChange).not.toHaveBeenCalled();
|
|
781
785
|
});
|
|
782
786
|
});
|
|
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,9 +103,27 @@ const styles = stylex.create({
|
|
|
103
103
|
},
|
|
104
104
|
},
|
|
105
105
|
selected: {
|
|
106
|
-
|
|
106
|
+
// Forced colors (Windows High Contrast) strips the painted surface fill
|
|
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
|
+
},
|
|
107
122
|
fontWeight: fontWeightVars['--font-weight-semibold'],
|
|
108
|
-
backgroundColor:
|
|
123
|
+
backgroundColor: {
|
|
124
|
+
default: colorVars['--color-background-surface'],
|
|
125
|
+
'@media (forced-colors: active)': 'Highlight',
|
|
126
|
+
},
|
|
109
127
|
boxShadow: shadowVars['--shadow-low'],
|
|
110
128
|
},
|
|
111
129
|
disabled: {
|
|
@@ -613,6 +613,28 @@ 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
|
+
|
|
616
638
|
it('calls onChange when selecting a filtered option', async () => {
|
|
617
639
|
const user = userEvent.setup();
|
|
618
640
|
const onChange = vi.fn();
|
|
@@ -963,8 +963,15 @@ 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.
|
|
966
970
|
return [
|
|
967
|
-
<div
|
|
971
|
+
<div
|
|
972
|
+
key="empty"
|
|
973
|
+
role="presentation"
|
|
974
|
+
{...stylex.props(styles.emptyState)}>
|
|
968
975
|
No results found
|
|
969
976
|
</div>,
|
|
970
977
|
];
|
|
@@ -3,6 +3,7 @@
|
|
|
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';
|
|
6
7
|
|
|
7
8
|
describe('Skeleton', () => {
|
|
8
9
|
it('renders a placeholder element', () => {
|
|
@@ -23,3 +24,18 @@ describe('Skeleton', () => {
|
|
|
23
24
|
expect(screen.getByTestId('sk')).toHaveAttribute('aria-hidden', 'false');
|
|
24
25
|
});
|
|
25
26
|
});
|
|
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,8 +55,20 @@ 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,
|
|
58
71
|
},
|
|
59
|
-
opacity: 0.25,
|
|
60
72
|
},
|
|
61
73
|
animate: {
|
|
62
74
|
animationDirection: 'alternate',
|
|
@@ -13,6 +13,10 @@ 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';
|
|
16
20
|
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
17
21
|
|
|
18
22
|
afterEach(() => {
|
|
@@ -729,3 +733,36 @@ describe('Switch', () => {
|
|
|
729
733
|
});
|
|
730
734
|
});
|
|
731
735
|
});
|
|
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,6 +173,21 @@ 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
|
+
},
|
|
176
191
|
},
|
|
177
192
|
trackFocus: {
|
|
178
193
|
outline: {
|
|
@@ -189,21 +204,39 @@ const styles = stylex.create({
|
|
|
189
204
|
trackOff: {
|
|
190
205
|
backgroundColor: {
|
|
191
206
|
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.
|
|
192
216
|
[stylex.when.ancestor(':hover', switchScope)]: {
|
|
193
|
-
'@media (hover: hover)': `color-mix(in srgb, ${colorVars['--color-background-gray']}, ${colorVars['--color-tint-hover']} 5%)`,
|
|
217
|
+
'@media (hover: hover) and (forced-colors: none)': `color-mix(in srgb, ${colorVars['--color-background-gray']}, ${colorVars['--color-tint-hover']} 5%)`,
|
|
194
218
|
},
|
|
195
219
|
},
|
|
196
220
|
},
|
|
197
221
|
trackOn: {
|
|
198
222
|
backgroundColor: {
|
|
199
223
|
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.
|
|
200
227
|
[stylex.when.ancestor(':hover', switchScope)]: {
|
|
201
|
-
'@media (hover: hover)': `color-mix(in srgb, ${colorVars['--color-accent']}, ${colorVars['--color-tint-hover']} 15%)`,
|
|
228
|
+
'@media (hover: hover) and (forced-colors: none)': `color-mix(in srgb, ${colorVars['--color-accent']}, ${colorVars['--color-tint-hover']} 15%)`,
|
|
202
229
|
},
|
|
203
230
|
},
|
|
204
231
|
},
|
|
205
232
|
trackDisabled: {
|
|
206
233
|
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
|
+
},
|
|
207
240
|
},
|
|
208
241
|
trackDisabledOff: {
|
|
209
242
|
backgroundColor: colorVars['--color-background-gray'],
|
|
@@ -213,7 +246,6 @@ const styles = stylex.create({
|
|
|
213
246
|
alignItems: 'center',
|
|
214
247
|
justifyContent: 'center',
|
|
215
248
|
borderRadius: radiusVars['--radius-full'],
|
|
216
|
-
backgroundColor: colorVars['--color-background-surface'],
|
|
217
249
|
transitionProperty: 'transform, width, height',
|
|
218
250
|
transitionDuration: {
|
|
219
251
|
default: durationVars['--duration-fast'],
|
|
@@ -221,6 +253,22 @@ const styles = stylex.create({
|
|
|
221
253
|
},
|
|
222
254
|
transitionTimingFunction: easeVars['--ease-standard'],
|
|
223
255
|
},
|
|
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
|
+
},
|
|
224
272
|
labelWrapper: {
|
|
225
273
|
display: 'flex',
|
|
226
274
|
flexDirection: 'column',
|
|
@@ -537,6 +585,7 @@ export function Switch({
|
|
|
537
585
|
stylex.props(
|
|
538
586
|
styles.thumb,
|
|
539
587
|
isOn ? thumbOnSizeStyles[size] : thumbOffSizeStyles[size],
|
|
588
|
+
isOn ? styles.thumbOn : styles.thumbOff,
|
|
540
589
|
),
|
|
541
590
|
)}>
|
|
542
591
|
{isBusy && <Spinner size="sm" />}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
useTableRowExpansion,
|
|
10
10
|
useTableRowExpansionState,
|
|
11
11
|
} from './useTableRowExpansion';
|
|
12
|
+
import {InternationalizationProvider} from '../../../i18n';
|
|
12
13
|
|
|
13
14
|
// popover mock for context-menu tests
|
|
14
15
|
beforeEach(() => {
|
|
@@ -118,6 +119,24 @@ describe('useTableRowExpansion', () => {
|
|
|
118
119
|
expect(items.length).toBeGreaterThan(0);
|
|
119
120
|
});
|
|
120
121
|
|
|
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
|
+
|
|
121
140
|
it('does not show chevron for leaf nodes', () => {
|
|
122
141
|
render(<Harness />);
|
|
123
142
|
// Leaf C has no children
|
|
@@ -576,7 +576,9 @@ export function useTableRowExpansion<T extends Record<string, unknown>>(
|
|
|
576
576
|
{
|
|
577
577
|
id: 'row-expansion-toggle',
|
|
578
578
|
group: 'row-expansion',
|
|
579
|
-
label: isExpanded
|
|
579
|
+
label: isExpanded
|
|
580
|
+
? t('@astryx.tableRowExpansion.collapseRow')
|
|
581
|
+
: t('@astryx.tableRowExpansion.expandRow'),
|
|
580
582
|
icon: (
|
|
581
583
|
<span {...stylex.props(rtlStyles.mirror)}>
|
|
582
584
|
<Icon
|
|
@@ -39,11 +39,11 @@ import {Skeleton} from '../Skeleton';
|
|
|
39
39
|
import {Spinner} from '../Spinner';
|
|
40
40
|
import {Tooltip} from '../Tooltip/Tooltip';
|
|
41
41
|
import {useDevWarning} from '../hooks/useDevWarning';
|
|
42
|
+
import {useContainerReveal} from '../hooks/useContainerReveal';
|
|
42
43
|
import type {BaseProps} from '../BaseProps';
|
|
43
44
|
import {mergeProps} from '../utils';
|
|
44
45
|
import {themeProps} from '../utils/themeProps';
|
|
45
46
|
import {useTranslator} from '../i18n';
|
|
46
|
-
import {thumbnailScope} from './thumbnail.markers.stylex';
|
|
47
47
|
|
|
48
48
|
export interface ThumbnailProps extends BaseProps<HTMLDivElement> {
|
|
49
49
|
/** Ref forwarded to the root element */
|
|
@@ -236,28 +236,6 @@ const styles = stylex.create({
|
|
|
236
236
|
zIndex: 1,
|
|
237
237
|
lineHeight: 0,
|
|
238
238
|
},
|
|
239
|
-
// showRemoveOn="hover": the remove button is hidden at rest and revealed
|
|
240
|
-
// when the thumbnail is hovered or when focus enters it (keyboard). Only
|
|
241
|
-
// opacity is animated — the button stays mounted and focusable so tabbing
|
|
242
|
-
// to it triggers the :focus-within reveal. Any touch-capable device (incl.
|
|
243
|
-
// hybrid laptops that also report hover) keeps the button visible so the
|
|
244
|
-
// remove path is never gated behind an unavailable hover.
|
|
245
|
-
removeOnHover: {
|
|
246
|
-
opacity: {
|
|
247
|
-
default: 0,
|
|
248
|
-
[stylex.when.ancestor(':hover', thumbnailScope)]: {
|
|
249
|
-
'@media (hover: hover)': 1,
|
|
250
|
-
},
|
|
251
|
-
[stylex.when.ancestor(':focus-within', thumbnailScope)]: 1,
|
|
252
|
-
'@media (any-pointer: coarse)': 1,
|
|
253
|
-
},
|
|
254
|
-
transitionProperty: 'opacity',
|
|
255
|
-
transitionDuration: {
|
|
256
|
-
default: durationVars['--duration-fast'],
|
|
257
|
-
'@media (prefers-reduced-motion: reduce)': '0s',
|
|
258
|
-
},
|
|
259
|
-
transitionTimingFunction: easeVars['--ease-standard'],
|
|
260
|
-
},
|
|
261
239
|
});
|
|
262
240
|
|
|
263
241
|
// =============================================================================
|
|
@@ -326,6 +304,9 @@ export function Thumbnail({
|
|
|
326
304
|
const isInteractive = onClick != null && !isDisabled && !isLoading;
|
|
327
305
|
const hasRemove = onRemove != null && !isDisabled;
|
|
328
306
|
const isHoverReveal = hasRemove && showRemoveOn === 'hover';
|
|
307
|
+
const {getContainerProps, getContentRevealProps} = useContainerReveal({
|
|
308
|
+
isEnabled: isHoverReveal,
|
|
309
|
+
});
|
|
329
310
|
const accessibleName =
|
|
330
311
|
label && alt
|
|
331
312
|
? `${label} — ${alt}`
|
|
@@ -377,9 +358,12 @@ export function Thumbnail({
|
|
|
377
358
|
|
|
378
359
|
const removeButtonEl = hasRemove ? (
|
|
379
360
|
<div
|
|
380
|
-
{...
|
|
381
|
-
styles.removeSlot,
|
|
382
|
-
|
|
361
|
+
{...mergeProps(
|
|
362
|
+
stylex.props(styles.removeSlot),
|
|
363
|
+
// The remove button is absolutely positioned in the corner, so it is
|
|
364
|
+
// already out of flow — an opacity-only reveal (layout preserved)
|
|
365
|
+
// matches its overlay placement instead of the clip recipe.
|
|
366
|
+
isHoverReveal ? getContentRevealProps({isLayoutPreserved: true}) : {},
|
|
383
367
|
)}>
|
|
384
368
|
<Button
|
|
385
369
|
icon={<Icon icon="close" size="xsm" />}
|
|
@@ -410,12 +394,14 @@ export function Thumbnail({
|
|
|
410
394
|
)}
|
|
411
395
|
{...props}>
|
|
412
396
|
<div
|
|
413
|
-
{...
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
397
|
+
{...mergeProps(
|
|
398
|
+
stylex.props(
|
|
399
|
+
styles.imageContainer,
|
|
400
|
+
isInteractive && styles.interactive,
|
|
401
|
+
isInteractive && styles.overlay,
|
|
402
|
+
isInteractive && styles.hoverOnPointer,
|
|
403
|
+
),
|
|
404
|
+
isHoverReveal ? getContainerProps() : {},
|
|
419
405
|
)}>
|
|
420
406
|
{isInteractive ? (
|
|
421
407
|
<button
|