Give a connected segment M3's 48px target and width
An <x-group> segment is a plain label, 32px or 40px tall, with nothing reaching past it, and nothing gave a connected <x-button-group> its 48dp minimum width either — both numbers M3 names and tells you not to reduce. The segment now carries the shared `touch-target` utility and a 48px floor, and groups.css puts the floor on the buttons a caller nests in a connected group. <x-button> drops its hand-rolled ::after for the same utility. Plan step 11, actions.md ACT-05. 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
c1ca157341
commit
e4de067350
@@ -47,6 +47,18 @@
|
|||||||
padding-inline-start: calc(var(--group-pad) - var(--group-grow));
|
padding-inline-start: calc(var(--group-pad) - var(--group-grow));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A connected segment at the two smallest sizes is 32px or 40px tall, and an icon-only one is as
|
||||||
|
* narrow as it is tall. M3 names both numbers and tells you not to reduce them: "XS and S
|
||||||
|
* connected button groups have a 48dp target area and a 48dp minimum width". The target is the
|
||||||
|
* `touch-target` utility on the segment itself; the width is here, where a group can reach the
|
||||||
|
* `<x-button>`s a caller put inside it.
|
||||||
|
*/
|
||||||
|
[data-button-group='connected'][data-size='xs'] > *,
|
||||||
|
[data-button-group='connected'][data-size='sm'] > * {
|
||||||
|
min-inline-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Connected segments and split halves take their inner corner from `--group-corner`, so pressing
|
* Connected segments and split halves take their inner corner from `--group-corner`, so pressing
|
||||||
* or selecting changes one variable and the rounded outer corners stay put.
|
* or selecting changes one variable and the rounded outer corners stay put.
|
||||||
|
|||||||
@@ -172,8 +172,8 @@
|
|||||||
$outline => $variant === 'outlined',
|
$outline => $variant === 'outlined',
|
||||||
'hover:shadow-elevation-1' => in_array($variant, ['filled', 'tonal'], true),
|
'hover:shadow-elevation-1' => in_array($variant, ['filled', 'tonal'], true),
|
||||||
'shadow-elevation-1 hover:shadow-elevation-2' => $variant === 'elevated',
|
'shadow-elevation-1 hover:shadow-elevation-2' => $variant === 'elevated',
|
||||||
// Below 48px the touch target reaches past the button, as M3 requires.
|
// Below 48px the touch target reaches past the button, as M3 requires (tokens/state.css).
|
||||||
'after:absolute after:top-1/2 after:left-1/2 after:size-full after:min-h-12 after:min-w-12 after:-translate-x-1/2 after:-translate-y-1/2' => in_array($size, ['xs', 'sm'], true),
|
'touch-target' => in_array($size, ['xs', 'sm'], true),
|
||||||
'disabled:cursor-not-allowed disabled:shadow-none aria-disabled:pointer-events-none aria-disabled:shadow-none',
|
'disabled:cursor-not-allowed disabled:shadow-none aria-disabled:pointer-events-none aria-disabled:shadow-none',
|
||||||
'disabled:bg-on-surface/10 disabled:text-on-surface/38 aria-disabled:bg-on-surface/10 aria-disabled:text-on-surface/38' => $contained,
|
'disabled:bg-on-surface/10 disabled:text-on-surface/38 aria-disabled:bg-on-surface/10 aria-disabled:text-on-surface/38' => $contained,
|
||||||
'disabled:text-on-surface/38 aria-disabled:text-on-surface/38' => ! $contained,
|
'disabled:text-on-surface/38 aria-disabled:text-on-surface/38' => ! $contained,
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
bind as on any input, the arrow keys move the choice, and a screen reader announces a group.
|
bind as on any input, the arrow keys move the choice, and a screen reader announces a group.
|
||||||
The chosen segment rounds fully and takes the selected colour; `variant` is `tonal` (the
|
The chosen segment rounds fully and takes the selected colour; `variant` is `tonal` (the
|
||||||
default), `filled` or `outlined`, as for toggle buttons. The segments share the row unless
|
default), `filled` or `outlined`, as for toggle buttons. The segments share the row unless
|
||||||
`inline`. An option with `'disabled' => true` greys its own segment.
|
`inline`. An option with `'disabled' => true` greys its own segment. At `xs` and `sm` a
|
||||||
|
segment carries `touch-target` and a 48px minimum width, which M3 asks for by name and tells
|
||||||
|
you never to reduce.
|
||||||
|
|
||||||
ReStride's props, kept: `label`, `hint`, `hint-class`, `name` (needed with `x-model`, which
|
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`,
|
names no property), `options`, `option-value`, `option-label`; plus `option-icon`, `size`,
|
||||||
@@ -43,6 +45,9 @@
|
|||||||
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
|
$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)) : [];
|
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
|
||||||
$size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm';
|
$size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm';
|
||||||
|
// 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);
|
||||||
|
|
||||||
$segment = [
|
$segment = [
|
||||||
'xs' => 'h-8 gap-2 px-3 type-label-lg',
|
'xs' => 'h-8 gap-2 px-3 type-label-lg',
|
||||||
@@ -75,7 +80,8 @@
|
|||||||
<label @class([
|
<label @class([
|
||||||
'state-layer relative flex cursor-pointer select-none items-center justify-center whitespace-nowrap',
|
'state-layer relative flex cursor-pointer select-none items-center justify-center whitespace-nowrap',
|
||||||
'transition-[border-radius,background-color,color] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast',
|
'transition-[border-radius,background-color,color] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast',
|
||||||
'min-w-0 flex-1' => ! $inline,
|
'flex-1' => ! $inline,
|
||||||
|
$small ? 'touch-target min-w-12' : 'min-w-0',
|
||||||
$segment,
|
$segment,
|
||||||
$colours,
|
$colours,
|
||||||
'has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-secondary',
|
'has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-secondary',
|
||||||
|
|||||||
@@ -46,6 +46,16 @@ it('draws a choice as connected radios', function () {
|
|||||||
->and(substr_count($html, '<svg'))->toBe(1);
|
->and(substr_count($html, '<svg'))->toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps a 48px target and a 48px minimum width on the smallest connected segments', function () {
|
||||||
|
$options = [['id' => 'a', 'name' => 'A']];
|
||||||
|
|
||||||
|
expect((string) $this->blade('<x-group size="sm" name="x" :$options />', ['options' => $options]))
|
||||||
|
->toContain('touch-target min-w-12')
|
||||||
|
->and((string) $this->blade('<x-group size="md" name="x" :$options />', ['options' => $options]))
|
||||||
|
->not->toContain('touch-target')
|
||||||
|
->toContain('min-w-0');
|
||||||
|
});
|
||||||
|
|
||||||
it('draws several choices as checkboxes', function () {
|
it('draws several choices as checkboxes', function () {
|
||||||
expect((string) $this->blade('<x-group name="days" multiple variant="outlined" :options="[[\'id\' => \'mon\', \'name\' => \'Mon\']]" />'))
|
expect((string) $this->blade('<x-group name="days" multiple variant="outlined" :options="[[\'id\' => \'mon\', \'name\' => \'Mon\']]" />'))
|
||||||
->toContain('type="checkbox"')
|
->toContain('type="checkbox"')
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ it('rounds by default, squares on request, and squares off further while pressed
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('reaches a 48px touch target below the medium size', function () {
|
it('reaches a 48px touch target below the medium size', function () {
|
||||||
expect(buttonClasses('<x-button label="Go" size="sm" />'))->toContain('after:min-h-12')
|
expect(buttonClasses('<x-button label="Go" size="sm" />'))->toContain('touch-target')
|
||||||
->and(buttonClasses('<x-button label="Go" size="md" />'))->not->toContain('after:min-h-12');
|
->and(buttonClasses('<x-button label="Go" size="md" />'))->not->toContain('touch-target');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is an icon button named by its tooltip when it has no label', function () {
|
it('is an icon button named by its tooltip when it has no label', function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user