From f49f07c8e3bb5eb70fd4902b276f2ed17f2df4c6 Mon Sep 17 00:00:00 2001 From: Erin McLaughlin Date: Mon, 20 May 2024 10:40:01 -0400 Subject: [PATCH] cleanup + added docblock comments --- src/HeadlessBlazor.Core/HBElement.cs | 18 +++- src/HeadlessBlazor.Core/HBElementBase.cs | 91 +++++++++++++------ src/HeadlessBlazor.Dropdown/HBDropdown.cs | 41 ++++++++- src/HeadlessBlazor.Dropdown/HBDropdownItem.cs | 10 ++ .../HBDropdownItemButton.cs | 4 + .../HBDropdownItemLink.cs | 4 + src/HeadlessBlazor.Dropdown/HBDropdownMenu.cs | 27 +++++- .../HBDropdownTrigger.cs | 12 ++- .../HBFloatAlignment.cs | 14 +++ .../HBFloatBehavior.razor | 23 ++++- .../HBFloatOptions.cs | 10 ++ .../HBFloatSide.cs | 18 ++++ .../HBOutsideClickBehavior.razor | 11 +++ 13 files changed, 240 insertions(+), 43 deletions(-) diff --git a/src/HeadlessBlazor.Core/HBElement.cs b/src/HeadlessBlazor.Core/HBElement.cs index 8db04c0..6fa5596 100644 --- a/src/HeadlessBlazor.Core/HBElement.cs +++ b/src/HeadlessBlazor.Core/HBElement.cs @@ -3,24 +3,38 @@ namespace HeadlessBlazor; +/// +/// A base class for implementing a HeadlessBlazor element with child content. +/// public class HBElement : HBElementBase { + /// + /// The child content. + /// [Parameter] public RenderFragment? ChildContent { get; set; } - protected override void AddChildContent(ref int sequence, RenderTreeBuilder builder) + /// + protected override void OnBeforeCloseElement(ref int sequence, RenderTreeBuilder builder) { if (ChildContent != null) builder.AddContent(sequence, ChildContent); } } +/// +/// A base class for implementing a HeadlessBlazor element with child content of type . +/// public class HBElement : HBElementBase where T : HBElement { + /// + /// The child content. + /// [Parameter] public RenderFragment? ChildContent { get; set; } - protected override void AddChildContent(ref int sequence, RenderTreeBuilder builder) + /// + protected override void OnBeforeCloseElement(ref int sequence, RenderTreeBuilder builder) { if (ChildContent != null) builder.AddContent(sequence, ChildContent, (T)this); diff --git a/src/HeadlessBlazor.Core/HBElementBase.cs b/src/HeadlessBlazor.Core/HBElementBase.cs index 5dbc9d2..fe9daa5 100644 --- a/src/HeadlessBlazor.Core/HBElementBase.cs +++ b/src/HeadlessBlazor.Core/HBElementBase.cs @@ -4,21 +4,35 @@ namespace HeadlessBlazor; +/// +/// A base class for all HeadlessBlazor components. +/// public abstract class HBElementBase : ComponentBase { + /// + /// The type of element to render. + /// public virtual string ElementName { get; set; } = "div"; + /// + /// The HTML attributes to apply to the element. + /// [Parameter(CaptureUnmatchedValues = true)] public virtual IDictionary UserAttributes { get; set; } = new Dictionary(); + /// + /// When , will stop propagation of click events. Default is . + /// [Parameter] public bool OnClickStopPropagation { get; set; } + /// + /// When , will stop default behavior of click events. Default is . + /// [Parameter] public bool OnClickPreventDefault { get; set; } - protected virtual void OnBeforeInitialized() { } - protected virtual void OnAfterInitialized() { } + /// protected sealed override void OnInitialized() { OnBeforeInitialized(); @@ -32,53 +46,76 @@ protected sealed override void OnInitialized() OnAfterInitialized(); } + /// + /// Called before the method. + /// + protected virtual void OnBeforeInitialized() + { + } + + /// + /// Called after the method. + /// + protected virtual void OnAfterInitialized() + { + } + + /// protected override void BuildRenderTree(RenderTreeBuilder builder) { var seq = 0; BuildRenderTree(ref seq, builder); } + /// + /// Renders the component to the supplied starting at the given number + /// + /// A reference to the sequence number. + /// A that will receive the render output. protected void BuildRenderTree(ref int sequence, RenderTreeBuilder builder) { - var createElement = !string.IsNullOrWhiteSpace(ElementName); + OnBeforeOpenElement(ref sequence, builder); - if (createElement) - { - builder.OpenElement(sequence++, ElementName); - - foreach (var attr in UserAttributes) - { - if (attr.Value != null) - builder.AddAttribute(sequence, attr.Key, attr.Value); - } - - AddEventHandlers(ref sequence, builder); - builder.AddEventStopPropagationAttribute(sequence, "onclick", OnClickStopPropagation); - builder.AddEventPreventDefaultAttribute(sequence, "onclick", OnClickPreventDefault); + builder.OpenElement(sequence++, ElementName); - AddElementReference(ref sequence, builder); + foreach (var attr in UserAttributes) + { + if (attr.Value != null) + builder.AddAttribute(sequence, attr.Key, attr.Value); } - AddBehaviors(ref sequence, builder); - AddChildContent(ref sequence, builder); + builder.AddEventPreventDefaultAttribute(sequence++, "onclick", OnClickPreventDefault); + builder.AddEventStopPropagationAttribute(sequence++, "onclick", OnClickStopPropagation); - if (createElement) - builder.CloseElement(); - } + OnBeforeCloseElement(ref sequence, builder); - protected virtual void AddChildContent(ref int sequence, RenderTreeBuilder builder) - { + builder.CloseElement(); } - protected virtual void AddBehaviors(ref int sequence, RenderTreeBuilder builder) + /// + /// Adds additional render content to the render tree before the element is opened. + /// + /// A reference to the sequence number. + /// A that will receive the render output. + protected virtual void OnBeforeOpenElement(ref int sequence, RenderTreeBuilder builder) { } - protected virtual void AddElementReference(ref int sequence, RenderTreeBuilder builder) + /// + /// Adds additional render content to the render tree before the element is closed. + /// + /// A reference to the sequence number. + /// A that will receive the render output. + protected virtual void OnBeforeCloseElement(ref int sequence, RenderTreeBuilder builder) { } - protected virtual void AddEventHandlers(ref int sequence, RenderTreeBuilder builder) + /// + /// Adds additional render content to the render tree after the element is closed. + /// + /// A reference to the sequence number. + /// A that will receive the render output. + protected virtual void OnAfterCloseElement(ref int sequence, RenderTreeBuilder builder) { } } diff --git a/src/HeadlessBlazor.Dropdown/HBDropdown.cs b/src/HeadlessBlazor.Dropdown/HBDropdown.cs index c2246e3..e0cea23 100644 --- a/src/HeadlessBlazor.Dropdown/HBDropdown.cs +++ b/src/HeadlessBlazor.Dropdown/HBDropdown.cs @@ -4,44 +4,77 @@ namespace HeadlessBlazor; +/// +/// The dropdown container. +/// public class HBDropdown : HBElement { + /// + /// A reference to the HTML element. + /// public ElementReference ElementReference { get; private set; } + /// + /// Indicates whether or not the dropdown menu is currently open. + /// Default is . + /// public bool IsOpen { get; private set; } + /// + /// When , the dropdown menu will close when the escape key is pressed. + /// Default is . + /// [Parameter] public bool CloseOnEscape { get; set; } = true; + /// + /// When , the dropdown menu will close when a click event occurs outside of the dropdown container. + /// Default is . + /// [Parameter] public bool CloseOnOutsideClick { get; set; } = true; + /// + /// The default behavior when a dropdown item is clicked. + /// If a value is not specified, the default click behavior is set to close the dropdown menu. + /// [Parameter] public EventCallback OnClickItem { get; set; } + /// + /// Opens the dropdown menu. + /// public async Task OpenAsync() => await InvokeAsync(() => { IsOpen = true; StateHasChanged(); }); + /// + /// Closes the dropdown menu. + /// public async Task CloseAsync() => await InvokeAsync(() => { IsOpen = false; StateHasChanged(); }); + /// + /// Toggles the dropdown menu. + /// public async Task ToggleAsync() { await (IsOpen ? CloseAsync() : OpenAsync()); } + /// protected override void OnAfterInitialized() { if (!OnClickItem.HasDelegate) OnClickItem = new EventCallback(this, CloseAsync); } + /// protected override void BuildRenderTree(RenderTreeBuilder builder) { var seq = 0; @@ -63,7 +96,8 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.CloseComponent(); } - protected override void AddEventHandlers(ref int sequence, RenderTreeBuilder builder) + /// + protected override void OnBeforeCloseElement(ref int sequence, RenderTreeBuilder builder) { if (CloseOnEscape) { @@ -73,14 +107,13 @@ protected override void AddEventHandlers(ref int sequence, RenderTreeBuilder bui await CloseAsync(); })); } - } - protected override void AddElementReference(ref int sequence, RenderTreeBuilder builder) - { builder.AddElementReferenceCapture(sequence++, async capturedRef => { ElementReference = capturedRef; await InvokeAsync(StateHasChanged); }); + + base.OnBeforeCloseElement(ref sequence, builder); } } diff --git a/src/HeadlessBlazor.Dropdown/HBDropdownItem.cs b/src/HeadlessBlazor.Dropdown/HBDropdownItem.cs index 4b6886d..ba77447 100644 --- a/src/HeadlessBlazor.Dropdown/HBDropdownItem.cs +++ b/src/HeadlessBlazor.Dropdown/HBDropdownItem.cs @@ -2,16 +2,26 @@ namespace HeadlessBlazor; +/// +/// A dropdown item. +/// public class HBDropdownItem : HBElement { + /// + /// The parent component. + /// [CascadingParameter] public HBDropdown Dropdown { get; set; } = default!; + /// protected override void OnParametersSet() { UserAttributes.TryAdd("onclick", new EventCallback(this, HandleClick)); } + /// + /// The click event handler. + /// protected virtual async Task HandleClick() { if (!OnClickStopPropagation) diff --git a/src/HeadlessBlazor.Dropdown/HBDropdownItemButton.cs b/src/HeadlessBlazor.Dropdown/HBDropdownItemButton.cs index 9773579..8426868 100644 --- a/src/HeadlessBlazor.Dropdown/HBDropdownItemButton.cs +++ b/src/HeadlessBlazor.Dropdown/HBDropdownItemButton.cs @@ -1,6 +1,10 @@ namespace HeadlessBlazor; +/// +/// A dropdown item that renders as an HTML button element. +/// public class HBDropdownItemButton : HBDropdownItem { + /// public override string ElementName { get; set; } = "button"; } diff --git a/src/HeadlessBlazor.Dropdown/HBDropdownItemLink.cs b/src/HeadlessBlazor.Dropdown/HBDropdownItemLink.cs index a647522..0c74e00 100644 --- a/src/HeadlessBlazor.Dropdown/HBDropdownItemLink.cs +++ b/src/HeadlessBlazor.Dropdown/HBDropdownItemLink.cs @@ -1,6 +1,10 @@ namespace HeadlessBlazor; +/// +/// A dropdown item that renders as an HTML anchor element. +/// public class HBDropdownItemLink : HBDropdownItem { + /// public override string ElementName { get; set; } = "a"; } diff --git a/src/HeadlessBlazor.Dropdown/HBDropdownMenu.cs b/src/HeadlessBlazor.Dropdown/HBDropdownMenu.cs index ed984d6..a393d12 100644 --- a/src/HeadlessBlazor.Dropdown/HBDropdownMenu.cs +++ b/src/HeadlessBlazor.Dropdown/HBDropdownMenu.cs @@ -3,23 +3,43 @@ namespace HeadlessBlazor; +/// +/// The dropdown menu. +/// public class HBDropdownMenu : HBElement { + /// + /// A reference to the HTML element. + /// public ElementReference ElementReference { get; private set; } + /// + /// The dropdown menu alignment, relative to the . + /// [Parameter] public HBFloatAlignment? Alignment { get; set; } + /// + /// When , the dropdown menu will be automatically positioned based on the available UI space. + /// Default is . + /// [Parameter] public bool AutoPosition { get; set; } = true; + /// + /// The parent component. + /// [CascadingParameter] protected HBDropdown Dropdown { get; set; } = default!; + /// + /// The dropdown menu side, relative to the . + /// [Parameter] public HBFloatSide? Side { get; set; } - protected override void AddBehaviors(ref int sequence, RenderTreeBuilder builder) + /// + protected override void OnBeforeCloseElement(ref int sequence, RenderTreeBuilder builder) { if (AutoPosition) { @@ -30,14 +50,13 @@ protected override void AddBehaviors(ref int sequence, RenderTreeBuilder builder builder.AddAttribute(sequence, nameof(HBFloatBehavior.Side), Side); builder.CloseComponent(); } - } - protected override void AddElementReference(ref int sequence, RenderTreeBuilder builder) - { builder.AddElementReferenceCapture(sequence, async (elementRef) => { ElementReference = elementRef; await InvokeAsync(StateHasChanged); }); + + base.OnBeforeCloseElement(ref sequence, builder); } } diff --git a/src/HeadlessBlazor.Dropdown/HBDropdownTrigger.cs b/src/HeadlessBlazor.Dropdown/HBDropdownTrigger.cs index 08bb261..7a7379b 100644 --- a/src/HeadlessBlazor.Dropdown/HBDropdownTrigger.cs +++ b/src/HeadlessBlazor.Dropdown/HBDropdownTrigger.cs @@ -2,17 +2,25 @@ namespace HeadlessBlazor; +/// +/// The dropdown trigger. +/// public class HBDropdownTrigger : HBElement { + /// + /// The parent component. + /// [CascadingParameter] public HBDropdown Dropdown { get; set; } = default!; + /// [Parameter] public override string ElementName { get; set; } = "button"; + /// protected override void OnParametersSet() { - UserAttributes.Add("hb-popover-anchor", ""); - UserAttributes.TryAdd("onclick", new EventCallback(this, Dropdown.ToggleAsync)); + if (!OnClickPreventDefault) + UserAttributes.TryAdd("onclick", new EventCallback(this, Dropdown.ToggleAsync)); } } diff --git a/src/HeadlessBlazor.FloatingElement/HBFloatAlignment.cs b/src/HeadlessBlazor.FloatingElement/HBFloatAlignment.cs index d0811a6..c3fc223 100644 --- a/src/HeadlessBlazor.FloatingElement/HBFloatAlignment.cs +++ b/src/HeadlessBlazor.FloatingElement/HBFloatAlignment.cs @@ -2,10 +2,24 @@ namespace HeadlessBlazor; +/// +/// The alignment of a floating element. +/// [JsonConverter(typeof(JsonStringEnumConverter))] public enum HBFloatAlignment { + /// + /// Align the floating element to the start of the anchor element. + /// Start, + + /// + /// Align the floating element to the center of the anchor element. + /// Center, + + /// + /// Align the floating element to the end of the anchor element. + /// End } diff --git a/src/HeadlessBlazor.FloatingElement/HBFloatBehavior.razor b/src/HeadlessBlazor.FloatingElement/HBFloatBehavior.razor index 1c86c53..41ef083 100644 --- a/src/HeadlessBlazor.FloatingElement/HBFloatBehavior.razor +++ b/src/HeadlessBlazor.FloatingElement/HBFloatBehavior.razor @@ -6,18 +6,31 @@ [Inject] private IJSRuntime JS { get; set; } = default!; + /// + /// The alignment of the floating element, relative to the . + /// [Parameter] public HBFloatAlignment? Alignment { get; set; } + /// + /// A reference to the anchor element. + /// [Parameter, EditorRequired] public required ElementReference Anchor { get; set; } + /// + /// A reference to the floating element. + /// [Parameter, EditorRequired] public required ElementReference Content { get; set; } + /// + /// The side to which the floating element should be attached. + /// [Parameter] public HBFloatSide? Side { get; set; } + /// public async ValueTask DisposeAsync() { if (popover is not null) @@ -28,6 +41,7 @@ } } + /// protected override async Task OnParametersSetAsync() { if (popover is not null) @@ -40,6 +54,7 @@ } } + /// protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) @@ -47,10 +62,10 @@ var moduleName = nameof(HBFloatBehavior); var module = await JS.InvokeAsync("import", $"./_content/HeadlessBlazor.FloatingElement/{moduleName}.razor.js"); popover = await module.InvokeAsync($"{moduleName}.createInstance", Anchor, Content, new HBFloatOptions - { - Alignment = Alignment, - Side = Side - }); + { + Alignment = Alignment, + Side = Side + }); await module.DisposeAsync(); } } diff --git a/src/HeadlessBlazor.FloatingElement/HBFloatOptions.cs b/src/HeadlessBlazor.FloatingElement/HBFloatOptions.cs index 4c0879b..03f4f04 100644 --- a/src/HeadlessBlazor.FloatingElement/HBFloatOptions.cs +++ b/src/HeadlessBlazor.FloatingElement/HBFloatOptions.cs @@ -1,7 +1,17 @@ namespace HeadlessBlazor; +/// +/// Options for rendering a floating element. +/// public sealed record HBFloatOptions { + /// + /// The alignment of the floating element, relative to the anchor. + /// public HBFloatAlignment? Alignment { get; init; } + + /// + /// The side of the anchor to render the floating element. + /// public HBFloatSide? Side { get; init; } } diff --git a/src/HeadlessBlazor.FloatingElement/HBFloatSide.cs b/src/HeadlessBlazor.FloatingElement/HBFloatSide.cs index cde0003..92bd1e4 100644 --- a/src/HeadlessBlazor.FloatingElement/HBFloatSide.cs +++ b/src/HeadlessBlazor.FloatingElement/HBFloatSide.cs @@ -2,11 +2,29 @@ namespace HeadlessBlazor; +/// +/// The side of the anchor to render the floating element. +/// [JsonConverter(typeof(JsonStringEnumConverter))] public enum HBFloatSide { + /// + /// Renders the floating element above the anchor. + /// Top, + + /// + /// Renders the floating element to the right of the anchor. + /// Right, + + /// + /// Renders the floating element below the anchor. + /// Bottom, + + /// + /// Renders the floating element to the left of the anchor. + /// Left } diff --git a/src/HeadlessBlazor.OutsideClick/HBOutsideClickBehavior.razor b/src/HeadlessBlazor.OutsideClick/HBOutsideClickBehavior.razor index 91bc7d9..6e0e2d1 100644 --- a/src/HeadlessBlazor.OutsideClick/HBOutsideClickBehavior.razor +++ b/src/HeadlessBlazor.OutsideClick/HBOutsideClickBehavior.razor @@ -10,12 +10,19 @@ [Inject] private IJSRuntime JS { get; set; } = default!; + /// + /// A reference to the container element. + /// [Parameter] public ElementReference Container { get; set; } = default!; + /// + /// A callback that will be invoked when the user clicks outside of the container. + /// [Parameter] public EventCallback OnClick { get; set; } + /// public async ValueTask DisposeAsync() { if (handlerRef is not null) @@ -28,12 +35,16 @@ dotNetObjRef?.Dispose(); } + /// + /// Invokes the event. + /// [JSInvokable] public async void NotifyClickOutside() { await OnClick.InvokeAsync(); } + /// protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender)