Skip to content

Fix Shadow DOM input binding by using composedPath()#65609

Open
ilonatommy wants to merge 2 commits intodotnet:mainfrom
ilonatommy:fix/shadow-dom-input-binding-60885
Open

Fix Shadow DOM input binding by using composedPath()#65609
ilonatommy wants to merge 2 commits intodotnet:mainfrom
ilonatommy:fix/shadow-dom-input-binding-60885

Conversation

@ilonatommy
Copy link
Member

@ilonatommy ilonatommy commented Mar 2, 2026

🤖 AI Summary

📋 Pre-Flight

Issue: #60885 - [Blazor] InputText binding is not supported well with Shadow DOM elements
Area: area-blazor (src/Components/)
PR: None — will create

Key Findings

  • When using Shadow DOM web components with Blazor's InputText binding (especially with oninput), event.target returns the shadow host element instead of the actual <input> element inside the shadow DOM
  • This causes EventFieldInfo.fromEvent() to fail to identify the input element, returning null, which breaks the binding update timing
  • The parseChangeEvent() function in EventTypes.ts has the same issue — uses event.target which returns the shadow host
  • The fix is to use event.composedPath()[0] when event.composed is true, which returns the actual element that originated the event inside the shadow DOM
  • Reporter @Poppyto has a working branch with the fix: commit b3a2b64

Team Guidance

  • @javiercn (MEMBER): Accepted the approach — if the change detects we are inside a shadow DOM and uses composedPath instead of event.target, that's fine
  • Requested an E2E test to validate the change

Test Command

dotnet build src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj --no-restore -v:q
dotnet test src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj --no-build --filter "FullyQualifiedName~ShadowDom"

Fix Candidates

# Source Approach Files Changed Notes
1 @Poppyto (commit b3a2b64) Use event.composed ? event.composedPath().find(_ => true) : event.target EventFieldInfo.ts, EventTypes.ts Team-approved approach

🧪 Test

Test Result: ✅ TESTS CREATED

Test Command: dotnet test src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj --no-build --filter "FullyQualifiedName~InputEvent_WorksWithShadowDomInput"

Tests Created:

  • src/Components/test/E2ETest/Tests/EventTest.csInputEvent_WorksWithShadowDomInput
  • src/Components/test/testassets/BasicTestApp/ShadowDomInputComponent.razor — Test component with shadow DOM input binding
  • src/Components/test/testassets/BasicTestApp/wwwroot/js/shadowInputElement.js — Web component defining <shadow-input> with shadow DOM

Tests Written

  • InputEvent_WorksWithShadowDomInput — Verifies that typing in an <input> inside a shadow DOM correctly updates the Blazor bound value via oninput event

Conclusion

The test creates a <shadow-input> web component with a shadow DOM containing an <input>. It types characters into the shadow input and verifies the bound value updates correctly. Without the fix, EventFieldInfo.fromEvent returns null for shadow DOM events because event.target returns the shadow host (not HTMLInputElement), breaking the binding update mechanism.


🚦 Gate

Gate Result: ✅ PASSED

Test Command: dotnet build src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj --no-restore -v:q

Build Verification

  • E2E test project builds successfully with 0 warnings, 0 errors
  • ShadowDomInputComponent.razor compiles correctly
  • shadowInputElement.js included in wwwroot

New Tests vs Buggy Code

  • InputEvent_WorksWithShadowDomInput: Expected to FAIL without fix (event.target returns shadow host, not HTMLInputElement)

Regression Check

  • Build succeeded with no new errors introduced
  • No changes to existing test files except adding new test method to EventTest.cs

Conclusion

Tests are properly written, build succeeds. The test will fail without the fix because EventFieldInfo.fromEvent uses event.target which returns the shadow host element, not the inner <input>. With the fix using composedPath()[0], the correct element will be returned.


🔧 Fix

Fix Exploration Summary

Total Attempts: 5
Passing Candidates: 5
Selected Fix: Attempt 1 — Inline ternary using event.composedPath()[0]

Attempt Results

# Model Approach Result Key Insight
1 claude-sonnet-4.6 Inline ternary event.composed ? event.composedPath()[0] : event.target Simplest, matches reporter's approved approach
2 claude-opus-4.6 Shared EventUtilities.ts with getEventTarget() helper DRY but adds unnecessary new file
3 gpt-5.2 composedPath() scanning for first form element Defensive but over-engineered
4 gpt-5.3-codex shadowRoot.activeElement / query inspection Fragile — depends on focus state
5 gemini-3-pro-preview Event Proxy in EventDelegator.ts Elegant but too invasive for a minimal fix

Cross-Pollination

Model Round New Ideas? Details
claude-sonnet-4.6 2 Yes Direct listener registration inside shadow DOM — avoid retargeting entirely
gpt-5.2 2 Yes composedPath-driven handler/field resolution using Blazor metadata

Exhausted: Yes (cross-pollination ideas were theoretical/complex — not worth implementing)

Comparison

Criterion Fix 1 (Inline) Fix 2 (Utility) Fix 3 (Scanning) Fix 4 (shadowRoot) Fix 5 (Proxy)
Correctness ⚠️
Simplicity ✅ 2 lines ➖ new file ➖ complex ➖ complex ❌ invasive
Team guidance ✅ matches
Backward compat ⚠️ ⚠️

Recommendation

Fix 1 is the best choice because:

  1. Minimal change — only 2 lines modified in 2 files
  2. Matches team-approved approach@javiercn explicitly approved using composedPath for shadow DOM detection
  3. Matches reporter's fix@Poppyto's branch (commit b3a2b64) uses the same approach
  4. Standard API usagecomposedPath()[0] is the standard way to get the original event target
  5. No regression riskevent.composed is false for non-shadow DOM events, so existing behavior is unchanged

kubaflo and others added 2 commits March 2, 2026 13:00
Introduce a set of GitHub "skills" and test helpers for an end-to-end fix workflow. Adds ai-summary-comment skill (SKILL.md) and a post-ai-summary-comment.sh script that builds/upserts a single unified AI Summary comment with nested <details> sections, dry-run support, and automatic state loading from CustomAgentLogsTmp/PRState. Adds a comprehensive fix-issue skill (SKILL.md) documenting a 4-phase workflow (pre-flight, test, gate, mandatory multi-model fix) plus try-fix, verify-tests, and write-tests skill definitions. Includes test scripts under fix-issue/tests to validate comment structure, skill definitions, and dry-run previews. These changes enable standardized outputs for automated agents and a consistent PR comment/reporting mechanism for multi-model exploration and fixes.
When an input event originates from inside a Shadow DOM, event.target
returns the shadow host element instead of the actual <input> element.
This caused EventFieldInfo.fromEvent() and parseChangeEvent() to fail
to identify the element as a form field, breaking two-way data binding.

Use event.composedPath()[0] when event.composed is true to get the
original target element that initiated the event inside the shadow DOM.

Fixes dotnet#60885

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 2, 2026 13:19
@ilonatommy ilonatommy requested review from a team and wtgodbe as code owners March 2, 2026 13:19
@github-actions github-actions bot added the area-blazor Includes: Blazor, Razor Components label Mar 2, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates Blazor’s client-side event processing to correctly identify the originating form field for events raised from inside a Shadow DOM (via composedPath()), and adds a new E2E regression test using a custom element with an inner <input> in an open shadow root.

Changes:

  • Add a BasicTestApp custom element (<shadow-input>) whose value is backed by an <input> inside Shadow DOM, plus a test component that binds to it.
  • Add an E2E test verifying @bind with oninput works when the real input is inside Shadow DOM.
  • Update Web.JS event parsing to use event.composedPath()[0] (when applicable) instead of event.target for field/value extraction.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/Components/test/testassets/BasicTestApp/wwwroot/js/shadowInputElement.js Adds a Shadow DOM custom element used by the new regression test.
src/Components/test/testassets/BasicTestApp/wwwroot/index.html Loads the new custom element script in the BasicTestApp harness.
src/Components/test/testassets/BasicTestApp/ShadowDomInputComponent.razor New test component binding to the shadow-hosted input via oninput.
src/Components/test/E2ETest/Tests/EventTest.cs Adds an E2E test covering Shadow DOM input binding behavior.
src/Components/Web.JS/src/Rendering/Events/EventTypes.ts Uses composedPath() for input/change event value parsing to avoid Shadow DOM retargeting issues.
src/Components/Web.JS/src/Rendering/Events/EventFieldInfo.ts Uses composedPath() for reverse-mapping event field info when Shadow DOM retargeting occurs.
.github/skills/write-tests/SKILL.md Adds a new “write-tests” skill definition (workflow/docs).
.github/skills/verify-tests/SKILL.md Adds a new “verify-tests” skill definition (workflow/docs).
.github/skills/try-fix/SKILL.md Adds a new “try-fix” skill definition (workflow/docs).
.github/skills/fix-issue/tests/test-skill-definition.sh Adds bash tests for the new fix-issue skill definition.
.github/skills/fix-issue/tests/test-ai-summary-comment.sh Adds bash tests for AI summary comment scripting.
.github/skills/fix-issue/SKILL.md Adds a comprehensive “fix-issue” skill definition.
.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.sh Adds a script to set PR body content from phase output files.
.github/skills/ai-summary-comment/SKILL.md Adds documentation for the ai-summary-comment skill.

Comment on lines +238 to +240
4. Save approach to CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix/attempt-{N}/approach.md
5. Save result to CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix/attempt-{N}/result.txt
6. Save diff: git diff > CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix/attempt-{N}/fix.diff
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

The fix-issue instructions write try-fix outputs under CustomAgentLogsTmp/PRState/{PRNumber}/..., but the ai-summary-comment tooling (and docs) load from CustomAgentLogsTmp/PRState/ISSUE-{IssueNumber}/.... This inconsistency will cause the summary scripts to miss phase outputs depending on which path the agent follows. Align the fix-issue workflow examples to the same directory key (ISSUE-... or PR-...) and update scripts/docs accordingly.

Suggested change
4. Save approach to CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix/attempt-{N}/approach.md
5. Save result to CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix/attempt-{N}/result.txt
6. Save diff: git diff > CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix/attempt-{N}/fix.diff
4. Save approach to CustomAgentLogsTmp/PRState/ISSUE-{IssueNumber}/PRAgent/try-fix/attempt-{N}/approach.md
5. Save result to CustomAgentLogsTmp/PRState/ISSUE-{IssueNumber}/PRAgent/try-fix/attempt-{N}/result.txt
6. Save diff: git diff > CustomAgentLogsTmp/PRState/ISSUE-{IssueNumber}/PRAgent/try-fix/attempt-{N}/fix.diff

Copilot uses AI. Check for mistakes.
### Output File

```bash
mkdir -p CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

The output path example here uses CustomAgentLogsTmp/PRState/{PRNumber}/..., but the ai-summary-comment scripts/documentation read from CustomAgentLogsTmp/PRState/ISSUE-{IssueNumber}/.... Using different directory keys across phases will break the final PR body generation unless everything is kept in sync.

Suggested change
mkdir -p CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix
mkdir -p CustomAgentLogsTmp/PRState/ISSUE-{IssueNumber}/PRAgent/try-fix

Copilot uses AI. Check for mistakes.
### Output File

```bash
mkdir -p CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/finalize
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

This finalize output path uses CustomAgentLogsTmp/PRState/{PRNumber}/..., while the ai-summary-comment scripts load phase outputs from CustomAgentLogsTmp/PRState/ISSUE-{IssueNumber}/.... Pick one convention (ISSUE-... seems to match the scripts) and make the fix-issue instructions consistent throughout.

Suggested change
mkdir -p CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/finalize
mkdir -p CustomAgentLogsTmp/PRState/ISSUE-{IssueNumber}/PRAgent/finalize

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +8
---
name: fix-issue
description: End-to-end issue fixer for dotnet/aspnetcore. Runs 5 phases — Pre-Flight, Test, Gate, Fix, Finalize — creating tests and always generating phase output files. Use when asked to "fix issue #XXXXX", "fix #XXXXX", or "work on issue #XXXXX".
---

# Fix Issue — Streamlined 5-Phase Workflow

End-to-end agent that takes a GitHub issue from investigation through to a completed PR.
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

This PR is titled as a Shadow DOM input binding fix, but it also introduces a large set of new .github/skills/* definitions and scripts (fix-issue, write-tests, etc.). Consider splitting the workflow/skills additions into a separate PR to keep scope focused and make review/rollback safer.

Copilot uses AI. Check for mistakes.
Comment on lines +450 to +451
var shadowRoot = (ShadowRoot)((IJavaScriptExecutor)Browser)
.ExecuteScript("return arguments[0].shadowRoot", shadowHost);
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

Other E2E tests in this repo use Selenium’s GetShadowRoot() API to access open shadow roots. Using ExecuteScript("return arguments[0].shadowRoot") + cast is less consistent and may be more brittle across drivers; consider switching to shadowHost.GetShadowRoot() for consistency with the rest of the suite.

Suggested change
var shadowRoot = (ShadowRoot)((IJavaScriptExecutor)Browser)
.ExecuteScript("return arguments[0].shadowRoot", shadowHost);
var shadowRoot = shadowHost.GetShadowRoot();

Copilot uses AI. Check for mistakes.
Comment on lines +153 to +154
OUTPUT="$(DRY_RUN=1 bash "$TRY_FIX_SCRIPT" 99999 2>&1)" || true
EXIT_CODE=$?
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

This test captures the exit code incorrectly: OUTPUT="$(...)" || true makes $? always 0, so failures in the try-fix script won’t be detected. Capture the exit code before forcing success (e.g., temporarily disable set -e around the command, or avoid || true).

Suggested change
OUTPUT="$(DRY_RUN=1 bash "$TRY_FIX_SCRIPT" 99999 2>&1)" || true
EXIT_CODE=$?
set +e
OUTPUT="$(DRY_RUN=1 bash "$TRY_FIX_SCRIPT" 99999 2>&1)"
EXIT_CODE=$?
set -e

Copilot uses AI. Check for mistakes.
@Poppyto
Copy link

Poppyto commented Mar 2, 2026

Thank you @ilonatommy for handling the tests with Copilot I couldn’t write them one year ago and gave up, it seems easier to do now with IA so thank for your contribution 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Includes: Blazor, Razor Components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants