From b1fc0c9cfa8fff7a7054521453b09db982fb2afa Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Sun, 13 Sep 2026 22:46:10 +0200 Subject: [PATCH] Keep a connected group's inner corners round Connected segments and split halves wrote their full corners as --md-sys-shape-corner-full, 9999px, beside 8px inner corners on the same element. A box whose radii add up to more than its side has every radius scaled by one factor, so the inner corners shrank to a hundredth of a pixel and drew square. Full is now half the size's height (--group-full), which no side can overflow. A browser test fails on any radius in a group or split button larger than half its height. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2 --- resources/css/components/groups.css | 27 +++++++++++++++++---------- tests/Browser/ActionsTest.php | 13 +++++++++++-- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/resources/css/components/groups.css b/resources/css/components/groups.css index 8256432b..ce05d2be 100644 --- a/resources/css/components/groups.css +++ b/resources/css/components/groups.css @@ -15,18 +15,25 @@ * * Split button (``): the same idea for two halves; the trailing half turns * round and its chevron turns over while its menu is open (SplitButton*Tokens). + * + * "Full" here is half the size's height (`--group-full`), never `--md-sys-shape-corner-full`'s + * 9999px. One element mixes full outer corners with small inner ones, and when a box's radii + * add up to more than its side, CSS scales every radius by the same factor: a 9999px corner + * beside an 8px one shrank the 8px one to a hundredth of a pixel, so the inner corners drew + * square. */ [data-button-group] { --group-inner: var(--md-sys-shape-corner-sm); --group-inner-pressed: var(--md-sys-shape-corner-xs); + --group-full: 1.25rem; } -[data-button-group][data-size='xs'] { --group-pad: 0.75rem; --group-grow: 4px; --group-inner: var(--md-sys-shape-corner-xs); --group-inner-pressed: 2px; } -[data-button-group][data-size='sm'] { --group-pad: 1rem; --group-grow: 6px; } -[data-button-group][data-size='md'] { --group-pad: 1.5rem; --group-grow: 8px; } -[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); } -[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); } +[data-button-group][data-size='xs'] { --group-pad: 0.75rem; --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='standard'] > :not([data-icon-button]):active:not(:disabled, [aria-disabled='true']) { padding-inline: calc(var(--group-pad) + var(--group-grow)); @@ -60,19 +67,19 @@ [data-button-group='connected'] > :is([aria-pressed='true'], :has(:checked)), [data-split='trailing'][aria-expanded='true'] { - --group-corner: var(--md-sys-shape-corner-full); + --group-corner: var(--group-full); } [data-button-group='connected'] > :first-child, [data-split='leading'] { - border-start-start-radius: var(--md-sys-shape-corner-full); - border-end-start-radius: var(--md-sys-shape-corner-full); + border-start-start-radius: var(--group-full); + border-end-start-radius: var(--group-full); } [data-button-group='connected'] > :last-child, [data-split='trailing'] { - border-start-end-radius: var(--md-sys-shape-corner-full); - border-end-end-radius: var(--md-sys-shape-corner-full); + border-start-end-radius: var(--group-full); + border-end-end-radius: var(--group-full); } [data-split='trailing'] svg { diff --git a/tests/Browser/ActionsTest.php b/tests/Browser/ActionsTest.php index f8c073c6..5936d5c0 100644 --- a/tests/Browser/ActionsTest.php +++ b/tests/Browser/ActionsTest.php @@ -218,7 +218,7 @@ it('moves a connected group\'s choice with the arrow keys', function () { $page->keys(':focus', 'ArrowLeft') ->assertScript($checked('dark')) - ->assertScript("getComputedStyle(document.querySelector('#buttons input[value=\"dark\"]').parentElement).borderTopLeftRadius === '9999px'"); + ->assertScript("(el => getComputedStyle(el).borderTopLeftRadius === (el.offsetHeight / 2) + 'px')(document.querySelector('#buttons input[value=\"dark\"]').parentElement)"); }); it('rounds a split button\'s trailing half while its menu is open', function () { @@ -227,7 +227,16 @@ it('rounds a split button\'s trailing half while its menu is open', function () showcase() ->click('[data-split="trailing"] >> nth=0') ->assertScript("{$trailing}.getAttribute('aria-expanded') === 'true'") - ->assertScript("getComputedStyle({$trailing}).borderTopLeftRadius === '9999px'"); + ->assertScript("getComputedStyle({$trailing}).borderTopLeftRadius === ({$trailing}.offsetHeight / 2) + 'px'"); +}); + +it('keeps a connected segment\'s small inner corners, which a 9999px outer corner would scale away', function () { + // When a box's radii add up to more than a side, CSS shrinks every radius by the same + // factor: a full corner written as 9999px drew the 8px inner corners square. Every radius + // in a connected group or a split button stays within half its height, so none is scaled. + showcase() + ->assertScript("[...document.querySelectorAll('[data-button-group=\"connected\"] > *, [data-split]')].every((el) => { const cs = getComputedStyle(el); const half = el.offsetHeight / 2 + 0.5; return el.offsetHeight === 0 || ['borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomLeftRadius', 'borderBottomRightRadius'].every((corner) => parseFloat(cs[corner]) <= half); })") + ->assertScript("(el => getComputedStyle(el).borderTopRightRadius === '8px')(document.querySelector('#buttons input[name=\"showcase-theme\"][value=\"light\"]').parentElement)"); }); it('turns the FAB into a close button while its menu is open', function () {