Skip to content

Widgets: raise the frame's chrome buttons to the 24px target-size floor - #791

Merged
AllTerrainDeveloper merged 3 commits into
WordPress:trunkfrom
juanlentino:fix/widget-frame-tap-target-floor
Sep 11, 2026
Merged

Widgets: raise the frame's chrome buttons to the 24px target-size floor#791
AllTerrainDeveloper merged 3 commits into
WordPress:trunkfrom
juanlentino:fix/widget-frame-tap-target-floor

Conversation

@juanlentino

@juanlentino juanlentino commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #790.

The change

Three rules in assets/css/desktop.css sized the widget frame's own controls below the WCAG 2.2 SC 2.5.8 floor of 24×24 CSS pixels:

rule was now
.os-widgets__card-redock 20×20 24×24
.os-widgets__chrome .os-widgets__card-close 20×20 24×24
.os-widgets__card-close (corner) 22×22 24×24

All three carry padding: 0 and box-sizing: border-box, so the declared size is the target size — there was no padding making up the difference. The SVG glyphs inside are untouched; only the target grows, so the chrome header gains 4px of height (its tallest flex child moves from 20 to 24 under align-items: center) and the title gives up 8px on a floating card, 4px when docked. No layout risk: the column is 320px wide, and even at the 160px floating minimum the title still has roughly 62px to ellipsize in.

Why raise the box rather than add an invisible hit area

The usual trick for this — a transparent ::after expanding the hit area while the visual stays small — is the wrong tool here. .os-widgets__chrome is the drag handle (cursor: grab, touch-action: none), and a pseudo-element overlay would sit between the pointer and the drag it is meant to start. Given the project's stated intent to protect the compact frame, I kept the change to the smallest thing that conforms; if you'd rather hold the visual size exactly, the hit-area approach is available, but it wants care around the drag.

The spacing exception, and why it does not cover this

SC 2.5.8 lets an undersized target pass when a 24px circle centred on it does not intersect the circle on an adjacent target. Redock and close are adjacent flex siblings inside .os-widgets__chrome (gap: 8px). At the old sizes their centres were already 10 + 8 + 10 = 28px apart, so the chrome pair did satisfy the spacing limb — an earlier version of this description said otherwise, and review corrected it.

What carries the change is the size limb alone: the corner close at 22px fails it outright with no exception available. And .os-widgets__chrome is itself a pointer target (the drag handle) enclosing both buttons, which is a good reason not to lean on the spacing exception there even where the arithmetic works. The gap test stays as a standing guard on that invariant, not as a pin on something this PR changed.

Tests

tests/vitest/widget-chrome-tap-target.test.ts reads desktop.css and asserts the floor per selector, following the shape of widget-card-token-contract.test.ts. It also guards the centre-separation arithmetic and carries a negative control that measures .os-widgets__grip's 10×16 box, so an extractor that stopped reading sizes correctly is caught by a small box reading small.

Verified locally under Node 26 after the trunk merge: the file passes, and the negative control goes red when the grip's width is mutated to 20.

How it was found

Measuring every interactive control in the phone layer on a running instance: 118 targets, 35 under 24×24. Five of those are correctly exempt under the inline rule, and twenty were these two classes — the largest single group, and the only one that repeats once per widget.

Open WordPress Playground Preview

juanlentino and others added 2 commits September 10, 2026 16:34
WCAG 2.2 SC 2.5.8 puts a 24x24 CSS-pixel floor under a pointer target.
Three rules sized the widget frame's own controls below it:

  .os-widgets__card-redock                     20x20
  .os-widgets__chrome .os-widgets__card-close  20x20
  .os-widgets__card-close (corner)             22x22

All three carry `padding: 0` and `box-sizing: border-box`, so the declared
size is the target size. None of the spec's exceptions apply: they are
standalone icon buttons, not inline in a sentence, not user-agent sized,
and no equivalent control exists elsewhere in the view.

The spacing exception did not cover them either. Redock and close are
adjacent flex siblings in `.os-widgets__chrome` (gap: 8px), so at the old
sizes a 24px circle on one intersected the circle on the other. At 24px
each the centres are 32px apart, which clears that limb too.

Raising the box rather than adding an invisible hit area is deliberate:
`.os-widgets__chrome` is the drag handle (`cursor: grab`,
`touch-action: none`), and a pseudo-element overlay would sit between the
pointer and the drag it is meant to start. The glyphs inside are
unchanged; only the target grows, so the header gains 2px of height.

Fixes WordPress#790

@epeicher epeicher left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution @juanlentino! 🙌

Locally the full vitest suite, typecheck and lint are clean too, and I reverted desktop.css to trunk and re-ran your new test to confirm it is failable. It is: four of the five assertions fail against the old stylesheet.

The change itself is right and nicely minimal. A few notes, none of them blockers. The two on the test file are left inline.

Stale comment right above a rule you touched. assets/css/desktop.css:1160 still describes the redock button as a "20×20 round tile". Worth fixing in the same PR so the comment and the declaration agree.

The numbers in the description drifted a little. The chrome header gains 4px rather than 2, since the tallest flex child moves from 20 to 24 under align-items: center. The title gives up 8px on a floating card (both buttons grew) and 4px when docked. No layout risk either way: the column is 320px wide, and even at the 160px floating minimum the title still has roughly 62px to ellipsize in. Just worth correcting so the next reader trusts the arithmetic.

Good news on your Node caveat. After the trunk merge, devEngines reads ^24.0.0 || ^26.0.0, so v26 should install and run the suite for you now.

Follow-up filed. Reviewing this made me notice that the corner close sits at opacity: 0 until hover or :focus-visible, so on touch the target you just grew is invisible. .os-widgets__add a few rules below already has the @media ( hover: none ) reveal for exactly that reason. I opened #799 for it, out of scope here.

Comment on lines +19 to +24
* The spacing exception does not save them either. Redock and close are
* adjacent flex siblings inside `.os-widgets__chrome` (`gap: 8px`), so at
* their old sizes a 24px circle centred on one intersected the circle on
* the other. At 24px each the centres sit 32px apart — clear of the 24px
* the exception requires — which is why raising the size fixes the
* spacing limb as well as the size limb.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One correction here: at the old sizes those two circles did not actually intersect. Redock at 20, gap: 8, close at 20 puts the centres 28px apart, and two 24px circles only need 24px of separation to stay clear. The chrome pair already satisfied the spacing limb.

The size limb carries this PR on its own anyway: the corner close at 22 fails it outright with no exception available. And .os-widgets__chrome is itself a pointer target (the drag handle) enclosing both buttons, which is a good reason not to lean on the spacing exception here even when the arithmetic works out.

The same sentence appears in the PR body and in #790, so worth correcting in all three places.

Comment on lines +65 to +78
test( 'the two chrome siblings stay clear of each other', () => {
// SC 2.5.8's spacing limb: 24px circles centred on adjacent targets
// must not intersect, i.e. centres at least 24px apart.
const chrome = /^\.os-widgets__chrome\s*\{([^}]*)\}/m.exec( DESKTOP );
expect( chrome, 'no .os-widgets__chrome rule' ).not.toBeNull();
const gap = /(?<!-)gap\s*:\s*([\d.]+)px/.exec( chrome![ 1 ] );
expect( gap, '.os-widgets__chrome declares no gap' ).not.toBeNull();

const redock = boxOf( '.os-widgets__card-redock' );
const close = boxOf( '.os-widgets__chrome .os-widgets__card-close' );
const centres = redock.width / 2 + Number( gap![ 1 ] ) + close.width / 2;

expect( centres ).toBeGreaterThanOrEqual( TARGET_MIN );
} );

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consequence of the comment above: this test passes on trunk too. When I reverted desktop.css, this was the one assertion of the five that stayed green, because 10 + 8 + 10 = 28 was already >= 24. So it is not pinning anything this PR changes.

No objection to keeping it as a standing guard on the gap, that is a real invariant. The comment just shouldn't imply the spacing limb was failing before.

Comment on lines +80 to +87
test( 'the reader can tell a short box from a tall one', () => {
// Negative control. Without it, a regex that stopped matching would
// report a clean sweep over nothing, and every pin above would pass
// while the stylesheet said 20px.
const short = /^\.os-widgets__grip\s*\{([^}]*)\}/m.exec( DESKTOP );
expect( short, 'no .os-widgets__grip rule to control against' ).not.toBeNull();
expect( boxOf( '.os-widgets__card-redock' ).width ).toBe( 24 );
} );

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one doesn't quite do what its name promises: it checks that .os-widgets__grip exists but never measures it, then re-asserts redock's width, which duplicates the test.each above with a tighter bound. A regex that stopped matching already fails loudly inside boxOf, via its two not.toBeNull() guards.

A control that earns the name would measure the short box:

expect( boxOf( '.os-widgets__grip' ) ).toEqual( { width: 10, height: 16 } );

That proves the extractor reports a small box as small, which is the thing the comment says you want.

Minor too: toBe( 24 ) contradicts the toBeGreaterThanOrEqual policy a few lines up. If someone later bumps redock to 28, this fails on a stylesheet that still conforms.

…the spacing limb failed

Review on WordPress#791: the grip control only checked the rule existed, then
re-asserted redock at exactly 24 — duplicating the floor test with a
tighter bound. It now measures the grip's 10x16 box, so a broken
extractor is caught by a small box reading small. Verified red by
mutating grip width to 20.

The header comment, the gap test, and the redock comment in desktop.css
no longer say the chrome pair's circles intersected at the old sizes:
centres were 28px apart, the spacing limb already held. The size limb
alone carries the change.

@AllTerrainDeveloper AllTerrainDeveloper left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@AllTerrainDeveloper
AllTerrainDeveloper merged commit d4f07b7 into WordPress:trunk Sep 11, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Widget-frame chrome buttons are 20×20 and 22×22, under the WCAG 2.2 24×24 target-size floor

3 participants