Skip to content

Commit e58b0da

Browse files
docs(swingset): align Field docs with StyleX
1 parent 737bcb8 commit e58b0da

8 files changed

Lines changed: 247 additions & 544 deletions

File tree

packages/swingset/src/lib/registry.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,7 @@ import { Default as DestructiveDefault, meta as destructiveMeta } from '../stori
2525
import { Default as DialogDefault, meta as dialogComponentMeta } from '../stories/dialog.component.stories';
2626
import { meta as dialogMeta } from '../stories/dialog.stories';
2727
import { meta as drawerMeta } from '../stories/drawer.stories';
28-
import {
29-
Default as FieldDefault,
30-
Invalid as FieldInvalid,
31-
meta as fieldMeta,
32-
SettingsRow as FieldSettingsRow,
33-
} from '../stories/field.stories';
28+
import { Default as FieldDefault, meta as fieldMeta } from '../stories/field.stories';
3429
import { meta as fileUploadMeta } from '../stories/file-upload.stories';
3530
import {
3631
Colors as HeadingColors,
@@ -209,8 +204,6 @@ const textModule: StoryModule = { meta: textMeta, Default: TextDefault, Sizes: T
209204
const fieldModule: StoryModule = {
210205
meta: fieldMeta,
211206
Default: FieldDefault,
212-
SettingsRow: FieldSettingsRow,
213-
Invalid: FieldInvalid,
214207
};
215208

216209
const iconModule: StoryModule = {

packages/swingset/src/stories/field.mdx

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,66 @@ import * as FieldStories from './field.stories';
22

33
# Field
44

5-
`Field` supplies accessible relationships and semantic state without choosing a layout or control. The caller arranges its parts, and Mosaic controls integrate when placed inside `Field.Root`.
5+
The Mosaic `Field` provides context-free, StyleX-themed parts for composing labels, supporting text, and validation errors around a form control. Callers explicitly connect the parts with native HTML and ARIA attributes.
66

7-
## Playground
7+
## Example
88

9-
<Preview
9+
<Story
1010
name='Default'
1111
storyModule={FieldStories}
1212
/>
1313

14-
## Props
15-
16-
<PropTable meta={FieldStories.meta} />
17-
1814
## Usage
1915

16+
Give the control an `id`, point the label's `htmlFor` at that ID, and reference supporting messages from the control's `aria-describedby`. The caller owns validation and decides when to render an error.
17+
2018
```tsx
2119
import { Field } from '@clerk/ui/mosaic/components/field';
2220
import { Input } from '@clerk/ui/mosaic/components/input';
2321

24-
<Field.Root required>
25-
<Field.Label>Email address</Field.Label>
22+
<Field.Root>
23+
<Field.Label htmlFor='email'>Email address</Field.Label>
2624
<Input
25+
id='email'
2726
name='email'
2827
type='email'
28+
required
29+
aria-invalid={Boolean(error)}
30+
aria-describedby={error ? 'email-error' : 'email-description'}
2931
/>
30-
<Field.Description>Used for account notifications.</Field.Description>
32+
{error ? (
33+
<Field.Error id='email-error'>{error}</Field.Error>
34+
) : (
35+
<Field.Description id='email-description'>Used for account notifications.</Field.Description>
36+
)}
3137
</Field.Root>;
3238
```
3339

34-
---
40+
`id` identifies the control in the document and creates label and ARIA relationships. `name` identifies the submitted form value and is typically what form libraries use for registration. They serve different purposes and often should both be present.
3541

36-
## Examples
42+
Field does not validate controls, generate IDs, propagate semantic state, or render errors automatically. Its parts may also be used independently without `Field.Root`.
3743

38-
### Caller-owned settings layout
44+
## Parts
3945

40-
<Story
41-
name='SettingsRow'
42-
storyModule={FieldStories}
43-
/>
46+
| Part | Stable slot class | Description |
47+
| ------------------- | ----------------------- | ---------------------------------------------------- |
48+
| `Field.Root` | `.cl-field-root` | Unstyled `div` container with no prescribed layout. |
49+
| `Field.Label` | `.cl-field-label` | Native `label`; accepts `id` and `htmlFor`. |
50+
| `Field.Description` | `.cl-field-description` | Supporting `p`; accepts a native `id`. |
51+
| `Field.Error` | `.cl-field-error` | Error `p` with an alert icon; accepts a native `id`. |
4452

45-
### Invalid
53+
## Styling
4654

47-
<Story
48-
name='Invalid'
49-
storyModule={FieldStories}
50-
/>
55+
The Mosaic field is themed with **StyleX**. Each styled part carries the stable public slot class shown above alongside the generated StyleX atoms. Consumers never target the hashed atomic classes—override a `.cl-field-*` class from a CSS layer that wins over `@clerk/ui/styles.css`:
56+
57+
```css
58+
@import '@clerk/ui/styles.css' layer(components);
59+
60+
@layer overrides {
61+
.cl-field-label {
62+
font-weight: 600;
63+
}
64+
}
65+
```
66+
67+
`Field.Root` ships no layout. Higher-level blocks own how its parts are arranged; for example, a settings row can provide the grid and alignment for a field. Customize an `Input` through its exposed tokens, `.cl-input`, `className`, and `style`; Field does not add control-specific styling.
Lines changed: 8 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,37 @@
1-
import type { FieldRootProps } from '@clerk/ui/mosaic/components/field';
21
import { Field } from '@clerk/ui/mosaic/components/field';
32
import { Input } from '@clerk/ui/mosaic/components/input';
43

54
import type { StoryMeta } from '@/lib/types';
65

6+
// Exposes this file's own source (via the `?raw` webpack rule) so each `<Story>` example
7+
// renders a code footer with its function's source. See `StoryModule.__source`.
78
export { default as __source } from './field.stories?raw';
89

910
export const meta: StoryMeta = {
1011
group: 'Components',
1112
title: 'Field',
1213
source: 'packages/ui/src/mosaic/components/field/field.tsx',
1314
styleEngine: 'stylex',
14-
styles: {
15-
_variants: {
16-
invalid: { true: {}, false: {} },
17-
disabled: { true: {}, false: {} },
18-
required: { true: {}, false: {} },
19-
},
20-
_defaultVariants: {
21-
invalid: false,
22-
disabled: false,
23-
required: false,
24-
},
25-
},
2615
};
2716

28-
function knobsAsProps(props: Record<string, unknown>) {
29-
return props as unknown as FieldRootProps;
30-
}
31-
3217
const stackStyles = {
3318
display: 'grid',
3419
gap: 8,
3520
maxWidth: 384,
3621
} as const;
3722

38-
export function Default(props: Record<string, unknown>) {
39-
const fieldProps = knobsAsProps(props);
40-
23+
export function Default() {
4124
return (
42-
<Field.Root
43-
{...fieldProps}
44-
style={stackStyles}
45-
>
46-
<Field.Label>Email address</Field.Label>
25+
<Field.Root style={stackStyles}>
26+
<Field.Label htmlFor='email'>Email address</Field.Label>
4727
<Input
28+
id='email'
4829
name='email'
4930
type='email'
5031
placeholder='you@example.com'
32+
aria-describedby='email-description'
5133
/>
52-
{fieldProps.invalid ? (
53-
<Field.Error>Enter a valid email address.</Field.Error>
54-
) : (
55-
<Field.Description>Used for account notifications.</Field.Description>
56-
)}
57-
</Field.Root>
58-
);
59-
}
60-
61-
export function SettingsRow() {
62-
return (
63-
<Field.Root
64-
style={{
65-
alignItems: 'start',
66-
columnGap: 24,
67-
display: 'grid',
68-
gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 24rem)',
69-
maxWidth: 960,
70-
}}
71-
>
72-
<div style={{ display: 'grid', gap: 4 }}>
73-
<Field.Label>Username</Field.Label>
74-
<Field.Description>This is how other members will identify you.</Field.Description>
75-
</div>
76-
<Input
77-
name='username'
78-
defaultValue='prestonxyz'
79-
/>
80-
</Field.Root>
81-
);
82-
}
83-
84-
export function Invalid() {
85-
return (
86-
<Field.Root
87-
invalid
88-
required
89-
style={stackStyles}
90-
>
91-
<Field.Label>Email address</Field.Label>
92-
<Input
93-
name='email'
94-
type='email'
95-
defaultValue='not-an-email'
96-
/>
97-
<Field.Error>Enter a valid email address.</Field.Error>
34+
<Field.Description id='email-description'>Used for account notifications.</Field.Description>
9835
</Field.Root>
9936
);
10037
}

packages/ui/src/mosaic/components/field/field.ssr.test.tsx

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,38 @@ import { Input } from '../input';
88
import { Field } from './field';
99

1010
describe('Mosaic Field SSR', () => {
11-
it('emits the field-owned control ID and stable part IDs', () => {
11+
it('emits complete caller-owned label and message relationships', () => {
1212
const html = renderToString(
13-
<Field.Root
14-
invalid
15-
required
16-
>
17-
<Field.Label>Email</Field.Label>
13+
<Field.Root>
14+
<Field.Label
15+
id='email-label'
16+
htmlFor='account-email'
17+
>
18+
Email
19+
</Field.Label>
1820
<Input
1921
id='account-email'
20-
aria-describedby='external'
22+
name='email'
23+
required
24+
aria-labelledby='email-label'
25+
aria-describedby='email-description email-error'
26+
aria-invalid='true'
2127
/>
22-
<Field.Description>Description</Field.Description>
23-
<Field.Error>Error</Field.Error>
28+
<Field.Description id='email-description'>Description</Field.Description>
29+
<Field.Error id='email-error'>Error</Field.Error>
2430
</Field.Root>,
2531
);
2632

27-
const labelControlId = html.match(/for="([^"]+)"/)?.[1];
28-
const inputControlId = html.match(/<input[^>]*\sid="([^"]+)"/)?.[1];
29-
expect(labelControlId).toBeDefined();
30-
expect(labelControlId).toBe(inputControlId);
31-
expect(inputControlId).not.toBe('account-email');
32-
expect(html).toMatch(/id="cl-field-[^"]+-label"/);
33-
expect(html).toMatch(/id="cl-field-[^"]+-description"/);
34-
expect(html).toMatch(/id="cl-field-[^"]+-error"/);
35-
expect(html).toContain('aria-describedby="external"');
36-
expect(html).not.toContain('aria-labelledby');
33+
expect(html).toContain('id="email-label"');
34+
expect(html).toContain('for="account-email"');
35+
expect(html).toContain('id="account-email"');
36+
expect(html).toContain('name="email"');
37+
expect(html).toContain('aria-labelledby="email-label"');
38+
expect(html).toContain('aria-describedby="email-description email-error"');
3739
expect(html).toContain('aria-invalid="true"');
40+
expect(html).toContain('id="email-description"');
41+
expect(html).toContain('id="email-error"');
3842
expect(html).toContain('required=""');
43+
expect(html).not.toContain('cl-field-control');
3944
});
4045
});

packages/ui/src/mosaic/components/field/field.styles.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,4 @@ export const styles = stylex.create({
2424
position: 'relative',
2525
top: 1,
2626
},
27-
disabledText: {
28-
opacity: 0.5,
29-
},
3027
});

0 commit comments

Comments
 (0)