fix(js,cdp): implement label activation behaviour - #728
Conversation
Activating a label runs a synthetic click on its labeled control: the element referenced by for, else the first labelable descendant. Neither HTMLElement.click() nor the CDP click path did this, so a checkbox or radio built as a styled label could not be operated at all. Checkbox and radio pre-click activation runs with it, flipping state before the click event so listeners observe the new value, reverting on cancel, and firing input then change after an uncancelled toggle. A disabled control has no activation behaviour, including under a disabled fieldset outside its first legend child, and only elements that can actually be disabled are affected. A click on interactive content inside a label keeps that content's own behaviour. Forwarded events inherit the trustedness of the click that caused them. The CDP path calls the same helpers for association, disabled state, and interactive content instead of restating those rules. Fixes h4ckf0r0day#721.
|
Reviewed the three PRs together (#727/#728/#729) — the two-level guard composes correctly, matching what we traced through the expanded matrix on our build. Own-label bounce, implicit label (checkbox C inside label L, C's handler calls L.click()):
So the element-level WeakSet (#729) stops direct self-reentry, and the closure-private One residual difference on our side: we went ahead and implemented |
|
Thanks for tracing it. I merged all three locally and the composed behaviour is what you describe, but one step of the mechanism is not: At step 3
So both guards are needed, and the ordering point in #729 holds: the element flag has to sit above the pre-activation flip. The merge is not clean, incidentally: #728 and #729 both edit On |
|
You're right — my step-3 claim was wrong. On |
Fixes #721.
Activating a
<label>runs a synthetic click on its labeled control: the element referenced byfor, else the first labelable descendant. NeitherHTMLElement.click()nor the CDP click path did this, so a checkbox or radio built as a styled label could not be operated at all. That is the whole of Webflow's.w-checkboxmarkup and most styled-checkbox patterns.Checkbox and radio pre-click activation runs with it, since without it a click event dispatched but nothing ever toggled: state flips before the click event so listeners observe the new value, reverts if the event is cancelled, and
inputthenchangefire after an uncancelled toggle.The association and activation rules that go with it, each verified against headless Chrome: an empty
forassociates nothing rather than falling back to a descendant; a danglingforactivates nothing;forwins over a nested control; a disabled control has no activation behaviour, including under a disabled<fieldset>outside its first<legend>child; a click on interactive content inside a label keeps that content's own behaviour, whilemeter,outputandprogressare labelable but not interactive and still forward; a control clicking its own label cannot bounce back.The CDP path calls shared helpers for association, disabled state, and interactive content rather than restating those rules, so the two click paths cannot drift apart on them. Forwarded events inherit the trustedness of the click that caused them, so a physical label click produces a trusted control click as it does in a real browser.
Verification
16 spec edge cases, each through both
element.click()and CDPInput.dispatchMouseEvent, on two DOM shapes: 64/64 exact match with headless Chrome 145. Stock fails 11-12 of 16.Cases that were wrong in my own first attempt and are now covered by tests, all measured against Chrome:
disabledCheckbox.click()<div disabled>.click()<a>inside<fieldset disabled><a href>inside a label<output>inside a label<legend>wrapped in a<div>cargo nextest run --release --features render --no-fail-fast: 1497 run, 1494 passed. All three failures pass individually and also fail on unmodifiedmain; they are timing-sensitiveintersection_observerand budget tests, measured at 2-3/10 on both trees.observer-intersectionidentically onmain.input_mouse_label_activation) asserting event type, order, count andisTrustedfor explicit, implicit and deep-descendant labels, plus a disabled control.Notes for review
HTMLInputElement.indeterminateis not implemented in obscura ('indeterminate' in checkboxisfalse), so the pre-click step that clears and restores it is deliberately omitted rather than faked with an expando. It belongs with implementing the property.__obscura_activateLabelcan be called by page script to request a trusted forwarded click. The token itself is closure-private, but the helper supplies it on request, exactly as the existing__obscura_markTrusteddoes for any event. No new capability, but worth knowing.detailor modifiers.<label>next to an<input>has no box until getBoundingClientRect returns 0x0 for inline elements that share a run with a replaced sibling (label + input pattern) #722 lands, so coordinate clicks on that shape need this and getBoundingClientRect returns 0x0 for inline elements that share a run with a replaced sibling (label + input pattern) #722 together. Labels with their own box, which is the common styled case, work with this change alone.