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 + +
+ +
+
+ Focus target + +
+
+ Closest selector + +
+
+ + + + +
+ +
+ + +
+ Class, data and style target. +
+
+
+ Class token + +
+
+ Dataset key + +
+
+ Dataset value + +
+
+
+ + + + + +
+
+ + + + +
+
+ + + +
+ +
+ +"); +await box.InsertAdjacentHtml(InsertPosition.AfterBegin, "trusted markup"); + +await box.SetHtml(untrustedMarkup); // sanitized; throws where unsupported +var html = await box.GetHtml(new GetHtmlOptions { SerializableShadowRoots = true }); +""")> +
+ Original content. +
+
+ Text or markup to insert + +
+
+ + + + + + + +
+ +
+ + +
+ ARIA target. +
+
+
+ aria-label + +
+
+ role + +
+
+
+ + + + +
+ +
+ + +
+ 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
+
+ +
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() {