@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useContainerReveal.d.ts","sourceRoot":"","sources":["../../src/hooks/useContainerReveal.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAsB,KAAK,aAAa,EAAC,MAAM,OAAO,CAAC;AA0C9D,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,wBAAwB;IACvC,4EAA4E;IAC5E,iBAAiB,EAAE,MAAM;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,aAAa,CAAA;KAAC,CAAC;IACrE,mDAAmD;IACnD,qBAAqB,EAAE,CACrB,OAAO,CAAC,EAAE,oBAAoB,KAC3B;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,aAAa,CAAA;KAAC,CAAC;CAClD;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,yBAA8B,GACtC,wBAAwB,CAiD1B"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file useContainerReveal.ts
|
|
7
|
+
* @input Uses React, the generated marker pool, useDevWarning
|
|
8
|
+
* @output Exports useContainerReveal — a headless hover/focus reveal primitive
|
|
9
|
+
* @position Core hook. Consumed by any component that reveals (or conceals)
|
|
10
|
+
* content when its container is hovered or focused — e.g. Thumbnail's remove
|
|
11
|
+
* button, TreeList row actions.
|
|
12
|
+
*
|
|
13
|
+
* Gives a container a scoped hover/focus-within trigger that reveals or
|
|
14
|
+
* conceals content inside it, entirely in CSS (no hover state in JS, no
|
|
15
|
+
* re-render on hover). The caller authors NO StyleX: the hook hands out a
|
|
16
|
+
* marker + matching reveal styles from a pre-compiled pool.
|
|
17
|
+
*
|
|
18
|
+
* ACCESSIBILITY (WCAG 2.2 by construction):
|
|
19
|
+
* - Revealed content is visually hidden at rest via the clip recipe, so it
|
|
20
|
+
* stays in the accessibility tree and tab order — never display:none.
|
|
21
|
+
* - Keyboard: revealed on :focus-within, so tabbing in shows it.
|
|
22
|
+
* - Touch: always visible on coarse pointers; never gated behind hover.
|
|
23
|
+
* - Concealed (inverted) content is a mouse-only visual swap: it ignores
|
|
24
|
+
* :focus-within (a keyboard user must never watch content vanish) and stays
|
|
25
|
+
* visible on touch and in the a11y tree.
|
|
26
|
+
* - Motion: honors prefers-reduced-motion.
|
|
27
|
+
*
|
|
28
|
+
* SYNC: When modified, update:
|
|
29
|
+
* - /packages/core/src/hooks/index.ts (export)
|
|
30
|
+
* - containerReveal.pool.stylex.ts (if the slot shape changes)
|
|
31
|
+
*/
|
|
32
|
+
import { useState, useEffect } from 'react';
|
|
33
|
+
import * as stylex from '@stylexjs/stylex';
|
|
34
|
+
import { POOL, POOL_SIZE } from "./containerReveal.pool.stylex.js";
|
|
35
|
+
import { useDevWarning } from "./useDevWarning.js";
|
|
36
|
+
|
|
37
|
+
// Module-level free-list. Each mounted, enabled useContainerReveal claims a
|
|
38
|
+
// distinct pool slot so that any two CONCURRENTLY mounted containers — in
|
|
39
|
+
// particular a nested container inside another's revealed content — get
|
|
40
|
+
// different markers and cannot leak hover/focus into one another. Slots are
|
|
41
|
+
// returned on unmount. Sibling containers that would only ever collide when
|
|
42
|
+
// the pool is exhausted are harmless (a sibling is never an ancestor), so the
|
|
43
|
+
// pool only needs to cover the number of concurrently mounted containers, not
|
|
44
|
+
// instances over time.
|
|
45
|
+
const claimed = new Array(POOL_SIZE).fill(false);
|
|
46
|
+
function claimSlot() {
|
|
47
|
+
for (let i = 0; i < POOL_SIZE; i++) {
|
|
48
|
+
if (!claimed[i]) {
|
|
49
|
+
claimed[i] = true;
|
|
50
|
+
return {
|
|
51
|
+
index: i,
|
|
52
|
+
isExhausted: false
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Exhausted: fall back to slot 0. Safe for siblings (a sibling is never an
|
|
57
|
+
// ancestor); only nesting deeper than POOL_SIZE could reintroduce a leak.
|
|
58
|
+
// Surfaced via useDevWarning so the pool can be grown.
|
|
59
|
+
return {
|
|
60
|
+
index: 0,
|
|
61
|
+
isExhausted: true
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function releaseSlot(index, isExhausted) {
|
|
65
|
+
// A fallback claim never owned the slot exclusively; leave it as-is.
|
|
66
|
+
if (!isExhausted) {
|
|
67
|
+
claimed[index] = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const EMPTY = Object.freeze({});
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Scoped, CSS-only hover/focus reveal for content inside a container.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```tsx
|
|
77
|
+
* const {getContainerProps, getContentRevealProps} = useContainerReveal({
|
|
78
|
+
* isEnabled: revealOn === 'hover',
|
|
79
|
+
* });
|
|
80
|
+
*
|
|
81
|
+
* <div {...mergeProps(getContainerProps(), stylex.props(styles.row))}>
|
|
82
|
+
* {label}
|
|
83
|
+
* <span {...mergeProps(getContentRevealProps(), stylex.props(styles.actions))}>
|
|
84
|
+
* {actions}
|
|
85
|
+
* </span>
|
|
86
|
+
* </div>
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function useContainerReveal(options = {}) {
|
|
90
|
+
const {
|
|
91
|
+
isEnabled = true
|
|
92
|
+
} = options;
|
|
93
|
+
|
|
94
|
+
// Claim a slot for this container's lifetime. Assigned in the initializer
|
|
95
|
+
// (not the render body) and released on unmount so the free-list stays
|
|
96
|
+
// effect-scoped and StrictMode-safe.
|
|
97
|
+
const [claim] = useState(() => isEnabled ? claimSlot() : null);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (claim == null) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
return () => releaseSlot(claim.index, claim.isExhausted);
|
|
103
|
+
}, [claim]);
|
|
104
|
+
useDevWarning('useContainerReveal', `More than ${POOL_SIZE} reveal containers are mounted at once; nested ` + `containers beyond the pool may share a marker. Add markers to ` + `containerReveal.pool.stylex.ts and raise POOL_SIZE.`, claim?.isExhausted ?? false);
|
|
105
|
+
if (claim == null) {
|
|
106
|
+
return {
|
|
107
|
+
getContainerProps: () => EMPTY,
|
|
108
|
+
getContentRevealProps: () => EMPTY
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const slot = POOL[claim.index];
|
|
112
|
+
return {
|
|
113
|
+
getContainerProps: () => stylex.props(slot.marker),
|
|
114
|
+
getContentRevealProps: (contentOptions = {}) => {
|
|
115
|
+
const {
|
|
116
|
+
isRevealInverted = false,
|
|
117
|
+
isLayoutPreserved = false
|
|
118
|
+
} = contentOptions;
|
|
119
|
+
const style = isRevealInverted ? isLayoutPreserved ? slot.concealLayoutPreserved : slot.conceal : isLayoutPreserved ? slot.revealLayoutPreserved : slot.reveal;
|
|
120
|
+
return stylex.props(style);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useInputStatusIcon.d.ts","sourceRoot":"","sources":["../../src/hooks/useInputStatusIcon.tsx"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AAErC,OAAO,EAAsB,KAAK,QAAQ,EAAC,MAAM,SAAS,CAAC;AAC3D,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,4BAA4B,CAAC;AACnE,OAAO,KAAK,EAAC,WAAW,EAAkB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"useInputStatusIcon.d.ts","sourceRoot":"","sources":["../../src/hooks/useInputStatusIcon.tsx"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AAErC,OAAO,EAAsB,KAAK,QAAQ,EAAC,MAAM,SAAS,CAAC;AAC3D,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,4BAA4B,CAAC;AACnE,OAAO,KAAK,EAAC,WAAW,EAAkB,MAAM,gBAAgB,CAAC;AAuDjE,MAAM,WAAW,yBAAyB;IACxC,2DAA2D;IAC3D,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,yDAAyD;IACzD,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,+EAA+E;IAC/E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+CAA+C;IAC/C,IAAI,CAAC,EAAE,QAAQ,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC;;;;;OAKG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;;;;OAOG;IACH,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,aAA0B,EAC1B,SAAiB,EACjB,IAAW,GACZ,EAAE,yBAAyB,GAAG,wBAAwB,CAqGtD"}
|
|
@@ -38,6 +38,7 @@ import { useTooltip } from "../Tooltip/index.js";
|
|
|
38
38
|
import { useTranslator } from "../i18n/index.js";
|
|
39
39
|
import "../theme/tokens.stylex.js";
|
|
40
40
|
import { colorVars, radiusVars } from "../theme/tokens.stylex.js";
|
|
41
|
+
import { themeProps } from "../utils/themeProps.js";
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* Maps each status type to its glyph. Shared so every input shows the same icon
|
|
@@ -128,6 +129,16 @@ export function useInputStatusIcon({
|
|
|
128
129
|
icon: STATUS_ICON[status.type],
|
|
129
130
|
size: size,
|
|
130
131
|
color: status.type
|
|
132
|
+
// Stable theme target on the status glyph itself, so a theme can restyle
|
|
133
|
+
// just this icon (color, size) — and each status — via `defineTheme`.
|
|
134
|
+
// Same-element rules in @layer astryx-theme win over the icon's own base
|
|
135
|
+
// width/height/fontSize, which a field-level target could not reach.
|
|
136
|
+
// Shared by the attached and tooltip variants across all bordered inputs.
|
|
137
|
+
,
|
|
138
|
+
...themeProps('input-status-icon', {
|
|
139
|
+
size,
|
|
140
|
+
status: status.type
|
|
141
|
+
})
|
|
131
142
|
});
|
|
132
143
|
|
|
133
144
|
// Attached (and tooltip-without-message) variants: plain, non-interactive
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file useKeyboardHint.tsx
|
|
3
|
-
* @input Uses React, StyleX, Kbd, useLayer
|
|
3
|
+
* @input Uses React, StyleX, Kbd, useLayer, i18n (useTranslator)
|
|
4
4
|
* @output Exports useKeyboardHint hook — ephemeral arrow-key navigation hint
|
|
5
5
|
* @position Core hook; shows sighted keyboard users how to navigate composite
|
|
6
6
|
* widgets that use roving tabindex (single Tab stop, arrows inside)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useKeyboardHint.d.ts","sourceRoot":"","sources":["../../src/hooks/useKeyboardHint.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,EAAiC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useKeyboardHint.d.ts","sourceRoot":"","sources":["../../src/hooks/useKeyboardHint.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,EAAiC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAiB5E,MAAM,MAAM,uBAAuB,GAAG,YAAY,GAAG,UAAU,GAAG,MAAM,CAAC;AAEzE,MAAM,WAAW,sBAAsB;IACrC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,uBAAuB,CAAC;IAEtC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,WAAW,EAAE,SAAS,CAAC;IAEvB;;;OAGG;IACH,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAEvC;;;OAGG;IACH,MAAM,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAEtC;;;OAGG;IACH,SAAS,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,KAAK,IAAI,CAAC;CAC7C;AAkED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,GAAE,sBAA2B,GACnC,qBAAqB,CAgKvB"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file useKeyboardHint.tsx
|
|
7
|
-
* @input Uses React, StyleX, Kbd, useLayer
|
|
7
|
+
* @input Uses React, StyleX, Kbd, useLayer, i18n (useTranslator)
|
|
8
8
|
* @output Exports useKeyboardHint hook — ephemeral arrow-key navigation hint
|
|
9
9
|
* @position Core hook; shows sighted keyboard users how to navigate composite
|
|
10
10
|
* widgets that use roving tabindex (single Tab stop, arrows inside)
|
|
@@ -21,6 +21,7 @@ import "../theme/tokens.stylex.js";
|
|
|
21
21
|
import { colorVars, radiusVars, shadowVars, spacingVars, typeScaleVars } from "../theme/tokens.stylex.js";
|
|
22
22
|
import { Kbd } from "../Kbd/index.js";
|
|
23
23
|
import { useLayer } from "../Layer/useLayer.js";
|
|
24
|
+
import { useTranslator } from "../i18n/index.js";
|
|
24
25
|
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
27
|
// Types
|
|
@@ -90,6 +91,7 @@ export function useKeyboardHint(options = {}) {
|
|
|
90
91
|
dismissAfterMs = 3000,
|
|
91
92
|
isEnabled = true
|
|
92
93
|
} = options;
|
|
94
|
+
const t = useTranslator();
|
|
93
95
|
const timeoutRef = useRef(null);
|
|
94
96
|
const dismissedRef = useRef(false);
|
|
95
97
|
const isVisibleRef = useRef(false);
|
|
@@ -201,7 +203,7 @@ export function useKeyboardHint(options = {}) {
|
|
|
201
203
|
...{
|
|
202
204
|
className: "x11g1kdw"
|
|
203
205
|
},
|
|
204
|
-
children:
|
|
206
|
+
children: t('@astryx.keyboardHint.toNavigate')
|
|
205
207
|
})]
|
|
206
208
|
}), {
|
|
207
209
|
placement: 'below',
|
package/locales/en.json
CHANGED
|
@@ -247,6 +247,10 @@
|
|
|
247
247
|
"defaultMessage": "Use arrow keys to move between avatars",
|
|
248
248
|
"description": "Screen-reader-only instruction attached (via aria-describedby) to a group of interactive avatars that share a single Tab stop. Tells keyboard users the Left/Right arrow keys move focus between the avatars. Only announced when the group has interactive (link/button) avatars."
|
|
249
249
|
},
|
|
250
|
+
"@astryx.avatarGroup.overflow": {
|
|
251
|
+
"defaultMessage": "{count, number} more",
|
|
252
|
+
"description": "Accessible name for the \"+N\" overflow indicator at the end of an AvatarGroup — announces how many additional avatars are not shown. Example: `5 more`. The visible \"+N\" text is unaffected; this is the aria-label only."
|
|
253
|
+
},
|
|
250
254
|
"@astryx.banner.dismiss": {
|
|
251
255
|
"defaultMessage": "Dismiss",
|
|
252
256
|
"description": "\"Dismiss\" = close/hide this notification (not \"reject a person\"). Aria label AND tooltip on the small X button on a Banner."
|
|
@@ -407,6 +411,10 @@
|
|
|
407
411
|
"defaultMessage": "Search options",
|
|
408
412
|
"description": "Screen-reader-only accessible name for that same search input inside a MultiSelector."
|
|
409
413
|
},
|
|
414
|
+
"@astryx.multiSelector.selectAllPartiallySelected": {
|
|
415
|
+
"defaultMessage": "{label}, partially selected",
|
|
416
|
+
"description": "Accessible name for the MultiSelector select-all option while only some options are selected. `{label}` is the visible select-all label (e.g. \"Select all\"). ARIA forbids aria-selected=\"mixed\" on options, so the indeterminate state is conveyed through the name instead. \"Partially\" = some but not all."
|
|
417
|
+
},
|
|
410
418
|
"@astryx.popover.close": {
|
|
411
419
|
"defaultMessage": "Close popover",
|
|
412
420
|
"description": "\"Close\" = shut/dismiss, not \"nearby\". Screen-reader-only label on the close button inside a Popover."
|
|
@@ -683,6 +691,10 @@
|
|
|
683
691
|
"defaultMessage": "Expand",
|
|
684
692
|
"description": "\"Expand\" = reveal more, not grow physically. Aria label AND tooltip on the same toggle when currently collapsed. Pairs with `banner.collapse`."
|
|
685
693
|
},
|
|
694
|
+
"@astryx.button.loading": {
|
|
695
|
+
"defaultMessage": "Loading",
|
|
696
|
+
"description": "Screen-reader-only live-region announcement while a Button is in its loading state (spinner shown, action in flight). Progressive sense (\"work in progress\"), not a noun. Kept separate from `typeahead.loading` — translations may diverge."
|
|
697
|
+
},
|
|
686
698
|
"@astryx.chatComposerDrawer.expand": {
|
|
687
699
|
"defaultMessage": "Expand {label}",
|
|
688
700
|
"description": "Screen-reader-only label on the ChatComposerDrawer toggle when the drawer is collapsed. `{label}` is the drawer's visible name — example: `Expand Attachments`. Pairs with `collapse`."
|
|
@@ -747,6 +759,14 @@
|
|
|
747
759
|
"defaultMessage": "Code",
|
|
748
760
|
"description": "Screen-reader-only fallback name for a code-snippet scroll container when the code's language is unknown. \"Code\" = source code, not secret code or code of conduct."
|
|
749
761
|
},
|
|
762
|
+
"@astryx.field.optional": {
|
|
763
|
+
"defaultMessage": "Optional",
|
|
764
|
+
"description": "Visible indicator shown next to a form field's label when the field is optional (isOptional). Mutually exclusive with the required indicator. Very short — appears inline after the label text."
|
|
765
|
+
},
|
|
766
|
+
"@astryx.field.required": {
|
|
767
|
+
"defaultMessage": "Required",
|
|
768
|
+
"description": "Visible indicator shown next to a form field's label when the field must be filled in (isRequired). Mutually exclusive with the optional indicator. Very short — appears inline after the label text."
|
|
769
|
+
},
|
|
750
770
|
"@astryx.fileInput.clearLabel": {
|
|
751
771
|
"defaultMessage": "Clear {label}",
|
|
752
772
|
"description": "Screen-reader-only label on the X that removes the selected file from a FileInput. Example: `Clear Attachment`, `Clear Résumé`."
|
|
@@ -759,10 +779,22 @@
|
|
|
759
779
|
"defaultMessage": "{label}, {fileNames}",
|
|
760
780
|
"description": "Accessible name for a FileInput trigger that has files attached, composing the field label with the selected filenames. Example: `Attachments, report.pdf, notes.txt`."
|
|
761
781
|
},
|
|
782
|
+
"@astryx.keyboardHint.toNavigate": {
|
|
783
|
+
"defaultMessage": "to navigate",
|
|
784
|
+
"description": "Trailing text in the keyboard-hint badge, shown after arrow-key icons — reads as `← → to navigate`. Lowercase sentence fragment completing the visual phrase; the badge is aria-hidden, so this is for sighted users only."
|
|
785
|
+
},
|
|
762
786
|
"@astryx.lightbox.mediaViewer": {
|
|
763
787
|
"defaultMessage": "Media viewer",
|
|
764
788
|
"description": "Screen-reader-only fallback name for the Lightbox dialog when the current media item has no alt text. Should rarely render — consumers should provide alt."
|
|
765
789
|
},
|
|
790
|
+
"@astryx.metadataList.showMore": {
|
|
791
|
+
"defaultMessage": "Show more",
|
|
792
|
+
"description": "Button label under a MetadataList that reveals items hidden beyond the configured maximum. Toggles with `metadataList.showLess` — keep the pair parallel in your language."
|
|
793
|
+
},
|
|
794
|
+
"@astryx.metadataList.showLess": {
|
|
795
|
+
"defaultMessage": "Show less",
|
|
796
|
+
"description": "Button label under a MetadataList that re-hides the extra items revealed by `metadataList.showMore`. Keep the pair parallel."
|
|
797
|
+
},
|
|
766
798
|
"@astryx.mobileNav.navigation": {
|
|
767
799
|
"defaultMessage": "Navigation",
|
|
768
800
|
"description": "Screen-reader-only fallback name for the MobileNav overlay dialog when no explicit label was passed."
|
package/locales/pseudo.json
CHANGED
|
@@ -185,6 +185,9 @@
|
|
|
185
185
|
"@astryx.avatarGroup.keyboardHint": {
|
|
186
186
|
"defaultMessage": "⟦Úšé àřřóŵ ķéýš ţó ɱóṽé ƀéţŵééñ àṽàţàřš⟧"
|
|
187
187
|
},
|
|
188
|
+
"@astryx.avatarGroup.overflow": {
|
|
189
|
+
"defaultMessage": "⟦{count, number} ɱóřé⟧"
|
|
190
|
+
},
|
|
188
191
|
"@astryx.banner.dismiss": {
|
|
189
192
|
"defaultMessage": "⟦Ðíšɱíšš⟧"
|
|
190
193
|
},
|
|
@@ -305,6 +308,9 @@
|
|
|
305
308
|
"@astryx.multiSelector.searchOptions": {
|
|
306
309
|
"defaultMessage": "⟦Šéàřçĥ óþţíóñš⟧"
|
|
307
310
|
},
|
|
311
|
+
"@astryx.multiSelector.selectAllPartiallySelected": {
|
|
312
|
+
"defaultMessage": "⟦{label}, þàřţíàłłý šéłéçţéð⟧"
|
|
313
|
+
},
|
|
308
314
|
"@astryx.popover.close": {
|
|
309
315
|
"defaultMessage": "⟦Çłóšé þóþóṽéř⟧"
|
|
310
316
|
},
|
|
@@ -512,6 +518,9 @@
|
|
|
512
518
|
"@astryx.banner.expand": {
|
|
513
519
|
"defaultMessage": "⟦Éẋþàñð⟧"
|
|
514
520
|
},
|
|
521
|
+
"@astryx.button.loading": {
|
|
522
|
+
"defaultMessage": "⟦Łóàðíñĝ⟧"
|
|
523
|
+
},
|
|
515
524
|
"@astryx.chatComposerDrawer.expand": {
|
|
516
525
|
"defaultMessage": "⟦Éẋþàñð {label}⟧"
|
|
517
526
|
},
|
|
@@ -560,6 +569,12 @@
|
|
|
560
569
|
"@astryx.codeBlock.code": {
|
|
561
570
|
"defaultMessage": "⟦Çóðé⟧"
|
|
562
571
|
},
|
|
572
|
+
"@astryx.field.optional": {
|
|
573
|
+
"defaultMessage": "⟦Óþţíóñàł⟧"
|
|
574
|
+
},
|
|
575
|
+
"@astryx.field.required": {
|
|
576
|
+
"defaultMessage": "⟦Řéɋúířéð⟧"
|
|
577
|
+
},
|
|
563
578
|
"@astryx.fileInput.clearLabel": {
|
|
564
579
|
"defaultMessage": "⟦Çłéàř {label}⟧"
|
|
565
580
|
},
|
|
@@ -569,9 +584,18 @@
|
|
|
569
584
|
"@astryx.fileInput.triggerWithFiles": {
|
|
570
585
|
"defaultMessage": "⟦{label}, {fileNames}⟧"
|
|
571
586
|
},
|
|
587
|
+
"@astryx.keyboardHint.toNavigate": {
|
|
588
|
+
"defaultMessage": "⟦ţó ñàṽíĝàţé⟧"
|
|
589
|
+
},
|
|
572
590
|
"@astryx.lightbox.mediaViewer": {
|
|
573
591
|
"defaultMessage": "⟦Ṁéðíà ṽíéŵéř⟧"
|
|
574
592
|
},
|
|
593
|
+
"@astryx.metadataList.showMore": {
|
|
594
|
+
"defaultMessage": "⟦Šĥóŵ ɱóřé⟧"
|
|
595
|
+
},
|
|
596
|
+
"@astryx.metadataList.showLess": {
|
|
597
|
+
"defaultMessage": "⟦Šĥóŵ łéšš⟧"
|
|
598
|
+
},
|
|
575
599
|
"@astryx.mobileNav.navigation": {
|
|
576
600
|
"defaultMessage": "⟦Ñàṽíĝàţíóñ⟧"
|
|
577
601
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.2.0-canary.
|
|
3
|
+
"version": "0.2.0-canary.fc53ff7",
|
|
4
4
|
"displayName": "Astryx Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -5,6 +5,7 @@ import userEvent from '@testing-library/user-event';
|
|
|
5
5
|
import {AvatarGroup} from './AvatarGroup';
|
|
6
6
|
import {AvatarGroupOverflow} from './AvatarGroupOverflow';
|
|
7
7
|
import {Avatar, AvatarStatusDot} from '../Avatar';
|
|
8
|
+
import {InternationalizationProvider} from '../i18n';
|
|
8
9
|
|
|
9
10
|
describe('AvatarGroup', () => {
|
|
10
11
|
it('renders all avatar children', () => {
|
|
@@ -238,7 +239,26 @@ describe('AvatarGroupOverflow — hardening', () => {
|
|
|
238
239
|
);
|
|
239
240
|
|
|
240
241
|
expect(screen.getByText('+4912')).toBeInTheDocument();
|
|
241
|
-
|
|
242
|
+
// The aria-label routes through the catalog's `{count, number}` ICU
|
|
243
|
+
// argument, so the en locale adds a grouping separator.
|
|
244
|
+
expect(screen.getByLabelText('4,912 more')).toBeInTheDocument();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('localizes the overflow label through the i18n catalog', () => {
|
|
248
|
+
render(
|
|
249
|
+
<InternationalizationProvider
|
|
250
|
+
locale="fr"
|
|
251
|
+
overrides={{
|
|
252
|
+
fr: {'@astryx.avatarGroup.overflow': '{count, number} de plus'},
|
|
253
|
+
}}>
|
|
254
|
+
<AvatarGroup>
|
|
255
|
+
<Avatar name="Alice" />
|
|
256
|
+
<AvatarGroupOverflow count={3} />
|
|
257
|
+
</AvatarGroup>
|
|
258
|
+
</InternationalizationProvider>,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
expect(screen.getByLabelText('3 de plus')).toBeInTheDocument();
|
|
242
262
|
});
|
|
243
263
|
});
|
|
244
264
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @file AvatarGroupOverflow.tsx
|
|
6
|
-
* @input Uses React, StyleX, AvatarGroupContext
|
|
6
|
+
* @input Uses React, StyleX, AvatarGroupContext, i18n (useTranslator)
|
|
7
7
|
* @output Exports AvatarGroupOverflow for overflow indicator
|
|
8
8
|
* @position Slot component used inside AvatarGroup
|
|
9
9
|
*
|
|
@@ -26,6 +26,7 @@ import {mergeProps} from '../utils';
|
|
|
26
26
|
import {useAvatarGroup} from './AvatarGroupContext';
|
|
27
27
|
import type {BaseProps} from '../BaseProps';
|
|
28
28
|
import {themeProps} from '../utils/themeProps';
|
|
29
|
+
import {useTranslator} from '../i18n';
|
|
29
30
|
|
|
30
31
|
const BORDER_WIDTH = 2;
|
|
31
32
|
const OVERFLOW_FONT_RATIO = 0.35;
|
|
@@ -150,11 +151,12 @@ export function AvatarGroupOverflow({
|
|
|
150
151
|
style,
|
|
151
152
|
...rest
|
|
152
153
|
}: AvatarGroupOverflowProps): ReactNode {
|
|
154
|
+
const t = useTranslator();
|
|
153
155
|
const group = useAvatarGroup();
|
|
154
156
|
const numericSize = group?.numericSize ?? 36;
|
|
155
157
|
const overlap = group?.overlap ?? 0;
|
|
156
158
|
|
|
157
|
-
const label =
|
|
159
|
+
const label = t('@astryx.avatarGroup.overflow', {count});
|
|
158
160
|
const content = children ?? `+${count}`;
|
|
159
161
|
|
|
160
162
|
if (onClick) {
|
|
@@ -14,6 +14,7 @@ import {render, screen, fireEvent, act} from '@testing-library/react';
|
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
15
|
import {Button} from './Button';
|
|
16
16
|
import {Badge} from '../Badge/Badge';
|
|
17
|
+
import {InternationalizationProvider} from '../i18n';
|
|
17
18
|
|
|
18
19
|
describe('Button', () => {
|
|
19
20
|
it('renders label as visible text', () => {
|
|
@@ -446,6 +447,21 @@ describe('Button', () => {
|
|
|
446
447
|
expect(liveRegion).toHaveTextContent('Loading');
|
|
447
448
|
});
|
|
448
449
|
|
|
450
|
+
it('localizes the loading announcement through the i18n catalog', () => {
|
|
451
|
+
render(
|
|
452
|
+
<InternationalizationProvider
|
|
453
|
+
locale="fr"
|
|
454
|
+
overrides={{fr: {'@astryx.button.loading': 'Chargement'}}}>
|
|
455
|
+
<Button label="Submit" isLoading />
|
|
456
|
+
</InternationalizationProvider>,
|
|
457
|
+
);
|
|
458
|
+
const button = screen.getByRole('button');
|
|
459
|
+
// The Spinner also has role="status", so grab the live region explicitly.
|
|
460
|
+
const regions = button.querySelectorAll('[role="status"]');
|
|
461
|
+
const liveRegion = regions[regions.length - 1];
|
|
462
|
+
expect(liveRegion).toHaveTextContent('Chargement');
|
|
463
|
+
});
|
|
464
|
+
|
|
449
465
|
describe('elevation', () => {
|
|
450
466
|
it('renders a distinct class for each elevation level', () => {
|
|
451
467
|
const classFor = (elevation: 'none' | 'low' | 'med' | 'high') => {
|
package/src/Button/Button.tsx
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file Button.tsx
|
|
7
|
-
* @input Uses React, ButtonHTMLAttributes, ReactNode
|
|
7
|
+
* @input Uses React, ButtonHTMLAttributes, ReactNode, i18n (useTranslator)
|
|
8
8
|
* @output Exports Button component, ButtonProps, ButtonVariant types
|
|
9
9
|
* @position Core implementation; consumed by index.ts, tested by Button.test.tsx
|
|
10
10
|
*
|
|
@@ -45,6 +45,7 @@ import {mergeProps, mergeRefs} from '../utils';
|
|
|
45
45
|
import {useLinkComponent} from '../Link/useLinkComponent';
|
|
46
46
|
import type {LinkComponentType} from '../Link/types';
|
|
47
47
|
import {themeProps} from '../utils/themeProps';
|
|
48
|
+
import {useTranslator} from '../i18n';
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
51
|
* Base button styles
|
|
@@ -611,6 +612,7 @@ export function Button({
|
|
|
611
612
|
ref,
|
|
612
613
|
...props
|
|
613
614
|
}: ButtonProps): ReactNode {
|
|
615
|
+
const t = useTranslator();
|
|
614
616
|
const size = useSize(sizeProp, 'md');
|
|
615
617
|
const buttonGroup = useButtonGroup();
|
|
616
618
|
|
|
@@ -760,7 +762,7 @@ export function Button({
|
|
|
760
762
|
</span>
|
|
761
763
|
{/* Live region for loading state announcements */}
|
|
762
764
|
<VisuallyHidden role="status" aria-live="polite">
|
|
763
|
-
{isLoadingState ? '
|
|
765
|
+
{isLoadingState ? t('@astryx.button.loading') : ''}
|
|
764
766
|
</VisuallyHidden>
|
|
765
767
|
</>
|
|
766
768
|
);
|
|
@@ -13,6 +13,7 @@ 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 {CheckboxInput} from './CheckboxInput';
|
|
16
|
+
import {getForcedColorsRules} from '../__tests__/forcedColors';
|
|
16
17
|
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
17
18
|
|
|
18
19
|
afterEach(() => {
|
|
@@ -543,3 +544,25 @@ describe('CheckboxInput', () => {
|
|
|
543
544
|
});
|
|
544
545
|
});
|
|
545
546
|
});
|
|
547
|
+
|
|
548
|
+
// jsdom cannot emulate forced-colors rendering, so this asserts that the
|
|
549
|
+
// compiled output includes the forced-colors rule; visual behavior needs
|
|
550
|
+
// manual verification under Windows High Contrast.
|
|
551
|
+
describe('forced colors (WCAG 1.4.11)', () => {
|
|
552
|
+
it('compiles a forced-colors fill so the indeterminate mark survives Windows High Contrast', () => {
|
|
553
|
+
render(
|
|
554
|
+
<CheckboxInput label="All" value="indeterminate" onChange={() => {}} />,
|
|
555
|
+
);
|
|
556
|
+
// The painted indeterminate bar would be stripped to Canvas (invisible);
|
|
557
|
+
// CanvasText keeps it perceivable.
|
|
558
|
+
expect(getForcedColorsRules()).toContain('background-color: canvastext;');
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('compiles a forced-colors color so the checkmark survives Windows High Contrast', () => {
|
|
562
|
+
render(<CheckboxInput label="Accept" value={true} onChange={() => {}} />);
|
|
563
|
+
// The check strokes with currentColor; forced colors leaves it the same
|
|
564
|
+
// white as the flattened box, so it needs its own CanvasText color to stay
|
|
565
|
+
// perceivable on the Canvas box.
|
|
566
|
+
expect(getForcedColorsRules()).toContain('color: canvastext;');
|
|
567
|
+
});
|
|
568
|
+
});
|
|
@@ -158,14 +158,29 @@ const styles = stylex.create({
|
|
|
158
158
|
},
|
|
159
159
|
checkmark: {
|
|
160
160
|
display: 'none',
|
|
161
|
-
color:
|
|
161
|
+
color: {
|
|
162
|
+
default: colorVars['--color-on-accent'],
|
|
163
|
+
// Forced colors (Windows High Contrast) does not reliably force an SVG
|
|
164
|
+
// stroke painted with currentColor, so the check stays the same white as
|
|
165
|
+
// the flattened (Canvas) box fill — a white check on a white box.
|
|
166
|
+
// CanvasText keeps it perceivable on the Canvas box, matching the
|
|
167
|
+
// indeterminate mark (WCAG 1.4.11).
|
|
168
|
+
'@media (forced-colors: active)': 'CanvasText',
|
|
169
|
+
},
|
|
162
170
|
},
|
|
163
171
|
checkmarkVisible: {
|
|
164
172
|
display: 'block',
|
|
165
173
|
},
|
|
166
174
|
indeterminateMark: {
|
|
167
175
|
display: 'none',
|
|
168
|
-
backgroundColor:
|
|
176
|
+
backgroundColor: {
|
|
177
|
+
default: colorVars['--color-on-accent'],
|
|
178
|
+
// Forced colors (Windows High Contrast) strips painted backgrounds,
|
|
179
|
+
// which would make the indeterminate bar invisible; CanvasText keeps it
|
|
180
|
+
// perceivable on the Canvas box fill (WCAG 1.4.11). The checkmark carries
|
|
181
|
+
// the matching CanvasText treatment on its own style.
|
|
182
|
+
'@media (forced-colors: active)': 'CanvasText',
|
|
183
|
+
},
|
|
169
184
|
borderRadius: radiusVars['--radius-full'],
|
|
170
185
|
},
|
|
171
186
|
indeterminateMarkVisible: {
|