diff --git a/resources/css/components/selection.css b/resources/css/components/selection.css index ade836cf..63306638 100644 --- a/resources/css/components/selection.css +++ b/resources/css/components/selection.css @@ -19,6 +19,10 @@ * Every state layer is on-surface when unselected and primary when selected, 8% on hover (only * where a pointer can hover) and 10% on focus and press; the focus indicator is the package's * secondary ring. A disabled control is on-surface at 38% and has no state layer. + * + * The state layer is a ::before with no pointer events, so it is not a target: a labelled control + * is pressed anywhere along its row, and a control with no label wears the `touch-target` utility + * (resources/css/tokens/state.css) so its box catches presses over M3's 48px minimum. */ @layer components { diff --git a/resources/views/components/checkbox.blade.php b/resources/views/components/checkbox.blade.php index 6b09cfe7..24a526de 100644 --- a/resources/views/components/checkbox.blade.php +++ b/resources/views/components/checkbox.blade.php @@ -6,7 +6,10 @@ partly ticked: HTML has no attribute for it, so the input is marked `data-indeterminate` and resources/js/field.js keeps the property in step, on load and after every morph. Errors are read from the bag under the `wire:model` name. Every other attribute reaches the `` - (resources/css/components/selection.css). --}} + (resources/css/components/selection.css). + + Without a label — a "select all" in a table header, a row's tick — the row is only the 18px box, + so the box carries `touch-target` and catches presses over M3's 48px minimum. --}} @props([ 'label' => null, @@ -30,7 +33,10 @@ 'items-center' => blank($hint), 'flex-row-reverse justify-between' => $right, ])> - filled($hint)])> + filled($hint), + 'touch-target' => blank($label) && blank($hint), + ])> except(['class', 'id', 'wire:key']) }} id="{{ $id }}" diff --git a/resources/views/components/radio.blade.php b/resources/views/components/radio.blade.php index 2162c963..f2acb11b 100644 --- a/resources/views/components/radio.blade.php +++ b/resources/views/components/radio.blade.php @@ -46,7 +46,10 @@ 'items-start' => filled($optionHintText), 'items-center' => blank($optionHintText), ])> - filled($optionHintText)])> + filled($optionHintText), + 'touch-target' => blank(data_get($option, $optionLabel)) && blank($optionHintText), + ])> except(['class', 'wire:key', 'name', 'id']) }} type="radio" diff --git a/resources/views/components/toggle.blade.php b/resources/views/components/toggle.blade.php index 93cad8b2..f49e9953 100644 --- a/resources/views/components/toggle.blade.php +++ b/resources/views/components/toggle.blade.php @@ -5,7 +5,10 @@ its name — a switch without `label` needs an `aria-label`. `right` puts it at the end of the row, where a setting's switch usually is. `icons` draws a check on the handle when on and a cross when off; `icons="selected"` only the check. Every other attribute reaches the `` - (resources/css/components/selection.css). --}} + (resources/css/components/selection.css). + + Without a label the row is only the 52×32 track, so the track carries `touch-target` and catches + presses over M3's 48px minimum. --}} @props([ 'label' => null, @@ -27,7 +30,7 @@ 'items-center' => blank($hint), 'flex-row-reverse justify-between' => $right, ])> - + blank($label) && blank($hint)])> except(['class', 'id', 'wire:key']) }} id="{{ $id }}" diff --git a/tests/Feature/Components/SelectionTest.php b/tests/Feature/Components/SelectionTest.php index 7e8b457e..4b0e1fbe 100644 --- a/tests/Feature/Components/SelectionTest.php +++ b/tests/Feature/Components/SelectionTest.php @@ -20,6 +20,17 @@ it('marks a checkbox that is partly ticked', function () { expect((string) $this->blade(''))->toContain('data-indeterminate'); }); +it('gives a selection control without a label a 48px target', function () { + expect((string) $this->blade(''))->toContain('touch-target') + ->and((string) $this->blade(''))->toContain('touch-target') + ->and((string) $this->blade(''))->toContain('touch-target'); + + // A labelled control is pressed anywhere along its row, so it needs no extra target. + expect((string) $this->blade(''))->not->toContain('touch-target') + ->and((string) $this->blade(''))->not->toContain('touch-target') + ->and((string) $this->blade(''))->not->toContain('touch-target'); +}); + it('puts a checkbox at the end of its row on request', function () { expect((string) $this->blade(''))->toContain('flex-row-reverse justify-between'); });