diff --git a/src/Butil/Bit.Butil.Demo/Client/Docs/DocsNav.cs b/src/Butil/Bit.Butil.Demo/Client/Docs/DocsNav.cs
index 6acdabdf519..54633470222 100644
--- a/src/Butil/Bit.Butil.Demo/Client/Docs/DocsNav.cs
+++ b/src/Butil/Bit.Butil.Demo/Client/Docs/DocsNav.cs
@@ -114,8 +114,8 @@ public static class DocsNav
]),
new("DOM & Interaction", "dom",
[
- new("Element", "element", "Attributes, scrolling, fullscreen, pointer capture and events on any ElementReference.", typeof(ElementPage), ApiSupport.Broad, ApiNeeds.None,
- ["ElementReferenceExtensions", "ElementReferenceEventExtensions", "ElementReferenceMediaExtensions"]),
+ new("Element", "element", "Attributes, ARIA, classes, content, scrolling, fullscreen, pointer capture and events on any ElementReference.", typeof(ElementPage), ApiSupport.Broad, ApiNeeds.None,
+ ["ElementReferenceExtensions", "ElementReferenceDomExtensions", "ElementReferenceStateExtensions", "ElementReferenceAriaExtensions", "ElementReferenceEventExtensions", "ElementReferenceMediaExtensions"]),
new("Animation", "animation", "Run and control Web Animations on any element, straight from C#.", typeof(AnimationPage), ApiSupport.Broad, ApiNeeds.None,
["ElementReferenceAnimationExtensions", "AnimationHandle"]),
new("PictureInPicture", "picture-in-picture", "Float a video in an always-on-top window outside the page.", typeof(PictureInPicturePage), ApiSupport.Broad, ApiNeeds.UserGesture),
diff --git a/src/Butil/Bit.Butil.Demo/Client/Pages/ElementPage.razor b/src/Butil/Bit.Butil.Demo/Client/Pages/ElementPage.razor
index 1714a1b3f26..11da0ae0180 100644
--- a/src/Butil/Bit.Butil.Demo/Client/Pages/ElementPage.razor
+++ b/src/Butil/Bit.Butil.Demo/Client/Pages/ElementPage.razor
@@ -334,6 +334,284 @@ await box.Remove(); // detaches the element from the DOM
+
+
+ I am a popover. Press Escape or click outside to dismiss me.
+
+
+
+
+
+
+
+
+
+
+
+
+
This paragraph is long enough to wrap over several line boxes, which is what makes GetClientRects report more than one rectangle for it - one per line the text occupies.
+
+
first
+
second
+
third
+
+
Scroll space.
+
+
+ Descendant selector
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Title
+
+
+
+ Lang
+
+
+
+ Autocapitalize
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@code {
@@ -468,6 +803,41 @@ await _video.Pause();
private DemoConsole selectorOutput = default!;
private DemoConsole mediaOutput = default!;
+ private DemoConsole activationOutput = default!;
+ private DemoConsole styleOutput = default!;
+ private DemoConsole contentInsertOutput = default!;
+ private DemoConsole ariaOutput = default!;
+ private DemoConsole popoverOutput = default!;
+ private DemoConsole queryOutput = default!;
+ private DemoConsole identityOutput = default!;
+
+ private ElementReference clickTargetRef;
+ private ElementReference focusInputRef;
+ private ElementReference styleBoxRef;
+ private ElementReference contentBoxRef;
+ private ElementReference ariaBoxRef;
+ private ElementReference popoverRef;
+ private ElementReference queryBoxRef;
+
+ private int syntheticClicks;
+ private bool ariaExpanded;
+ private bool isDraggable;
+
+ private string closestSelector = ".stack";
+ private string classToken = "highlighted";
+ private string dataKey = "userId";
+ private string dataValue = "42";
+ private string insertText = "inserted";
+ private string ariaLabel = "Close the dialog";
+ private string ariaRole = "button";
+ private string descendantSelector = "li";
+ private string titleValue = "Shown on hover";
+ private string langValue = "fa-IR";
+ private Autocapitalize autocapitalize = Autocapitalize.Words;
+
+ /// The namespace SVG's href attribute lives in - the classic reason to reach for the NS overloads.
+ private const string XlinkNamespace = "http://www.w3.org/1999/xlink";
+
private ElementReference mediaElement;
private MediaElementState? mediaState;
@@ -873,6 +1243,265 @@ await _video.Pause();
return "data:audio/wav;base64," + Convert.ToBase64String(data);
}
+ // ─── Activation, focus and visibility ─────────────────────────────────
+
+ private Task ClickTarget() => Run(activationOutput, async () =>
+ {
+ await clickTargetRef.Click();
+ await activationOutput.Success("Click() →", $"the button's own handler ran; count is now {syntheticClicks}");
+ });
+
+ private Task FocusInput() => Run(activationOutput, async () =>
+ {
+ await focusInputRef.Focus(new FocusOptions { PreventScroll = true });
+ await activationOutput.Success("Focus(preventScroll: true) →", "focused without scrolling the page");
+ });
+
+ private Task CheckVisibility() => Run(activationOutput, async () =>
+ {
+ var visible = await targetRef.CheckVisibility(new CheckVisibilityOptions
+ {
+ OpacityProperty = true,
+ VisibilityProperty = true,
+ ContentVisibilityAuto = true,
+ });
+ await activationOutput.Success("CheckVisibility() →", visible);
+ });
+
+ private Task Closest() => Run(activationOutput, async () =>
+ await activationOutput.Success($"Closest(\"{closestSelector}\") →", await targetRef.Closest(closestSelector)));
+
+ // ─── Classes, data attributes and inline style ────────────────────────
+
+ private Task AddClass() => Run(styleOutput, async () =>
+ {
+ await styleBoxRef.AddClass(classToken);
+ await styleOutput.Success($"AddClass(\"{classToken}\") →", string.Join(' ', await styleBoxRef.GetClassList()));
+ });
+
+ private Task RemoveClass() => Run(styleOutput, async () =>
+ {
+ await styleBoxRef.RemoveClass(classToken);
+ await styleOutput.Success($"RemoveClass(\"{classToken}\") →", string.Join(' ', await styleBoxRef.GetClassList()));
+ });
+
+ private Task ToggleClass() => Run(styleOutput, async () =>
+ await styleOutput.Success($"ToggleClass(\"{classToken}\") → on the element:", await styleBoxRef.ToggleClass(classToken)));
+
+ private Task ContainsClass() => Run(styleOutput, async () =>
+ await styleOutput.Success($"ContainsClass(\"{classToken}\") →", await styleBoxRef.ContainsClass(classToken)));
+
+ private Task GetClassList() => Run(styleOutput, async () =>
+ await styleOutput.Success("GetClassList() →", await styleBoxRef.GetClassList()));
+
+ private Task SetData() => Run(styleOutput, async () =>
+ {
+ await styleBoxRef.SetData(dataKey, dataValue);
+ await styleOutput.Success($"SetData(\"{dataKey}\", \"{dataValue}\") →", "written as a data-* attribute");
+ });
+
+ private Task GetData() => Run(styleOutput, async () =>
+ await styleOutput.Success($"GetData(\"{dataKey}\") →", await styleBoxRef.GetData(dataKey)));
+
+ private Task GetDataNames() => Run(styleOutput, async () =>
+ await styleOutput.Success("GetDataNames() →", await styleBoxRef.GetDataNames()));
+
+ private Task RemoveData() => Run(styleOutput, async () =>
+ {
+ await styleBoxRef.RemoveData(dataKey);
+ await styleOutput.Success($"RemoveData(\"{dataKey}\") →", "removed");
+ });
+
+ private Task SetStyleProperty() => Run(styleOutput, async () =>
+ {
+ await styleBoxRef.SetStyleProperty("--accent", "#7c3aed");
+ await styleBoxRef.SetStyleProperty("outline", "2px solid var(--accent)", important: true);
+ await styleOutput.Success("SetStyleProperty(...) →", await styleBoxRef.GetStyleProperty("outline"));
+ });
+
+ private Task GetStyleText() => Run(styleOutput, async () =>
+ await styleOutput.Success("GetStyleText() →", await styleBoxRef.GetStyleText()));
+
+ private Task RemoveStyleProperty() => Run(styleOutput, async () =>
+ await styleOutput.Success("RemoveStyleProperty(\"outline\") → was:", await styleBoxRef.RemoveStyleProperty("outline")));
+
+ // ─── Content insertion and serialization ──────────────────────────────
+
+ private Task AppendText() => Run(contentInsertOutput, async () =>
+ {
+ await contentBoxRef.Append(insertText);
+ await contentInsertOutput.Success("Append(...) →", "added as a text node - markup in the string stays text");
+ });
+
+ private Task PrependText() => Run(contentInsertOutput, async () =>
+ {
+ await contentBoxRef.Prepend(insertText);
+ await contentInsertOutput.Success("Prepend(...) →", "added as a text node at the start");
+ });
+
+ private Task InsertAdjacentText() => Run(contentInsertOutput, async () =>
+ {
+ await contentBoxRef.InsertAdjacentText(InsertPosition.BeforeEnd, insertText);
+ await contentInsertOutput.Success("InsertAdjacentText(BeforeEnd, ...) →", "inserted as text");
+ });
+
+ private Task InsertAdjacentHtml() => Run(contentInsertOutput, async () =>
+ {
+ await contentBoxRef.InsertAdjacentHtml(InsertPosition.AfterBegin, insertText);
+ await contentInsertOutput.Warn("InsertAdjacentHtml(AfterBegin, ...) →", "parsed as markup - trusted input only");
+ });
+
+ private Task GetHtml() => Run(contentInsertOutput, async () =>
+ await contentInsertOutput.Success("GetHtml() →", await contentBoxRef.GetHtml(new GetHtmlOptions { SerializableShadowRoots = true })));
+
+ private Task SetHtmlSanitized() => Run(contentInsertOutput, async () =>
+ {
+ // The sanitizer strips the onerror handler and leaves the rest; where the browser has none,
+ // SetHtml throws rather than quietly writing the markup through without sanitizing.
+ await contentBoxRef.SetHtml($"{insertText}");
+ await contentInsertOutput.Success("SetHtml(...) →", await contentBoxRef.GetHtml());
+ });
+
+ private Task ReplaceChildren() => Run(contentInsertOutput, async () =>
+ {
+ await contentBoxRef.ReplaceChildren("everything else is gone");
+ await contentInsertOutput.Success("ReplaceChildren(...) →", "children replaced");
+ });
+
+ // ─── ARIA and roles ───────────────────────────────────────────────────
+
+ private Task SetAria() => Run(ariaOutput, async () =>
+ {
+ await ariaBoxRef.SetRole(ariaRole);
+ await ariaBoxRef.SetAriaLabel(ariaLabel);
+ await ariaOutput.Success("SetRole + SetAriaLabel →", $"role=\"{ariaRole}\" aria-label=\"{ariaLabel}\"");
+ });
+
+ private Task ToggleAriaExpanded() => Run(ariaOutput, async () =>
+ {
+ ariaExpanded = !ariaExpanded;
+ // A string, not a bool: ARIA defines these as enumerated attributes, and "false" and an
+ // absent attribute do not mean the same thing to a screen reader.
+ await ariaBoxRef.SetAriaExpanded(ariaExpanded ? "true" : "false");
+ await ariaOutput.Success("SetAriaExpanded →", ariaExpanded ? "true" : "false");
+ });
+
+ private Task ReadAria() => Run(ariaOutput, async () =>
+ await ariaOutput.Success("GetRole / GetAriaLabel / GetAriaExpanded →",
+ await ariaBoxRef.GetRole(), await ariaBoxRef.GetAriaLabel(), await ariaBoxRef.GetAriaExpanded()));
+
+ private Task AriaNotify() => Run(ariaOutput, async () =>
+ {
+ await ariaBoxRef.AriaNotify("Butil finished the demo step", new AriaNotifyOptions { Priority = AriaNotifyPriority.High });
+ await ariaOutput.Info("AriaNotify(...) →", "announced where supported; a no-op outside Chromium");
+ });
+
+ // ─── Popover ──────────────────────────────────────────────────────────
+
+ private Task MakePopover() => Run(popoverOutput, async () =>
+ {
+ await popoverRef.SetPopover(ElementPopover.Auto);
+ await popoverOutput.Success("SetPopover(Auto) →", await popoverRef.GetPopover());
+ });
+
+ private Task ShowPopover() => Run(popoverOutput, async () =>
+ {
+ await popoverRef.ShowPopover();
+ await popoverOutput.Success("ShowPopover() →", "showing in the top layer");
+ });
+
+ private Task TogglePopover() => Run(popoverOutput, async () =>
+ await popoverOutput.Success("TogglePopover() → showing:", await popoverRef.TogglePopover()));
+
+ private Task HidePopover() => Run(popoverOutput, async () =>
+ {
+ await popoverRef.HidePopover();
+ await popoverOutput.Success("HidePopover() →", "hidden");
+ });
+
+ // ─── Namespaced attributes, queries and scroll offsets ────────────────
+
+ private Task SetAttributeNS() => Run(queryOutput, async () =>
+ {
+ await queryBoxRef.SetAttributeNS(XlinkNamespace, "xlink:href", "#star");
+ await queryOutput.Success("SetAttributeNS(xlink, \"xlink:href\", \"#star\") →", "written");
+ });
+
+ private Task GetAttributeNS() => Run(queryOutput, async () =>
+ await queryOutput.Success("GetAttributeNS / HasAttributeNS →",
+ await queryBoxRef.GetAttributeNS(XlinkNamespace, "href"),
+ await queryBoxRef.HasAttributeNS(XlinkNamespace, "href")));
+
+ private Task RemoveAttributeNS() => Run(queryOutput, async () =>
+ {
+ await queryBoxRef.RemoveAttributeNS(XlinkNamespace, "href");
+ await queryOutput.Success("RemoveAttributeNS(...) →", "removed");
+ });
+
+ private Task QuerySelectorMatches() => Run(queryOutput, async () =>
+ await queryOutput.Success($"QuerySelectorMatches(\"{descendantSelector}\") →", await queryBoxRef.QuerySelectorMatches(descendantSelector)));
+
+ private Task QuerySelectorAllCount() => Run(queryOutput, async () =>
+ await queryOutput.Success($"QuerySelectorAllCount(\"{descendantSelector}\") →", await queryBoxRef.QuerySelectorAllCount(descendantSelector)));
+
+ private Task GetClientRects() => Run(queryOutput, async () =>
+ {
+ var rects = await queryBoxRef.GetClientRects();
+ await queryOutput.Success($"GetClientRects() → {rects.Length} rect(s), first:",
+ rects.Length == 0 ? "none - the element generates no boxes" : $"{rects[0].Width:0.#} × {rects[0].Height:0.#}");
+ });
+
+ private Task SetScrollTop() => Run(queryOutput, async () =>
+ {
+ await queryBoxRef.SetScrollTop(120);
+ await queryOutput.Success("SetScrollTop(120) → now:", await queryBoxRef.GetScrollTop());
+ });
+
+ private Task ScrollToSmooth() => Run(queryOutput, async () =>
+ {
+ await queryBoxRef.ScrollTo(new ScrollOptions { Top = 240, Behavior = ScrollBehavior.Smooth });
+ await queryOutput.Success("ScrollTo(top: 240, smooth) →", "easing to the new offset");
+ });
+
+ private Task ReadScrollMax() => Run(queryOutput, async () =>
+ await queryOutput.Success("GetScrollTopMax / GetScrollLeftMax →",
+ await queryBoxRef.GetScrollTopMax(), await queryBoxRef.GetScrollLeftMax()));
+
+ // ─── Identity, hints and tree facts ───────────────────────────────────
+
+ private Task SetIdentityHints() => Run(identityOutput, async () =>
+ {
+ await targetRef.SetTitle(titleValue);
+ await targetRef.SetLang(langValue);
+ await targetRef.SetAutocapitalize(autocapitalize);
+ await identityOutput.Success("SetTitle / SetLang / SetAutocapitalize →", titleValue, langValue, autocapitalize);
+ });
+
+ private Task ToggleDraggable() => Run(identityOutput, async () =>
+ {
+ isDraggable = !isDraggable;
+ await targetRef.SetDraggable(isDraggable);
+ await identityOutput.Success("SetDraggable →", await targetRef.GetDraggable());
+ });
+
+ private Task ReadIdentityHints() => Run(identityOutput, async () =>
+ await identityOutput.Success("Title / Lang / Spellcheck / Translate / WritingSuggestions / VirtualKeyboardPolicy →",
+ await targetRef.GetTitle(),
+ await targetRef.GetLang(),
+ await targetRef.GetSpellcheck(),
+ await targetRef.GetTranslate(),
+ await targetRef.GetWritingSuggestions(),
+ await targetRef.GetVirtualKeyboardPolicy()));
+
+ private Task ReadTreeFacts() => Run(identityOutput, async () =>
+ await identityOutput.Success("LocalName / NamespaceUri / ChildElementCount / CurrentCssZoom / OffsetParentTagName / HasShadowRoot →",
+ await targetRef.GetLocalName(),
+ await targetRef.GetNamespaceUri(),
+ await targetRef.GetChildElementCount(),
+ await targetRef.GetCurrentCssZoom(),
+ await targetRef.GetOffsetParentTagName(),
+ await targetRef.HasShadowRoot()));
+
public async ValueTask DisposeAsync()
{
try
diff --git a/src/Butil/Bit.Butil/Internals/Element/AriaNotifyJsOptions.cs b/src/Butil/Bit.Butil/Internals/Element/AriaNotifyJsOptions.cs
new file mode 100644
index 00000000000..8987c91d652
--- /dev/null
+++ b/src/Butil/Bit.Butil/Internals/Element/AriaNotifyJsOptions.cs
@@ -0,0 +1,6 @@
+namespace Bit.Butil;
+
+internal class AriaNotifyJsOptions
+{
+ public string Priority { get; set; } = default!;
+}
diff --git a/src/Butil/Bit.Butil/Internals/Element/CheckVisibilityJsOptions.cs b/src/Butil/Bit.Butil/Internals/Element/CheckVisibilityJsOptions.cs
new file mode 100644
index 00000000000..9d572f6fc7c
--- /dev/null
+++ b/src/Butil/Bit.Butil/Internals/Element/CheckVisibilityJsOptions.cs
@@ -0,0 +1,14 @@
+namespace Bit.Butil;
+
+internal class CheckVisibilityJsOptions
+{
+ public bool? ContentVisibilityAuto { get; set; }
+
+ public bool? OpacityProperty { get; set; }
+
+ public bool? VisibilityProperty { get; set; }
+
+ public bool? CheckOpacity { get; set; }
+
+ public bool? CheckVisibilityCSS { get; set; }
+}
diff --git a/src/Butil/Bit.Butil/Internals/Element/FocusJsOptions.cs b/src/Butil/Bit.Butil/Internals/Element/FocusJsOptions.cs
new file mode 100644
index 00000000000..3c3fa73eae8
--- /dev/null
+++ b/src/Butil/Bit.Butil/Internals/Element/FocusJsOptions.cs
@@ -0,0 +1,8 @@
+namespace Bit.Butil;
+
+internal class FocusJsOptions
+{
+ public bool? PreventScroll { get; set; }
+
+ public bool? FocusVisible { get; set; }
+}
diff --git a/src/Butil/Bit.Butil/Internals/Element/GetHtmlJsOptions.cs b/src/Butil/Bit.Butil/Internals/Element/GetHtmlJsOptions.cs
new file mode 100644
index 00000000000..76cce12c72c
--- /dev/null
+++ b/src/Butil/Bit.Butil/Internals/Element/GetHtmlJsOptions.cs
@@ -0,0 +1,6 @@
+namespace Bit.Butil;
+
+internal class GetHtmlJsOptions
+{
+ public bool? SerializableShadowRoots { get; set; }
+}
diff --git a/src/Butil/Bit.Butil/Publics/Element/AriaNotifyOptions.cs b/src/Butil/Bit.Butil/Publics/Element/AriaNotifyOptions.cs
new file mode 100644
index 00000000000..6eb0e980bd7
--- /dev/null
+++ b/src/Butil/Bit.Butil/Publics/Element/AriaNotifyOptions.cs
@@ -0,0 +1,20 @@
+namespace Bit.Butil;
+
+///
+/// How an
+/// announcement should be queued.
+///
+public class AriaNotifyOptions
+{
+ /// Where the announcement goes in the queue. Defaults to .
+ public AriaNotifyPriority? Priority { get; set; }
+
+ internal AriaNotifyJsOptions ToJsObject() => new()
+ {
+ Priority = Priority switch
+ {
+ AriaNotifyPriority.High => "high",
+ _ => "normal"
+ }
+ };
+}
diff --git a/src/Butil/Bit.Butil/Publics/Element/AriaNotifyPriority.cs b/src/Butil/Bit.Butil/Publics/Element/AriaNotifyPriority.cs
new file mode 100644
index 00000000000..e9bc60cc4b0
--- /dev/null
+++ b/src/Butil/Bit.Butil/Publics/Element/AriaNotifyPriority.cs
@@ -0,0 +1,11 @@
+namespace Bit.Butil;
+
+/// Where an ariaNotify announcement goes in the screen reader's queue.
+public enum AriaNotifyPriority
+{
+ /// Announced after whatever the screen reader is already saying. The default.
+ Normal,
+
+ /// Interrupts the current announcement. For things the user must hear now.
+ High
+}
diff --git a/src/Butil/Bit.Butil/Publics/Element/Autocapitalize.cs b/src/Butil/Bit.Butil/Publics/Element/Autocapitalize.cs
new file mode 100644
index 00000000000..bd49f111dc5
--- /dev/null
+++ b/src/Butil/Bit.Butil/Publics/Element/Autocapitalize.cs
@@ -0,0 +1,26 @@
+namespace Bit.Butil;
+
+/// How a virtual keyboard should capitalize what the user types into the element.
+public enum Autocapitalize
+{
+ /// The attribute is absent, so the element inherits the behaviour of its form or document.
+ NotSet,
+
+ /// No automatic capitalization.
+ None,
+
+ /// The historical spelling of , still accepted.
+ Off,
+
+ /// The historical spelling of , still accepted.
+ On,
+
+ /// Capitalize the first letter of each sentence. The default for most inputs.
+ Sentences,
+
+ /// Capitalize the first letter of every word.
+ Words,
+
+ /// Capitalize every letter.
+ Characters
+}
diff --git a/src/Butil/Bit.Butil/Publics/Element/CheckVisibilityOptions.cs b/src/Butil/Bit.Butil/Publics/Element/CheckVisibilityOptions.cs
new file mode 100644
index 00000000000..cf974bbd3bc
--- /dev/null
+++ b/src/Butil/Bit.Butil/Publics/Element/CheckVisibilityOptions.cs
@@ -0,0 +1,34 @@
+namespace Bit.Butil;
+
+///
+/// Which of the several reasons an element can be invisible
+///
+/// should take into account. Everything left null uses the browser's default, which is to consider
+/// only whether the element is rendered at all.
+///
+public class CheckVisibilityOptions
+{
+ /// True to report an element inside a content-visibility: auto subtree that is currently skipped as invisible.
+ public bool? ContentVisibilityAuto { get; set; }
+
+ /// True to report an element with opacity: 0 - on itself or on an ancestor - as invisible.
+ public bool? OpacityProperty { get; set; }
+
+ /// True to report an element hidden by visibility: hidden or collapse as invisible.
+ public bool? VisibilityProperty { get; set; }
+
+ /// The earlier spelling of , still accepted by shipped engines.
+ public bool? CheckOpacity { get; set; }
+
+ /// The earlier spelling of , still accepted by shipped engines.
+ public bool? CheckVisibilityCSS { get; set; }
+
+ internal CheckVisibilityJsOptions ToJsObject() => new()
+ {
+ ContentVisibilityAuto = ContentVisibilityAuto,
+ OpacityProperty = OpacityProperty,
+ VisibilityProperty = VisibilityProperty,
+ CheckOpacity = CheckOpacity,
+ CheckVisibilityCSS = CheckVisibilityCSS
+ };
+}
diff --git a/src/Butil/Bit.Butil/Publics/Element/ElementPopover.cs b/src/Butil/Bit.Butil/Publics/Element/ElementPopover.cs
new file mode 100644
index 00000000000..b82c2167fa9
--- /dev/null
+++ b/src/Butil/Bit.Butil/Publics/Element/ElementPopover.cs
@@ -0,0 +1,17 @@
+namespace Bit.Butil;
+
+/// The popover behaviour of an element - the values of its popover attribute.
+public enum ElementPopover
+{
+ /// Not a popover.
+ NotSet,
+
+ /// Light-dismissed: opening it closes other auto popovers, and clicking away or pressing Escape closes it.
+ Auto,
+
+ /// Closed only by the code that opened it. Several can be open at once.
+ Manual,
+
+ /// Light-dismissed like , but does not close other popovers - for tooltips over an open menu.
+ Hint
+}
diff --git a/src/Butil/Bit.Butil/Publics/Element/ElementReferenceAriaExtensions.cs b/src/Butil/Bit.Butil/Publics/Element/ElementReferenceAriaExtensions.cs
new file mode 100644
index 00000000000..11ce46443db
--- /dev/null
+++ b/src/Butil/Bit.Butil/Publics/Element/ElementReferenceAriaExtensions.cs
@@ -0,0 +1,705 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components;
+
+namespace Bit.Butil;
+
+///
+/// The ARIA reflection properties of
+/// Element: the
+/// role and every aria-* attribute, read and written as properties rather than as
+/// attribute strings.
+///
+///
+/// Prefer rendering these as attributes in your markup - Blazor passes aria-label and friends
+/// straight through, it costs no interop, and a re-render cannot undo it. These are for the
+/// attributes that have to change in response to something outside the render tree, and for reading
+/// what another script or a component library put on an element.
+///
+/// Every value is a string, including the numeric ones (AriaLevel, AriaValueNow) and
+/// the boolean ones (AriaExpanded, AriaHidden) - that is how ARIA itself is defined,
+/// and "false" and "" mean different things to a screen reader. An attribute that is
+/// not set reads as null.
+///
+/// During prerender/SSR (no JS runtime) every read returns an empty string rather than throwing, so
+/// it cannot be told apart from a genuinely empty one. Defer reads you branch on to
+/// OnAfterRenderAsync.
+///
+public static class ElementReferenceAriaExtensions
+{
+ ///
+ /// Whether assistive technology presents the whole changed region or only the part that changed. Reflects aria-atomic.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaAtomic
+ ///
+ public static ValueTask GetAriaAtomic(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaAtomic");
+ ///
+ /// Whether assistive technology presents the whole changed region or only the part that changed. Reflects aria-atomic.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaAtomic
+ ///
+ public static ValueTask SetAriaAtomic(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaAtomic", value);
+
+ ///
+ /// What kind of completion an input offers: inline, list, both or none. Reflects aria-autocomplete.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaAutoComplete
+ ///
+ public static ValueTask GetAriaAutoComplete(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaAutoComplete");
+ ///
+ /// What kind of completion an input offers: inline, list, both or none. Reflects aria-autocomplete.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaAutoComplete
+ ///
+ public static ValueTask SetAriaAutoComplete(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaAutoComplete", value);
+
+ ///
+ /// The label a braille display shows in place of the accessible name. Reflects aria-braillelabel.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaBrailleLabel
+ ///
+ public static ValueTask GetAriaBrailleLabel(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaBrailleLabel");
+ ///
+ /// The label a braille display shows in place of the accessible name. Reflects aria-braillelabel.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaBrailleLabel
+ ///
+ public static ValueTask SetAriaBrailleLabel(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaBrailleLabel", value);
+
+ ///
+ /// The role description a braille display shows in place of the spoken one. Reflects aria-brailleroledescription.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaBrailleRoleDescription
+ ///
+ public static ValueTask GetAriaBrailleRoleDescription(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaBrailleRoleDescription");
+ ///
+ /// The role description a braille display shows in place of the spoken one. Reflects aria-brailleroledescription.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaBrailleRoleDescription
+ ///
+ public static ValueTask SetAriaBrailleRoleDescription(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaBrailleRoleDescription", value);
+
+ ///
+ /// Whether the element is still being updated, so assistive technology waits before announcing it. Reflects aria-busy.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaBusy
+ ///
+ public static ValueTask GetAriaBusy(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaBusy");
+ ///
+ /// Whether the element is still being updated, so assistive technology waits before announcing it. Reflects aria-busy.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaBusy
+ ///
+ public static ValueTask SetAriaBusy(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaBusy", value);
+
+ ///
+ /// The checked state of a checkbox, radio or switch that is not a native input: true, false or mixed. Reflects aria-checked.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaChecked
+ ///
+ public static ValueTask GetAriaChecked(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaChecked");
+ ///
+ /// The checked state of a checkbox, radio or switch that is not a native input: true, false or mixed. Reflects aria-checked.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaChecked
+ ///
+ public static ValueTask SetAriaChecked(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaChecked", value);
+
+ ///
+ /// How many columns the whole table has, when the DOM holds only some of them. Reflects aria-colcount.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaColCount
+ ///
+ public static ValueTask GetAriaColCount(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaColCount");
+ ///
+ /// How many columns the whole table has, when the DOM holds only some of them. Reflects aria-colcount.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaColCount
+ ///
+ public static ValueTask SetAriaColCount(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaColCount", value);
+
+ ///
+ /// Which column of the whole table this cell sits in, counting from one. Reflects aria-colindex.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaColIndex
+ ///
+ public static ValueTask GetAriaColIndex(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaColIndex");
+ ///
+ /// Which column of the whole table this cell sits in, counting from one. Reflects aria-colindex.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaColIndex
+ ///
+ public static ValueTask SetAriaColIndex(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaColIndex", value);
+
+ ///
+ /// A human-readable column label, announced instead of the column number. Reflects aria-colindextext.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaColIndexText
+ ///
+ public static ValueTask GetAriaColIndexText(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaColIndexText");
+ ///
+ /// A human-readable column label, announced instead of the column number. Reflects aria-colindextext.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaColIndexText
+ ///
+ public static ValueTask SetAriaColIndexText(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaColIndexText", value);
+
+ ///
+ /// How many columns the cell spans, for a grid not built from table elements. Reflects aria-colspan.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaColSpan
+ ///
+ public static ValueTask GetAriaColSpan(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaColSpan");
+ ///
+ /// How many columns the cell spans, for a grid not built from table elements. Reflects aria-colspan.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaColSpan
+ ///
+ public static ValueTask SetAriaColSpan(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaColSpan", value);
+
+ ///
+ /// Which item of a set is the current one: page, step, location, date, time or true. Reflects aria-current.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaCurrent
+ ///
+ public static ValueTask GetAriaCurrent(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaCurrent");
+ ///
+ /// Which item of a set is the current one: page, step, location, date, time or true. Reflects aria-current.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaCurrent
+ ///
+ public static ValueTask SetAriaCurrent(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaCurrent", value);
+
+ ///
+ /// A longer description of the element, announced after its name. Reflects aria-description.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaDescription
+ ///
+ public static ValueTask GetAriaDescription(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaDescription");
+ ///
+ /// A longer description of the element, announced after its name. Reflects aria-description.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaDescription
+ ///
+ public static ValueTask SetAriaDescription(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaDescription", value);
+
+ ///
+ /// Whether the element is perceivable but not operable. Unlike the disabled attribute it stays focusable. Reflects aria-disabled.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaDisabled
+ ///
+ public static ValueTask GetAriaDisabled(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaDisabled");
+ ///
+ /// Whether the element is perceivable but not operable. Unlike the disabled attribute it stays focusable. Reflects aria-disabled.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaDisabled
+ ///
+ public static ValueTask SetAriaDisabled(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaDisabled", value);
+
+ ///
+ /// Whether the thing this element controls is expanded or collapsed. Reflects aria-expanded.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaExpanded
+ ///
+ public static ValueTask GetAriaExpanded(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaExpanded");
+ ///
+ /// Whether the thing this element controls is expanded or collapsed. Reflects aria-expanded.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaExpanded
+ ///
+ public static ValueTask SetAriaExpanded(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaExpanded", value);
+
+ ///
+ /// What kind of popup the element opens: menu, listbox, tree, grid or dialog. Reflects aria-haspopup.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaHasPopup
+ ///
+ public static ValueTask GetAriaHasPopup(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaHasPopup");
+ ///
+ /// What kind of popup the element opens: menu, listbox, tree, grid or dialog. Reflects aria-haspopup.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaHasPopup
+ ///
+ public static ValueTask SetAriaHasPopup(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaHasPopup", value);
+
+ ///
+ /// Whether the element and its subtree are hidden from the accessibility tree while staying visible on screen. Reflects aria-hidden.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaHidden
+ ///
+ public static ValueTask GetAriaHidden(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaHidden");
+ ///
+ /// Whether the element and its subtree are hidden from the accessibility tree while staying visible on screen. Reflects aria-hidden.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaHidden
+ ///
+ public static ValueTask SetAriaHidden(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaHidden", value);
+
+ ///
+ /// Whether the entered value is rejected, and why: true, grammar or spelling. Reflects aria-invalid.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaInvalid
+ ///
+ public static ValueTask GetAriaInvalid(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaInvalid");
+ ///
+ /// Whether the entered value is rejected, and why: true, grammar or spelling. Reflects aria-invalid.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaInvalid
+ ///
+ public static ValueTask SetAriaInvalid(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaInvalid", value);
+
+ ///
+ /// The keyboard shortcuts that activate the element, as a space-separated list. Reflects aria-keyshortcuts.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaKeyShortcuts
+ ///
+ public static ValueTask GetAriaKeyShortcuts(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaKeyShortcuts");
+ ///
+ /// The keyboard shortcuts that activate the element, as a space-separated list. Reflects aria-keyshortcuts.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaKeyShortcuts
+ ///
+ public static ValueTask SetAriaKeyShortcuts(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaKeyShortcuts", value);
+
+ ///
+ /// The element's accessible name, for when no visible text supplies one. Reflects aria-label.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaLabel
+ ///
+ public static ValueTask GetAriaLabel(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaLabel");
+ ///
+ /// The element's accessible name, for when no visible text supplies one. Reflects aria-label.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaLabel
+ ///
+ public static ValueTask SetAriaLabel(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaLabel", value);
+
+ ///
+ /// The element's level in a hierarchy - a heading's rank, a tree item's depth. Reflects aria-level.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaLevel
+ ///
+ public static ValueTask GetAriaLevel(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaLevel");
+ ///
+ /// The element's level in a hierarchy - a heading's rank, a tree item's depth. Reflects aria-level.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaLevel
+ ///
+ public static ValueTask SetAriaLevel(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaLevel", value);
+
+ ///
+ /// How urgently updates to this region are announced: off, polite or assertive. Reflects aria-live.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaLive
+ ///
+ public static ValueTask GetAriaLive(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaLive");
+ ///
+ /// How urgently updates to this region are announced: off, polite or assertive. Reflects aria-live.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaLive
+ ///
+ public static ValueTask SetAriaLive(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaLive", value);
+
+ ///
+ /// Whether a dialog is modal, so assistive technology confines itself to its contents. Reflects aria-modal.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaModal
+ ///
+ public static ValueTask GetAriaModal(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaModal");
+ ///
+ /// Whether a dialog is modal, so assistive technology confines itself to its contents. Reflects aria-modal.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaModal
+ ///
+ public static ValueTask SetAriaModal(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaModal", value);
+
+ ///
+ /// Whether a textbox takes more than one line, so Enter inserts a newline rather than submitting. Reflects aria-multiline.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaMultiline
+ ///
+ public static ValueTask GetAriaMultiline(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaMultiline");
+ ///
+ /// Whether a textbox takes more than one line, so Enter inserts a newline rather than submitting. Reflects aria-multiline.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaMultiline
+ ///
+ public static ValueTask SetAriaMultiline(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaMultiline", value);
+
+ ///
+ /// Whether more than one item of the list, grid or tree can be selected at once. Reflects aria-multiselectable.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaMultiSelectable
+ ///
+ public static ValueTask GetAriaMultiSelectable(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaMultiSelectable");
+ ///
+ /// Whether more than one item of the list, grid or tree can be selected at once. Reflects aria-multiselectable.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaMultiSelectable
+ ///
+ public static ValueTask SetAriaMultiSelectable(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaMultiSelectable", value);
+
+ ///
+ /// Whether the element is laid out horizontally or vertically. Reflects aria-orientation.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaOrientation
+ ///
+ public static ValueTask GetAriaOrientation(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaOrientation");
+ ///
+ /// Whether the element is laid out horizontally or vertically. Reflects aria-orientation.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaOrientation
+ ///
+ public static ValueTask SetAriaOrientation(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaOrientation", value);
+
+ ///
+ /// The hint shown in an empty input, for controls with no native placeholder. Reflects aria-placeholder.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaPlaceholder
+ ///
+ public static ValueTask GetAriaPlaceholder(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaPlaceholder");
+ ///
+ /// The hint shown in an empty input, for controls with no native placeholder. Reflects aria-placeholder.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaPlaceholder
+ ///
+ public static ValueTask SetAriaPlaceholder(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaPlaceholder", value);
+
+ ///
+ /// Which position this item holds in its set, counting from one - for a list the DOM holds only part of. Reflects aria-posinset.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaPosInSet
+ ///
+ public static ValueTask GetAriaPosInSet(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaPosInSet");
+ ///
+ /// Which position this item holds in its set, counting from one - for a list the DOM holds only part of. Reflects aria-posinset.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaPosInSet
+ ///
+ public static ValueTask SetAriaPosInSet(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaPosInSet", value);
+
+ ///
+ /// The pressed state of a toggle button: true, false or mixed. Reflects aria-pressed.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaPressed
+ ///
+ public static ValueTask GetAriaPressed(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaPressed");
+ ///
+ /// The pressed state of a toggle button: true, false or mixed. Reflects aria-pressed.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaPressed
+ ///
+ public static ValueTask SetAriaPressed(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaPressed", value);
+
+ ///
+ /// Whether the value can be read but not changed. Reflects aria-readonly.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaReadOnly
+ ///
+ public static ValueTask GetAriaReadOnly(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaReadOnly");
+ ///
+ /// Whether the value can be read but not changed. Reflects aria-readonly.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaReadOnly
+ ///
+ public static ValueTask SetAriaReadOnly(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaReadOnly", value);
+
+ ///
+ /// Which changes in a live region are worth announcing: additions, removals, text or all. Reflects aria-relevant.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaRelevant
+ ///
+ public static ValueTask GetAriaRelevant(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaRelevant");
+ ///
+ /// Which changes in a live region are worth announcing: additions, removals, text or all. Reflects aria-relevant.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaRelevant
+ ///
+ public static ValueTask SetAriaRelevant(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaRelevant", value);
+
+ ///
+ /// Whether a value must be supplied before the form can be submitted. Reflects aria-required.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaRequired
+ ///
+ public static ValueTask GetAriaRequired(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaRequired");
+ ///
+ /// Whether a value must be supplied before the form can be submitted. Reflects aria-required.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaRequired
+ ///
+ public static ValueTask SetAriaRequired(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaRequired", value);
+
+ ///
+ /// A human-readable name for the element's role, announced instead of the standard one. Reflects aria-roledescription.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaRoleDescription
+ ///
+ public static ValueTask GetAriaRoleDescription(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaRoleDescription");
+ ///
+ /// A human-readable name for the element's role, announced instead of the standard one. Reflects aria-roledescription.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaRoleDescription
+ ///
+ public static ValueTask SetAriaRoleDescription(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaRoleDescription", value);
+
+ ///
+ /// How many rows the whole table has, when the DOM holds only some of them. Reflects aria-rowcount.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaRowCount
+ ///
+ public static ValueTask GetAriaRowCount(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaRowCount");
+ ///
+ /// How many rows the whole table has, when the DOM holds only some of them. Reflects aria-rowcount.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaRowCount
+ ///
+ public static ValueTask SetAriaRowCount(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaRowCount", value);
+
+ ///
+ /// Which row of the whole table this row or cell sits in, counting from one. Reflects aria-rowindex.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaRowIndex
+ ///
+ public static ValueTask GetAriaRowIndex(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaRowIndex");
+ ///
+ /// Which row of the whole table this row or cell sits in, counting from one. Reflects aria-rowindex.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaRowIndex
+ ///
+ public static ValueTask SetAriaRowIndex(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaRowIndex", value);
+
+ ///
+ /// A human-readable row label, announced instead of the row number. Reflects aria-rowindextext.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaRowIndexText
+ ///
+ public static ValueTask GetAriaRowIndexText(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaRowIndexText");
+ ///
+ /// A human-readable row label, announced instead of the row number. Reflects aria-rowindextext.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaRowIndexText
+ ///
+ public static ValueTask SetAriaRowIndexText(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaRowIndexText", value);
+
+ ///
+ /// How many rows the cell spans, for a grid not built from table elements. Reflects aria-rowspan.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaRowSpan
+ ///
+ public static ValueTask GetAriaRowSpan(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaRowSpan");
+ ///
+ /// How many rows the cell spans, for a grid not built from table elements. Reflects aria-rowspan.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaRowSpan
+ ///
+ public static ValueTask SetAriaRowSpan(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaRowSpan", value);
+
+ ///
+ /// Whether the item is selected - for options, tabs, rows and grid cells. Reflects aria-selected.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaSelected
+ ///
+ public static ValueTask GetAriaSelected(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaSelected");
+ ///
+ /// Whether the item is selected - for options, tabs, rows and grid cells. Reflects aria-selected.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaSelected
+ ///
+ public static ValueTask SetAriaSelected(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaSelected", value);
+
+ ///
+ /// How many items the whole set holds, when the DOM holds only some of them. Reflects aria-setsize.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaSetSize
+ ///
+ public static ValueTask GetAriaSetSize(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaSetSize");
+ ///
+ /// How many items the whole set holds, when the DOM holds only some of them. Reflects aria-setsize.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaSetSize
+ ///
+ public static ValueTask SetAriaSetSize(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaSetSize", value);
+
+ ///
+ /// How a column or row is sorted: ascending, descending, other or none. Reflects aria-sort.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaSort
+ ///
+ public static ValueTask GetAriaSort(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaSort");
+ ///
+ /// How a column or row is sorted: ascending, descending, other or none. Reflects aria-sort.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaSort
+ ///
+ public static ValueTask SetAriaSort(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaSort", value);
+
+ ///
+ /// The largest value a range widget accepts. Reflects aria-valuemax.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaValueMax
+ ///
+ public static ValueTask GetAriaValueMax(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaValueMax");
+ ///
+ /// The largest value a range widget accepts. Reflects aria-valuemax.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaValueMax
+ ///
+ public static ValueTask SetAriaValueMax(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaValueMax", value);
+
+ ///
+ /// The smallest value a range widget accepts. Reflects aria-valuemin.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaValueMin
+ ///
+ public static ValueTask GetAriaValueMin(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaValueMin");
+ ///
+ /// The smallest value a range widget accepts. Reflects aria-valuemin.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaValueMin
+ ///
+ public static ValueTask SetAriaValueMin(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaValueMin", value);
+
+ ///
+ /// The current value of a range widget. Reflects aria-valuenow.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaValueNow
+ ///
+ public static ValueTask GetAriaValueNow(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaValueNow");
+ ///
+ /// The current value of a range widget. Reflects aria-valuenow.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaValueNow
+ ///
+ public static ValueTask SetAriaValueNow(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaValueNow", value);
+
+ ///
+ /// A human-readable rendering of the current value, announced instead of the number. Reflects aria-valuetext.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaValueText
+ ///
+ public static ValueTask GetAriaValueText(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "ariaValueText");
+ ///
+ /// A human-readable rendering of the current value, announced instead of the number. Reflects aria-valuetext.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaValueText
+ ///
+ public static ValueTask SetAriaValueText(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "ariaValueText", value);
+
+ ///
+ /// The element's ARIA role - what it is to assistive technology, for when the tag alone does not say. Reflects role.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/role
+ ///
+ public static ValueTask GetRole(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAria", element, "role");
+ ///
+ /// The element's ARIA role - what it is to assistive technology, for when the tag alone does not say. Reflects role.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/role
+ ///
+ public static ValueTask SetRole(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAria", element, "role", value);
+
+ ///
+ /// Announces to assistive technology without changing the page - the
+ /// direct alternative to mutating a live region so that a screen reader reads it out.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaNotify
+ ///
+ ///
+ /// Experimental and Chromium-only. A no-op everywhere else rather than a throw: an announcement
+ /// that does not happen is not a failure of the page that asked for it, so a caller does not
+ /// have to feature-detect. Keep the visible UI telling the same story, since most users will
+ /// never receive this.
+ ///
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AriaNotifyJsOptions))]
+ public static ValueTask AriaNotify(this ElementReference element, string message, AriaNotifyOptions? options = null)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.ariaNotify", element, message, options?.ToJsObject());
+}
diff --git a/src/Butil/Bit.Butil/Publics/Element/ElementReferenceDomExtensions.cs b/src/Butil/Bit.Butil/Publics/Element/ElementReferenceDomExtensions.cs
new file mode 100644
index 00000000000..8d5830c3dad
--- /dev/null
+++ b/src/Butil/Bit.Butil/Publics/Element/ElementReferenceDomExtensions.cs
@@ -0,0 +1,360 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components;
+
+namespace Bit.Butil;
+
+///
+/// The parts of Element
+/// and HTMLElement
+/// that change what an element contains or how it is presented: inserting text and markup around
+/// and inside it, its class list, its data-* attributes, its inline style, its popover state
+/// and the queries that count what is under it.
+///
+///
+/// Blazor owns the DOM it rendered, and a diff can undo anything written here on the next render.
+/// These are for elements Blazor does not re-render - a container it renders once, or an element
+/// outside the component's own markup.
+///
+/// The DOM insertion methods take nodes as well as strings; only the strings cross this boundary.
+/// A is minted by Blazor's renderer and cannot be handed back to it
+/// from JavaScript, so there is no way to name another element as the thing being inserted. Each
+/// string becomes a text node - use to insert markup.
+///
+public static class ElementReferenceDomExtensions
+{
+ ///
+ /// Inserts text nodes immediately after the element, as siblings.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/after
+ ///
+ public static ValueTask After(this ElementReference element, params string[] nodes)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.after", element, nodes);
+
+ ///
+ /// Appends text nodes inside the element, after its last child.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/append
+ ///
+ public static ValueTask Append(this ElementReference element, params string[] nodes)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.append", element, nodes);
+
+ ///
+ /// Inserts text nodes immediately before the element, as siblings.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/before
+ ///
+ public static ValueTask Before(this ElementReference element, params string[] nodes)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.before", element, nodes);
+
+ ///
+ /// Prepends text nodes inside the element, before its first child.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/prepend
+ ///
+ public static ValueTask Prepend(this ElementReference element, params string[] nodes)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.prepend", element, nodes);
+
+ ///
+ /// Replaces every child of the element with the given text nodes. Passing nothing empties it.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceChildren
+ ///
+ public static ValueTask ReplaceChildren(this ElementReference element, params string[] nodes)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.replaceChildren", element, nodes);
+
+ ///
+ /// Replaces the element itself with the given text nodes. The reference is dangling afterwards.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceWith
+ ///
+ public static ValueTask ReplaceWith(this ElementReference element, params string[] nodes)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.replaceWith", element, nodes);
+
+ ///
+ /// Parses as markup and inserts the result at the given position,
+ /// without reparsing the element's existing children.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
+ ///
+ ///
+ /// Security note: the markup is parsed as-is and bypasses Blazor's encoding. Never pass
+ /// untrusted input - use , which cannot introduce elements, or
+ /// , which sanitizes.
+ ///
+ public static ValueTask InsertAdjacentHtml(this ElementReference element, InsertPosition position, string html)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.insertAdjacentHTML", element, PositionName(position), html);
+
+ ///
+ /// Inserts as a text node at the given position. Markup in the string
+ /// stays text.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentText
+ ///
+ public static ValueTask InsertAdjacentText(this ElementReference element, InsertPosition position, string text)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.insertAdjacentText", element, PositionName(position), text);
+
+ ///
+ /// Serializes the element's contents to HTML, optionally including the shadow roots that were
+ /// attached as serializable - which InnerHtml always leaves out.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/getHTML
+ ///
+ ///
+ /// Where the browser has no getHTML, this falls back to innerHTML - the same
+ /// answer for a tree with no shadow roots in it.
+ ///
+ /// During prerender/SSR (no JS runtime) this returns an empty string rather than throwing.
+ ///
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GetHtmlJsOptions))]
+ public static ValueTask GetHtml(this ElementReference element, GetHtmlOptions? options = null)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getHTML", element, options?.ToJsObject());
+
+ ///
+ /// Replaces the element's contents with , running it through the
+ /// browser's HTML sanitizer first: scripts, event-handler attributes and javascript: URLs
+ /// are stripped.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/setHTML
+ ///
+ ///
+ /// This is the one to reach for when the markup came from a user. It throws where the browser
+ /// has no setHTML: quietly falling back to an unsanitized write would turn the safe call
+ /// into the unsafe one. Feature-detect with a try and fall back to your own sanitizer.
+ ///
+ public static ValueTask SetHtml(this ElementReference element, string html)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setHTML", element, html, null);
+
+ ///
+ /// Replaces the element's contents with without sanitizing, parsing
+ /// declarative shadow roots in it along the way.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/setHTMLUnsafe
+ ///
+ ///
+ /// Security note: "unsafe" is the spec's own word for it. Only for markup you produced.
+ /// Use for anything else.
+ ///
+ public static ValueTask SetHtmlUnsafe(this ElementReference element, string html)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setHTMLUnsafe", element, html);
+
+ ///
+ /// Every border box the element occupies, in viewport coordinates - more than one for an inline
+ /// element that wraps across lines.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/getClientRects
+ ///
+ ///
+ /// Empty for an element that generates no boxes at all, which is how display: none reads
+ /// here - and also what prerender/SSR (no JS runtime) hands back, so the two cannot be told
+ /// apart. Defer the read to OnAfterRenderAsync if you branch on it.
+ ///
+ public static async ValueTask GetClientRects(this ElementReference element)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getClientRects", element);
+
+ ///
+ /// Adds classes to the element, ignoring the ones it already has.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/classList
+ ///
+ public static ValueTask AddClass(this ElementReference element, params string[] tokens)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.classListAdd", element, tokens);
+
+ ///
+ /// Removes classes from the element, ignoring the ones it does not have.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/classList
+ ///
+ public static ValueTask RemoveClass(this ElementReference element, params string[] tokens)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.classListRemove", element, tokens);
+
+ ///
+ /// Adds the class when absent and removes it when present, or pins it to
+ /// . Returns whether the class is on the element afterwards.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/classList
+ ///
+ public static async ValueTask ToggleClass(this ElementReference element, string token, bool? force = null)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.classListToggle", element, token, force);
+
+ ///
+ /// Swaps for in place, keeping its
+ /// position in the list. Returns false when the old class was not there, in which case nothing
+ /// is added.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/classList
+ ///
+ public static async ValueTask ReplaceClass(this ElementReference element, string oldToken, string newToken)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.classListReplace", element, oldToken, newToken);
+
+ ///
+ /// Returns whether the element carries the class.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/classList
+ ///
+ ///
+ /// During prerender/SSR (no JS runtime) this returns false rather than throwing, so the
+ /// result can't be distinguished from a genuine one. Defer the read to OnAfterRenderAsync
+ /// if you branch on it.
+ ///
+ public static async ValueTask ContainsClass(this ElementReference element, string token)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.classListContains", element, token);
+
+ ///
+ /// The element's classes, in document order - className split into its tokens.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/classList
+ ///
+ public static async ValueTask GetClassList(this ElementReference element)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getClassList", element);
+
+ ///
+ /// Reads one data-* attribute by its dataset key - userId for
+ /// data-user-id. Null when the attribute is absent.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset
+ ///
+ public static ValueTask GetData(this ElementReference element, string key)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getData", element, key);
+
+ ///
+ /// Writes one data-* attribute by its dataset key, creating it when absent.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset
+ ///
+ public static ValueTask SetData(this ElementReference element, string key, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setData", element, key, value);
+
+ ///
+ /// Removes one data-* attribute by its dataset key.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset
+ ///
+ public static ValueTask RemoveData(this ElementReference element, string key)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.removeData", element, key);
+
+ ///
+ /// The dataset keys the element carries - userId for data-user-id, not the
+ /// attribute names.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset
+ ///
+ public static async ValueTask GetDataNames(this ElementReference element)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getDataNames", element);
+
+ ///
+ /// The element's whole inline style, as it would be written in a style attribute.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style
+ ///
+ ///
+ /// Inline style only. What a stylesheet contributes is not here - that is
+ /// getComputedStyle, which belongs to the window rather than to the element.
+ ///
+ public static ValueTask GetStyleText(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getStyleText", element);
+
+ ///
+ /// Replaces the element's whole inline style. Anything already there is dropped.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style
+ ///
+ public static ValueTask SetStyleText(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setStyleText", element, value);
+
+ ///
+ /// Reads one inline style declaration by its CSS property name - "background-color", and
+ /// custom properties ("--accent") too. Empty when the property is not set inline.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration/getPropertyValue
+ ///
+ public static ValueTask GetStyleProperty(this ElementReference element, string name)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getStyleProperty", element, name);
+
+ ///
+ /// Sets one inline style declaration by its CSS property name, leaving the rest alone.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration/setProperty
+ ///
+ /// Pass to mark the declaration !important.
+ public static ValueTask SetStyleProperty(this ElementReference element, string name, string value, bool important = false)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setStyleProperty", element, name, value, important ? "important" : null);
+
+ ///
+ /// Removes one inline style declaration and returns what it held.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration/removeProperty
+ ///
+ public static ValueTask RemoveStyleProperty(this ElementReference element, string name)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.removeStyleProperty", element, name);
+
+ ///
+ /// Shows the element as a popover, in the top layer above the rest of the page.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/showPopover
+ ///
+ ///
+ /// The element needs a popover attribute - see
+ /// . A no-op where the browser has no
+ /// popover support, so a page can call it without feature-detecting first.
+ ///
+ public static ValueTask ShowPopover(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.showPopover", element);
+
+ ///
+ /// Hides the element if it is showing as a popover.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/hidePopover
+ ///
+ public static ValueTask HidePopover(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.hidePopover", element);
+
+ ///
+ /// Shows the popover when hidden and hides it when shown, or pins it to
+ /// . Returns whether it is showing afterwards.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/togglePopover
+ ///
+ ///
+ /// False where the browser has no popover support, which is indistinguishable from a popover
+ /// that ended up hidden - as it also is during prerender/SSR (no JS runtime).
+ ///
+ public static async ValueTask TogglePopover(this ElementReference element, bool? force = null)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.togglePopover", element, force);
+
+ ///
+ /// Returns whether any descendant of the element matches .
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelector
+ ///
+ ///
+ /// The DOM method hands back the matched element. An can only be
+ /// minted by Blazor's renderer, so there is nothing to hand back across the boundary - capture
+ /// the element you need with @ref instead, and use this for the existence question.
+ ///
+ /// During prerender/SSR (no JS runtime) this returns false rather than throwing.
+ ///
+ public static async ValueTask QuerySelectorMatches(this ElementReference element, string selectors)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.querySelectorMatches", element, selectors);
+
+ ///
+ /// How many descendants of the element match .
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelectorAll
+ ///
+ ///
+ /// The count rather than the elements, for the same reason as
+ /// . During prerender/SSR (no JS runtime) this returns
+ /// 0 rather than throwing.
+ ///
+ public static async ValueTask QuerySelectorAllCount(this ElementReference element, string selectors)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.querySelectorAllCount", element, selectors);
+
+ private static string PositionName(InsertPosition position) => position switch
+ {
+ InsertPosition.BeforeBegin => "beforebegin",
+ InsertPosition.AfterBegin => "afterbegin",
+ InsertPosition.AfterEnd => "afterend",
+ _ => "beforeend",
+ };
+}
diff --git a/src/Butil/Bit.Butil/Publics/Element/ElementReferenceStateExtensions.cs b/src/Butil/Bit.Butil/Publics/Element/ElementReferenceStateExtensions.cs
new file mode 100644
index 00000000000..21443d6c26a
--- /dev/null
+++ b/src/Butil/Bit.Butil/Publics/Element/ElementReferenceStateExtensions.cs
@@ -0,0 +1,474 @@
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components;
+
+namespace Bit.Butil;
+
+///
+/// The remaining state an
+/// Element or an
+/// HTMLElement
+/// carries: its identity in the document tree, its tooltip and language, the hints it gives a
+/// virtual keyboard and a spell checker, its popover kind and its shadow-DOM wiring.
+///
+///
+/// Every read is one interop round trip, and every one of these has a Blazor equivalent that costs
+/// nothing: title, lang and draggable are attributes you can simply render.
+/// Reach for these when the element is not yours to re-render, or when you need to read what
+/// something else put there.
+///
+/// During prerender/SSR (no JS runtime) every read returns a safe default - an empty string, an
+/// empty array, false, 0 - rather than throwing, so a value read there cannot be told
+/// apart from a genuine one. Defer reads you branch on to OnAfterRenderAsync.
+///
+public static class ElementReferenceStateExtensions
+{
+ ///
+ /// The label a keyboard shortcut for this element would be shown with - "Alt+S" or "⌃⌥S",
+ /// depending on the platform. Empty on engines that do not compute one.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/accessKeyLabel
+ ///
+ public static ValueTask GetAccessKeyLabel(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.accessKeyLabel", element);
+
+ ///
+ /// The name of the slot this element is assigned to inside its shadow host, or null when it is
+ /// not slotted. This is where the element landed; is where it asked
+ /// to go.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/assignedSlot
+ ///
+ ///
+ /// The DOM property is the slot element itself; an can only be
+ /// minted by Blazor's renderer, so what crosses the boundary is the slot's name.
+ ///
+ public static ValueTask GetAssignedSlotName(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.assignedSlotName", element);
+
+ ///
+ /// How a virtual keyboard should capitalize text typed into the element.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/autocapitalize
+ ///
+ public static async ValueTask GetAutocapitalize(this ElementReference element)
+ {
+ var value = await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAutocapitalize", element);
+ return value switch
+ {
+ "none" => Autocapitalize.None,
+ "off" => Autocapitalize.Off,
+ "on" => Autocapitalize.On,
+ "sentences" => Autocapitalize.Sentences,
+ "words" => Autocapitalize.Words,
+ "characters" => Autocapitalize.Characters,
+ _ => Autocapitalize.NotSet,
+ };
+ }
+ ///
+ /// How a virtual keyboard should capitalize text typed into the element.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/autocapitalize
+ ///
+ public static async ValueTask SetAutocapitalize(this ElementReference element, Autocapitalize value)
+ {
+ var v = value switch
+ {
+ Autocapitalize.None => "none",
+ Autocapitalize.Off => "off",
+ Autocapitalize.On => "on",
+ Autocapitalize.Sentences => "sentences",
+ Autocapitalize.Words => "words",
+ Autocapitalize.Characters => "characters",
+ _ => "",
+ };
+ await ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAutocapitalize", element, v);
+ }
+
+ ///
+ /// Whether the browser may autocorrect what the user types into the element. Safari and
+ /// Chromium; false elsewhere, where the feature does not exist.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/autocorrect
+ ///
+ public static async ValueTask GetAutocorrect(this ElementReference element)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAutocorrect", element);
+ ///
+ /// Whether the browser may autocorrect what the user types into the element.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/autocorrect
+ ///
+ public static async ValueTask SetAutocorrect(this ElementReference element, bool value)
+ => await ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAutocorrect", element, value);
+
+ ///
+ /// Whether the element asks for focus when the page loads.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/autofocus
+ ///
+ ///
+ /// Setting this after load does nothing on its own - the browser has already decided where
+ /// focus goes. Use Focus for that.
+ ///
+ public static async ValueTask GetAutofocus(this ElementReference element)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getAutofocus", element);
+ ///
+ /// Whether the element asks for focus when the page loads.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/autofocus
+ ///
+ public static async ValueTask SetAutofocus(this ElementReference element, bool value)
+ => await ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setAutofocus", element, value);
+
+ ///
+ /// How many element children the element has - text nodes and comments not counted.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/childElementCount
+ ///
+ public static async ValueTask GetChildElementCount(this ElementReference element)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.childElementCount", element);
+
+ ///
+ /// The effective CSS zoom applied to the element by itself and its ancestors - the factor
+ /// between the numbers GetBoundingClientRect reports and the ones the layout was written
+ /// in. 1 when nothing is zoomed, and on engines with no CSS zoom.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/currentCSSZoom
+ ///
+ public static async ValueTask GetCurrentCssZoom(this ElementReference element)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.currentCSSZoom", element);
+
+ ///
+ /// Whether the element can be dragged. Note that this is a tri-state attribute in HTML - the
+ /// property collapses it to the effective boolean the browser acts on.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/draggable
+ ///
+ public static async ValueTask GetDraggable(this ElementReference element)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getDraggable", element);
+ ///
+ /// Whether the element can be dragged.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/draggable
+ ///
+ public static async ValueTask SetDraggable(this ElementReference element, bool value)
+ => await ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setDraggable", element, value);
+
+ ///
+ /// The name this element is reported under in Element Timing performance entries. Empty when
+ /// the element is not being timed.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/elementTiming
+ ///
+ public static ValueTask GetElementTiming(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getElementTiming", element);
+ ///
+ /// Marks the element for Element Timing under the given name, so a
+ /// PerformanceObserver watching "element" entries reports when it was painted.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/elementTiming
+ ///
+ ///
+ /// Only an element that has not been painted yet can be timed - setting this on something
+ /// already on screen reports nothing.
+ ///
+ public static ValueTask SetElementTiming(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setElementTiming", element, value);
+
+ ///
+ /// Whether the element hosts a shadow root that scripts can reach - an open one.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/shadowRoot
+ ///
+ ///
+ /// False for a closed shadow root as well as for no shadow root at all: a closed one is not
+ /// exposed to script, which is the point of closing it.
+ ///
+ public static async ValueTask HasShadowRoot(this ElementReference element)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.hasShadowRoot", element);
+
+ ///
+ /// The element's language, as a BCP 47 tag. Empty when it inherits one.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/lang
+ ///
+ public static ValueTask GetLang(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getLang", element);
+ ///
+ /// The element's language, as a BCP 47 tag.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/lang
+ ///
+ public static ValueTask SetLang(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setLang", element, value);
+
+ ///
+ /// The element's local name, without a namespace prefix and in the case the document uses -
+ /// lowercase in HTML, where GetTagName reports uppercase.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/localName
+ ///
+ public static ValueTask GetLocalName(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.localName", element);
+
+ ///
+ /// The namespace the element belongs to - the XHTML namespace for HTML elements, and the SVG
+ /// one for anything inside an <svg>.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/namespaceURI
+ ///
+ public static ValueTask GetNamespaceUri(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.namespaceURI", element);
+
+ ///
+ /// The element's CSP nonce. Empty when it has none.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/nonce
+ ///
+ ///
+ /// Browsers hide the nonce attribute from scripts so an injected selector cannot read it;
+ /// the property is what remains, and only same-origin script can reach it.
+ ///
+ public static ValueTask GetNonce(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getNonce", element);
+ ///
+ /// The element's CSP nonce.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/nonce
+ ///
+ public static ValueTask SetNonce(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setNonce", element, value);
+
+ ///
+ /// The tag name of the element the offset metrics are measured against - the nearest positioned
+ /// ancestor. Empty when there is none, which is also what a display:none element reports.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
+ ///
+ ///
+ /// The DOM property is the ancestor element itself; an can only
+ /// be minted by Blazor's renderer, so what crosses the boundary is its tag name.
+ ///
+ public static ValueTask GetOffsetParentTagName(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.offsetParentTagName", element);
+
+ ///
+ /// The element's rendered text, the way InnerText reports it.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/outerText
+ ///
+ public static ValueTask GetOuterText(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getOuterText", element);
+ ///
+ /// Replaces the element itself - not its contents - with the given text.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/outerText
+ ///
+ ///
+ /// The element is gone afterwards and the reference is dangling. This is the asymmetry the DOM
+ /// itself has: reading gives the text inside, writing removes the element.
+ ///
+ public static ValueTask SetOuterText(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setOuterText", element, value);
+
+ ///
+ /// The shadow parts the element exposes to the outside - what a ::part() selector can
+ /// reach it by.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/part
+ ///
+ public static async ValueTask GetPart(this ElementReference element)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getPart", element);
+ ///
+ /// Sets the shadow parts the element exposes, as a space-separated list.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/part
+ ///
+ public static ValueTask SetPart(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setPart", element, value);
+
+ ///
+ /// What kind of popover the element is, if any.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/popover
+ ///
+ public static async ValueTask GetPopover(this ElementReference element)
+ {
+ var value = await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getPopover", element);
+ return value switch
+ {
+ "auto" => ElementPopover.Auto,
+ "manual" => ElementPopover.Manual,
+ "hint" => ElementPopover.Hint,
+ _ => ElementPopover.NotSet,
+ };
+ }
+ ///
+ /// Makes the element a popover of the given kind, or - with
+ /// - stops it being one.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/popover
+ ///
+ public static async ValueTask SetPopover(this ElementReference element, ElementPopover value)
+ {
+ var v = value switch
+ {
+ ElementPopover.Auto => "auto",
+ ElementPopover.Manual => "manual",
+ ElementPopover.Hint => "hint",
+ _ => null,
+ };
+ await ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setPopover", element, v);
+ }
+
+ ///
+ /// The element's namespace prefix - "svg" in <svg:rect>. Empty when it has none,
+ /// which is the usual case in an HTML document.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/prefix
+ ///
+ public static ValueTask GetPrefix(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.prefix", element);
+
+ ///
+ /// The largest value SetScrollLeft will take - the element's scrollable width minus what
+ /// fits.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeftMax
+ ///
+ ///
+ /// A Firefox property. Everywhere else this computes the same figure from scrollWidth and
+ /// clientWidth, which is how the property is defined.
+ ///
+ public static async ValueTask GetScrollLeftMax(this ElementReference element)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.scrollLeftMax", element);
+
+ ///
+ /// The largest value SetScrollTop will take - the element's scrollable height minus what
+ /// fits.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTopMax
+ ///
+ ///
+ /// A Firefox property. Everywhere else this computes the same figure from scrollHeight
+ /// and clientHeight, which is how the property is defined.
+ ///
+ public static async ValueTask GetScrollTopMax(this ElementReference element)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.scrollTopMax", element);
+
+ ///
+ /// The name of the shadow-DOM slot the element asks to be placed in.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/slot
+ ///
+ public static ValueTask GetSlot(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getSlot", element);
+ ///
+ /// The name of the shadow-DOM slot the element asks to be placed in.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/slot
+ ///
+ public static ValueTask SetSlot(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setSlot", element, value);
+
+ ///
+ /// Whether the browser should spell-check what the user types into the element.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/spellcheck
+ ///
+ public static async ValueTask GetSpellcheck(this ElementReference element)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getSpellcheck", element);
+ ///
+ /// Whether the browser should spell-check what the user types into the element.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/spellcheck
+ ///
+ public static async ValueTask SetSpellcheck(this ElementReference element, bool value)
+ => await ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setSpellcheck", element, value);
+
+ ///
+ /// The element's advisory text - what a browser shows as a tooltip on hover.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/title
+ ///
+ public static ValueTask GetTitle(this ElementReference element)
+ => ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getTitle", element);
+ ///
+ /// The element's advisory text - what a browser shows as a tooltip on hover.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/title
+ ///
+ public static ValueTask SetTitle(this ElementReference element, string value)
+ => ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setTitle", element, value);
+
+ ///
+ /// Whether the element's text should be translated when the page is. True by default.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/translate
+ ///
+ public static async ValueTask GetTranslate(this ElementReference element)
+ => await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getTranslate", element);
+ ///
+ /// Whether the element's text should be translated when the page is. Set it false for code,
+ /// identifiers and proper nouns a machine translator would mangle.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/translate
+ ///
+ public static async ValueTask SetTranslate(this ElementReference element, bool value)
+ => await ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setTranslate", element, value);
+
+ ///
+ /// Who controls the on-screen keyboard for this editable element.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/virtualKeyboardPolicy
+ ///
+ /// Chromium only; reads as elsewhere.
+ public static async ValueTask GetVirtualKeyboardPolicy(this ElementReference element)
+ {
+ var value = await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getVirtualKeyboardPolicy", element);
+ return value switch
+ {
+ "auto" => VirtualKeyboardPolicy.Auto,
+ "manual" => VirtualKeyboardPolicy.Manual,
+ _ => VirtualKeyboardPolicy.NotSet,
+ };
+ }
+ ///
+ /// Who controls the on-screen keyboard for this editable element.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/virtualKeyboardPolicy
+ ///
+ public static async ValueTask SetVirtualKeyboardPolicy(this ElementReference element, VirtualKeyboardPolicy value)
+ {
+ var v = value switch
+ {
+ VirtualKeyboardPolicy.Auto => "auto",
+ VirtualKeyboardPolicy.Manual => "manual",
+ _ => "",
+ };
+ await ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setVirtualKeyboardPolicy", element, v);
+ }
+
+ ///
+ /// Whether the browser may offer inline writing suggestions inside the element. True unless the
+ /// element - or an ancestor - turned them off.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/writingSuggestions
+ ///
+ ///
+ /// The DOM property is the string "true" or "false"; this reports the boolean it stands for.
+ /// Engines without the feature report true, which is what they behave as.
+ ///
+ public static async ValueTask GetWritingSuggestions(this ElementReference element)
+ {
+ var value = await ElementReferenceExtensions.GetRuntime(element).Invoke("BitButil.element.getWritingSuggestions", element);
+ return value != "false";
+ }
+ ///
+ /// Whether the browser may offer inline writing suggestions inside the element. Set it false for
+ /// a field where an autocompleted phrase would be wrong - a password hint, a code editor.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/writingSuggestions
+ ///
+ public static async ValueTask SetWritingSuggestions(this ElementReference element, bool value)
+ => await ElementReferenceExtensions.GetRuntime(element).InvokeVoid("BitButil.element.setWritingSuggestions", element, value ? "true" : "false");
+}
diff --git a/src/Butil/Bit.Butil/Publics/Element/FocusOptions.cs b/src/Butil/Bit.Butil/Publics/Element/FocusOptions.cs
new file mode 100644
index 00000000000..064acb75e20
--- /dev/null
+++ b/src/Butil/Bit.Butil/Publics/Element/FocusOptions.cs
@@ -0,0 +1,27 @@
+namespace Bit.Butil;
+
+///
+/// How
+/// should behave beyond moving focus.
+///
+public class FocusOptions
+{
+ ///
+ /// True to leave the scroll position alone. By default the browser scrolls the newly focused
+ /// element into view, which is wrong for focus moved programmatically during an animation or
+ /// while restoring state.
+ ///
+ public bool? PreventScroll { get; set; }
+
+ ///
+ /// Whether the focus ring should be drawn, overriding the browser's own heuristic. Firefox only;
+ /// ignored elsewhere.
+ ///
+ public bool? FocusVisible { get; set; }
+
+ internal FocusJsOptions ToJsObject() => new()
+ {
+ PreventScroll = PreventScroll,
+ FocusVisible = FocusVisible
+ };
+}
diff --git a/src/Butil/Bit.Butil/Publics/Element/GetHtmlOptions.cs b/src/Butil/Bit.Butil/Publics/Element/GetHtmlOptions.cs
new file mode 100644
index 00000000000..f5dcc123000
--- /dev/null
+++ b/src/Butil/Bit.Butil/Publics/Element/GetHtmlOptions.cs
@@ -0,0 +1,20 @@
+namespace Bit.Butil;
+
+///
+/// How
+/// should treat shadow roots it meets while serializing.
+///
+public class GetHtmlOptions
+{
+ ///
+ /// True to serialize the contents of every shadow root that was attached with
+ /// serializable: true. Shadow content is omitted by default, which is why the innerHTML
+ /// of a component-heavy tree so often reads as empty.
+ ///
+ public bool? SerializableShadowRoots { get; set; }
+
+ internal GetHtmlJsOptions ToJsObject() => new()
+ {
+ SerializableShadowRoots = SerializableShadowRoots
+ };
+}
diff --git a/src/Butil/Bit.Butil/Publics/Element/InsertPosition.cs b/src/Butil/Bit.Butil/Publics/Element/InsertPosition.cs
new file mode 100644
index 00000000000..ddbcc524e0a
--- /dev/null
+++ b/src/Butil/Bit.Butil/Publics/Element/InsertPosition.cs
@@ -0,0 +1,17 @@
+namespace Bit.Butil;
+
+/// Where insertAdjacentHTML and insertAdjacentText put what they are given, relative to the element.
+public enum InsertPosition
+{
+ /// Immediately before the element itself.
+ BeforeBegin,
+
+ /// Inside the element, before its first child.
+ AfterBegin,
+
+ /// Inside the element, after its last child.
+ BeforeEnd,
+
+ /// Immediately after the element itself.
+ AfterEnd
+}
diff --git a/src/Butil/Bit.Butil/Publics/Element/VirtualKeyboardPolicy.cs b/src/Butil/Bit.Butil/Publics/Element/VirtualKeyboardPolicy.cs
new file mode 100644
index 00000000000..0802181cede
--- /dev/null
+++ b/src/Butil/Bit.Butil/Publics/Element/VirtualKeyboardPolicy.cs
@@ -0,0 +1,14 @@
+namespace Bit.Butil;
+
+/// Who decides when the on-screen keyboard appears for a contenteditable element.
+public enum VirtualKeyboardPolicy
+{
+ /// The attribute is absent - the browser behaves as if it were .
+ NotSet,
+
+ /// The browser shows and hides the keyboard as focus moves. The default.
+ Auto,
+
+ /// The page controls it through the VirtualKeyboard API instead.
+ Manual
+}
diff --git a/src/Butil/Bit.Butil/Publics/ElementReferenceExtensions.cs b/src/Butil/Bit.Butil/Publics/ElementReferenceExtensions.cs
index 609a081a121..1b9f498afeb 100644
--- a/src/Butil/Bit.Butil/Publics/ElementReferenceExtensions.cs
+++ b/src/Butil/Bit.Butil/Publics/ElementReferenceExtensions.cs
@@ -47,6 +47,68 @@ private static IJSRuntime GetJSRuntime(ElementReference elementReference)
public static ValueTask Blur(this ElementReference element)
=> GetJSRuntime(element).InvokeVoid("BitButil.element.blur", element);
+ ///
+ /// Returns whether the element is visible: laid out, not display:none, and - when asked
+ /// for by - not transparent, not visibility:hidden and not
+ /// inside a skipped content-visibility:auto subtree.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/checkVisibility
+ ///
+ ///
+ /// During prerender/SSR (no JS runtime) this returns default (e.g. false/0)
+ /// rather than throwing, so the result can't be distinguished from a genuine value. If you
+ /// branch on it, defer the read to OnAfterRenderAsync.
+ ///
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CheckVisibilityJsOptions))]
+ public static async ValueTask CheckVisibility(this ElementReference element, CheckVisibilityOptions? options = null)
+ => await GetJSRuntime(element).Invoke("BitButil.element.checkVisibility", element, options?.ToJsObject());
+
+ ///
+ /// Sends a synthetic click to the element, exactly as a real one would arrive: the element's own
+ /// handlers run, the event bubbles, and default behaviour (submitting a form, following a link,
+ /// toggling a checkbox) happens.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click
+ ///
+ ///
+ /// A synthetic click is not a user gesture. APIs that require one - fullscreen, clipboard writes,
+ /// popup windows - still refuse when reached this way.
+ ///
+ public static ValueTask Click(this ElementReference element)
+ => GetJSRuntime(element).InvokeVoid("BitButil.element.click", element);
+
+ ///
+ /// Returns whether the element, or any ancestor of it, matches -
+ /// the "am I inside one of these?" test.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
+ ///
+ ///
+ /// The DOM method hands back the matching ancestor itself. An can
+ /// only be minted by Blazor's renderer for an element it rendered, so there is nothing to hand
+ /// back across the boundary and the answer is the match itself.
+ ///
+ /// During prerender/SSR (no JS runtime) this returns default (e.g. false/0)
+ /// rather than throwing, so the result can't be distinguished from a genuine value. If you
+ /// branch on it, defer the read to OnAfterRenderAsync.
+ ///
+ public static async ValueTask Closest(this ElementReference element, string selectors)
+ => await GetJSRuntime(element).Invoke("BitButil.element.closest", element, selectors);
+
+ ///
+ /// Gives the element keyboard focus, scrolling it into view unless
+ /// says otherwise.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
+ ///
+ ///
+ /// Blazor's own ElementReference.FocusAsync covers the no-options case; this overload is
+ /// for when the scroll or the focus ring has to be controlled.
+ ///
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FocusJsOptions))]
+ public static ValueTask Focus(this ElementReference element, FocusOptions? options = null)
+ => GetJSRuntime(element).InvokeVoid("BitButil.element.focus", element, options?.ToJsObject());
+
///
/// Retrieves the value of the named attribute from the current node and returns it as a string.
///
@@ -55,6 +117,15 @@ public static ValueTask Blur(this ElementReference element)
public static ValueTask GetAttribute(this ElementReference element, string name)
=> GetJSRuntime(element).Invoke("BitButil.element.getAttribute", element, name);
+ ///
+ /// Retrieves the value of an attribute in the given namespace - what SVG's xlink:href and
+ /// XML's xml:lang need, and what GetAttribute cannot reach.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttributeNS
+ ///
+ public static ValueTask GetAttributeNS(this ElementReference element, string namespaceUri, string localName)
+ => GetJSRuntime(element).Invoke("BitButil.element.getAttributeNS", element, namespaceUri, localName);
+
///
/// Returns an array of attribute names from the current element.
///
@@ -84,6 +155,19 @@ public static async ValueTask GetBoundingClientRect(this ElementReference
public static async ValueTask HasAttribute(this ElementReference element, string name)
=> await GetJSRuntime(element).Invoke("BitButil.element.hasAttribute", element, name);
+ ///
+ /// Returns whether the element carries the named attribute in the given namespace.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/hasAttributeNS
+ ///
+ ///
+ /// During prerender/SSR (no JS runtime) this returns default (e.g. false/0)
+ /// rather than throwing, so the result can't be distinguished from a genuine value. If you
+ /// branch on it, defer the read to OnAfterRenderAsync.
+ ///
+ public static async ValueTask HasAttributeNS(this ElementReference element, string namespaceUri, string localName)
+ => await GetJSRuntime(element).Invoke("BitButil.element.hasAttributeNS", element, namespaceUri, localName);
+
///
/// Returns a boolean value indicating if the element has one or more HTML attributes present.
///
@@ -147,6 +231,14 @@ public static async ValueTask Remove(this ElementReference element)
public static async ValueTask RemoveAttribute(this ElementReference element, string name)
=> await GetJSRuntime(element).InvokeVoid("BitButil.element.removeAttribute", element, name);
+ ///
+ /// Removes the named attribute in the given namespace.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttributeNS
+ ///
+ public static async ValueTask RemoveAttributeNS(this ElementReference element, string namespaceUri, string localName)
+ => await GetJSRuntime(element).InvokeVoid("BitButil.element.removeAttributeNS", element, namespaceUri, localName);
+
///
/// Asynchronously asks the browser to make the element fullscreen.
///
@@ -218,6 +310,24 @@ public static async ValueTask ScrollIntoView(this ElementReference element, bool
public static async ValueTask ScrollIntoView(this ElementReference element, ScrollIntoViewOptions options)
=> await GetJSRuntime(element).InvokeVoid("BitButil.element.scrollIntoView", element, null, options?.ToJsObject());
+ ///
+ /// Scrolls to a particular set of coordinates inside a given element. The same operation as
+ /// Scroll, under the name the DOM also publishes it as.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTo
+ ///
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ScrollJsOptions))]
+ public static async ValueTask ScrollTo(this ElementReference element, ScrollOptions? options)
+ => await GetJSRuntime(element).InvokeVoid("BitButil.element.scrollTo", element, options?.ToJsObject(), null, null);
+ ///
+ /// Scrolls to a particular set of coordinates inside a given element. The same operation as
+ /// Scroll, under the name the DOM also publishes it as.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTo
+ ///
+ public static async ValueTask ScrollTo(this ElementReference element, double? x, double? y)
+ => await GetJSRuntime(element).InvokeVoid("BitButil.element.scrollTo", element, null, x, y);
+
///
/// Sets the value of a named attribute of the current node.
///
@@ -233,6 +343,19 @@ public static async ValueTask ScrollIntoView(this ElementReference element, Scro
public static async ValueTask SetAttribute(this ElementReference element, string name, string value)
=> await GetJSRuntime(element).InvokeVoid("BitButil.element.setAttribute", element, name, value);
+ ///
+ /// Sets an attribute in the given namespace, creating it when absent.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttributeNS
+ ///
+ ///
+ /// Security note: the same caveat as SetAttribute - values are written verbatim and
+ /// bypass Blazor's encoding, so a namespaced xlink:href holding a javascript: URL is
+ /// an XSS vector. Validate untrusted input before passing it here.
+ ///
+ public static async ValueTask SetAttributeNS(this ElementReference element, string namespaceUri, string qualifiedName, string value)
+ => await GetJSRuntime(element).InvokeVoid("BitButil.element.setAttributeNS", element, namespaceUri, qualifiedName, value);
+
///
/// Designates a specific element as the capture target of future pointer events.
///
@@ -417,6 +540,14 @@ public static async ValueTask GetScrollHeight(this ElementReference eleme
///
public static async ValueTask GetScrollLeft(this ElementReference element)
=> await GetJSRuntime(element).Invoke("BitButil.element.scrollLeft", element);
+ ///
+ /// Sets how far the element's content is scrolled from its left edge. Clamped by the browser to
+ /// the scrollable range.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft
+ ///
+ public static async ValueTask SetScrollLeft(this ElementReference element, double value)
+ => await GetJSRuntime(element).InvokeVoid("BitButil.element.setScrollLeft", element, value);
///
/// A number representing number of pixels the top of the element is scrolled vertically.
@@ -430,6 +561,14 @@ public static async ValueTask GetScrollLeft(this ElementReference element
///
public static async ValueTask GetScrollTop(this ElementReference element)
=> await GetJSRuntime(element).Invoke("BitButil.element.scrollTop", element);
+ ///
+ /// Sets how far the element's content is scrolled from its top edge. Clamped by the browser to
+ /// the scrollable range.
+ ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop
+ ///
+ public static async ValueTask SetScrollTop(this ElementReference element, double value)
+ => await GetJSRuntime(element).InvokeVoid("BitButil.element.setScrollTop", element, value);
///
/// Returns a number representing the scroll view width of the element.
diff --git a/src/Butil/Bit.Butil/Scripts/element.ts b/src/Butil/Bit.Butil/Scripts/element.ts
index 131972d4bd0..d2fb6f98802 100644
--- a/src/Butil/Bit.Butil/Scripts/element.ts
+++ b/src/Butil/Bit.Butil/Scripts/element.ts
@@ -5,44 +5,135 @@ var BitButil = (window as any).BitButil = (window as any).BitButil || {};
const _elementHandlers: { [listenerId: string]: { element: HTMLElement, eventName: string, handler: any, options: any } } = {};
butil.element = {
+ after(element: HTMLElement, nodes: string[]) { element.after(...nodes) },
+ append(element: HTMLElement, nodes: string[]) { element.append(...nodes) },
+ ariaNotify,
+ before(element: HTMLElement, nodes: string[]) { element.before(...nodes) },
blur(element: HTMLElement) { element.blur() },
+ checkVisibility,
+ classListAdd(element: HTMLElement, tokens: string[]) { element.classList.add(...tokens) },
+ classListContains(element: HTMLElement, token: string) { return element.classList.contains(token) },
+ classListRemove(element: HTMLElement, tokens: string[]) { element.classList.remove(...tokens) },
+ classListReplace(element: HTMLElement, oldToken: string, newToken: string) { return element.classList.replace(oldToken, newToken) },
+ classListToggle(element: HTMLElement, token: string, force?: boolean) { return element.classList.toggle(token, force ?? undefined) },
+ click(element: HTMLElement) { element.click() },
+ closest(element: HTMLElement, selectors: string) { return !!element.closest(selectors) },
+ focus(element: HTMLElement, options?: FocusOptions) { options ? element.focus(options) : element.focus() },
getAttribute(element: HTMLElement, name: string) { return element.getAttribute(name) },
+ getAttributeNS(element: HTMLElement, namespaceUri: string, localName: string) { return element.getAttributeNS(namespaceUri, localName) },
getAttributeNames(element: HTMLElement) { return element.getAttributeNames() },
getBoundingClientRect(element: HTMLElement) { return element.getBoundingClientRect() },
+ getClassList(element: HTMLElement) { return Array.from(element.classList) },
+ getClientRects(element: HTMLElement) { return Array.from(element.getClientRects()).map(r => ({ x: r.x, y: r.y, width: r.width, height: r.height })) },
+ getData(element: HTMLElement, key: string) { return element.dataset[key] ?? null },
+ getDataNames(element: HTMLElement) { return Object.keys(element.dataset) },
+ getHTML,
hasAttribute(element: HTMLElement, name: string) { return element.hasAttribute(name) },
+ hasAttributeNS(element: HTMLElement, namespaceUri: string, localName: string) { return element.hasAttributeNS(namespaceUri, localName) },
hasAttributes(element: HTMLElement) { return element.hasAttributes() },
hasPointerCapture(element: HTMLElement, pointerId: number) { return element.hasPointerCapture(pointerId) },
+ hasShadowRoot(element: HTMLElement) { return !!element.shadowRoot },
+ hidePopover,
+ insertAdjacentHTML(element: HTMLElement, position: string, html: string) { element.insertAdjacentHTML(position as InsertPosition, html) },
+ insertAdjacentText(element: HTMLElement, position: string, text: string) { element.insertAdjacentText(position as InsertPosition, text) },
matches(element: HTMLElement, selectors: string) { return element.matches(selectors) },
+ prepend(element: HTMLElement, nodes: string[]) { element.prepend(...nodes) },
+ querySelectorAllCount(element: HTMLElement, selectors: string) { return element.querySelectorAll(selectors).length },
+ querySelectorMatches(element: HTMLElement, selectors: string) { return !!element.querySelector(selectors) },
releasePointerCapture(element: HTMLElement, pointerId: number) { element.releasePointerCapture(pointerId) },
remove(element: HTMLElement) { element.remove() },
removeAttribute(element: HTMLElement, name: string) { element.removeAttribute(name) },
+ removeAttributeNS(element: HTMLElement, namespaceUri: string, localName: string) { element.removeAttributeNS(namespaceUri, localName) },
+ removeData(element: HTMLElement, key: string) { delete element.dataset[key] },
+ replaceChildren(element: HTMLElement, nodes: string[]) { element.replaceChildren(...nodes) },
+ replaceWith(element: HTMLElement, nodes: string[]) { element.replaceWith(...nodes) },
requestFullScreen(element: HTMLElement, options?: FullscreenOptions) { return element.requestFullscreen(options) },
requestPointerLock(element: HTMLElement) { return element.requestPointerLock() },
scroll,
scrollBy,
scrollIntoView,
+ scrollTo: scroll,
setAttribute(element: HTMLElement, name: string, value: string) { return element.setAttribute(name, value) },
+ setAttributeNS(element: HTMLElement, namespaceUri: string, qualifiedName: string, value: string) { element.setAttributeNS(namespaceUri, qualifiedName, value) },
+ setData(element: HTMLElement, key: string, value: string) { element.dataset[key] = value },
+ setHTML,
+ setHTMLUnsafe,
setPointerCapture(element: HTMLElement, pointerId: number) { element.setPointerCapture(pointerId) },
+ showPopover,
toggleAttribute(element: HTMLElement, name: string, force?: boolean) { return element.toggleAttribute(name, force) },
+ togglePopover,
getAccessKey(element: HTMLElement) { return element.accessKey },
setAccessKey(element: HTMLElement, key: string) { element.accessKey = key },
+ accessKeyLabel(element: HTMLElement) { return element.accessKeyLabel },
+ getAria(element: HTMLElement, name: string) { return (element as any)[name] ?? null },
+ setAria(element: HTMLElement, name: string, value: string) { (element as any)[name] = value },
+ assignedSlotName(element: HTMLElement) { return element.assignedSlot?.name ?? null },
+ getAutocapitalize(element: HTMLElement) { return element.autocapitalize },
+ setAutocapitalize(element: HTMLElement, value: string) { element.autocapitalize = value },
+ getAutocorrect(element: HTMLElement) { return (element as any).autocorrect },
+ setAutocorrect(element: HTMLElement, value: boolean) { (element as any).autocorrect = value },
+ getAutofocus(element: HTMLElement) { return element.autofocus },
+ setAutofocus(element: HTMLElement, value: boolean) { element.autofocus = value },
getClassName(element: HTMLElement) { return element.className },
setClassName(element: HTMLElement, className: string) { element.className = className },
+ childElementCount(element: HTMLElement) { return element.childElementCount },
clientHeight(element: HTMLElement) { return element.clientHeight },
clientLeft(element: HTMLElement) { return element.clientLeft },
clientTop(element: HTMLElement) { return element.clientTop },
clientWidth(element: HTMLElement) { return element.clientWidth },
+ // Chromium-only; 1 is the value every other engine behaves as if it had.
+ currentCSSZoom(element: HTMLElement) { return (element as any).currentCSSZoom ?? 1 },
+ getDraggable(element: HTMLElement) { return element.draggable },
+ setDraggable(element: HTMLElement, value: boolean) { element.draggable = value },
+ getElementTiming(element: HTMLElement) { return (element as any).elementTiming ?? element.getAttribute('elementtiming') },
+ setElementTiming(element: HTMLElement, value: string) { element.setAttribute('elementtiming', value) },
getId(element: HTMLElement) { return element.id },
setId(element: HTMLElement, id: string) { element.id = id },
getInnerHTML(element: HTMLElement) { return element.innerHTML },
setInnerHTML(element: HTMLElement, innerHTML: string) { element.innerHTML = innerHTML },
+ getLang(element: HTMLElement) { return element.lang },
+ setLang(element: HTMLElement, value: string) { element.lang = value },
+ localName(element: HTMLElement) { return element.localName },
+ namespaceURI(element: HTMLElement) { return element.namespaceURI },
+ getNonce(element: HTMLElement) { return element.nonce ?? null },
+ setNonce(element: HTMLElement, value: string) { element.nonce = value },
+ offsetParentTagName(element: HTMLElement) { return element.offsetParent?.tagName ?? null },
getOuterHTML(element: HTMLElement) { return element.outerHTML },
setOuterHTML(element: HTMLElement, outerHTML: string) { element.outerHTML = outerHTML },
+ getOuterText(element: HTMLElement) { return element.outerText },
+ setOuterText(element: HTMLElement, value: string) { element.outerText = value },
+ getPart(element: HTMLElement) { return Array.from(element.part) },
+ setPart(element: HTMLElement, value: string) { element.setAttribute('part', value) },
+ getPopover(element: HTMLElement) { return element.popover ?? null },
+ setPopover(element: HTMLElement, value: string) { element.popover = value },
+ prefix(element: HTMLElement) { return element.prefix },
scrollHeight(element: HTMLElement) { return element.scrollHeight },
scrollLeft(element: HTMLElement) { return element.scrollLeft },
+ setScrollLeft(element: HTMLElement, value: number) { element.scrollLeft = value },
+ // Firefox-only, and its definition everywhere else is the difference of the two box widths.
+ scrollLeftMax(element: HTMLElement) { return (element as any).scrollLeftMax ?? (element.scrollWidth - element.clientWidth) },
scrollTop(element: HTMLElement) { return element.scrollTop },
+ setScrollTop(element: HTMLElement, value: number) { element.scrollTop = value },
+ scrollTopMax(element: HTMLElement) { return (element as any).scrollTopMax ?? (element.scrollHeight - element.clientHeight) },
scrollWidth(element: HTMLElement) { return element.scrollWidth },
+ getSlot(element: HTMLElement) { return element.slot },
+ setSlot(element: HTMLElement, value: string) { element.slot = value },
+ getSpellcheck(element: HTMLElement) { return element.spellcheck },
+ setSpellcheck(element: HTMLElement, value: boolean) { element.spellcheck = value },
+ getStyleProperty(element: HTMLElement, name: string) { return element.style.getPropertyValue(name) },
+ setStyleProperty(element: HTMLElement, name: string, value: string, priority?: string) { element.style.setProperty(name, value, priority ?? undefined) },
+ removeStyleProperty(element: HTMLElement, name: string) { return element.style.removeProperty(name) },
+ getStyleText(element: HTMLElement) { return element.style.cssText },
+ setStyleText(element: HTMLElement, value: string) { element.style.cssText = value },
tagName(element: HTMLElement) { return element.tagName },
+ getTitle(element: HTMLElement) { return element.title },
+ setTitle(element: HTMLElement, value: string) { element.title = value },
+ getTranslate(element: HTMLElement) { return element.translate },
+ setTranslate(element: HTMLElement, value: boolean) { element.translate = value },
+ getVirtualKeyboardPolicy(element: HTMLElement) { return (element as any).virtualKeyboardPolicy ?? null },
+ setVirtualKeyboardPolicy(element: HTMLElement, value: string) { (element as any).virtualKeyboardPolicy = value },
+ getWritingSuggestions(element: HTMLElement) { return (element as any).writingSuggestions ?? null },
+ setWritingSuggestions(element: HTMLElement, value: string) { (element as any).writingSuggestions = value },
getContentEditable(element: HTMLElement) { return element.contentEditable },
setContentEditable(element: HTMLElement, value: string) { return element.contentEditable = value },
isContentEditable(element: HTMLElement) { return element.isContentEditable },
@@ -95,6 +186,57 @@ var BitButil = (window as any).BitButil = (window as any).BitButil || {};
element.scrollIntoView(alignToTop ?? options);
}
+ // checkVisibility shipped later than the rest of this module. Where it is missing, the two
+ // conditions it started life as - a laid-out box, and no visibility:hidden above it - are what
+ // a caller is asking about, so answer those rather than reporting a visible element invisible.
+ function checkVisibility(element: HTMLElement, options?: any) {
+ const check = (element as any).checkVisibility;
+ if (typeof check === 'function') return options ? check.call(element, options) : check.call(element);
+
+ return element.getClientRects().length > 0 && getComputedStyle(element).visibility !== 'hidden';
+ }
+
+ function getHTML(element: HTMLElement, options?: any) {
+ const get = (element as any).getHTML;
+ return typeof get === 'function' ? get.call(element, options ?? undefined) : element.innerHTML;
+ }
+
+ // setHTML sanitizes; setHTMLUnsafe does not. Falling back from the sanitizing one to innerHTML
+ // would turn a safe call into an unsafe one silently, so it reports the gap instead.
+ function setHTML(element: HTMLElement, html: string, options?: any) {
+ const set = (element as any).setHTML;
+ if (typeof set !== 'function') throw new Error('Element.setHTML is not supported by this browser.');
+
+ set.call(element, html, options ?? undefined);
+ }
+
+ function setHTMLUnsafe(element: HTMLElement, html: string) {
+ const set = (element as any).setHTMLUnsafe;
+ if (typeof set === 'function') set.call(element, html);
+ else element.innerHTML = html;
+ }
+
+ function showPopover(element: HTMLElement) {
+ if (typeof element.showPopover === 'function') element.showPopover();
+ }
+
+ function hidePopover(element: HTMLElement) {
+ if (typeof element.hidePopover === 'function') element.hidePopover();
+ }
+
+ function togglePopover(element: HTMLElement, force?: boolean) {
+ if (typeof element.togglePopover !== 'function') return false;
+
+ return element.togglePopover(force ?? undefined);
+ }
+
+ // Experimental and Chromium-only: a no-op elsewhere rather than a throw, because an
+ // announcement that does not happen is not a failure of the page that asked for it.
+ function ariaNotify(element: HTMLElement, message: string, options?: any) {
+ const notify = (element as any).ariaNotify;
+ if (typeof notify === 'function') notify.call(element, message, options ?? undefined);
+ }
+
function subscribeEvent(element: HTMLElement, elementId: string, eventName: string, methodName: string,
dotNetRef: any, listenerId: string, argsMembers: string[], options: AddEventListenerOptions | boolean,
preventDefault: boolean, stopPropagation: boolean) {
@@ -120,4 +262,4 @@ var BitButil = (window as any).BitButil = (window as any).BitButil || {};
entry.element.removeEventListener(entry.eventName, entry.handler, entry.options);
} catch { /* element may already be detached */ }
}
-}(BitButil));
\ No newline at end of file
+}(BitButil));
diff --git a/src/Butil/README.md b/src/Butil/README.md
index 3fe7de0f711..d4b2972aaa3 100644
--- a/src/Butil/README.md
+++ b/src/Butil/README.md
@@ -97,7 +97,7 @@ registering everything.
| Service | What it wraps |
| --- | --- |
-| `ElementReference` extensions | Attributes, scrolling, fullscreen, pointer capture, per-element events |
+| `ElementReference` extensions | Attributes (namespaced too), ARIA and `role`, classes, `data-*`, inline style, content insertion, scrolling, layout metrics, fullscreen, popovers, pointer capture, per-element events |
| Animation extensions | The Web Animations API on any element |
| `Keyboard` | App-wide keyboard shortcuts with modifier support |
| `IntersectionObserver` | Element visibility inside the viewport or a scroll container |
diff --git a/src/Butil/Samples/Bit.Butil.Samples.Core/Pages/E2EPage.razor b/src/Butil/Samples/Bit.Butil.Samples.Core/Pages/E2EPage.razor
index c37d5dab3ec..695d12e42b8 100644
--- a/src/Butil/Samples/Bit.Butil.Samples.Core/Pages/E2EPage.razor
+++ b/src/Butil/Samples/Bit.Butil.Samples.Core/Pages/E2EPage.razor
@@ -1,4 +1,4 @@
-@page "/e2e"
+@page "/e2e"
@inject Bit.Butil.LocalStorage localStorage
@inject Bit.Butil.SessionStorage sessionStorage
@inject Bit.Butil.Cookie cookie
@@ -81,6 +81,37 @@
+
+
Element
+ @* The element extensions run against elements this harness renders once and never re-renders,
+ so a Blazor diff cannot undo what the interop writes between the click and the assertion. *@
+
+
Element extension target.
+
one
two
three
+
scroll space
+
+
hidden
+
original
+
popover body
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@code {
private const string TestCookieName = "butil_e2e";
private const string TestCookieValue = "v=1; b=hello world & again";
@@ -88,6 +119,17 @@
private void Set(string value) { _status = value; StateHasChanged(); }
+ private ElementReference _elementBox;
+ private ElementReference _elementContent;
+ private ElementReference _elementHidden;
+ private ElementReference _elementPopover;
+ private ElementReference _elementButton;
+ private ElementReference _elementInput;
+ private int _syntheticClicks;
+
+ /// The namespace SVG's href lives in - the usual reason for the *NS attribute overloads.
+ private const string XlinkNamespace = "http://www.w3.org/1999/xlink";
+
// ─── Storage ────────────────────────────────────────────────────────────────
private async Task LocalStorageSet()
{
@@ -292,6 +334,132 @@
Set($"history:scroll:{value}");
}
+ // ─── Element extensions ─────────────────────────────────────────────────────
+ private async Task ElementClick()
+ {
+ var before = _syntheticClicks;
+ await _elementButton.Click();
+ // The button's own @onclick ran, which is the whole point: a synthetic click is a real
+ // click as far as the page is concerned, gesture-gated APIs aside.
+ Set($"el:click:{_syntheticClicks - before}");
+ }
+ private async Task ElementFocus()
+ {
+ await _elementInput.Focus(new FocusOptions { PreventScroll = true });
+ var focused = await _elementInput.Matches(":focus");
+ Set($"el:focus:{focused}");
+ }
+ private async Task ElementVisibility()
+ {
+ var options = new CheckVisibilityOptions { OpacityProperty = true, VisibilityProperty = true };
+ var shown = await _elementBox.CheckVisibility(options);
+ var hidden = await _elementHidden.CheckVisibility(options);
+ Set($"el:vis:{shown}/{hidden}");
+ }
+ private async Task ElementClosest()
+ {
+ var inSection = await _elementBox.Closest("section");
+ var inTable = await _elementBox.Closest("table");
+ Set($"el:closest:{inSection}/{inTable}");
+ }
+ private async Task ElementClasses()
+ {
+ await _elementBox.AddClass("alpha", "beta");
+ var has = await _elementBox.ContainsClass("alpha");
+ var swapped = await _elementBox.ReplaceClass("beta", "gamma");
+ var toggled = await _elementBox.ToggleClass("alpha");
+ var list = await _elementBox.GetClassList();
+ await _elementBox.RemoveClass("gamma");
+ Set($"el:class:{has}/{swapped}/{toggled}/{list.Length}");
+ }
+ private async Task ElementData()
+ {
+ await _elementBox.SetData("userId", "42");
+ var value = await _elementBox.GetData("userId");
+ var names = await _elementBox.GetDataNames();
+ await _elementBox.RemoveData("userId");
+ var afterRemoval = await _elementBox.GetData("userId");
+ Set($"el:data:{value}/{names.Contains("userId")}/{string.IsNullOrEmpty(afterRemoval)}");
+ }
+ private async Task ElementStyle()
+ {
+ await _elementBox.SetStyleProperty("--butil-accent", "#123456");
+ var custom = await _elementBox.GetStyleProperty("--butil-accent");
+ await _elementBox.SetStyleProperty("outline-width", "3px", important: true);
+ var removed = await _elementBox.RemoveStyleProperty("outline-width");
+ Set($"el:style:{custom.Trim()}/{removed}");
+ }
+ private async Task ElementContent()
+ {
+ await _elementContent.ReplaceChildren("base");
+ await _elementContent.Append("+append");
+ await _elementContent.Prepend("prepend+");
+ await _elementContent.InsertAdjacentText(InsertPosition.BeforeEnd, "!");
+ var html = await _elementContent.GetHtml();
+ Set($"el:content:{html}");
+ }
+ private async Task ElementAria()
+ {
+ await _elementBox.SetRole("region");
+ await _elementBox.SetAriaLabel("butil-e2e");
+ await _elementBox.SetAriaExpanded("true");
+ var role = await _elementBox.GetRole();
+ var label = await _elementBox.GetAriaLabel();
+ var expanded = await _elementBox.GetAriaExpanded();
+ Set($"el:aria:{role}/{label}/{expanded}");
+ }
+ private async Task ElementNamespacedAttributes()
+ {
+ await _elementBox.SetAttributeNS(XlinkNamespace, "xlink:href", "#star");
+ var value = await _elementBox.GetAttributeNS(XlinkNamespace, "href");
+ var present = await _elementBox.HasAttributeNS(XlinkNamespace, "href");
+ await _elementBox.RemoveAttributeNS(XlinkNamespace, "href");
+ var afterRemoval = await _elementBox.HasAttributeNS(XlinkNamespace, "href");
+ Set($"el:ns:{value}/{present}/{afterRemoval}");
+ }
+ private async Task ElementQueries()
+ {
+ var matches = await _elementBox.QuerySelectorMatches("li");
+ var count = await _elementBox.QuerySelectorAllCount("li");
+ var none = await _elementBox.QuerySelectorAllCount("table");
+ Set($"el:query:{matches}/{count}/{none}");
+ }
+ private async Task ElementScrollOffsets()
+ {
+ await _elementBox.SetScrollTop(60);
+ var top = await _elementBox.GetScrollTop();
+ var max = await _elementBox.GetScrollTopMax();
+ await _elementBox.SetScrollTop(0);
+ Set($"el:scroll:{top > 0}/{max > 0}");
+ }
+ private async Task ElementClientRects()
+ {
+ var rects = await _elementBox.GetClientRects();
+ Set($"el:rects:{rects.Length > 0}/{rects.Length > 0 && rects[0].Width > 0}");
+ }
+ private async Task ElementPopoverState()
+ {
+ await _elementPopover.SetPopover(ElementPopover.Manual);
+ var kind = await _elementPopover.GetPopover();
+ var showing = await _elementPopover.TogglePopover();
+ await _elementPopover.HidePopover();
+ await _elementPopover.SetPopover(ElementPopover.NotSet);
+ Set($"el:popover:{kind}/{showing}");
+ }
+ private async Task ElementIdentity()
+ {
+ await _elementBox.SetTitle("butil-e2e-title");
+ await _elementBox.SetLang("fa-IR");
+ await _elementBox.SetDraggable(true);
+ var title = await _elementBox.GetTitle();
+ var lang = await _elementBox.GetLang();
+ var draggable = await _elementBox.GetDraggable();
+ var localName = await _elementBox.GetLocalName();
+ var children = await _elementBox.GetChildElementCount();
+ var zoom = await _elementBox.GetCurrentCssZoom();
+ Set($"el:identity:{title}/{lang}/{draggable}/{localName}/{children}/{zoom > 0}");
+ }
+
public class SamplePayload(int number, string label)
{
public int Number { get; set; } = number;
diff --git a/src/Butil/tests/Bit.Butil.Tests.E2E/ElementTests.cs b/src/Butil/tests/Bit.Butil.Tests.E2E/ElementTests.cs
new file mode 100644
index 00000000000..c2e201e21a6
--- /dev/null
+++ b/src/Butil/tests/Bit.Butil.Tests.E2E/ElementTests.cs
@@ -0,0 +1,118 @@
+using Bit.Butil.Tests.E2E.Infrastructure;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Bit.Butil.Tests.E2E;
+
+///
+/// The extensions, driven against a
+/// real element in a real engine.
+///
+///
+/// These wrappers are thin, which is exactly why they need a browser: nothing but a live DOM can
+/// tell a correctly spelled property from one the engine ignores, and a misspelling on either side
+/// of the interop boundary compiles cleanly and reads back as an empty string. Every case below
+/// therefore round-trips - it writes through the wrapper and reads back through another one, or
+/// through a state the DOM itself changes.
+///
+[TestClass]
+public class ElementTests : ButilPageTest
+{
+ [TestMethod]
+ public async Task Click_Runs_The_Elements_Own_Handler()
+ {
+ // The harness counts the Blazor @onclick, so this passing means the synthetic click really
+ // dispatched rather than merely not throwing.
+ await ClickAndExpectAsync("el-click", "el:click:1");
+ }
+
+ [TestMethod]
+ public async Task Focus_Moves_Keyboard_Focus_To_The_Element()
+ {
+ // Read back through Matches(":focus") rather than through anything Butil wrote itself.
+ await ClickAndExpectAsync("el-focus", "el:focus:True");
+ }
+
+ [TestMethod]
+ public async Task CheckVisibility_Separates_A_Rendered_Element_From_A_Display_None_One()
+ {
+ await ClickAndExpectAsync("el-visibility", "el:vis:True/False");
+ }
+
+ [TestMethod]
+ public async Task Closest_Reports_Matching_And_Non_Matching_Ancestors()
+ {
+ await ClickAndExpectAsync("el-closest", "el:closest:True/False");
+ }
+
+ [TestMethod]
+ public async Task ClassList_Add_Contains_Replace_Toggle_And_Remove_Round_Trip()
+ {
+ // The element starts with one class; add two, replace one, toggle one back off, so what
+ // survives is the original plus the replacement.
+ await ClickAndExpectAsync("el-classes", "el:class:True/True/False/2");
+ }
+
+ [TestMethod]
+ public async Task Dataset_Set_Get_Names_And_Remove_Round_Trip()
+ {
+ await ClickAndExpectAsync("el-data", "el:data:42/True/True");
+ }
+
+ [TestMethod]
+ public async Task Inline_Style_Handles_Custom_Properties_And_Removal()
+ {
+ // A custom property proves the write goes through setProperty rather than through a
+ // camel-cased CSSStyleDeclaration member, which cannot express "--butil-accent" at all.
+ await ClickAndExpectAsync("el-style", "el:style:#123456/3px");
+ }
+
+ [TestMethod]
+ public async Task Content_Insertion_Places_Text_In_The_Right_Order()
+ {
+ await ClickAndExpectAsync("el-content", "el:content:prepend+base+append!");
+ }
+
+ [TestMethod]
+ public async Task Aria_Properties_And_Role_Round_Trip()
+ {
+ // aria-expanded reads back as the string "true": these are enumerated attributes, and an
+ // absent one does not mean the same thing to a screen reader as "false".
+ await ClickAndExpectAsync("el-aria", "el:aria:region/butil-e2e/true");
+ }
+
+ [TestMethod]
+ public async Task Namespaced_Attributes_Round_Trip_And_Remove()
+ {
+ await ClickAndExpectAsync("el-ns", "el:ns:#star/True/False");
+ }
+
+ [TestMethod]
+ public async Task Query_Helpers_Report_Matches_And_Counts()
+ {
+ await ClickAndExpectAsync("el-query", "el:query:True/3/0");
+ }
+
+ [TestMethod]
+ public async Task Scroll_Offsets_Can_Be_Written_And_Their_Maximum_Read()
+ {
+ await ClickAndExpectAsync("el-scroll", "el:scroll:True/True");
+ }
+
+ [TestMethod]
+ public async Task GetClientRects_Reports_At_Least_One_Laid_Out_Box()
+ {
+ await ClickAndExpectAsync("el-rects", "el:rects:True/True");
+ }
+
+ [TestMethod]
+ public async Task Popover_Kind_Round_Trips_And_Toggle_Shows_It()
+ {
+ await ClickAndExpectAsync("el-popover", "el:popover:Manual/True");
+ }
+
+ [TestMethod]
+ public async Task Identity_Hints_And_Tree_Facts_Read_Back()
+ {
+ await ClickAndExpectAsync("el-identity", "el:identity:butil-e2e-title/fa-IR/True/div/3/True");
+ }
+}
diff --git a/src/Butil/tests/Bit.Butil.Tests.Mcp/ToolBehaviourTests.cs b/src/Butil/tests/Bit.Butil.Tests.Mcp/ToolBehaviourTests.cs
index 4225cff47df..f1d5d88a2bf 100644
--- a/src/Butil/tests/Bit.Butil.Tests.Mcp/ToolBehaviourTests.cs
+++ b/src/Butil/tests/Bit.Butil.Tests.Mcp/ToolBehaviourTests.cs
@@ -99,7 +99,7 @@ public async Task No_type_reference_exceeds_the_documented_cap()
// types are enormous - the extension classes are sixty members with their remarks - and one
// of them uncapped was 30,000 characters. The members are the answer, so the remarks are
// what goes, and the reference says where they went.
- foreach (var typeName in new[] { "ElementReferenceExtensions", "Window", "ButilKeyCodes", "Clipboard" })
+ foreach (var typeName in new[] { "ElementReferenceExtensions", "ElementReferenceAriaExtensions", "Window", "ButilKeyCodes", "Clipboard" })
{
var result = await CallStructuredAsync("GetButilApiDetails", new { typeName });
var text = Text(await CallRawAsync("GetButilApiDetails", new { typeName }));
@@ -125,6 +125,42 @@ public async Task No_type_reference_exceeds_the_documented_cap()
}
}
+ [TestMethod]
+ public async Task The_element_page_reaches_every_extension_class_its_members_live_on()
+ {
+ // The ElementReference surface is spread over several static classes rather than one, and
+ // nothing about a class makes an agent look for it: a member is only findable through the
+ // page that names the class it is on. A class added to the library and not to the docs nav
+ // is a member the tools cannot answer about, which no consistency check can notice - the
+ // page and the reflected assembly would each still be perfectly coherent on their own.
+ var expected = new Dictionary(StringComparer.Ordinal)
+ {
+ ["ElementReferenceExtensions"] = "Click",
+ ["ElementReferenceDomExtensions"] = "InsertAdjacentHtml",
+ ["ElementReferenceStateExtensions"] = "GetTitle",
+ ["ElementReferenceAriaExtensions"] = "SetAriaLabel",
+ ["ElementReferenceEventExtensions"] = "SubscribeEvent",
+ ["ElementReferenceMediaExtensions"] = "Play",
+ };
+
+ var page = (await DocsIndexAsync()).Single(row => row.Slug == "element");
+
+ using (Assert.Scope())
+ {
+ Assert.IsEmpty(expected.Keys.Except(page.Services, StringComparer.Ordinal),
+ "The Element docs page does not name every class its members live on.");
+
+ foreach (var (typeName, member) in expected)
+ {
+ var details = (await CallStructuredAsync("GetButilApiDetails", new { typeName })).Details;
+
+ Assert.IsNotNull(details, $"{typeName} has no reference.");
+ Assert.Contains(member, details!.Members.Select(m => m.Name),
+ $"{typeName} answers without {member}.");
+ }
+ }
+ }
+
[TestMethod]
public async Task Api_details_carry_the_signatures_and_the_shipped_documentation()
{