@astryxdesign/core 0.7.2-canary.fdd384f → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +156 -2
- package/README.md +4 -4
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +8 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +4 -1
- package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
- package/dist/Chat/ChatLayoutScrollButton.js +1 -0
- package/dist/CheckboxList/CheckboxList.d.ts +1 -1
- package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxList.js +6 -2
- package/dist/DropdownMenu/DropdownMenu.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +38 -110
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuGroup.d.ts +6 -4
- package/dist/DropdownMenu/DropdownMenuGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuGroup.js +4 -3
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +24 -0
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -0
- package/dist/DropdownMenu/lazyMenuBottomSheet.js +46 -0
- package/dist/DropdownMenu/menuRegionStops.d.ts +38 -0
- package/dist/DropdownMenu/menuRegionStops.d.ts.map +1 -0
- package/dist/DropdownMenu/menuRegionStops.js +131 -0
- package/dist/FormLayout/FormLayout.d.ts +5 -4
- package/dist/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/FormLayout/FormLayout.js +1 -1
- package/dist/FormLayout/FormLayoutContext.d.ts +3 -3
- package/dist/FormLayout/FormLayoutContext.js +3 -3
- package/dist/Grid/Grid.d.ts +8 -7
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +5 -4
- package/dist/Grid/GridSpan.d.ts +1 -1
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +8 -2
- package/dist/Markdown/plugins/headingLinks.d.ts.map +1 -1
- package/dist/Markdown/plugins/headingLinks.js +9 -1
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +5 -0
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchValueEditor.js +45 -11
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +6 -2
- package/dist/Token/Token.js +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +2 -0
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +2 -0
- package/dist/astryx.css +1 -1
- package/dist/reset.a11y.chromium.spec.js +50 -1
- package/docs.mjs +4 -4
- package/package.json +4 -4
- package/scripts/agent-doc-state.mjs +66 -6
- package/scripts/postinstall.mjs +44 -3
- package/src/Avatar/Avatar.tsx +11 -2
- package/src/AvatarGroup/AvatarGroupOverflow.test.tsx +24 -1
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +2 -0
- package/src/Chat/ChatLayoutScrollButton.test.tsx +13 -0
- package/src/Chat/ChatLayoutScrollButton.tsx +4 -2
- package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +64 -19
- package/src/CheckboxList/CheckboxList.fr10.test.tsx +29 -2
- package/src/CheckboxList/CheckboxList.tsx +4 -1
- package/src/Code/Code.doc.mjs +1 -0
- package/src/CommandPalette/CommandPalette.doc.mjs +2 -0
- package/src/ContextMenu/ContextMenu.test.tsx +12 -2
- package/src/Drawer/Drawer.doc.mjs +1 -0
- package/src/DropdownMenu/DropdownMenu.ssr.test.tsx +57 -0
- package/src/DropdownMenu/DropdownMenu.test.tsx +17 -5
- package/src/DropdownMenu/DropdownMenu.tsx +43 -142
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +1 -1
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuGroup.doc.mjs +169 -10
- package/src/DropdownMenu/DropdownMenuGroup.tsx +6 -4
- package/src/DropdownMenu/DropdownMenuLazySheet.test.tsx +126 -0
- package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +1 -1
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +1 -1
- package/src/DropdownMenu/__tests__/DropdownMenuGroupAudit.a11y.chromium.spec.ts +334 -0
- package/src/DropdownMenu/__tests__/warmMenuBottomSheet.tsx +43 -0
- package/src/DropdownMenu/lazyMenuBottomSheet.ts +71 -0
- package/src/DropdownMenu/menuRegionStops.ts +167 -0
- package/src/Field/FieldLabel.doc.mjs +133 -26
- package/src/Field/FieldLabel.spec.md +172 -0
- package/src/Field/__tests__/FieldLabelAudit.a11y.chromium.spec.ts +296 -0
- package/src/FormLayout/FormLayout.doc.mjs +6 -3
- package/src/FormLayout/FormLayout.spec.md +192 -0
- package/src/FormLayout/FormLayout.test.tsx +15 -0
- package/src/FormLayout/FormLayout.tsx +7 -6
- package/src/FormLayout/FormLayoutContext.ts +3 -3
- package/src/Grid/Grid.doc.mjs +7 -1
- package/src/Grid/Grid.spec.md +93 -113
- package/src/Grid/Grid.test.tsx +22 -0
- package/src/Grid/Grid.tsx +10 -9
- package/src/Grid/GridSpan.doc.mjs +3 -3
- package/src/Grid/GridSpan.tsx +1 -1
- package/src/Icon/Icon.doc.mjs +36 -0
- package/src/Indicator/Indicator.doc.mjs +3 -3
- package/src/Layout/LayoutHeader.doc.mjs +5 -0
- package/src/Markdown/parser.ts +8 -2
- package/src/Markdown/plugins/headingLinks.ts +9 -1
- package/src/MetadataList/MetadataList.doc.mjs +1 -1
- package/src/MoreMenu/MoreMenu.test.tsx +5 -1
- package/src/PowerSearch/PowerSearch.test.tsx +62 -0
- package/src/PowerSearch/PowerSearch.tsx +9 -0
- package/src/PowerSearch/PowerSearchValueEditor.test.tsx +71 -0
- package/src/PowerSearch/PowerSearchValueEditor.tsx +45 -0
- package/src/ProgressBar/ProgressBar.doc.mjs +1 -0
- package/src/Resizable/useResizable.doc.mjs +2 -0
- package/src/ScrollableArea/ScrollableArea.doc.mjs +1 -1
- package/src/SideNav/SideNav.spec.md +227 -0
- package/src/SideNav/SideNav.test.tsx +41 -0
- package/src/SideNav/SideNavItem.tsx +5 -2
- package/src/Spinner/Spinner.doc.mjs +1 -1
- package/src/Table/Table.spec.md +6 -1
- package/src/Table/plugins/activation/useTableActivation.spec.md +254 -0
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/ToggleButton/ToggleButtonGroup.doc.mjs +4 -0
- package/src/Token/Token.tsx +1 -1
- package/src/Token/__tests__/TokenRemoveTarget.a11y.chromium.spec.ts +86 -0
- package/src/Toolbar/Toolbar.test.tsx +0 -8
- package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +163 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.renders.tsx +63 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.states.ts +142 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.test.tsx +126 -0
- package/src/TopNav/TopNavMegaMenu.test.tsx +25 -0
- package/src/TopNav/TopNavMegaMenu.tsx +2 -0
- package/src/TopNav/TopNavMenu.test.tsx +27 -1
- package/src/TopNav/TopNavMenu.tsx +2 -0
- package/src/postinstall.test.mjs +79 -0
- package/src/reset.a11y.chromium.spec.ts +52 -1
- package/src/reset.css +9 -0
- package/src/reset.test.ts +11 -0
- package/src/unicodePropertyEscapes.source-build.test.mjs +69 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file FieldLabelAudit.a11y.chromium.spec.ts
|
|
5
|
+
* @input Exact-head FieldLabel story across semantics, themes, direction, and viewport sizes
|
|
6
|
+
* @output Screenshots and fail-closed sensor receipts for the FieldLabel whole-component audit
|
|
7
|
+
* @position Component-owned Night Watch browser evidence; CI runs it only for a FieldLabel audit pull request
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {createHash} from 'node:crypto';
|
|
11
|
+
import {execFileSync} from 'node:child_process';
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import {expect, test, type Page} from '@playwright/test';
|
|
15
|
+
import {holdMotionStill} from '@astryxdesign/a11y-spec/chromium';
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_STORYBOOK_DIR,
|
|
18
|
+
serveStorybook,
|
|
19
|
+
type StaticServer,
|
|
20
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
21
|
+
|
|
22
|
+
const OUTPUT = path.resolve('field-label-audit-evidence');
|
|
23
|
+
const WIDE = {width: 1024, height: 900};
|
|
24
|
+
const NARROW = {width: 320, height: 900};
|
|
25
|
+
|
|
26
|
+
type Mode = 'light' | 'dark';
|
|
27
|
+
type Direction = 'ltr' | 'rtl';
|
|
28
|
+
type Theme = 'neutral' | 'butter';
|
|
29
|
+
|
|
30
|
+
type AuditCase = {
|
|
31
|
+
key: string;
|
|
32
|
+
mode: Mode;
|
|
33
|
+
direction: Direction;
|
|
34
|
+
theme: Theme;
|
|
35
|
+
viewport: {width: number; height: number};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type AuditReceipt = {
|
|
39
|
+
passed: boolean;
|
|
40
|
+
expected: Record<string, unknown>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const cases: AuditCase[] = [];
|
|
44
|
+
function addPair(
|
|
45
|
+
key: string,
|
|
46
|
+
options: Partial<Omit<AuditCase, 'key' | 'mode'>> = {},
|
|
47
|
+
) {
|
|
48
|
+
for (const mode of ['light', 'dark'] as const) {
|
|
49
|
+
cases.push({
|
|
50
|
+
key: `${key}-${mode}`,
|
|
51
|
+
mode,
|
|
52
|
+
direction: options.direction ?? 'ltr',
|
|
53
|
+
theme: options.theme ?? 'neutral',
|
|
54
|
+
viewport: options.viewport ?? WIDE,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
addPair('default');
|
|
60
|
+
addPair('custom-theme', {theme: 'butter'});
|
|
61
|
+
addPair('rtl', {direction: 'rtl'});
|
|
62
|
+
addPair('narrow', {viewport: NARROW});
|
|
63
|
+
|
|
64
|
+
test.describe.configure({mode: 'serial', retries: 0});
|
|
65
|
+
let server: StaticServer;
|
|
66
|
+
let checkoutSha: string;
|
|
67
|
+
let storybookSha: string;
|
|
68
|
+
|
|
69
|
+
function storyUrl(item: AuditCase) {
|
|
70
|
+
return `${server.origin}/iframe.html?id=core-fieldlabel--states&viewMode=story&globals=colorMode:${item.mode};astryxTheme:${item.theme};direction:${item.direction}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function settle(page: Page) {
|
|
74
|
+
await holdMotionStill(page);
|
|
75
|
+
await page.evaluate(async () => document.fonts.ready);
|
|
76
|
+
await page.waitForTimeout(40);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
test.beforeAll(async () => {
|
|
80
|
+
fs.rmSync(OUTPUT, {recursive: true, force: true});
|
|
81
|
+
fs.mkdirSync(OUTPUT, {recursive: true});
|
|
82
|
+
checkoutSha = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
83
|
+
encoding: 'utf8',
|
|
84
|
+
}).trim();
|
|
85
|
+
const expected = process.env.ASTRYX_HEAD_SHA;
|
|
86
|
+
if (expected && expected !== checkoutSha) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`checkout ${checkoutSha} does not match expected head ${expected}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
server = await serveStorybook(
|
|
92
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
93
|
+
);
|
|
94
|
+
const response = await fetch(`${server.origin}/astryx-build-sha.txt`);
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
throw new Error(`Storybook build has no SHA stamp (${response.status})`);
|
|
97
|
+
}
|
|
98
|
+
storybookSha = (await response.text()).trim();
|
|
99
|
+
if (storybookSha !== checkoutSha) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Storybook ${storybookSha} does not match checkout ${checkoutSha}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test.afterAll(async () => {
|
|
107
|
+
await server?.close();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
for (const item of cases) {
|
|
111
|
+
test(item.key, async ({browser}) => {
|
|
112
|
+
const context = await browser.newContext({
|
|
113
|
+
viewport: item.viewport,
|
|
114
|
+
colorScheme: item.mode,
|
|
115
|
+
reducedMotion: 'reduce',
|
|
116
|
+
});
|
|
117
|
+
const page = await context.newPage();
|
|
118
|
+
const pageErrors: string[] = [];
|
|
119
|
+
page.on('pageerror', error => pageErrors.push(String(error)));
|
|
120
|
+
await page.goto(storyUrl(item), {waitUntil: 'load'});
|
|
121
|
+
await page.locator('#storybook-root').waitFor();
|
|
122
|
+
|
|
123
|
+
const targets = page.locator('.astryx-field-label');
|
|
124
|
+
await expect(targets).toHaveCount(7);
|
|
125
|
+
await expect(page.locator('label[for="field-label-default"]')).toHaveText(
|
|
126
|
+
'Email',
|
|
127
|
+
);
|
|
128
|
+
const description = page.locator('#field-label-description-help');
|
|
129
|
+
await expect(description).toHaveText(
|
|
130
|
+
'Shown in navigation and invitations.',
|
|
131
|
+
);
|
|
132
|
+
await expect(description.locator('xpath=ancestor::label')).toHaveCount(0);
|
|
133
|
+
await expect(page.locator('#field-label-group-label')).toHaveJSProperty(
|
|
134
|
+
'tagName',
|
|
135
|
+
'SPAN',
|
|
136
|
+
);
|
|
137
|
+
await expect(
|
|
138
|
+
page.getByRole('radiogroup', {name: 'Visibility'}),
|
|
139
|
+
).toBeVisible();
|
|
140
|
+
await expect(page.getByText(/Required/)).toBeVisible();
|
|
141
|
+
await expect(page.getByText(/Optional/)).toBeVisible();
|
|
142
|
+
await expect(page.getByLabel('Search')).toBeVisible();
|
|
143
|
+
const tooltipAnchor = page
|
|
144
|
+
.locator('label[for="field-label-optional"] [aria-describedby]')
|
|
145
|
+
.first();
|
|
146
|
+
await expect(tooltipAnchor).toHaveCount(1);
|
|
147
|
+
|
|
148
|
+
await settle(page);
|
|
149
|
+
const observed = await page.evaluate(() => {
|
|
150
|
+
const labels = Array.from(
|
|
151
|
+
document.querySelectorAll<HTMLElement>('.astryx-field-label'),
|
|
152
|
+
).map(label => {
|
|
153
|
+
const style = getComputedStyle(label);
|
|
154
|
+
const box = label.getBoundingClientRect();
|
|
155
|
+
return {
|
|
156
|
+
text: label.textContent?.replace(/\s+/g, ' ').trim(),
|
|
157
|
+
tagName: label.tagName,
|
|
158
|
+
htmlFor: label.getAttribute('for'),
|
|
159
|
+
color: style.color,
|
|
160
|
+
cursor: style.cursor,
|
|
161
|
+
geometry: {x: box.x, y: box.y, width: box.width, height: box.height},
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
const hidden = document.querySelector<HTMLElement>(
|
|
165
|
+
'label[for="field-label-hidden"]',
|
|
166
|
+
);
|
|
167
|
+
const hiddenBox = hidden?.getBoundingClientRect();
|
|
168
|
+
const firstLabel = document.querySelector<HTMLElement>(
|
|
169
|
+
'.astryx-field-label',
|
|
170
|
+
);
|
|
171
|
+
const currentTooltipAnchor = document.querySelector<HTMLElement>(
|
|
172
|
+
'label[for="field-label-optional"] [aria-describedby]',
|
|
173
|
+
);
|
|
174
|
+
return {
|
|
175
|
+
theme: document
|
|
176
|
+
.querySelector('[data-astryx-theme]')
|
|
177
|
+
?.getAttribute('data-astryx-theme'),
|
|
178
|
+
mode: document.documentElement.getAttribute('data-theme'),
|
|
179
|
+
direction: firstLabel ? getComputedStyle(firstLabel).direction : null,
|
|
180
|
+
viewport: {width: innerWidth, height: innerHeight},
|
|
181
|
+
reducedMotion: matchMedia('(prefers-reduced-motion: reduce)').matches,
|
|
182
|
+
horizontalOverflow:
|
|
183
|
+
document.documentElement.scrollWidth > innerWidth + 1,
|
|
184
|
+
fonts: document.fonts.status,
|
|
185
|
+
labels,
|
|
186
|
+
hiddenGeometry: hiddenBox
|
|
187
|
+
? {width: hiddenBox.width, height: hiddenBox.height}
|
|
188
|
+
: null,
|
|
189
|
+
tooltipAnchor: currentTooltipAnchor
|
|
190
|
+
? {
|
|
191
|
+
ariaHidden: currentTooltipAnchor.getAttribute('aria-hidden'),
|
|
192
|
+
ariaLabel: currentTooltipAnchor.getAttribute('aria-label'),
|
|
193
|
+
describedBy:
|
|
194
|
+
currentTooltipAnchor.getAttribute('aria-describedby'),
|
|
195
|
+
tabIndex: currentTooltipAnchor.getAttribute('tabindex'),
|
|
196
|
+
}
|
|
197
|
+
: null,
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const semanticsPass =
|
|
202
|
+
observed.labels.length === 7 &&
|
|
203
|
+
observed.labels.filter(label => label.tagName === 'SPAN').length === 1 &&
|
|
204
|
+
observed.labels.every(label => label.geometry.width > 0) &&
|
|
205
|
+
observed.hiddenGeometry?.width === 1 &&
|
|
206
|
+
observed.hiddenGeometry.height === 1;
|
|
207
|
+
const knownTooltipGapObserved =
|
|
208
|
+
observed.tooltipAnchor?.ariaHidden === 'true' &&
|
|
209
|
+
observed.tooltipAnchor.ariaLabel == null &&
|
|
210
|
+
observed.tooltipAnchor.tabIndex == null;
|
|
211
|
+
|
|
212
|
+
const png = await page.screenshot({animations: 'disabled', fullPage: true});
|
|
213
|
+
const image = `${item.key}.png`;
|
|
214
|
+
const receipt = {
|
|
215
|
+
schemaVersion: 1,
|
|
216
|
+
expected: {
|
|
217
|
+
build: checkoutSha,
|
|
218
|
+
storyId: 'core-fieldlabel--states',
|
|
219
|
+
mode: item.mode,
|
|
220
|
+
direction: item.direction,
|
|
221
|
+
theme: item.theme,
|
|
222
|
+
viewport: item.viewport,
|
|
223
|
+
targetCount: 7,
|
|
224
|
+
groupLabelTag: 'SPAN',
|
|
225
|
+
hiddenGeometry: {width: 1, height: 1},
|
|
226
|
+
knownGap:
|
|
227
|
+
'the tooltip anchor is anonymous and nonfocusable on this audited head',
|
|
228
|
+
},
|
|
229
|
+
observed: {
|
|
230
|
+
build: storybookSha,
|
|
231
|
+
...observed,
|
|
232
|
+
pageErrors,
|
|
233
|
+
},
|
|
234
|
+
image: {
|
|
235
|
+
file: image,
|
|
236
|
+
sha256: createHash('sha256').update(png).digest('hex'),
|
|
237
|
+
width: png.readUInt32BE(16),
|
|
238
|
+
height: png.readUInt32BE(20),
|
|
239
|
+
},
|
|
240
|
+
checks: {semanticsPass, knownTooltipGapObserved},
|
|
241
|
+
passed:
|
|
242
|
+
storybookSha === checkoutSha &&
|
|
243
|
+
pageErrors.length === 0 &&
|
|
244
|
+
observed.theme === item.theme &&
|
|
245
|
+
observed.mode === item.mode &&
|
|
246
|
+
observed.direction === item.direction &&
|
|
247
|
+
observed.viewport.width === item.viewport.width &&
|
|
248
|
+
observed.viewport.height === item.viewport.height &&
|
|
249
|
+
observed.reducedMotion &&
|
|
250
|
+
!observed.horizontalOverflow &&
|
|
251
|
+
observed.fonts === 'loaded' &&
|
|
252
|
+
semanticsPass,
|
|
253
|
+
};
|
|
254
|
+
fs.writeFileSync(path.join(OUTPUT, image), png);
|
|
255
|
+
fs.writeFileSync(
|
|
256
|
+
path.join(OUTPUT, `${item.key}.sensors.json`),
|
|
257
|
+
`${JSON.stringify(receipt, null, 2)}\n`,
|
|
258
|
+
);
|
|
259
|
+
await context.close();
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
test('manifest is complete', async () => {
|
|
264
|
+
const receiptFiles = fs
|
|
265
|
+
.readdirSync(OUTPUT)
|
|
266
|
+
.filter(file => file.endsWith('.sensors.json'));
|
|
267
|
+
const receipts = receiptFiles.map(
|
|
268
|
+
(file): AuditReceipt =>
|
|
269
|
+
JSON.parse(
|
|
270
|
+
fs.readFileSync(path.join(OUTPUT, file), 'utf8'),
|
|
271
|
+
) as AuditReceipt,
|
|
272
|
+
);
|
|
273
|
+
fs.writeFileSync(
|
|
274
|
+
path.join(OUTPUT, 'manifest.json'),
|
|
275
|
+
`${JSON.stringify(
|
|
276
|
+
{
|
|
277
|
+
schemaVersion: 1,
|
|
278
|
+
component: 'core/FieldLabel',
|
|
279
|
+
build: checkoutSha,
|
|
280
|
+
frames: cases.map(item => ({
|
|
281
|
+
key: item.key,
|
|
282
|
+
image: `${item.key}.png`,
|
|
283
|
+
receipt: `${item.key}.sensors.json`,
|
|
284
|
+
})),
|
|
285
|
+
},
|
|
286
|
+
null,
|
|
287
|
+
2,
|
|
288
|
+
)}\n`,
|
|
289
|
+
);
|
|
290
|
+
expect(receiptFiles).toHaveLength(cases.length);
|
|
291
|
+
expect(
|
|
292
|
+
receipts
|
|
293
|
+
.filter(receipt => !receipt.passed)
|
|
294
|
+
.map(receipt => receipt.expected),
|
|
295
|
+
).toEqual([]);
|
|
296
|
+
});
|
|
@@ -13,7 +13,7 @@ export const docs = {
|
|
|
13
13
|
name: 'direction',
|
|
14
14
|
type: "'vertical' | 'horizontal' | 'horizontal-labels'",
|
|
15
15
|
description:
|
|
16
|
-
'Controls field arrangement. Vertical stacks top-to-bottom, horizontal
|
|
16
|
+
'Controls field arrangement. Vertical stacks top-to-bottom, horizontal uses one fractional grid track per field in logical inline order, and horizontal-labels uses CSS Grid with labels beside inputs on the inline-start side (collapsing to vertical when the viewport is <=480px).',
|
|
17
17
|
default: "'vertical'",
|
|
18
18
|
},
|
|
19
19
|
{
|
|
@@ -51,6 +51,7 @@ export const docs = {
|
|
|
51
51
|
{ guidance: false, description: 'Nest horizontal-labels inside another FormLayout. It uses CSS Grid and needs to be the outermost container.' },
|
|
52
52
|
],
|
|
53
53
|
anatomy: [
|
|
54
|
+
{name: 'Layout container', required: true, description: 'Owns field arrangement, spacing, direction, and form-level optionality.'},
|
|
54
55
|
{name: 'Form title', required: false, description: 'Heading that describes the purpose of the form.'},
|
|
55
56
|
{name: 'Fields', required: true, description: 'Input components with labels for collecting user data.'},
|
|
56
57
|
{name: 'Footer', required: false, description: 'Contains confirmation buttons such as Submit or Cancel.'},
|
|
@@ -67,7 +68,7 @@ export const docsZh = {
|
|
|
67
68
|
name: 'direction',
|
|
68
69
|
type: "'vertical' | 'horizontal' | 'horizontal-labels'",
|
|
69
70
|
description:
|
|
70
|
-
'控制字段排列方式。vertical
|
|
71
|
+
'控制字段排列方式。vertical 从上到下堆叠;horizontal 为每个字段使用一个 1fr 网格轨道,并按逻辑行内方向排列;horizontal-labels 使用 CSS Grid,将标签放在输入框的行内起始侧(当视口宽度 <=480px 时折叠为垂直布局)。',
|
|
71
72
|
default: "'vertical'",
|
|
72
73
|
},
|
|
73
74
|
{
|
|
@@ -105,6 +106,7 @@ export const docsZh = {
|
|
|
105
106
|
{ guidance: false, description: 'Nest horizontal-labels inside another FormLayout. It uses CSS Grid and needs to be the outermost container.' },
|
|
106
107
|
],
|
|
107
108
|
anatomy: [
|
|
109
|
+
{name: 'Layout container', required: true, description: 'Owns field arrangement, spacing, direction, and form-level optionality.'},
|
|
108
110
|
{name: 'Form title', required: false, description: 'Heading that describes the purpose of the form.'},
|
|
109
111
|
{name: 'Fields', required: true, description: 'Input components with labels for collecting user data.'},
|
|
110
112
|
{name: 'Footer', required: false, description: 'Contains confirmation buttons such as Submit or Cancel.'},
|
|
@@ -127,13 +129,14 @@ export const docsDense = {
|
|
|
127
129
|
{ guidance: false, description: 'Nest horizontal-labels inside another FormLayout. It uses CSS Grid and needs to be the outermost container.' },
|
|
128
130
|
],
|
|
129
131
|
anatomy: [
|
|
132
|
+
{name: 'Layout container', required: true, description: 'Owns field arrangement, spacing, direction, and form-level optionality.'},
|
|
130
133
|
{name: 'Form title', required: false, description: 'Heading that describes the purpose of the form.'},
|
|
131
134
|
{name: 'Fields', required: true, description: 'Input components with labels for collecting user data.'},
|
|
132
135
|
{name: 'Footer', required: false, description: 'Contains confirmation buttons such as Submit or Cancel.'},
|
|
133
136
|
],
|
|
134
137
|
},
|
|
135
138
|
propDescriptions: {
|
|
136
|
-
direction: 'Field arrangement. Vertical stacks top-to-bottom
|
|
139
|
+
direction: 'Field arrangement. Vertical stacks top-to-bottom; horizontal uses one fractional grid track per field in logical inline order; horizontal-labels places labels on the inline-start side and collapses when the viewport is <=480px.',
|
|
137
140
|
defaultOptionality: 'State the form treats as default, so only the exception is marked. "optional" → only isRequired fields show an indicator; "required" → only isOptional fields do. Under "required" unmarked fields also expose aria-required (aria only, not native required). Unset = per-field behavior.',
|
|
138
141
|
children: 'Form fields to arrange. Accepts Astryx inputs + Field-wrapped custom controls.',
|
|
139
142
|
xstyle: 'StyleX styles for layout customization. Must be stylex.create() value.',
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 3
|
|
3
|
+
template_version: 7
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:FormLayout
|
|
6
|
+
authority: draft
|
|
7
|
+
archive_reason: null
|
|
8
|
+
superseded_by: null
|
|
9
|
+
approved_by: null
|
|
10
|
+
approved_at: null
|
|
11
|
+
owners: [cixzhang]
|
|
12
|
+
review_triggers: [public-api, behavior, layout, theming, accessibility]
|
|
13
|
+
verified_by:
|
|
14
|
+
[
|
|
15
|
+
packages/core/src/FormLayout/FormLayout.test.tsx,
|
|
16
|
+
apps/storybook/stories/FormLayout.stories.tsx,
|
|
17
|
+
packages/core/src/theme/themingTargets.test.ts,
|
|
18
|
+
packages/core/src/docPropLiterals.test.ts,
|
|
19
|
+
packages/core/src/docPropReferences.test.ts,
|
|
20
|
+
]
|
|
21
|
+
modules: []
|
|
22
|
+
families: [family:input-fields]
|
|
23
|
+
design_specs: []
|
|
24
|
+
architecture:
|
|
25
|
+
[
|
|
26
|
+
architecture:public-component-api,
|
|
27
|
+
architecture:component-theming-surface,
|
|
28
|
+
architecture:component-test-sufficiency,
|
|
29
|
+
]
|
|
30
|
+
contributing: []
|
|
31
|
+
system_specs: []
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
# FormLayout component contract
|
|
35
|
+
|
|
36
|
+
## Contract at a glance
|
|
37
|
+
|
|
38
|
+
| Area | Contract |
|
|
39
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
+
| Public contract | `FormLayout` arranges caller-owned fields through `vertical`, `horizontal`, and `horizontal-labels` directions, provides a form-level optionality default, exports its context and supporting types, and accepts root `BaseProps`. |
|
|
41
|
+
| Behavior | Vertical stacks children; horizontal creates one fractional grid track per child in logical inline order; horizontal-labels pairs Field labels and controls, then collapses below a 481px viewport. |
|
|
42
|
+
| End-user impact | People encounter predictable field spacing and order, and assistive technology receives required-state information even when the form-wide visual convention leaves the common case unmarked. |
|
|
43
|
+
| Builder impact | Builders supply the fields and any semantic `<form>`, title, or footer; nested layouts replace the outer direction and optionality context for their descendants. |
|
|
44
|
+
| Compatibility/readiness | This draft records released behavior. It changes no public API, default, or runtime behavior. |
|
|
45
|
+
| Review checks | Reject lost root props, physical inline ordering, changed collapse boundaries, caller-overridable direction reflection, lost nested-context shadowing, or a visible optionality convention that disagrees with ARIA. Narrow label/control grouping remains open in OQ1. |
|
|
46
|
+
| Governing rules | Established public FormLayout interface; `family:input-fields` outer-arrangement ownership; `architecture:public-component-api/INV5–INV9`; `architecture:component-theming-surface/INV2–INV6`. |
|
|
47
|
+
|
|
48
|
+
This table is a review projection; the body below is authoritative.
|
|
49
|
+
|
|
50
|
+
## Intent
|
|
51
|
+
|
|
52
|
+
`FormLayout` owns the outer spatial relationship among caller-owned form fields. It does not create form submission, field state, labels, controls, or action regions.
|
|
53
|
+
|
|
54
|
+
## Compatibility and migration
|
|
55
|
+
|
|
56
|
+
- Released default preserved: yes; direction defaults to `vertical`, and optionality remains field-owned when `defaultOptionality` is unset.
|
|
57
|
+
- Compatibility class: observational only; this draft changes no public prop, default, or behavior.
|
|
58
|
+
- Controlled/uncontrolled behavior: not applicable; FormLayout owns no form value or submission state.
|
|
59
|
+
- Migration decision: none.
|
|
60
|
+
|
|
61
|
+
## Ownership boundary
|
|
62
|
+
|
|
63
|
+
**Owns**
|
|
64
|
+
|
|
65
|
+
- Outer field arrangement, spacing, direction, and the viewport collapse for horizontal labels.
|
|
66
|
+
- Form-level optionality context and nested-layout shadowing.
|
|
67
|
+
- The root DOM element, root prop composition, and the `form-layout` theme target.
|
|
68
|
+
|
|
69
|
+
**Does not own / non-goals**
|
|
70
|
+
|
|
71
|
+
- Form semantics, submission, validation, or action placement — owned by the product callsite and native `<form>`.
|
|
72
|
+
- Field labels, controls, status, and internal geometry — owned by `component:Field`, each input, and `family:input-fields`.
|
|
73
|
+
- The content or visual contract of caller-supplied titles and footers — owned by their rendered components or the callsite.
|
|
74
|
+
|
|
75
|
+
## Public concepts
|
|
76
|
+
|
|
77
|
+
| Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
|
|
78
|
+
| ------------------- | ----------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------ | -------------------- | ---------------------- | --------- | ---------------------- |
|
|
79
|
+
| direction | `vertical`; `horizontal`; `horizontal-labels` | Selects the outer arrangement and provides that value to descendants. | All layouts. | `vertical` | `component:FormLayout` | released | Type-constrained |
|
|
80
|
+
| default optionality | unset; `optional`; `required` | Marks only exceptions while controls preserve resolved required state. | All directions; the nearest nested layout shadows the outer. | unset / per-field | `component:FormLayout` | released | Type-constrained |
|
|
81
|
+
| root composition | DOM/data/ARIA/events; ref; class/style/`xstyle` | Extends the root layout container without replacing owned reflection. | All directions. | no caller decoration | `component:FormLayout` | released | Shared merge contract |
|
|
82
|
+
| layout context | direction plus optionality | Lets Field and inputs adapt their layout and required-state projection. | Descendants in the same layer scope. | vertical / unset | `component:FormLayout` | released | Nearest provider wins |
|
|
83
|
+
|
|
84
|
+
## Behavioral and layout contract
|
|
85
|
+
|
|
86
|
+
Draft requirements identify their basis so observed code is not mistaken for an intentional decision.
|
|
87
|
+
|
|
88
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
89
|
+
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------- |
|
|
90
|
+
| FR1 | Vertical direction stacks children with the shared spacing token. | Established public interface; current source and stories | settled observation |
|
|
91
|
+
| FR2 | Horizontal direction places every direct child in one `1fr` grid track and follows the document's logical inline order without a component-owned wrap rule. | Established public interface; current source and stories | settled observation |
|
|
92
|
+
| FR3 | Horizontal-labels direction lets Field labels and control groups participate in a two-column grid, with the label on the inline-start side; at a viewport width of 480px or below it becomes one vertical flex column. Whether the collapsed label/control gap distinguishes each pair strongly enough remains open in OQ1. | Established public interface; Field composition and browser fixture | wide behavior settled; narrow grouping human decision |
|
|
93
|
+
| FR4 | A nested FormLayout replaces the outer direction and optionality context for its descendants. | Existing context and unit tests | settled observation |
|
|
94
|
+
| FR5 | `defaultOptionality` suppresses only the visible marker that restates the form default; descendant controls still expose resolved `aria-required`, while native `required` remains tied to explicit field intent. | Existing tests; `family:input-fields` ownership | settled observation |
|
|
95
|
+
| FR6 | Root ref, DOM/data/ARIA/event props, `xstyle`, class name, and inline style reach or compose on the root without replacing `data-direction` or the canonical target. | `architecture:public-component-api/INV5–INV8` | settled observation |
|
|
96
|
+
|
|
97
|
+
### Allowed variation
|
|
98
|
+
|
|
99
|
+
- **AV1 — Caller content.** Any caller-owned field, heading, explanatory content, or action region may be a child; FormLayout does not inspect or rewrite it.
|
|
100
|
+
- **AV2 — Nested arrangement.** Builders may nest vertical and horizontal layouts; each provider owns only its descendant scope.
|
|
101
|
+
- **AV3 — Root styling.** Caller styling may change the container's presentation while preserving the public direction and child contracts.
|
|
102
|
+
|
|
103
|
+
### Representative states
|
|
104
|
+
|
|
105
|
+
| State | Required invariant | Allowed variation |
|
|
106
|
+
| -------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
107
|
+
| vertical | One block-axis stack with tokenized gaps. | Any caller-owned children. |
|
|
108
|
+
| horizontal | One `1fr` grid track per child in logical inline order. | Child count and content. |
|
|
109
|
+
| horizontal labels / wide | Label/control pairs share consistent two-column tracks. | Label and control content. |
|
|
110
|
+
| horizontal labels / narrow | One vertical column at 480px or below with no component-owned overflow. | Label/control grouping remains OQ1; caller-owned content may wrap. |
|
|
111
|
+
| optional by default | Only explicitly required fields show the exception marker. | Field and input type. |
|
|
112
|
+
| required by default | Only explicitly optional fields show the exception marker. | Field and input type. |
|
|
113
|
+
| nested | Inner direction and optionality replace outer values. | Any supported inner direction/default combination. |
|
|
114
|
+
|
|
115
|
+
### Transformation and precedence order
|
|
116
|
+
|
|
117
|
+
- **ORD1 — Context.** Explicit props resolve against `vertical` and unset defaults, then the nearest provider exposes the resolved values to descendants.
|
|
118
|
+
- **ORD2 — Root props.** FormLayout-owned target and direction reflection compose with StyleX and caller styling; caller DOM props reach the same root without replacing owned reflection.
|
|
119
|
+
- **ORD3 — Optionality.** The layout default establishes the unmarked majority; a field-level `isRequired` or `isOptional` marks the exception and controls native validation only when explicitly requested.
|
|
120
|
+
|
|
121
|
+
### Performance and resources
|
|
122
|
+
|
|
123
|
+
- **PR1 — Stable context identity.** Descendants receive one memoized context value per direction/optionality pair; FormLayout owns no listener, observer, timer, or external resource.
|
|
124
|
+
|
|
125
|
+
## Accessibility contract
|
|
126
|
+
|
|
127
|
+
- **AR1 — Layout remains nonsemantic.** FormLayout renders a `<div>` and does not imply `<form>`, group, label, or submission semantics; callers provide those when their composition needs them.
|
|
128
|
+
- **AR2 — Required state remains perceivable.** A form-wide optionality convention may remove redundant visible markers, but descendant controls preserve the resolved required state for assistive technology.
|
|
129
|
+
- **AR3 — Logical order is preserved.** Every direction retains DOM order and logical inline flow; FormLayout does not create a separate keyboard order.
|
|
130
|
+
|
|
131
|
+
## Design relationships
|
|
132
|
+
|
|
133
|
+
No current component-local or shared design record prescribes a FormLayout-specific representation. Spacing uses the current token scale; exact tuning remains token-owned.
|
|
134
|
+
|
|
135
|
+
### Theming anatomy
|
|
136
|
+
|
|
137
|
+
<!-- anatomy-theming:v1 -->
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"Layout container": {"target": "form-layout"},
|
|
142
|
+
"Form title": {
|
|
143
|
+
"none": {
|
|
144
|
+
"reason": "intentional: A title is caller-owned content; FormLayout neither renders nor styles a title part."
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"Fields": {
|
|
148
|
+
"none": {
|
|
149
|
+
"reason": "intentional: Caller-owned Field and input components retain their own theming targets."
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"Footer": {
|
|
153
|
+
"none": {
|
|
154
|
+
"reason": "intentional: A footer is caller-owned content; FormLayout neither renders nor styles an action region."
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The root target reflects the closed `direction` axis. It owns the outer arrangement container and does not claim the visual parts rendered by child components.
|
|
161
|
+
|
|
162
|
+
## Family and system relationships
|
|
163
|
+
|
|
164
|
+
- `family:input-fields` identifies FormLayout as the owner of outer arrangement, spacing, direction, and form-level optionality.
|
|
165
|
+
- `component:Field` consumes horizontal-label direction and optionality while retaining label, control, description, and status ownership.
|
|
166
|
+
- `architecture:public-component-api` owns root prop, styling, event, ref, and released-type composition.
|
|
167
|
+
- `architecture:component-theming-surface` owns the root target's anatomy mapping and reflected direction axis.
|
|
168
|
+
- `architecture:component-test-sufficiency` owns bounded, mutation-sensitive evidence selection.
|
|
169
|
+
|
|
170
|
+
## Verification map
|
|
171
|
+
|
|
172
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
173
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------- |
|
|
174
|
+
| FR1–FR4, ORD1 | `FormLayout.test.tsx`, exact-head `AuditMatrix` story-play guards, and independent rendered inspection | vertical, horizontal, horizontal-label wide/narrow, nested | Changed display, order, breakpoint, or provider shadowing fails; narrow grouping remains visible for OQ1. | `audit:FormLayout/behavior` |
|
|
175
|
+
| FR5, ORD3, AR2 | `FormLayout.test.tsx`, Field tests, and scoped browser accessibility audit | required/optional/unset defaults; explicit exceptions | A redundant marker returns, resolved `aria-required` disappears, or native validation changes. | `audit:FormLayout/accessibility` |
|
|
176
|
+
| FR6, ORD2 | root composition tests, BaseProps lint, theming-target tests, and exact-head AuditMatrix play | ref, DOM/data/ARIA/events, class/style/`xstyle`, every direction | A caller input drops, direction reflection is replaced, or the public target disappears. | `audit:FormLayout/api` |
|
|
177
|
+
| AR1, AR3 | DOM tests and exact-head browser/a11y evidence | plain root, native form wrapper, LTR/RTL | Layout invents semantics, changes DOM/keyboard order, or uses physical inline placement. | `audit:FormLayout/accessibility` |
|
|
178
|
+
| Theming anatomy | knowledge validation, consumer-doc projection, and theming-target tests | root target; caller-owned title, fields, and footer | The target loses its semantic owner or FormLayout claims a child component's painter. | `audit:FormLayout/theming` |
|
|
179
|
+
| Documentation | docs typecheck and prop literal/reference tests | all directions, breakpoint, optionality, root anatomy | Docs claim wrapping, physical order, flex growth, or another behavior the released code lacks. | `audit:FormLayout/docs` |
|
|
180
|
+
| RTL applicability | verified-N/A registry plus exact-head AuditMatrix logical-order assertion | horizontal and horizontal-labels in LTR/RTL | A physical side, directional glyph, transform, scroll/drag, overlay, or directional key path makes N/A stale. | `audit:FormLayout/i18n-rtl` |
|
|
181
|
+
|
|
182
|
+
## Decision log
|
|
183
|
+
|
|
184
|
+
No component-local decision is asserted. This draft records released behavior and its current shared owners.
|
|
185
|
+
|
|
186
|
+
## Open questions
|
|
187
|
+
|
|
188
|
+
- **OQ1 — At 480px and below, should each horizontal-label pair preserve a smaller intra-field label/control gap than the gap between adjacent fields?** (`human-design`)
|
|
189
|
+
|
|
190
|
+
## Content boundary
|
|
191
|
+
|
|
192
|
+
This draft records verified shipped behavior and the unresolved narrow grouping relationship. It does not create form semantics, prescribe caller content, approve a new API or target, or duplicate consumer prop tables and audit-run scores.
|
|
@@ -71,6 +71,21 @@ describe('FormLayout', () => {
|
|
|
71
71
|
expect(el).toHaveAttribute('role', 'group');
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
it('keeps component-owned direction metadata over a colliding pass-through', () => {
|
|
75
|
+
render(
|
|
76
|
+
<FormLayout
|
|
77
|
+
direction="horizontal"
|
|
78
|
+
data-direction="vertical"
|
|
79
|
+
data-testid="layout">
|
|
80
|
+
content
|
|
81
|
+
</FormLayout>,
|
|
82
|
+
);
|
|
83
|
+
expect(screen.getByTestId('layout')).toHaveAttribute(
|
|
84
|
+
'data-direction',
|
|
85
|
+
'horizontal',
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
74
89
|
// ─── Direction modes ────────────────────────────────────────────────────
|
|
75
90
|
|
|
76
91
|
it('defaults to vertical direction', () => {
|
|
@@ -82,10 +82,11 @@ export interface FormLayoutProps extends BaseProps<HTMLDivElement> {
|
|
|
82
82
|
* Direction of field arrangement.
|
|
83
83
|
*
|
|
84
84
|
* - `'vertical'` — Fields stack top-to-bottom (default). Most common.
|
|
85
|
-
* - `'horizontal'` — Fields
|
|
86
|
-
*
|
|
87
|
-
* - `'horizontal-labels'` — CSS Grid with labels
|
|
88
|
-
* Collapses to vertical when the
|
|
85
|
+
* - `'horizontal'` — Fields occupy one `1fr` CSS Grid track per child in
|
|
86
|
+
* logical inline order.
|
|
87
|
+
* - `'horizontal-labels'` — CSS Grid with labels beside their inputs on the
|
|
88
|
+
* inline-start side. Collapses to vertical when the viewport is narrow
|
|
89
|
+
* (≤480px).
|
|
89
90
|
*
|
|
90
91
|
* @default 'vertical'
|
|
91
92
|
*/
|
|
@@ -153,6 +154,7 @@ export function FormLayout({
|
|
|
153
154
|
<FormLayoutContext value={contextValue}>
|
|
154
155
|
<div
|
|
155
156
|
ref={ref}
|
|
157
|
+
{...props}
|
|
156
158
|
{...mergeProps(
|
|
157
159
|
themeProps('form-layout', {direction}),
|
|
158
160
|
stylex.props(
|
|
@@ -163,8 +165,7 @@ export function FormLayout({
|
|
|
163
165
|
),
|
|
164
166
|
className,
|
|
165
167
|
style,
|
|
166
|
-
)}
|
|
167
|
-
{...props}>
|
|
168
|
+
)}>
|
|
168
169
|
{children}
|
|
169
170
|
</div>
|
|
170
171
|
</FormLayoutContext>
|
|
@@ -20,9 +20,9 @@ import {createLayerScopedContext as createContext} from '../Layer/layerScopedCon
|
|
|
20
20
|
* Direction of form field arrangement.
|
|
21
21
|
*
|
|
22
22
|
* - `'vertical'` — Fields stack top-to-bottom (default). Most common.
|
|
23
|
-
* - `'horizontal'` — Fields
|
|
24
|
-
* - `'horizontal-labels'` — Fields stack vertically
|
|
25
|
-
*
|
|
23
|
+
* - `'horizontal'` — Fields occupy one `1fr` grid track per child in logical inline order.
|
|
24
|
+
* - `'horizontal-labels'` — Fields stack vertically while labels sit beside
|
|
25
|
+
* their inputs on the inline-start side (settings/admin panel pattern).
|
|
26
26
|
*/
|
|
27
27
|
export type FormLayoutDirection =
|
|
28
28
|
'vertical' | 'horizontal' | 'horizontal-labels';
|
package/src/Grid/Grid.doc.mjs
CHANGED
|
@@ -60,7 +60,7 @@ export const docs = {
|
|
|
60
60
|
{
|
|
61
61
|
name: 'columns',
|
|
62
62
|
type: "number | {minWidth: number, max?: number, repeat?: 'fill' | 'fit'}",
|
|
63
|
-
description: 'Column configuration. A number
|
|
63
|
+
description: 'Column configuration. A number creates exactly N `1fr` tracks at every width and never reflows (e.g. `columns={3}`); a child\'s minimum content may widen its track. For columns that reflow on small screens, use `{minWidth, max}`. Use an object for responsive columns: `minWidth` sets the minimum column width in px, `repeat` controls track behavior (`"fill"` preserves empty tracks for consistent widths, `"fit"` collapses empty tracks so items stretch; defaults to `"fill"`), and `max` caps the maximum number of columns.',
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
name: 'width',
|
|
@@ -97,6 +97,12 @@ export const docs = {
|
|
|
97
97
|
type: '0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10',
|
|
98
98
|
description: 'Column spacing; overrides `gap` for the column axis.',
|
|
99
99
|
},
|
|
100
|
+
{
|
|
101
|
+
name: 'rowHeight',
|
|
102
|
+
type: 'number',
|
|
103
|
+
description:
|
|
104
|
+
'Height of each implicit row track in pixels. Pair with `GridSpan rows={N}` for masonry-style layouts.',
|
|
105
|
+
},
|
|
100
106
|
{
|
|
101
107
|
name: 'align',
|
|
102
108
|
type: "'start' | 'center' | 'end' | 'stretch'",
|