Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/Components/Web.JS/src/Rendering/Events/EventTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,11 @@ registerBuiltInEventType(['wheel', 'mousewheel'], {
createEventArgs: e => parseWheelEvent(e as WheelEvent),
});

registerBuiltInEventType(['cancel', 'close', 'toggle'], createBlankEventArgsOptions);
registerBuiltInEventType(['cancel', 'close'], createBlankEventArgsOptions);

registerBuiltInEventType(['toggle'], {
createEventArgs: e => parseToggleEvent(e),
});

function parseChangeEvent(event: Event): ChangeEventArgs {
const element = event.target as Element;
Expand Down Expand Up @@ -190,6 +194,18 @@ function parseWheelEvent(event: WheelEvent): WheelEventArgs {
};
}

function parseToggleEvent(event: Event): ChangeEventArgs {
// For <details>, <dialog>, and popover elements, return a ChangeEventArgs
// compatible object with the open state as a boolean value. This enables @bind-open support.
// We use ToggleEvent.newState which works for all element types that fire toggle events.
const toggleEvent = event as ToggleEvent;
const isOpen = toggleEvent.newState === 'open';

return {
value: isOpen,
};
}

function parsePointerEvent(event: PointerEvent): PointerEventArgs {
return {
...parseMouseEvent(event),
Expand Down
4 changes: 4 additions & 0 deletions src/Components/Web/src/Web/BindAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ namespace Microsoft.AspNetCore.Components.Web;

[BindElement("select", null, "value", "onchange")]
[BindElement("textarea", null, "value", "onchange")]

// <details> element binds to the 'open' attribute using the 'ontoggle' event
[BindElement("details", null, "open", "ontoggle")]
[BindElement("details", "open", "open", "ontoggle")]
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The PR title mentions "Add @bind-open support for details and dialog elements", and the technical notes in the PR description explain that toggle events are fired by both <details> and <dialog> elements. However, only BindElement attributes for the details element are added here. To fully support the stated functionality, you should also add:

[BindElement("dialog", null, "open", "ontoggle")]
[BindElement("dialog", "open", "open", "ontoggle")]

Without these attributes, @bind-open will not work for <dialog> elements despite the infrastructure being in place.

Suggested change
[BindElement("details", "open", "open", "ontoggle")]
[BindElement("details", "open", "open", "ontoggle")]
// <dialog> element binds to the 'open' attribute using the 'ontoggle' event
[BindElement("dialog", null, "open", "ontoggle")]
[BindElement("dialog", "open", "open", "ontoggle")]

Copilot uses AI. Check for mistakes.
public static class BindAttributes
{
}
4 changes: 2 additions & 2 deletions src/Components/Web/src/Web/EventHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ namespace Microsoft.AspNetCore.Components.Web;
[EventHandler("onreadystatechange", typeof(EventArgs), true, true)]
[EventHandler("onscroll", typeof(EventArgs), true, true)]

// <details>
[EventHandler("ontoggle", typeof(EventArgs), true, true)]
// <details> and elements with popover attribute
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The comment should mention <dialog> elements in addition to <details> and popover elements, since dialog elements also fire toggle events. Suggested change:

// <details>, <dialog>, and elements with popover attribute
Suggested change
// <details> and elements with popover attribute
// <details>, <dialog>, and elements with popover attribute

Copilot uses AI. Check for mistakes.
[EventHandler("ontoggle", typeof(ChangeEventArgs), true, true)]

// <dialog>
[EventHandler("oncancel", typeof(EventArgs), false, true)]
Expand Down
6 changes: 5 additions & 1 deletion src/Components/Web/src/WebEventData/WebEventData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,14 @@ private static bool TryDeserializeStandardWebEventArgs(

case "cancel":
case "close":
case "toggle":
eventArgs = EventArgs.Empty;
return true;

case "toggle":
// Toggle events return ChangeEventArgs with value=boolean to support @bind-open
eventArgs = ChangeEventArgsReader.Read(eventArgsJson);
return true;

default:
// For custom event types, there are no built-in rules, so the deserialization type is
// determined by the parameter declared on the delegate.
Expand Down
50 changes: 50 additions & 0 deletions src/Components/test/E2ETest/Tests/BindTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,56 @@ public void CanBindCheckbox_InitiallyChecked()
Browser.Equal("True", () => boundValue.Text);
}

[Fact]
public void CanBindDetailsElement_InitiallyClosed()
{
var target = Browser.Exists(By.Id("details-initially-closed"));
var boundValue = Browser.Exists(By.Id("details-initially-closed-value"));
var toggleButton = Browser.Exists(By.Id("details-initially-closed-toggle"));
Assert.Null(target.GetDomAttribute("open"));
Assert.Equal("False", boundValue.Text);

// Click to expand; verify value is updated
target.FindElement(By.TagName("summary")).Click();
Browser.NotEqual(null, () => target.GetDomAttribute("open"));
Browser.Equal("True", () => boundValue.Text);

// Click to collapse; verify value is updated
target.FindElement(By.TagName("summary")).Click();
Browser.Equal(null, () => target.GetDomAttribute("open"));
Browser.Equal("False", () => boundValue.Text);

// Modify data programmatically; verify details is updated
toggleButton.Click();
Browser.NotEqual(null, () => target.GetDomAttribute("open"));
Browser.Equal("True", () => boundValue.Text);
}

[Fact]
public void CanBindDetailsElement_InitiallyOpen()
{
var target = Browser.Exists(By.Id("details-initially-open"));
var boundValue = Browser.Exists(By.Id("details-initially-open-value"));
var toggleButton = Browser.Exists(By.Id("details-initially-open-toggle"));
Assert.NotNull(target.GetDomAttribute("open"));
Assert.Equal("True", boundValue.Text);

// Click to collapse; verify value is updated
target.FindElement(By.TagName("summary")).Click();
Browser.Equal(null, () => target.GetDomAttribute("open"));
Browser.Equal("False", () => boundValue.Text);

// Click to expand; verify value is updated
target.FindElement(By.TagName("summary")).Click();
Browser.NotEqual(null, () => target.GetDomAttribute("open"));
Browser.Equal("True", () => boundValue.Text);

// Modify data programmatically; verify details is updated
toggleButton.Click();
Browser.Equal(null, () => target.GetDomAttribute("open"));
Browser.Equal("False", () => boundValue.Text);
}
Comment on lines +197 to +245
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The PR title mentions adding @bind-open support for both details and dialog elements. While tests are provided for the <details> element, there are no tests for <dialog> element binding. Consider adding E2E tests for <dialog> element binding to ensure it works correctly, since dialog elements also fire toggle events and should support @bind-open according to the PR description.

Copilot uses AI. Check for mistakes.

[Fact]
public void CanBindSelect()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,26 @@
<button id="checkbox-initially-checked-invert" @onclick="@(() => { checkboxInitiallyCheckedValue = !checkboxInitiallyCheckedValue; })">Invert</button>
</p>

<h2>Details element</h2>
<p>
Initially closed:
<details id="details-initially-closed" @bind-open="detailsInitiallyClosedValue">
<summary>Click to expand</summary>
<p>Details content</p>
</details>
<span id="details-initially-closed-value">@detailsInitiallyClosedValue</span>
<button id="details-initially-closed-toggle" @onclick="@(() => { detailsInitiallyClosedValue = !detailsInitiallyClosedValue; })">Toggle</button>
</p>
<p>
Initially open:
<details id="details-initially-open" @bind-open="detailsInitiallyOpenValue">
<summary>Click to collapse</summary>
<p>Details content</p>
</details>
<span id="details-initially-open-value">@detailsInitiallyOpenValue</span>
<button id="details-initially-open-toggle" @onclick="@(() => { detailsInitiallyOpenValue = !detailsInitiallyOpenValue; })">Toggle</button>
</p>

<h2>Select</h2>
<p>
<select id="select-box" @bind="selectValue">
Expand Down Expand Up @@ -483,6 +503,9 @@
bool checkboxInitiallyUncheckedValue = false;
bool checkboxInitiallyCheckedValue = true;

bool detailsInitiallyClosedValue = false;
bool detailsInitiallyOpenValue = true;

int textboxIntValue = -42;
int? textboxNullableIntValue = null;
long textboxLongValue = 3_000_000_000;
Expand Down
Loading