Make :enabled the complement of :disabled, fieldsets included - #190
Open
jdalton wants to merge 1 commit into
Open
Make :enabled the complement of :disabled, fieldsets included#190jdalton wants to merge 1 commit into
jdalton wants to merge 1 commit into
Conversation
jdalton
force-pushed
the
fix/disabled-complement
branch
from
September 4, 2026 18:00
e64c1a8 to
a8ec97d
Compare
':disabled' walks the ancestry for a disabled fieldset, as the spec requires, while ':enabled' reads only the element's own disabled property. An input inside a disabled fieldset therefore matches both, and browsers match neither pseudo-class twice: Blink runs them off one predicate, where MatchesEnabledPseudoClass() is !IsDisabledFormControl(). https://github.com/chromium/chromium/blob/155.0.8041.1/third_party/blink/renderer/core/html/forms/html_form_control_element.cc#L337 The ancestry rule moves into one isDisabled() helper that both ask, which also lets the rule follow the spec rather than approximate it. A disabled fieldset disables its descendants unless they sit in that fieldset's first legend child; a legend excuses only the fieldset it belongs to, so the walk carries on outward past it; and an option is disabled by the optgroup it is a child of, whose own disabled property reflects only that optgroup's attribute. Blink walks it the same way, keeping a legend ancestor and comparing it against that fieldset's own legend before continuing. https://github.com/chromium/chromium/blob/155.0.8041.1/third_party/blink/renderer/core/html/forms/listed_element.cc#L702 ':read-only' and ':read-write' read the same own-property, so a control inside a disabled fieldset came out read-write there too; they ask the helper now. References: - Spec: https://html.spec.whatwg.org/#enabling-and-disabling-form-controls:-the-disabled-attribute — the fieldset, legend and optgroup rules - Spec: https://drafts.csswg.org/selectors-4/#enableddisabled — ':enabled' and ':disabled' as complements - Chromium: https://github.com/chromium/chromium/blob/155.0.8041.1/third_party/blink/renderer/core/html/forms/listed_element.cc#L702 — the ancestry walk, including why it continues past a legend - Chromium: https://github.com/chromium/chromium/blob/155.0.8041.1/third_party/blink/renderer/core/html/forms/listed_element.cc#L738 — IsActuallyDisabled(): the own attribute or the ancestor state - Chromium: https://github.com/chromium/chromium/blob/155.0.8041.1/third_party/blink/renderer/core/html/forms/html_form_control_element.cc#L337 — the two pseudo-classes as one predicate - MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/:disabled
jdalton
force-pushed
the
fix/disabled-complement
branch
from
September 5, 2026 02:48
a8ec97d to
cfa2a0e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
:disabledwalks up the tree looking for a disabled<fieldset>, the way the HTML spec asks it to, but:enabledonly reads the element's owndisabledproperty. An<input>inside a disabled fieldset therefore matches both pseudo-classes at once, which no browser does.This moves the rule into one
isDisabled()helper that both pseudo-classes ask, so they cannot disagree, and fixes two more cases the old walk got wrong along the way.What the two pseudo-classes answer before and after, on one document
Before this change,
fi1,fsinandfi3all match:enabledand:disabledtogether. After it, nothing matches both, and each element lands where Chromium puts it:li1is enabled because it sits in the fieldset's first legend,fi1andfi3are disabled by the fieldset above them, andop1is disabled by its optgroup.Why the rule is shaped this way, and where browsers implement it
Three parts of the spec rule are easy to lose, and the previous code lost two of them.
A
<legend>excuses only the fieldset it belongs to, so the walk has to carry on outward rather than stop at the first disabled fieldset it finds an excuse for. Blink keeps the legend ancestor it last saw and compares it against that fieldset's own first legend before continuing, inlisted_element.cc#L702.An
<option>is disabled by the<optgroup>it is a child of, whose owndisabledproperty reflects only that optgroup's attribute, so the option has to read its parent.And the two pseudo-classes are one predicate seen from both sides. Blink's
MatchesEnabledPseudoClass()is exactly!IsDisabledFormControl(), inhtml_form_control_element.cc#L337, where the disabled side isIsActuallyDisabled()— the element's own attribute or the ancestor state — inlisted_element.cc#L738.The spec text is enabling and disabling form controls.
The two read-* pseudo-classes were reading the same property
:read-onlyand:read-writealso asked for the element's owndisabled, so a control inside a disabled fieldset came out read-write. They ask the new helper now, which is why the diff touches them.On the document above, Chromium answers
fieldset :read-writewithli1and nothing else from inside the disabled fieldset. Before this change the engine also returnedfi1andfi3.References: the spec, the browser source, and what each part was reasoned from
This patch applies to master on its own. The sixteen in this series were checked by cherry-picking them onto master one after another, in this order and in reverse, and all sixteen land without a conflict.
:enabledand:disabledas complements.listed_element.cc#L702— the ancestry walk, including why it continues past a legend.listed_element.cc#L738—IsActuallyDisabled(): the own attribute or the ancestor state.html_form_control_element.cc#L337— the two pseudo-classes as one predicate.:disabled.