Square a button group and the choice drawn as one
Plan step 22, actions.md § Missing (Square-by-default button groups): M3 lists "Default shape | Round, square" as a button-group configuration and neither `<x-button-group>` nor `<x-group>` had a `shape` prop, so a group of square buttons still drew round outer corners. `shape="square"` passes the square corner scale to every child through `data-shape` and two new variables in groups.css: `--group-outer` is what the two ends of a group round to (half the height when round, the corner its inner edges take when square — M3's square table, 4/8/8/16/20dp), and `--group-square`/`--group-square-pressed` give a standard group's buttons the scale `<x-button shape="square">` draws. The selected rules keep `--group-full`, so a selected button in a square group rounds: M3 has a toggle inside a group swap shape on selection either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
88c46001fb
commit
5d0e9bc12f
@@ -280,7 +280,7 @@ Clusters: `<x-menu-separator />` draws M3's line, `<x-menu-group gap>` M3 Expres
|
||||
|
||||
### `<x-button-group>`
|
||||
|
||||
A row of `<x-button>`s: `<x-button-group label="View" size="md">…</x-button-group>`. `connected` sets them 2px apart with small inner corners (a selected toggle rounds fully). Pass the `size` of the buttons inside.
|
||||
A row of `<x-button>`s: `<x-button-group label="View" size="md">…</x-button-group>`. `connected` sets them 2px apart with small inner corners (a selected toggle rounds fully). Pass the `size` of the buttons inside. `shape="square"` is M3's square group and covers every button in it, so do not write `shape` on each one: a connected group's ends square to the corner its inner edges take, a standard group's buttons take the square corner scale, and a selected button still rounds — M3 has the toggle morph the other way.
|
||||
|
||||
### `<x-group>`
|
||||
|
||||
@@ -294,7 +294,7 @@ A choice between a few options as a connected button group of native radios (che
|
||||
]" hint="Recipients lose access after that" />
|
||||
```
|
||||
|
||||
Props: `label`, `hint`, `hint-class` (classes for the hint, as on the fields; a colour there paints it), `name` (required with `x-model`), `options`, `option-value` (`id`), `option-label` (`name`), `option-icon` (`icon`), `size`, `variant` (`tonal`, `filled`, `outlined`), `multiple`, `inline` (intrinsic width instead of sharing the row). A validation error for the bound property replaces the hint.
|
||||
Props: `label`, `hint`, `hint-class` (classes for the hint, as on the fields; a colour there paints it), `name` (required with `x-model`), `options`, `option-value` (`id`), `option-label` (`name`), `option-icon` (`icon`), `size`, `variant` (`tonal`, `filled`, `outlined`), `shape` (`round`, `square`), `multiple`, `inline` (intrinsic width instead of sharing the row). A validation error for the bound property replaces the hint.
|
||||
|
||||
### `<x-split-button>`
|
||||
|
||||
|
||||
@@ -13,6 +13,16 @@
|
||||
* full, the inner corners small, smaller still while pressed, and a selected segment fully round
|
||||
* (ConnectedButtonGroupSmallTokens and the M3 Expressive connected button group spec).
|
||||
*
|
||||
* Shape (`shape="square"`, M3's "Default shape | Round, square" configuration): `--group-outer`
|
||||
* is what the two ends of a group round to — half the height for a round group, and for a square
|
||||
* one the same corner its inner edges take, which is the square table M3 publishes for connected
|
||||
* groups (4/8/8/16/20dp by size). A square *standard* group has no inner edges, so it hands its
|
||||
* buttons the square corner scale `<x-button shape="square">` draws (`--group-square`, and
|
||||
* `--group-square-pressed` under the finger) instead of asking the caller to write it on each
|
||||
* one. Selection morphs the other way in both: M3 has a toggle inside a group "swap shape
|
||||
* square/round on selection", so a selected button in a square group turns round, which is why
|
||||
* the selected rules keep `--group-full` rather than `--group-outer`.
|
||||
*
|
||||
* Split button (`<x-split-button>`): the same idea for two halves, on its own corner scale —
|
||||
* SplitButton*Tokens gives the inner corner 4/4/4/8/12px at rest and *grows* it to 8/12/12/20/20px
|
||||
* under the finger, the opposite of a connected group, so `--split-inner*` is separate from
|
||||
@@ -34,13 +44,32 @@
|
||||
--group-inner: var(--md-sys-shape-corner-sm);
|
||||
--group-inner-pressed: var(--md-sys-shape-corner-xs);
|
||||
--group-full: 1.25rem;
|
||||
--group-square: var(--md-sys-shape-corner-md);
|
||||
--group-square-pressed: var(--md-sys-shape-corner-sm);
|
||||
--group-outer: var(--group-full);
|
||||
}
|
||||
|
||||
[data-button-group][data-size='xs'] { --group-pad: 1rem; --group-grow: 4px; --group-inner: var(--md-sys-shape-corner-xs); --group-inner-pressed: 2px; --group-full: 1rem; }
|
||||
[data-button-group][data-size='sm'] { --group-pad: 1rem; --group-grow: 6px; --group-full: 1.25rem; }
|
||||
[data-button-group][data-size='md'] { --group-pad: 1.5rem; --group-grow: 8px; --group-full: 1.75rem; }
|
||||
[data-button-group][data-size='lg'] { --group-pad: 3rem; --group-grow: 16px; --group-inner: var(--md-sys-shape-corner-lg); --group-inner-pressed: var(--md-sys-shape-corner-md); --group-full: 3rem; }
|
||||
[data-button-group][data-size='xl'] { --group-pad: 4rem; --group-grow: 20px; --group-inner: var(--md-sys-shape-corner-lg-increased); --group-inner-pressed: var(--md-sys-shape-corner-lg); --group-full: 4.25rem; }
|
||||
[data-button-group][data-size='md'] { --group-pad: 1.5rem; --group-grow: 8px; --group-full: 1.75rem; --group-square: var(--md-sys-shape-corner-lg); --group-square-pressed: var(--md-sys-shape-corner-md); }
|
||||
[data-button-group][data-size='lg'] { --group-pad: 3rem; --group-grow: 16px; --group-inner: var(--md-sys-shape-corner-lg); --group-inner-pressed: var(--md-sys-shape-corner-md); --group-full: 3rem; --group-square: var(--md-sys-shape-corner-xl); --group-square-pressed: var(--md-sys-shape-corner-lg); }
|
||||
[data-button-group][data-size='xl'] { --group-pad: 4rem; --group-grow: 20px; --group-inner: var(--md-sys-shape-corner-lg-increased); --group-inner-pressed: var(--md-sys-shape-corner-lg); --group-full: 4.25rem; --group-square: var(--md-sys-shape-corner-xl); --group-square-pressed: var(--md-sys-shape-corner-lg); }
|
||||
|
||||
/* A square group's ends take the corner its inner edges take — M3's square table for connected
|
||||
groups — and a square standard group hands its buttons the button's own square scale. */
|
||||
[data-button-group][data-shape='square'] { --group-outer: var(--group-inner); }
|
||||
|
||||
[data-button-group='standard'][data-shape='square'] > * {
|
||||
border-radius: var(--group-square);
|
||||
}
|
||||
|
||||
[data-button-group='standard'][data-shape='square'] > :active:not(:disabled, [aria-disabled='true']) {
|
||||
border-radius: var(--group-square-pressed);
|
||||
}
|
||||
|
||||
[data-button-group='standard'][data-shape='square'] > [aria-pressed='true'] {
|
||||
border-radius: var(--group-full);
|
||||
}
|
||||
|
||||
[data-button-group='standard'] > :not([data-icon-button]):active:not(:disabled, [aria-disabled='true']) {
|
||||
padding-inline: calc(var(--group-pad) + var(--group-grow));
|
||||
@@ -107,14 +136,14 @@
|
||||
|
||||
[data-button-group='connected'] > :first-child,
|
||||
[data-split='leading'] {
|
||||
border-start-start-radius: var(--group-full);
|
||||
border-end-start-radius: var(--group-full);
|
||||
border-start-start-radius: var(--group-outer);
|
||||
border-end-start-radius: var(--group-outer);
|
||||
}
|
||||
|
||||
[data-button-group='connected'] > :last-child,
|
||||
[data-split='trailing'] {
|
||||
border-start-end-radius: var(--group-full);
|
||||
border-end-end-radius: var(--group-full);
|
||||
border-start-end-radius: var(--group-outer);
|
||||
border-end-end-radius: var(--group-outer);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
the size of the buttons inside, so the spacing and corners match. For a choice bound to a
|
||||
property, `<x-group>` draws a connected group of radios.
|
||||
|
||||
`shape` is M3's "Default shape | Round, square" configuration, and covers every button in the
|
||||
group so it need not be written on each one: `square` squares a connected group's two ends to
|
||||
the corner its inner edges take (M3's square table, 4/8/8/16/20dp by size) and gives a
|
||||
standard group's buttons the square corner scale `<x-button shape="square">` draws. Selection
|
||||
still morphs the other way — M3 has a toggle inside a group "swap shape square/round on
|
||||
selection" — so a selected button in a square group rounds.
|
||||
|
||||
A group never wraps to a second line — M3's rule, and the press expansion only reaches a
|
||||
neighbour on the same line anyway. Where a row is too long for its window the answer is a
|
||||
smaller `size` or fewer buttons, or two groups stacked.
|
||||
@@ -22,10 +29,12 @@
|
||||
'connected' => false,
|
||||
'size' => 'sm',
|
||||
'label' => null,
|
||||
'shape' => 'round',
|
||||
])
|
||||
|
||||
@php
|
||||
$size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm';
|
||||
$shape = $shape === 'square' ? 'square' : 'round';
|
||||
@endphp
|
||||
|
||||
<div
|
||||
@@ -33,6 +42,7 @@
|
||||
@if ($label) aria-label="{{ $label }}" @endif
|
||||
data-button-group="{{ $connected ? 'connected' : 'standard' }}"
|
||||
data-size="{{ $size }}"
|
||||
data-shape="{{ $shape }}"
|
||||
{{ $attributes->class([
|
||||
'inline-flex items-center',
|
||||
'gap-0.5' => $connected,
|
||||
|
||||
@@ -15,9 +15,14 @@
|
||||
you never to reduce. Corners move on the spatial spring and colours on the effects one
|
||||
(`state-transition-fast`).
|
||||
|
||||
`shape` is M3's "Default shape | Round, square" configuration: `square` squares the two ends
|
||||
of the group to the corner its inner edges take (M3's square table, 4/8/8/16/20dp by size),
|
||||
and the chosen segment still rounds fully, so selection reads the same either way.
|
||||
|
||||
ReStride's props, kept: `label`, `hint`, `hint-class`, `name` (needed with `x-model`, which
|
||||
names no property), `options`, `option-value`, `option-label`; plus `option-icon`, `size`,
|
||||
`variant`, `multiple`, `inline`. A validation message for the bound property replaces the hint.
|
||||
`variant`, `shape`, `multiple`, `inline`. A validation message for the bound property replaces
|
||||
the hint.
|
||||
|
||||
`hint-class` adds classes to the hint, as on `<x-field>`: a colour there paints it
|
||||
(`hint-class="text-warning"` for a hint that warns). The hint's own colour then carries no
|
||||
@@ -35,6 +40,7 @@
|
||||
'optionIcon' => 'icon',
|
||||
'size' => 'sm',
|
||||
'variant' => 'tonal',
|
||||
'shape' => 'round',
|
||||
'multiple' => false,
|
||||
'inline' => false,
|
||||
])
|
||||
@@ -46,6 +52,9 @@
|
||||
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
|
||||
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
|
||||
$size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm';
|
||||
// M3's "Default shape | Round, square": a square group's ends take the corner its inner edges
|
||||
// take; the chosen segment still rounds fully, which is the cue selection has always carried.
|
||||
$shape = $shape === 'square' ? 'square' : 'round';
|
||||
// M3: an xs or sm connected segment keeps a 48px target and a 48px minimum width, whatever
|
||||
// its 32px/40px container measures. From md the segment is wider than that on its own.
|
||||
$small = in_array($size, ['xs', 'sm'], true);
|
||||
@@ -78,7 +87,7 @@
|
||||
<legend class="mb-2 type-label-lg text-on-surface-variant">{{ $label }}</legend>
|
||||
@endif
|
||||
|
||||
<div data-button-group="connected" data-size="{{ $size }}" @class(['flex gap-0.5', 'w-full' => ! $inline, 'w-fit' => $inline])>
|
||||
<div data-button-group="connected" data-size="{{ $size }}" data-shape="{{ $shape }}" @class(['flex gap-0.5', 'w-full' => ! $inline, 'w-fit' => $inline])>
|
||||
@foreach ($options as $option)
|
||||
<label @class([
|
||||
'state-layer relative flex cursor-pointer select-none items-center justify-center whitespace-nowrap',
|
||||
|
||||
@@ -76,6 +76,27 @@
|
||||
<x-button icon="format_underlined" aria-label="Underline" variant="tonal" :selected="false" />
|
||||
</x-button-group>
|
||||
BLADE,
|
||||
'Square groups (hold a button down)' => <<<'BLADE'
|
||||
<x-button-group label="Square standard" size="md" shape="square">
|
||||
<x-button label="Day" variant="tonal" size="md" />
|
||||
<x-button label="Week" variant="tonal" size="md" />
|
||||
<x-button label="Month" variant="tonal" size="md" />
|
||||
</x-button-group>
|
||||
|
||||
<x-button-group label="Square formatting" connected shape="square">
|
||||
<x-button icon="format_bold" aria-label="Bold" variant="tonal" :selected="true" />
|
||||
<x-button icon="format_italic" aria-label="Italic" variant="tonal" :selected="false" />
|
||||
<x-button icon="format_underlined" aria-label="Underline" variant="tonal" :selected="false" />
|
||||
</x-button-group>
|
||||
|
||||
<div x-data="{ density: 'cosy' }" class="w-full">
|
||||
<x-group label="Density" name="showcase-density" shape="square" x-model="density" inline :options="[
|
||||
['id' => 'compact', 'name' => 'Compact'],
|
||||
['id' => 'cosy', 'name' => 'Cosy'],
|
||||
['id' => 'roomy', 'name' => 'Roomy'],
|
||||
]" />
|
||||
</div>
|
||||
BLADE,
|
||||
'A choice as a connected group' => <<<'BLADE'
|
||||
<div x-data="{ theme: 'system', days: ['mon'] }" class="grid w-full gap-6 medium:grid-cols-2">
|
||||
<x-group label="Theme" name="showcase-theme" x-model="theme" :options="[
|
||||
|
||||
@@ -20,6 +20,22 @@ it('connects buttons 2px apart', function () {
|
||||
->toContain('gap-0.5');
|
||||
});
|
||||
|
||||
it('squares a group, and a choice drawn as one', function () {
|
||||
// M3 lists "Default shape | Round, square" as a button-group configuration; the shape covers
|
||||
// every button in the group, so it need not be written on each one.
|
||||
expect((string) $this->blade('<x-button-group shape="square"><x-button label="Day" /></x-button-group>'))
|
||||
->toContain('data-shape="square"')
|
||||
->and((string) $this->blade('<x-button-group connected shape="square"><x-button label="Day" /></x-button-group>'))
|
||||
->toContain('data-button-group="connected"')
|
||||
->toContain('data-shape="square"')
|
||||
->and((string) $this->blade('<x-button-group><x-button label="Day" /></x-button-group>'))
|
||||
->toContain('data-shape="round"')
|
||||
->and((string) $this->blade('<x-group shape="square" name="x" :options="[[\'id\' => \'a\', \'name\' => \'A\']]" />'))
|
||||
->toContain('data-shape="square"')
|
||||
->and((string) $this->blade('<x-group name="x" :options="[[\'id\' => \'a\', \'name\' => \'A\']]" />'))
|
||||
->toContain('data-shape="round"');
|
||||
});
|
||||
|
||||
it('never wraps a group onto a second line', function () {
|
||||
expect((string) $this->blade('<x-button-group><x-button label="Day" /></x-button-group>'))
|
||||
->not->toContain('flex-wrap');
|
||||
|
||||
Reference in New Issue
Block a user