@astryxdesign/core 0.7.1-canary.f2beffa → 0.7.1-canary.fb02ee9
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/Field/InputClearButton.d.ts +8 -4
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +23 -7
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +1 -0
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +1 -0
- package/locales/en.json +4 -0
- package/locales/pseudo.json +3 -0
- package/package.json +3 -3
- package/src/Collapsible/CollapsibleGroup.spec.md +17 -8
- package/src/Field/InputClearButton.test.tsx +29 -4
- package/src/Field/InputClearButton.tsx +26 -8
- package/src/Grid/Grid.spec.md +50 -32
- package/src/Markdown/Markdown.fr23.perf.test.ts +161 -40
- package/src/i18n/generated-locales/en.generated.ts +1 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +1 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file InputClearButton.tsx
|
|
3
|
-
* @input Uses React, Button, and
|
|
4
|
-
* @output Exports the public InputClearButton and an internal popup-aware variant
|
|
5
|
-
*
|
|
3
|
+
* @input Uses React, Button, Icon, and the i18n translator
|
|
4
|
+
* @output Exports the public InputClearButton and an internal popup-aware variant.
|
|
5
|
+
* Both show the localized generic "Clear" tooltip and keep the caller's
|
|
6
|
+
* contextual label as the accessible name.
|
|
6
7
|
* @position Shared primitive. Every input that renders a clear affordance —
|
|
7
8
|
* TextInput, NumberInput, TimeInput, DateInput, DateTimeInput,
|
|
8
9
|
* DateRangeInput, Selector, MultiSelector, Typeahead, Tokenizer, FileInput —
|
|
@@ -13,7 +14,10 @@
|
|
|
13
14
|
import type { ReactNode } from 'react';
|
|
14
15
|
import * as stylex from '@stylexjs/stylex';
|
|
15
16
|
export interface InputClearButtonProps {
|
|
16
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Contextual accessible name for the button, such as "Clear Search". The
|
|
19
|
+
* visible tooltip is always the localized generic "Clear".
|
|
20
|
+
*/
|
|
17
21
|
label: string;
|
|
18
22
|
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
|
19
23
|
xstyle?: stylex.StyleXStyles;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InputClearButton.d.ts","sourceRoot":"","sources":["../../src/Field/InputClearButton.tsx"],"names":[],"mappings":"AAIA
|
|
1
|
+
{"version":3,"file":"InputClearButton.d.ts","sourceRoot":"","sources":["../../src/Field/InputClearButton.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AACrC,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAmD3C,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC,YAAY,CAAC;IAC7B,iFAAiF;IACjF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,6BAA8B,SAAQ,qBAAqB;IACnE,aAAa,EAAE,KAAK,CAAC,mBAAmB,CAAC,WAAW,CAAC,CAAC;IACtD,cAAc,EAAE,KAAK,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;CACtD;AAsDD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,SAAS,CAMxE;yBANe,gBAAgB;;;AAQhC,4EAA4E;AAC5E,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,6BAA6B,GACnC,SAAS,CAMX"}
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file InputClearButton.tsx
|
|
7
|
-
* @input Uses React, Button, and
|
|
8
|
-
* @output Exports the public InputClearButton and an internal popup-aware variant
|
|
9
|
-
*
|
|
7
|
+
* @input Uses React, Button, Icon, and the i18n translator
|
|
8
|
+
* @output Exports the public InputClearButton and an internal popup-aware variant.
|
|
9
|
+
* Both show the localized generic "Clear" tooltip and keep the caller's
|
|
10
|
+
* contextual label as the accessible name.
|
|
10
11
|
* @position Shared primitive. Every input that renders a clear affordance —
|
|
11
12
|
* TextInput, NumberInput, TimeInput, DateInput, DateTimeInput,
|
|
12
13
|
* DateRangeInput, Selector, MultiSelector, Typeahead, Tokenizer, FileInput —
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
import * as stylex from '@stylexjs/stylex';
|
|
18
19
|
import { Button } from "../Button/index.js";
|
|
19
20
|
import { Icon } from "../Icon/index.js";
|
|
21
|
+
import { useTranslator } from "../i18n/index.js";
|
|
20
22
|
import { themeProps } from "../utils/themeProps.js";
|
|
21
23
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
22
24
|
const styles = {
|
|
@@ -34,6 +36,7 @@ const styles = {
|
|
|
34
36
|
};
|
|
35
37
|
function renderInputClearButton({
|
|
36
38
|
label,
|
|
39
|
+
tooltip,
|
|
37
40
|
onClick,
|
|
38
41
|
onPointerDown,
|
|
39
42
|
onClickCapture,
|
|
@@ -49,8 +52,13 @@ function renderInputClearButton({
|
|
|
49
52
|
return /*#__PURE__*/_jsx(Button, {
|
|
50
53
|
variant: "ghost",
|
|
51
54
|
size: "sm",
|
|
52
|
-
label: label
|
|
53
|
-
|
|
55
|
+
label: label
|
|
56
|
+
// The field's label already sits beside the glyph, so the tooltip names
|
|
57
|
+
// only the action; repeating a long label reads as awkward copy. The
|
|
58
|
+
// contextual label stays the accessible name, which still contains
|
|
59
|
+
// the visible word (WCAG 2.5.3, label in name).
|
|
60
|
+
,
|
|
61
|
+
tooltip: tooltip,
|
|
54
62
|
className: buttonTargetClassName,
|
|
55
63
|
icon: /*#__PURE__*/_jsx(Icon, {
|
|
56
64
|
icon: "close",
|
|
@@ -70,11 +78,19 @@ function renderInputClearButton({
|
|
|
70
78
|
});
|
|
71
79
|
}
|
|
72
80
|
export function InputClearButton(props) {
|
|
73
|
-
|
|
81
|
+
const t = useTranslator();
|
|
82
|
+
return renderInputClearButton({
|
|
83
|
+
...props,
|
|
84
|
+
tooltip: t('@astryx.inputClearButton.tooltip')
|
|
85
|
+
});
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
/** Internal variant used while popup-aware clear behavior is rolled out. */
|
|
77
89
|
export function InternalInputClearButton(props) {
|
|
78
|
-
|
|
90
|
+
const t = useTranslator();
|
|
91
|
+
return renderInputClearButton({
|
|
92
|
+
...props,
|
|
93
|
+
tooltip: t('@astryx.inputClearButton.tooltip')
|
|
94
|
+
});
|
|
79
95
|
}
|
|
80
96
|
InputClearButton.displayName = 'InputClearButton';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.generated.d.ts","sourceRoot":"","sources":["../../../src/i18n/generated-locales/en.generated.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,QAAA,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"en.generated.d.ts","sourceRoot":"","sources":["../../../src/i18n/generated-locales/en.generated.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,QAAA,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmcpD,CAAC;AAEF,eAAe,cAAc,CAAC"}
|
|
@@ -359,6 +359,7 @@ const runtimeCatalog = {
|
|
|
359
359
|
'@astryx.tableTree.expandAllRows': 'Expand all rows',
|
|
360
360
|
'@astryx.textArea.charactersRemaining': '{count, number} {count, plural, one {character} other {characters}} remaining',
|
|
361
361
|
'@astryx.textArea.charactersOverLimit': '{count, number} {count, plural, one {character} other {characters}} over the limit',
|
|
362
|
+
'@astryx.inputClearButton.tooltip': 'Clear',
|
|
362
363
|
'@astryx.textInput.clearLabel': 'Clear {label}',
|
|
363
364
|
'@astryx.thumbnail.remove': 'Remove {accessibleName}',
|
|
364
365
|
'@astryx.thumbnail.open': 'Open {accessibleName}',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pseudo.generated.d.ts","sourceRoot":"","sources":["../../../src/i18n/generated-locales/pseudo.generated.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,QAAA,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"pseudo.generated.d.ts","sourceRoot":"","sources":["../../../src/i18n/generated-locales/pseudo.generated.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,QAAA,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA4cpD,CAAC;AAEF,eAAe,cAAc,CAAC"}
|
|
@@ -359,6 +359,7 @@ const runtimeCatalog = {
|
|
|
359
359
|
'@astryx.tableTree.expandAllRows': '⟦Éẋþàñð àłł řóŵš⟧',
|
|
360
360
|
'@astryx.textArea.charactersRemaining': '⟦{count, number} {count, plural, one {character} other {characters}} řéɱàíñíñĝ⟧',
|
|
361
361
|
'@astryx.textArea.charactersOverLimit': '⟦{count, number} {count, plural, one {character} other {characters}} óṽéř ţĥé łíɱíţ⟧',
|
|
362
|
+
'@astryx.inputClearButton.tooltip': '⟦Çłéàř⟧',
|
|
362
363
|
'@astryx.textInput.clearLabel': '⟦Çłéàř {label}⟧',
|
|
363
364
|
'@astryx.thumbnail.remove': '⟦Řéɱóṽé {accessibleName}⟧',
|
|
364
365
|
'@astryx.thumbnail.open': '⟦Óþéñ {accessibleName}⟧',
|
package/locales/en.json
CHANGED
|
@@ -1407,6 +1407,10 @@
|
|
|
1407
1407
|
"defaultMessage": "{count, number} {count, plural, one {character} other {characters}} over the limit",
|
|
1408
1408
|
"description": "Screen-reader-only assertive announcement when the user has typed past a TextArea's maxLength. `{count}` is how many characters over — example: `3 characters over the limit`."
|
|
1409
1409
|
},
|
|
1410
|
+
"@astryx.inputClearButton.tooltip": {
|
|
1411
|
+
"defaultMessage": "Clear",
|
|
1412
|
+
"description": "Tooltip shown on hover or keyboard focus over the X button inside an input that clears its value. Verb, imperative. Shown alone beside the field, so it names only the action; the screen-reader label names the field separately."
|
|
1413
|
+
},
|
|
1410
1414
|
"@astryx.textInput.clearLabel": {
|
|
1411
1415
|
"defaultMessage": "Clear {label}",
|
|
1412
1416
|
"description": "Screen-reader-only label on the X that clears the value from a TextInput. Example: `Clear Email`, `Clear Name`."
|
package/locales/pseudo.json
CHANGED
|
@@ -1055,6 +1055,9 @@
|
|
|
1055
1055
|
"@astryx.textArea.charactersOverLimit": {
|
|
1056
1056
|
"defaultMessage": "⟦{count, number} {count, plural, one {character} other {characters}} óṽéř ţĥé łíɱíţ⟧"
|
|
1057
1057
|
},
|
|
1058
|
+
"@astryx.inputClearButton.tooltip": {
|
|
1059
|
+
"defaultMessage": "⟦Çłéàř⟧"
|
|
1060
|
+
},
|
|
1058
1061
|
"@astryx.textInput.clearLabel": {
|
|
1059
1062
|
"defaultMessage": "⟦Çłéàř {label}⟧"
|
|
1060
1063
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.7.1-canary.
|
|
3
|
+
"version": "0.7.1-canary.fb02ee9",
|
|
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",
|
|
@@ -787,8 +787,8 @@
|
|
|
787
787
|
"react-dom": ">=19.0.0"
|
|
788
788
|
},
|
|
789
789
|
"devDependencies": {
|
|
790
|
-
"@astryxdesign/a11y-spec": "0.7.1-canary.
|
|
791
|
-
"@astryxdesign/cli": "0.7.1-canary.
|
|
790
|
+
"@astryxdesign/a11y-spec": "0.7.1-canary.fb02ee9",
|
|
791
|
+
"@astryxdesign/cli": "0.7.1-canary.fb02ee9",
|
|
792
792
|
"@babel/cli": "^7.29.7",
|
|
793
793
|
"@babel/core": "^7.29.7",
|
|
794
794
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -133,6 +133,7 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
133
133
|
- **AR1 — Child semantics remain primary.** Group coordination MUST preserve each Collapsible button's accessible name, `aria-expanded`, `aria-controls`, controlled region, and keyboard activation.
|
|
134
134
|
- **AR2 — Presentation adds no competing interaction.** The divider wrapper MUST NOT add a role, focus target, accessible name, or keyboard behavior.
|
|
135
135
|
- **AR3 — Direction stays with Collapsible.** Group chevron defaults MUST preserve `component:Collapsible/AR1–AR3`, including the inward leading cue under LTR and RTL.
|
|
136
|
+
- **AR4 — The group adopts the APG Accordion pattern's required parts** (`spec:AST-020/FR15`): each direct item's header is a `button` whose accessible name is the item's title, with `aria-expanded` reflecting its panel, `aria-controls` naming the panel it reveals, and `aria-disabled` when it cannot be toggled; every enabled header sits in the page Tab order and Enter or Space toggles its panel, in `single` mode collapsing the sibling that was open; a disabled header carries `aria-disabled`, drops out of the Tab order, and cannot be toggled. Not adopted: a heading element around each header (this record requires none; the caller's document structure supplies headings), the optional `region` role on panels, and arrow-key or Home/End movement between headers — the group is a set of independent disclosures, not one composite control.
|
|
136
137
|
|
|
137
138
|
## Design relationships
|
|
138
139
|
|
|
@@ -171,17 +172,25 @@ The Group container is CollapsibleGroup's conditional aggregate wrapper. The oth
|
|
|
171
172
|
|
|
172
173
|
## Verification map
|
|
173
174
|
|
|
174
|
-
| Contract | Verification
|
|
175
|
-
| ---------------------- |
|
|
176
|
-
| FR1–FR4, ORD1 | `CollapsibleGroup.test.tsx`; `useCollapsible.test.tsx`
|
|
177
|
-
| FR5–FR6, ORD2–ORD3 | `CollapsibleGroup.test.tsx`; `Collapsible.test.tsx`; `themingTargets.test.ts`
|
|
178
|
-
|
|
|
179
|
-
|
|
|
180
|
-
|
|
|
175
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
176
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
|
177
|
+
| FR1–FR4, ORD1 | `CollapsibleGroup.test.tsx`; `useCollapsible.test.tsx` | single/multiple; controlled/uncontrolled; valued/unvalued | A sibling stays open in single mode, closes in multiple mode, controlled state mutates internally, or an unvalued item loses local state. | `audit:CollapsibleGroup/behavior` |
|
|
178
|
+
| FR5–FR6, ORD2–ORD3 | `CollapsibleGroup.test.tsx`; `Collapsible.test.tsx`; `themingTargets.test.ts` | plain/divided; each density; group/item/nested position | Wrapper/target reachability changes, defaults stop reaching direct items, or presentation leaks into nested content. | `audit:CollapsibleGroup/theming` |
|
|
179
|
+
| AR4 | `Collapsible.test.tsx` (disabled trigger: `aria-disabled`, out of the Tab order, no toggle); `CollapsibleGroup.test.tsx` (single/multiple coordination, `aria-expanded`, Enter activation); `Collapsible.a11y.test.tsx` and `Collapsible.a11y.chromium.spec.ts` (disclosure semantics, Space, Tab reachability for a lone item); grouped Tab order, Space in a group, and no arrow-key movement under the Accordion contract (`internal/a11y-spec`, pending) | single/multiple; open/closed; a disabled item, open or closed; Tab order; Enter/Space round trip | A header loses its button role, name, `aria-expanded`, or `aria-controls`; Enter or Space stops toggling; arrow keys move focus between headers | `audit:CollapsibleGroup/accessibility` |
|
|
180
|
+
| AR1–AR3 | DOM and Chromium accessibility suites; component-owned Storybook fixture; RTL audit | open/closed; single/multiple; leading/trailing; LTR/RTL | Disclosure semantics diverge from state, the wrapper becomes interactive, or leading direction stops mirroring. | `audit:CollapsibleGroup/accessibility` |
|
|
181
|
+
| PR1–PR2, FR7 | source review plus context and interaction tests | unchanged and changed state/presentation inputs | An Effect/resource appears, unchanged payload broadcasts a new identity, or changed group behavior fails to reach consumers. | `audit:CollapsibleGroup/code-health` |
|
|
182
|
+
| Consumer documentation | CLI component projection, docs checks, and `CollapsibleGroup.stories.tsx` | every public concept and representative composition | A released prop/default is undiscoverable or the owned browser route loses representative state coverage. | `audit:CollapsibleGroup/docs` |
|
|
181
183
|
|
|
182
184
|
## Decision log
|
|
183
185
|
|
|
184
|
-
|
|
186
|
+
### DEC-1 — Adopt Accordion's required parts; keep the headers independent
|
|
187
|
+
|
|
188
|
+
**Reference:** `component:CollapsibleGroup/DEC-1`
|
|
189
|
+
**Decider:** `cixzhang`, `2026-10-13`
|
|
190
|
+
|
|
191
|
+
The group's headers already play the accordion header's role, so they carry its name, state, relationship, and Enter/Space activation (AR4). They stay in the page Tab order without arrow-key or Home/End movement because the group coordinates independent disclosures rather than acting as one composite control; a heading wrapper is left to the caller's document structure.
|
|
192
|
+
|
|
193
|
+
Rejected: arrow-key movement between headers; a required heading element around each header.
|
|
185
194
|
|
|
186
195
|
## Open questions
|
|
187
196
|
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type InputClearButtonProps,
|
|
26
26
|
} from './InputClearButton';
|
|
27
27
|
import {Icon, registerIcons, resetIcons} from '../Icon';
|
|
28
|
+
import {InternationalizationProvider} from '../i18n';
|
|
28
29
|
import {defineTheme} from '../theme/defineTheme';
|
|
29
30
|
import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
30
31
|
import {declaredValue} from '../__tests__/stylexDeclarations';
|
|
@@ -95,13 +96,23 @@ describe('InputClearButton public props', () => {
|
|
|
95
96
|
});
|
|
96
97
|
|
|
97
98
|
describe('InputClearButton', () => {
|
|
98
|
-
it('
|
|
99
|
+
it('shows a generic "Clear" tooltip while the accessible name keeps the field label', async () => {
|
|
100
|
+
// A long field label repeated in the tooltip reads as awkward copy
|
|
101
|
+
// ("Clear Assigned engineering partner"); the field's own label already
|
|
102
|
+
// sits beside the glyph. The contextual name stays for assistive tech.
|
|
99
103
|
const showPopover = vi.spyOn(HTMLElement.prototype, 'showPopover');
|
|
100
|
-
render(
|
|
104
|
+
render(
|
|
105
|
+
<InputClearButton
|
|
106
|
+
label="Clear Assigned engineering partner"
|
|
107
|
+
onClick={() => {}}
|
|
108
|
+
/>,
|
|
109
|
+
);
|
|
101
110
|
|
|
102
|
-
const button = screen.getByRole('button', {
|
|
111
|
+
const button = screen.getByRole('button', {
|
|
112
|
+
name: 'Clear Assigned engineering partner',
|
|
113
|
+
});
|
|
103
114
|
const tooltip = screen.getByRole('tooltip', {hidden: true});
|
|
104
|
-
expect(tooltip).toHaveTextContent(/^Clear
|
|
115
|
+
expect(tooltip).toHaveTextContent(/^Clear$/);
|
|
105
116
|
expect(button.getAttribute('aria-describedby')).toBe(tooltip.id);
|
|
106
117
|
|
|
107
118
|
fireEvent.mouseEnter(button);
|
|
@@ -109,6 +120,20 @@ describe('InputClearButton', () => {
|
|
|
109
120
|
showPopover.mockRestore();
|
|
110
121
|
});
|
|
111
122
|
|
|
123
|
+
it('localizes the tooltip through the catalog', () => {
|
|
124
|
+
render(
|
|
125
|
+
<InternationalizationProvider
|
|
126
|
+
locale="en"
|
|
127
|
+
overrides={{en: {'@astryx.inputClearButton.tooltip': 'Leeren'}}}>
|
|
128
|
+
<InputClearButton label="Suche leeren" onClick={() => {}} />
|
|
129
|
+
</InternationalizationProvider>,
|
|
130
|
+
);
|
|
131
|
+
expect(screen.getByRole('button', {name: 'Suche leeren'})).toBeTruthy();
|
|
132
|
+
expect(screen.getByRole('tooltip', {hidden: true})).toHaveTextContent(
|
|
133
|
+
/^Leeren$/,
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
112
137
|
it('renders a real button with the given accessible label', () => {
|
|
113
138
|
render(<InputClearButton label="Clear" onClick={() => {}} />);
|
|
114
139
|
const button = screen.getByRole('button', {name: 'Clear'});
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file InputClearButton.tsx
|
|
7
|
-
* @input Uses React, Button, and
|
|
8
|
-
* @output Exports the public InputClearButton and an internal popup-aware variant
|
|
9
|
-
*
|
|
7
|
+
* @input Uses React, Button, Icon, and the i18n translator
|
|
8
|
+
* @output Exports the public InputClearButton and an internal popup-aware variant.
|
|
9
|
+
* Both show the localized generic "Clear" tooltip and keep the caller's
|
|
10
|
+
* contextual label as the accessible name.
|
|
10
11
|
* @position Shared primitive. Every input that renders a clear affordance —
|
|
11
12
|
* TextInput, NumberInput, TimeInput, DateInput, DateTimeInput,
|
|
12
13
|
* DateRangeInput, Selector, MultiSelector, Typeahead, Tokenizer, FileInput —
|
|
@@ -19,6 +20,7 @@ import type {ReactNode} from 'react';
|
|
|
19
20
|
import * as stylex from '@stylexjs/stylex';
|
|
20
21
|
import {Button} from '../Button';
|
|
21
22
|
import {Icon} from '../Icon';
|
|
23
|
+
import {useTranslator} from '../i18n';
|
|
22
24
|
import {themeProps} from '../utils/themeProps';
|
|
23
25
|
|
|
24
26
|
const styles = stylex.create({
|
|
@@ -67,7 +69,10 @@ const styles = stylex.create({
|
|
|
67
69
|
});
|
|
68
70
|
|
|
69
71
|
export interface InputClearButtonProps {
|
|
70
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Contextual accessible name for the button, such as "Clear Search". The
|
|
74
|
+
* visible tooltip is always the localized generic "Clear".
|
|
75
|
+
*/
|
|
71
76
|
label: string;
|
|
72
77
|
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
|
73
78
|
xstyle?: stylex.StyleXStyles;
|
|
@@ -83,10 +88,11 @@ interface InternalInputClearButtonProps extends InputClearButtonProps {
|
|
|
83
88
|
type InputClearButtonRenderProps = InputClearButtonProps &
|
|
84
89
|
Partial<
|
|
85
90
|
Pick<InternalInputClearButtonProps, 'onPointerDown' | 'onClickCapture'>
|
|
86
|
-
|
|
91
|
+
> & {tooltip: string};
|
|
87
92
|
|
|
88
93
|
function renderInputClearButton({
|
|
89
94
|
label,
|
|
95
|
+
tooltip,
|
|
90
96
|
onClick,
|
|
91
97
|
onPointerDown,
|
|
92
98
|
onClickCapture,
|
|
@@ -100,7 +106,11 @@ function renderInputClearButton({
|
|
|
100
106
|
variant="ghost"
|
|
101
107
|
size="sm"
|
|
102
108
|
label={label}
|
|
103
|
-
tooltip
|
|
109
|
+
// The field's label already sits beside the glyph, so the tooltip names
|
|
110
|
+
// only the action; repeating a long label reads as awkward copy. The
|
|
111
|
+
// contextual label stays the accessible name, which still contains
|
|
112
|
+
// the visible word (WCAG 2.5.3, label in name).
|
|
113
|
+
tooltip={tooltip}
|
|
104
114
|
className={buttonTargetClassName}
|
|
105
115
|
icon={
|
|
106
116
|
<Icon
|
|
@@ -128,14 +138,22 @@ function renderInputClearButton({
|
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
export function InputClearButton(props: InputClearButtonProps): ReactNode {
|
|
131
|
-
|
|
141
|
+
const t = useTranslator();
|
|
142
|
+
return renderInputClearButton({
|
|
143
|
+
...props,
|
|
144
|
+
tooltip: t('@astryx.inputClearButton.tooltip'),
|
|
145
|
+
});
|
|
132
146
|
}
|
|
133
147
|
|
|
134
148
|
/** Internal variant used while popup-aware clear behavior is rolled out. */
|
|
135
149
|
export function InternalInputClearButton(
|
|
136
150
|
props: InternalInputClearButtonProps,
|
|
137
151
|
): ReactNode {
|
|
138
|
-
|
|
152
|
+
const t = useTranslator();
|
|
153
|
+
return renderInputClearButton({
|
|
154
|
+
...props,
|
|
155
|
+
tooltip: t('@astryx.inputClearButton.tooltip'),
|
|
156
|
+
});
|
|
139
157
|
}
|
|
140
158
|
|
|
141
159
|
InputClearButton.displayName = 'InputClearButton';
|
package/src/Grid/Grid.spec.md
CHANGED
|
@@ -44,36 +44,6 @@ layout, targets, or public API.
|
|
|
44
44
|
|
|
45
45
|
Consumer migration instructions belong in consumer docs and release notes.
|
|
46
46
|
|
|
47
|
-
### DEP-0010 — Deprecate the fixed meaning of numeric `Grid` `columns`
|
|
48
|
-
|
|
49
|
-
| Field | Value |
|
|
50
|
-
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
51
|
-
| id | `DEP-0010` |
|
|
52
|
-
| cleanup | `CLN-0019` |
|
|
53
|
-
| package | `@astryxdesign/core` |
|
|
54
|
-
| surface | `Grid` `columns={N}`, which renders exactly N equal tracks (`repeat(N, 1fr)`) |
|
|
55
|
-
| old contract | A numeric `columns` value is an explicit equal-width track count at every width |
|
|
56
|
-
| replacement | `columns={{count: N, isFixed: true}}` keeps exactly N tracks |
|
|
57
|
-
| direct authority | `component:Grid`, with `family:layout-primitives` representative matrix row "Grid / fixed columns". Owners: `cixzhang` (`component:Grid`); `cixzhang`, `imdreamrunner` (`family:layout-primitives`) |
|
|
58
|
-
| warning | Consumer docs and the release notes name `DEP-0010` and the fixed shape. No runtime warning (`spec:AST-017` FR29) |
|
|
59
|
-
| migration | Mechanical where the fixed count is wanted: rewrite `columns={N}` to `columns={{count: N, isFixed: true}}`. Keeping the number accepts the new reflow |
|
|
60
|
-
| codemod | None yet. A preserving codemod is mechanical, but it would opt every caller out of the change it exists to deliver |
|
|
61
|
-
| downstream | In-repo fixed rows such as week rows use the fixed shape |
|
|
62
|
-
| transition | no prior transition |
|
|
63
|
-
| state | `proposed` |
|
|
64
|
-
| open decision | This record is draft and lists new responsive behavior as a non-goal; the family matrix pins numeric columns as fixed. Both need an owner ruling |
|
|
65
|
-
| target plan | `CLN-0019` in the next minor release |
|
|
66
|
-
|
|
67
|
-
### CLN-0019 — Numeric `columns` means at most N columns
|
|
68
|
-
|
|
69
|
-
| Field | Value |
|
|
70
|
-
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
71
|
-
| id | `CLN-0019` |
|
|
72
|
-
| lifecycle | `DEP-0010` |
|
|
73
|
-
| delta | A numeric `columns` value renders up to N columns of at least 12rem and fewer when the grid is narrower; a `GridSpan` in such a grid takes the full row once the grid has fewer columns than it spans |
|
|
74
|
-
| rollback | Restore `repeat(N, 1fr)` for numeric `columns` from the final-patch baseline |
|
|
75
|
-
| state | `pending` — lands in a declared minor with its `[breaking]` Changeset |
|
|
76
|
-
|
|
77
47
|
## Ownership boundary
|
|
78
48
|
|
|
79
49
|
**Owns**
|
|
@@ -110,6 +80,43 @@ in the canonical `Grid.doc.mjs` and the subordinate `GridSpan.doc.mjs`;
|
|
|
110
80
|
- A Spanning item may span columns, rows, both, or neither while retaining the
|
|
111
81
|
same target.
|
|
112
82
|
|
|
83
|
+
### Responsive contract
|
|
84
|
+
|
|
85
|
+
The `columns` prop decides how many column tracks Grid lays out and whether
|
|
86
|
+
that count changes with the container's width. Other inputs change the result
|
|
87
|
+
only by changing what `columns` has to fit: `width` and `maxWidth` set the
|
|
88
|
+
container width a reflowing shape measures against; an `xstyle`, `className`,
|
|
89
|
+
or `style` that sets the track template (including under a media query)
|
|
90
|
+
replaces the computed one; and a numeric GridSpan wider than the grid's tracks
|
|
91
|
+
makes the browser add implicit tracks to the whole grid, which later items flow
|
|
92
|
+
into. No other prop changes the count.
|
|
93
|
+
|
|
94
|
+
| `columns` | Behavior | Reflows |
|
|
95
|
+
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
|
96
|
+
| `N` (number) | Exactly N tracks that share the row equally at every container width; a child wider than its share widens its track and can push the grid past a narrow container. | never |
|
|
97
|
+
| `{minWidth}` | As many tracks as fit while each stays at least `minWidth` px wide. A container narrower than `minWidth` keeps one `minWidth`-wide track that overflows it. Empty tracks keep their width. | yes |
|
|
98
|
+
| `{minWidth, repeat: 'fit'}` | As above, and empty tracks collapse so the present items stretch across the row. | yes |
|
|
99
|
+
| `{minWidth, max}` | Up to `max` tracks, each at least `minWidth` px and never narrower than an equal share of the row; fewer tracks as the container narrows, down to one track that shrinks to the container rather than overflowing it. `repeat: 'fit'` applies the same way. | yes |
|
|
100
|
+
| omitted, `0`, or negative | One track. | never |
|
|
101
|
+
|
|
102
|
+
- `gap` and `columnGap` count toward the width a reflowing shape has to fit, so
|
|
103
|
+
the full count is reached only when the container has room for the tracks and
|
|
104
|
+
the gaps between them. They never change the count of a numeric `columns`.
|
|
105
|
+
- `height`, `minHeight`, `rowHeight`, `align`, and `justify` size or place the
|
|
106
|
+
container and its items and do not change the column count.
|
|
107
|
+
- GridSpan spans `k` tracks, or the whole row for `'full'`. `'full'` never
|
|
108
|
+
adds tracks. A numeric span wider than the grid's current tracks makes the
|
|
109
|
+
browser add implicit tracks to the grid for the remainder; they persist for
|
|
110
|
+
every row, items placed before and after the span flow into them, and once
|
|
111
|
+
they exist `'full'` covers only the explicit tracks. In a reflowing grid keep
|
|
112
|
+
spans within the smallest count the grid can reach or use `'full'`.
|
|
113
|
+
- For columns that reflow on small screens, use `{minWidth, max}`: `max` is
|
|
114
|
+
what lets the last track shrink to a narrow container, so `{minWidth}` alone
|
|
115
|
+
still overflows a phone narrower than `minWidth`. A numeric `columns` never
|
|
116
|
+
reflows, so a four-up numeric row on a phone renders four narrow tracks, or
|
|
117
|
+
overflows when a child cannot shrink to its share; that is the fixed
|
|
118
|
+
contract, not a defect.
|
|
119
|
+
|
|
113
120
|
### Representative states
|
|
114
121
|
|
|
115
122
|
| State | Required invariant | Allowed variation |
|
|
@@ -178,8 +185,19 @@ relationship.
|
|
|
178
185
|
|
|
179
186
|
## Decision log
|
|
180
187
|
|
|
181
|
-
|
|
182
|
-
|
|
188
|
+
### DEC-1 — Numeric `columns` stays fixed; reflow uses the existing object form
|
|
189
|
+
|
|
190
|
+
**Decider:** `cixzhang`, `2026-10-11`
|
|
191
|
+
|
|
192
|
+
`columns={N}` means exactly N equal tracks at every width, and a grid that
|
|
193
|
+
reflows on small screens uses `{minWidth, max?, repeat?}`. Two shapes, one
|
|
194
|
+
responsibility each: the number is the fixed contract, the object form is the
|
|
195
|
+
responsive one, and the record and the docs say so where a caller first meets
|
|
196
|
+
`columns`.
|
|
197
|
+
|
|
198
|
+
Rejected: redefining the number as "at most N" (breaks every released numeric
|
|
199
|
+
grid and needs an opt-out shape); a `{count, isFixed}` shape (duplicates the
|
|
200
|
+
number and the `max` cap).
|
|
183
201
|
|
|
184
202
|
## Open questions
|
|
185
203
|
|
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
* The timed region inside each process is the spec's protocol, unsmoothed:
|
|
20
20
|
* ten untimed warmups, then one median of nine alternating paired rounds,
|
|
21
21
|
* for a stable plugin list and a recreated equivalent one, against the
|
|
22
|
-
* omitted baseline.
|
|
23
|
-
*
|
|
22
|
+
* omitted baseline. A first attempt within budget on both paths certifies
|
|
23
|
+
* them. Otherwise up to nine fresh-process attempts decide each path on its
|
|
24
|
+
* own: a path fails the unchanged budget when a majority of the nine (five)
|
|
25
|
+
* exceed it, and collection stops as soon as both paths are decided.
|
|
24
26
|
*/
|
|
25
27
|
|
|
26
28
|
import {execFileSync} from 'node:child_process';
|
|
@@ -166,18 +168,64 @@ function logBenchmarkResult(result: BenchmarkResult, attempt: number): void {
|
|
|
166
168
|
);
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
/** The two paths FR23 measures; each is held to the budget on its own. */
|
|
172
|
+
const FR23_PATHS = ['stable', 'recreated'] as const;
|
|
173
|
+
type FR23Path = (typeof FR23_PATHS)[number];
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Fresh-process attempts that decide a path after a noisy first one. On CI a
|
|
177
|
+
* clean representative set measures about 1.19 at 500 sections with about
|
|
178
|
+
* ±0.04 of sample noise, so a single attempt lands over the budget roughly
|
|
179
|
+
* one time in ten; a majority of nine does so well under one time in a
|
|
180
|
+
* thousand, while a slowdown of 0.1 or more still exceeds it nearly always.
|
|
181
|
+
*/
|
|
182
|
+
const FR23_ATTEMPTS = 9;
|
|
183
|
+
const FR23_MAJORITY = Math.floor(FR23_ATTEMPTS / 2) + 1;
|
|
184
|
+
|
|
185
|
+
function attemptsOverBudget(
|
|
186
|
+
attempts: ReadonlyArray<BenchmarkResult>,
|
|
187
|
+
fr23Path: FR23Path,
|
|
188
|
+
): number {
|
|
189
|
+
return attempts.filter(result => result[fr23Path] > FR23_BUDGET).length;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Whether the remaining attempts can no longer change this path's verdict. */
|
|
193
|
+
function pathDecided(
|
|
194
|
+
attempts: ReadonlyArray<BenchmarkResult>,
|
|
195
|
+
fr23Path: FR23Path,
|
|
196
|
+
): boolean {
|
|
197
|
+
const over = attemptsOverBudget(attempts, fr23Path);
|
|
198
|
+
return over >= FR23_MAJORITY || attempts.length - over >= FR23_MAJORITY;
|
|
199
|
+
}
|
|
200
|
+
|
|
169
201
|
function collectFR23Attempts(
|
|
170
202
|
sections: number,
|
|
171
203
|
runAttempt: (sections: number) => BenchmarkResult = runSettledBenchmark,
|
|
172
204
|
): ReadonlyArray<BenchmarkResult> {
|
|
173
205
|
const attempts = [runAttempt(sections)];
|
|
174
|
-
if (exceedsFR23Budget(attempts[0])) {
|
|
175
|
-
attempts
|
|
206
|
+
if (!exceedsFR23Budget(attempts[0])) {
|
|
207
|
+
return attempts;
|
|
208
|
+
}
|
|
209
|
+
while (
|
|
210
|
+
attempts.length < FR23_ATTEMPTS &&
|
|
211
|
+
!FR23_PATHS.every(fr23Path => pathDecided(attempts, fr23Path))
|
|
212
|
+
) {
|
|
176
213
|
attempts.push(runAttempt(sections));
|
|
177
214
|
}
|
|
178
215
|
return attempts;
|
|
179
216
|
}
|
|
180
217
|
|
|
218
|
+
/** The paths whose attempts exceed the budget in a majority of nine. */
|
|
219
|
+
function pathsOverBudget(
|
|
220
|
+
attempts: ReadonlyArray<BenchmarkResult>,
|
|
221
|
+
): ReadonlyArray<FR23Path> {
|
|
222
|
+
return FR23_PATHS.filter(fr23Path =>
|
|
223
|
+
attempts.length > 1
|
|
224
|
+
? attemptsOverBudget(attempts, fr23Path) >= FR23_MAJORITY
|
|
225
|
+
: attemptsOverBudget(attempts, fr23Path) > 0,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
181
229
|
describe('Markdown FR23 helper overhead', () => {
|
|
182
230
|
it('reports one nine-pair median without selecting the cheapest baseline', () => {
|
|
183
231
|
// Paired ratios: four 1.1s, one 1.2, four 10s. The median is 1.2,
|
|
@@ -218,35 +266,104 @@ describe('Markdown FR23 helper overhead', () => {
|
|
|
218
266
|
}
|
|
219
267
|
});
|
|
220
268
|
|
|
221
|
-
|
|
269
|
+
describe('deciding each path from fresh-process attempts', () => {
|
|
222
270
|
const result = (stable: number, recreated: number): BenchmarkResult => ({
|
|
223
|
-
sections:
|
|
271
|
+
sections: 500,
|
|
224
272
|
stable,
|
|
225
273
|
recreated,
|
|
226
274
|
control: 1,
|
|
227
275
|
});
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
result(1.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
276
|
+
const run = (results: BenchmarkResult[]) => {
|
|
277
|
+
const queue = [...results];
|
|
278
|
+
let calls = 0;
|
|
279
|
+
const attempts = collectFR23Attempts(500, () => {
|
|
280
|
+
calls++;
|
|
281
|
+
const next = queue.shift();
|
|
282
|
+
if (next == null) {
|
|
283
|
+
throw new Error('asked for an unplanned attempt');
|
|
284
|
+
}
|
|
285
|
+
return next;
|
|
286
|
+
});
|
|
287
|
+
return {attempts, calls, failing: pathsOverBudget(attempts)};
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
it('certifies both paths from one attempt within budget', () => {
|
|
291
|
+
const {calls, failing} = run([result(1.2, 1.22)]);
|
|
292
|
+
expect(calls).toBe(1);
|
|
293
|
+
expect(failing).toEqual([]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('fails a single attempt over budget only if no retry ran', () => {
|
|
297
|
+
expect(pathsOverBudget([result(1.26, 1.2)])).toEqual(['stable']);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('judges each path by its own majority, not either path per attempt', () => {
|
|
301
|
+
// Main run 38393555595: stable was over in attempt 1 and recreated in
|
|
302
|
+
// attempt 2, which the old either-path-per-attempt count read as a
|
|
303
|
+
// two-of-three failure although neither path was over in a majority.
|
|
304
|
+
const {attempts, failing} = run([
|
|
305
|
+
result(1.256, 1.201),
|
|
306
|
+
result(1.145, 1.275),
|
|
307
|
+
result(1.183, 1.187),
|
|
308
|
+
result(1.18, 1.2),
|
|
309
|
+
result(1.19, 1.21),
|
|
310
|
+
result(1.2, 1.22),
|
|
311
|
+
result(1.17, 1.19),
|
|
312
|
+
]);
|
|
313
|
+
// Each path needs five attempts within budget; each had one over.
|
|
314
|
+
expect(attempts).toHaveLength(6);
|
|
315
|
+
expect(failing).toEqual([]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('stops as soon as both paths are decided', () => {
|
|
319
|
+
// Recreated is decided after five passes, stable after one more.
|
|
320
|
+
const {calls} = run([
|
|
321
|
+
result(1.3, 1.2),
|
|
322
|
+
result(1.2, 1.2),
|
|
323
|
+
result(1.2, 1.2),
|
|
324
|
+
result(1.2, 1.2),
|
|
325
|
+
result(1.2, 1.2),
|
|
326
|
+
result(1.2, 1.2),
|
|
327
|
+
]);
|
|
328
|
+
expect(calls).toBe(6);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('fails a path that is over budget in a majority of nine', () => {
|
|
332
|
+
const {attempts, failing} = run([
|
|
333
|
+
result(1.3, 1.2),
|
|
334
|
+
result(1.27, 1.2),
|
|
335
|
+
result(1.2, 1.2),
|
|
336
|
+
result(1.26, 1.21),
|
|
337
|
+
result(1.22, 1.2),
|
|
338
|
+
result(1.28, 1.19),
|
|
339
|
+
result(1.26, 1.2),
|
|
340
|
+
]);
|
|
341
|
+
expect(attempts).toHaveLength(7);
|
|
342
|
+
expect(failing).toEqual(['stable']);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('fails a consistent slowdown on both paths after five attempts', () => {
|
|
346
|
+
const {calls, failing} = run(
|
|
347
|
+
Array.from({length: 9}, () => result(1.4, 1.42)),
|
|
348
|
+
);
|
|
349
|
+
expect(calls).toBe(5);
|
|
350
|
+
expect(failing).toEqual(['stable', 'recreated']);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('never runs more than nine attempts', () => {
|
|
354
|
+
const {calls} = run([
|
|
355
|
+
result(1.3, 1.3),
|
|
356
|
+
result(1.2, 1.2),
|
|
357
|
+
result(1.3, 1.3),
|
|
358
|
+
result(1.2, 1.2),
|
|
359
|
+
result(1.3, 1.3),
|
|
360
|
+
result(1.2, 1.2),
|
|
361
|
+
result(1.3, 1.3),
|
|
362
|
+
result(1.2, 1.2),
|
|
363
|
+
result(1.3, 1.3),
|
|
364
|
+
]);
|
|
365
|
+
expect(calls).toBe(9);
|
|
366
|
+
});
|
|
250
367
|
});
|
|
251
368
|
|
|
252
369
|
it.each([200, 500])(
|
|
@@ -257,28 +374,32 @@ describe('Markdown FR23 helper overhead', () => {
|
|
|
257
374
|
// attempt counts: a value over budget is machine noise, so that process
|
|
258
375
|
// is replaced before its A/B result can certify or fail the helpers.
|
|
259
376
|
//
|
|
260
|
-
// A single over-budget A/B is also noisy at this threshold.
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
// 1.25 budget.
|
|
377
|
+
// A single over-budget A/B is also noisy at this threshold. Each path
|
|
378
|
+
// is then decided by a majority of up to nine fresh-process attempts
|
|
379
|
+
// against the unchanged 1.25 budget.
|
|
264
380
|
const attempts = collectFR23Attempts(sections);
|
|
265
|
-
if (attempts.length
|
|
381
|
+
if (attempts.length > 1) {
|
|
266
382
|
console.log(
|
|
267
383
|
` ${sections} sections: first A/B exceeded ${FR23_BUDGET};` +
|
|
268
|
-
|
|
384
|
+
` decided each path from ${attempts.length} fresh processes`,
|
|
269
385
|
);
|
|
270
386
|
}
|
|
271
387
|
attempts.forEach((result, index) =>
|
|
272
388
|
logBenchmarkResult(result, index + 1),
|
|
273
389
|
);
|
|
274
|
-
const
|
|
390
|
+
const failing = pathsOverBudget(attempts);
|
|
275
391
|
expect(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
392
|
+
failing,
|
|
393
|
+
failing
|
|
394
|
+
.map(
|
|
395
|
+
fr23Path =>
|
|
396
|
+
`${fr23Path}: ${attemptsOverBudget(attempts, fr23Path)} of ` +
|
|
397
|
+
`${attempts.length} fresh FR23 attempts exceeded the ${FR23_BUDGET} budget`,
|
|
398
|
+
)
|
|
399
|
+
.join('; '),
|
|
400
|
+
).toEqual([]);
|
|
280
401
|
},
|
|
281
|
-
|
|
402
|
+
600_000,
|
|
282
403
|
);
|
|
283
404
|
|
|
284
405
|
it('fails loudly when the benchmark process cannot produce a result', () => {
|
|
@@ -386,6 +386,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
|
|
|
386
386
|
'{count, number} {count, plural, one {character} other {characters}} remaining',
|
|
387
387
|
'@astryx.textArea.charactersOverLimit':
|
|
388
388
|
'{count, number} {count, plural, one {character} other {characters}} over the limit',
|
|
389
|
+
'@astryx.inputClearButton.tooltip': 'Clear',
|
|
389
390
|
'@astryx.textInput.clearLabel': 'Clear {label}',
|
|
390
391
|
'@astryx.thumbnail.remove': 'Remove {accessibleName}',
|
|
391
392
|
'@astryx.thumbnail.open': 'Open {accessibleName}',
|
|
@@ -393,6 +393,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
|
|
|
393
393
|
'⟦{count, number} {count, plural, one {character} other {characters}} řéɱàíñíñĝ⟧',
|
|
394
394
|
'@astryx.textArea.charactersOverLimit':
|
|
395
395
|
'⟦{count, number} {count, plural, one {character} other {characters}} óṽéř ţĥé łíɱíţ⟧',
|
|
396
|
+
'@astryx.inputClearButton.tooltip': '⟦Çłéàř⟧',
|
|
396
397
|
'@astryx.textInput.clearLabel': '⟦Çłéàř {label}⟧',
|
|
397
398
|
'@astryx.thumbnail.remove': '⟦Řéɱóṽé {accessibleName}⟧',
|
|
398
399
|
'@astryx.thumbnail.open': '⟦Óþéñ {accessibleName}⟧',
|