Widgets: raise the frame's chrome buttons to the 24px target-size floor - #791
Conversation
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
There was a problem hiding this comment.
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.
| * 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. |
There was a problem hiding this comment.
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.
| 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 ); | ||
| } ); |
There was a problem hiding this comment.
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.
| 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 ); | ||
| } ); |
There was a problem hiding this comment.
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.
Fixes #790.
The change
Three rules in
assets/css/desktop.csssized the widget frame's own controls below the WCAG 2.2 SC 2.5.8 floor of 24×24 CSS pixels:.os-widgets__card-redock.os-widgets__chrome .os-widgets__card-close.os-widgets__card-close(corner)All three carry
padding: 0andbox-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 underalign-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
::afterexpanding the hit area while the visual stays small — is the wrong tool here..os-widgets__chromeis 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 already10 + 8 + 10 = 28pxapart, 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__chromeis 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.tsreadsdesktop.cssand asserts the floor per selector, following the shape ofwidget-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.