Keep a button at its label's width in a stack and a form
<x-stack> stretches its children across by default and <x-form> is a one-track grid, so a button written directly in either drew as a full-width pill. M3 keeps a button's width "dynamic to fit label" and says not to "stretch buttons into long flat shapes on large windows". A button there now sits at the start edge at its own width; a stack aligned start, centre or end is untouched, and an application's own width still wins (a cap on the button itself was rejected for exactly that: it would have limited an app's inline-size, which CascadeTest guards). Chromium test in both directions, failing without the rules. Decided with the user (plan step 46). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f5f040516e
commit
92d3d761f3
@@ -85,6 +85,8 @@ over a colour, use the role directly.
|
||||
- `<x-list dividers>` draws a 16px-inset rule; `<x-list selectable>` or `selection="single|multi"`
|
||||
makes it a `listbox` of `option`s with `aria-selected`.
|
||||
- `<x-card>` changes elevation on hover instead of its corner; `data-md-card` carries the variant.
|
||||
- A button directly in `<x-stack>` (stretched, the default) or `<x-form>` keeps its label's width at
|
||||
the start edge instead of filling the width, as M3 asks; a full-width submit is your own CSS.
|
||||
- `<x-modal>` pins its header and actions and scrolls only the body; a `box-class` that set
|
||||
`overflow` no longer applies.
|
||||
- `<x-table>` rows are 52px and the automatic fine-pointer density is gone: pass `dense` for 36px
|
||||
|
||||
@@ -611,7 +611,7 @@ With `scroll`, M3's overflow affordance is drawn for you: the edge the row can s
|
||||
|
||||
### `<x-form>`
|
||||
|
||||
A one-column grid of fields with an `actions` slot at the foot (the slot takes its own `class`); `separator` draws a divider above the actions.
|
||||
A one-column grid of fields with an `actions` slot at the foot, end-aligned (the slot takes its own `class`); `separator` draws a divider above the actions. Put a form's buttons in `actions`; a button written among the fields keeps its label's width at the start edge rather than stretching across.
|
||||
|
||||
```blade
|
||||
<x-form wire:submit="save">
|
||||
@@ -925,7 +925,7 @@ A tonal region: `<x-surface level="surface-container-low" padding="space300" cor
|
||||
|
||||
#### `<x-stack>`
|
||||
|
||||
Children one under another inside a pane: `<x-stack gap="space200">…</x-stack>`. `align` across it: `stretch` (default), `start`, `center`, `end`.
|
||||
Children one under another inside a pane: `<x-stack gap="space200">…</x-stack>`. `align` across it: `stretch` (default), `start`, `center`, `end`. A button keeps its label's width at the start edge even when the rest stretch: M3 keeps a button's width dynamic and never stretches one into a long flat shape. Widen one only in your own CSS, and only with a reason.
|
||||
|
||||
#### `<x-row>`
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
* floors at its content's min-content, so on a phone a wide field would push the form past its
|
||||
* pane. Rows take their content's height. 16px (`space200`) between fields, because a text field
|
||||
* floats its label half above its outline and any less lets the hint under one field run into the
|
||||
* label of the next.
|
||||
* label of the next. A button written among the fields keeps its label's width at the start edge,
|
||||
* where the grid would stretch it across: M3 keeps a button's width "dynamic to fit label" and
|
||||
* says not to "stretch buttons into long flat shapes"
|
||||
* (docs/reference/m3/components-actions-communication-containment.md § Buttons).
|
||||
*
|
||||
* The actions wrap, end-aligned, 8px (`space100`) apart — M3's gap between buttons in a row. A
|
||||
* caller's class on the `actions` slot lands on the row and outranks this layer.
|
||||
@@ -27,6 +30,10 @@
|
||||
gap: var(--md-sys-measurement-space200);
|
||||
}
|
||||
|
||||
[data-md-form] > [data-md-button] {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
[data-md-form-actions] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
* <x-stack>: its children one under another, in a pane.
|
||||
*
|
||||
* A flex column, stretched across the stack's width unless `data-md-align` says `start`, `center`
|
||||
* or `end`. The space between the children is `--md-gap` (spacing.css), one of M3's spacing
|
||||
* or `end` — except a button, which keeps its label's width at the start edge: M3 keeps a button's
|
||||
* width "dynamic to fit label" and says not to "stretch buttons into long flat shapes on large
|
||||
* windows" (docs/reference/m3/components-actions-communication-containment.md § Buttons). An
|
||||
* application's own width or `align-self` on it still wins. The space between the children is `--md-gap` (spacing.css), one of M3's spacing
|
||||
* tokens and none by default; M3 groups with proximity, so the gap is the grouping
|
||||
* (docs/reference/m3/foundations.md § Layout → Grids & spacing). M3 defines no in-pane arrangement
|
||||
* component; this is the neutral one, beside <x-row> and <x-grid>.
|
||||
@@ -26,6 +29,10 @@
|
||||
gap: var(--md-gap);
|
||||
}
|
||||
|
||||
[data-md-stack]:is(:not([data-md-align]), [data-md-align='stretch']) > [data-md-button] {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
[data-md-stack][data-md-align='start'] {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
`gap` is a spacing token's name, `space25` … `space900` (docs/reference/m3/styles-supplement.md
|
||||
§ Spacing), and none when left out or unknown. `align` places the children across the stack:
|
||||
`stretch` (the default), `start`, `center` or `end`.
|
||||
`stretch` (the default), `start`, `center` or `end`. A button keeps its label's width even
|
||||
when the rest stretch, as M3 asks.
|
||||
|
||||
M3's layout has no arrangement component inside a pane — its "column" is a grid column — so
|
||||
this is the package's neutral one, beside `<x-row>` and `<x-grid>`. It takes `as`,
|
||||
|
||||
@@ -72,6 +72,29 @@ it('spaces a stack with its token, never with its parent\'s, and hides it from a
|
||||
->assertScript(layoutStyle('#hidden-from', 'display')." === 'none'");
|
||||
});
|
||||
|
||||
it('keeps a button at its label\'s width in a stretched stack and a form, where the rest stretch', function () {
|
||||
// M3: keep a button's width "dynamic to fit label"; don't "stretch buttons into long flat shapes
|
||||
// on large windows". An application's own width still wins, and a centred stack centres it.
|
||||
$body = '<style>.probe-full { inline-size: 100%; }</style>'
|
||||
.'<x-stack id="stack" gap="space200" style="inline-size: 600px"><p id="stack-field">Name</p><x-button label="Save" id="stack-save" /><x-button label="Wide" class="probe-full" id="stack-wide" /></x-stack>'
|
||||
.'<x-stack id="centred" align="center" style="inline-size: 600px"><x-button label="Save" id="centred-save" /></x-stack>'
|
||||
.'<x-form id="form" style="inline-size: 600px"><x-input label="Name" /><x-button label="Go" id="form-go" /></x-form>';
|
||||
|
||||
layoutPage($body, 1280)
|
||||
->assertScript(layoutRect('#stack-field', 'width').' === 600')
|
||||
->assertScript(layoutRect('#stack-save', 'width').' < 200')
|
||||
->assertScript(layoutRect('#stack-save', 'left').' === '.layoutRect('#stack', 'left'))
|
||||
->assertScript(layoutRect('#stack-wide', 'width').' === 600')
|
||||
->assertScript('Math.abs(('.layoutRect('#centred-save', 'left').' + '.layoutRect('#centred-save', 'right').') / 2 - ('.layoutRect('#centred', 'left').' + '.layoutRect('#centred', 'right').') / 2) <= 1')
|
||||
->assertScript(layoutRect('#form-go', 'width').' < 200')
|
||||
->assertScript(layoutRect('#form-go', 'left').' === '.layoutRect('#form', 'left'));
|
||||
|
||||
// In a right-to-left document the start edge is the right.
|
||||
layoutPage($body, 1280, 900, 'rtl')
|
||||
->assertScript(layoutRect('#stack-save', 'right').' === '.layoutRect('#stack', 'right'))
|
||||
->assertScript(layoutRect('#form-go', 'right').' === '.layoutRect('#form', 'right'));
|
||||
});
|
||||
|
||||
it('stacks a row below its breakpoint and lays it side by side from it', function () {
|
||||
$body = '<x-row id="row" gap="space200" stack-below="medium"><span>One</span><span>Two</span></x-row>'
|
||||
.'<x-row id="aligned" align="start" stack-below="medium"><span>One</span></x-row>'
|
||||
|
||||
@@ -297,3 +297,9 @@ it('draws the file picker\'s button from its own stylesheet', function () {
|
||||
->and((string) file_get_contents(__DIR__.'/../../../resources/css/components/field.css'))->not->toContain("[type='file']")
|
||||
->and((string) file_get_contents(__DIR__.'/../../../resources/css/all.css'))->toContain("@import './components/file.css';");
|
||||
});
|
||||
|
||||
it('keeps a button written among a form\'s fields at its label\'s width', function () {
|
||||
// M3: keep a button's width "dynamic to fit label"; don't "stretch buttons into long flat shapes".
|
||||
expect(ComponentStylesheet::read('form')->declarations('[data-md-form] > [data-md-button]'))
|
||||
->toBe(['justify-self' => 'start']);
|
||||
});
|
||||
|
||||
@@ -41,3 +41,10 @@ it('draws the stack from a stylesheet in the layout layer, imported by all.css',
|
||||
->toContain(":where([data-md-stack]) {\n --md-gap: 0px;")
|
||||
->and((string) file_get_contents(__DIR__.'/../../../resources/css/all.css'))->toContain("@import './layout/stack.css';");
|
||||
});
|
||||
|
||||
it('keeps a button at its label\'s width while the stack stretches the rest', function () {
|
||||
// M3: keep a button's width "dynamic to fit label"; don't "stretch buttons into long flat shapes".
|
||||
$css = (string) file_get_contents(__DIR__.'/../../../resources/css/layout/stack.css');
|
||||
|
||||
expect($css)->toContain("[data-md-stack]:is(:not([data-md-align]), [data-md-align='stretch']) > [data-md-button] {\n align-self: start;\n }");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user