fix(web): a control inherits disabled from a fieldset and an aria-disabled ancestor
Merged
OK
1mo ago+226+226+012345678901234567890123456789-8-8-0123456789
- Repro:
<fieldset disabled><button>Pay</button></fieldset>.button.disabledisfalse,button.matches(':disabled')istrue. The reader read the property, sotoBeEnabledpassed,toBeDisabledfailed,isDisabled()answeredfalse, and the agent's tree listed a dead button as a live one. Playwright'stoBeDisabledpasses on the same button. - Same for a widget under
<div aria-disabled="true">: Playwright inherits the state down the tree, e2e read the element's own attribute only. disabledis now the effective state (packages/web/src/in-page/read-semantics.ts,isDisabled):
flowchart LR
A[el.matches ':disabled'] -- true --> D[disabled]
A -- false --> B[own aria-disabled=true]
B -- true --> D
B -- false --> C{role takes aria-disabled?}
C -- no --> E[enabled]
C -- yes --> W[nearest aria-disabled up the chain, across shadow hosts]
W -- true --> D
W -- false or none --> E
:disabledis the browser's own answer for a form control: own attribute, a disabled fieldset with the first-legend exception, nested fieldsets, a disabled optgroup. No hand-written fieldset walk.- The aria chain uses Playwright's role list (
kAriaDisabledRoles) and stop rule (aria-disabled="false"cuts it), memoized per read so a subtree under one disabled ancestor costs one walk. - Own
aria-disabled="true"still counts on any element, as before. Playwright limits that to the role list too; kept the wider reading so no existingtoBeDisabledon adivflips. packages/web/tests/integration/read-node-states.test.tschecks fieldset, first legend, second legend, nested fieldsets, aria ancestor,falsecut, shadow root, and compares each case with Playwright'sisDisabled()on the same page. The runner's kitchen-sink test gets a fenced button and a legend button soisDisabled(),toBeDisabled, andtoBeEnabledagree through the CLI.docs/reference/expect.mdxsays whatdisabledmeans on the web.