diff --git a/src/Htmxor/Http/HtmxResponse.cs b/src/Htmxor/Http/HtmxResponse.cs index 8c391aa..74d06ee 100644 --- a/src/Htmxor/Http/HtmxResponse.cs +++ b/src/Htmxor/Http/HtmxResponse.cs @@ -137,12 +137,25 @@ public HtmxResponse ReplaceUrl(string url) return this; } + /// + /// Allows you to specify how the response will be swapped. + /// + /// The hx-swap attributes supports modifiers for changing the behavior of the swap. + /// This object instance. + public HtmxResponse Reswap(string modifier) + { + headers[HtmxResponseHeaderNames.Reswap] = modifier; + + return this; + } + /// /// Allows you to specify how the response will be swapped. /// /// + /// The hx-swap attributes supports modifiers for changing the behavior of the swap. /// This object instance. - public HtmxResponse Reswap(SwapStyle swapStyle) + public HtmxResponse Reswap(SwapStyle swapStyle, string? modifier = null) { AssertIsHtmxRequest(); @@ -159,11 +172,26 @@ public HtmxResponse Reswap(SwapStyle swapStyle) _ => throw new SwitchExpressionException(swapStyle), }; - headers[HtmxResponseHeaderNames.Reswap] = style; + var value = modifier != null ? $"{style} {modifier}" : style; + + headers[HtmxResponseHeaderNames.Reswap] = value; return this; } + /// + /// Allows you to specify how the response will be swapped. + /// + /// + /// + /// + public HtmxResponse Reswap(SwapStyleBuilder swapStyle) + { + var (style, modifier) = swapStyle.Build(); + + return style is null ? Reswap(modifier) : Reswap((SwapStyle)style, modifier); + } + /// /// A CSS selector that updates the target of the content update to a different element on the page. /// @@ -192,6 +220,17 @@ public HtmxResponse Reselect(string selector) return this; } + /// + /// Sets response code to stop polling + /// + /// + public HtmxResponse StopPolling() + { + context.Response.StatusCode = HtmxStatusCodes.StopPolling; + + return this; + } + /// /// Allows you to trigger client-side events. /// diff --git a/src/Htmxor/Http/HtmxStatusCodes.cs b/src/Htmxor/Http/HtmxStatusCodes.cs new file mode 100644 index 0000000..06b2e78 --- /dev/null +++ b/src/Htmxor/Http/HtmxStatusCodes.cs @@ -0,0 +1,6 @@ +namespace Htmxor.Http; + +public static class HtmxStatusCodes +{ + public static readonly int StopPolling = 286; +} diff --git a/src/Htmxor/Http/SwapStyleBuilder.cs b/src/Htmxor/Http/SwapStyleBuilder.cs new file mode 100644 index 0000000..433eb40 --- /dev/null +++ b/src/Htmxor/Http/SwapStyleBuilder.cs @@ -0,0 +1,591 @@ +using System.Collections; +using System.Collections.Specialized; +using System.Numerics; + +namespace Htmxor.Http; + +/// +/// A builder class for constructing a swap style command string for HTMX responses. +/// +public sealed class SwapStyleBuilder +{ + private readonly SwapStyle? style; + private readonly OrderedDictionary modifiers = new(); + + /// + /// Initializes a new instance of the SwapStyleBuilder with a specified swap style. + /// + /// The initial swap style to be applied. + public SwapStyleBuilder(SwapStyle? style = null) + { + this.style = style; + } + + /// + /// Modifies the amount of time that htmx will wait after receiving a + /// response to swap the content by including the modifier swap:. + /// + /// + /// will be converted to milliseconds if less than 1000, otherwise seconds, + /// meaning the resulting modifier will be swap:500ms for a of 500 milliseconds + /// or swap:2s for a of 2 seconds.. + /// + /// The amount of time htmx should wait after receiving a + /// response to swap the content. + public SwapStyleBuilder AfterSwapDelay(TimeSpan time) + { + AddModifier("swap", time.TotalMilliseconds < 1000 ? $"{time.TotalMilliseconds}ms" : $"{time.TotalSeconds}s"); + + return this; + } + + /// + /// Modifies the amount of time that htmx will wait between the swap + /// and the settle logic by including the modifier settle:. + /// + /// + /// will be converted to milliseconds if less than 1000, otherwise seconds, + /// meaning the resulting modifier will be settle:500ms for a of 500 milliseconds + /// or settle:2s for a of 2 seconds.. + /// + /// The amount of time htmx should wait after receiving a + /// response to swap the content. + public SwapStyleBuilder AfterSettleDelay(TimeSpan time) + { + AddModifier("settle", time.TotalMilliseconds < 1000 ? $"{time.TotalMilliseconds}ms" : $"{time.TotalSeconds}s"); + + return this; + } + + /// + /// Specifies how to set the content scrollbar position after the swap and appends the modifier scroll:. + /// + /// + /// Sets the swapped content scrollbar position after swapping immediately (without animation). For instance, using + /// will add the modifier scroll:top which sets the scrollbar position to the top of swap content after the swap. + /// If css is present then the page is scrolled to the of the content identified by the css selector. + /// + /// The scroll direction after the swap. + /// Optional CSS selector of the target element. + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder Scroll(ScrollDirection direction, string? selector = null) + { + switch (direction) + { + case ScrollDirection.Top: + AddModifier("scroll", selector is null ? "top" : $"{selector}:top"); + break; + case ScrollDirection.Bottom: + AddModifier("scroll", selector is null ? "bottom" : $"{selector}:bottom"); + break; + } + + return this; + } + + /// + /// Sets the content scrollbar position to the top of the swapped content after a swap. + /// + /// + /// This method adds the modifier scroll:top to the swap commands, instructing the page to scroll to + /// the top of the content after content is swapped immediately and without animation. If css + /// is present then the page is scrolled to the top of the content identified by the css selector. + /// + /// Optional CSS selector of the target element. + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder ScrollTop(string? selector = null) => Scroll(ScrollDirection.Top, selector); + + /// + /// Sets the content scrollbar position to the bottom of the swapped content after a swap. + /// + /// + /// This method adds the modifier scroll:bottom to the swap commands, instructing the page to scroll to + /// the bottom of the content after content is swapped immediately and without animation. If css + /// is present then the page is scrolled to the bottom of the content identified by the css selector. + /// + /// Optional CSS selector of the target element. + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder ScrollBottom(string? selector = null) => Scroll(ScrollDirection.Bottom, selector); + + /// + /// Determines whether to ignore the document title in the swap response by appending the modifier + /// ignoreTitle:. + /// + /// + /// When set to true, the document title in the swap response will be ignored by adding the modifier + /// ignoreTitle:true. + /// This keeps the current title unchanged regardless of the incoming swap content's title tag. + /// + /// Whether to ignore the title. + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder IgnoreTitle(bool ignore = true) + { + AddModifier("ignoreTitle", ignore); + + return this; + } + + /// + /// Includes the document title from the swap response in the current page. + /// + /// + /// This method ensures the title of the document is updated according to the swap response by removing any + /// ignoreTitle modifiers, effectively setting ignoreTitle:false. + /// + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder IncludeTitle() => IgnoreTitle(false); + + /// + /// Enables or disables transition effects for the swap by appending the modifier transition:. + /// + /// + /// Controls the display of transition effects during the swap. For example, setting to true + /// will add the modifier transition:true to enable smooth transitions. + /// + /// Whether to show transition effects. + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder Transition(bool show) + { + AddModifier("transition", show); + + return this; + } + + /// + /// Explicitly includes transition effects for the swap. + /// + /// + /// By calling this method, transition effects are enabled for the swap, adding the modifier transition:true. + /// + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder IncludeTransition() => Transition(true); + + /// + /// Explicitly ignores transition effects for the swap. + /// + /// + /// This method disables transition effects by adding the modifier transition:false to the swap commands. + /// + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder IgnoreTransition() => Transition(false); + + /// + /// Allows you to specify that htmx should scroll to the focused element when a request completes. + /// htmx preserves focus between requests for inputs that have a defined id attribute. By + /// default htmx prevents auto-scrolling to focused inputs between requests which can be + /// unwanted behavior on longer requests when the user has already scrolled away. + /// + /// + /// when true will be focus-scroll:true, otherwise when false + /// will be focus-scroll:false + /// + /// Whether to scroll to the focus element. + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder ScrollFocus(bool scroll = true) + { + AddModifier("focus-scroll", scroll); + + return this; + } + + /// + /// Explicitly preserves focus between requests for inputs that have a defined id attribute without + /// scrolling. + /// + /// + /// Adds a modifier of focus-scroll:false + /// + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder PreserveFocus() => ScrollFocus(false); + + /// + /// Specifies a CSS selector to target for the swap operation, smoothly animating the scrollbar position to either the + /// top or the bottom of the target element after the swap. + /// + /// + /// Adds a show modifier with the specified CSS selector and scroll direction. For example, if + /// is ".item" and is , the modifier show:.item:top + /// is added. + /// + /// The scroll direction after swap. + /// Optional CSS selector of the target element. + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder ShowOn(ScrollDirection direction, string? selector = null) + { + switch (direction) + { + case ScrollDirection.Top: + AddModifier("show", selector is null ? "top" : $"{selector}:top"); + break; + case ScrollDirection.Bottom: + AddModifier("show", selector is null ? "bottom" : $"{selector}:bottom"); + break; + } + + return this; + } + + /// + /// Specifies that the swap should show the top of the element matching the CSS selector. + /// + /// + /// This method adds the modifier show::top, smoothly scrolling to the top of the element identified by + /// . + /// + /// Optional CSS selector of the target element. + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder ShowOnTop(string? selector = null) => ShowOn(ScrollDirection.Top, selector); + + /// + /// Specifies that the swap should show the bottom of the element matching the CSS selector. + /// + /// + /// This method adds the modifier show::bottom, smoothly scrolling to the bottom of the element identified by + /// . + /// + /// Optional CSS selector of the target element. + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder ShowOnBottom(string? selector = null) => ShowOn(ScrollDirection.Bottom, selector); + + /// + /// Specifies that the swap should show in the window by smoothly scrolling to either the top or bottom of the window. + /// + /// The direction to scroll the window after the swap. + /// + /// This method adds the modifier show:window:, directing the swap to display the specified + /// element at the bottom of the window. + /// + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder ShowWindow(ScrollDirection direction) + { + switch (direction) + { + case ScrollDirection.Top: + AddModifier("show", "window:top"); + break; + case ScrollDirection.Bottom: + AddModifier("show", "window:bottom"); + break; + } + + return this; + } + + /// + /// Specifies that the swap should smoothly scroll to the top of the window. + /// + /// + /// This method adds the modifier show:window:top, instructing the content to be displayed + /// at the top of the window following a swap by smoothly animating the scrollbar position. This can be useful + /// for ensuring that important content or notifications at the top of the page are immediately visible to + /// the user after a swap operation. + /// + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder ShowWindowTop() => ShowWindow(ScrollDirection.Top); + + /// + /// Specifies that the swap should smoothly scroll to the bottom of the window. + /// + /// + /// This method adds the modifier show:window:bottom, instructing the content to be displayed + /// at the bottom of the window following a swap by smoothly animating the scrollbar position. This positioning + /// can be used for infinite scrolling, footers, or information appended at the end of the page. + /// + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder ShowWindowBottom() => ShowWindow(ScrollDirection.Bottom); + + /// + /// Turns off scrolling after swap. + /// + /// + /// This method disables automatic scrolling by setting the modifier show:none, ensuring the page + /// position remains unchanged after the swap. + /// + /// The SwapStyleBuilder instance for chaining. + public SwapStyleBuilder ShowNone() + { + AddModifier("show", "none"); + + return this; + } + + /// + /// Builds the swap style command string with all specified modifiers. + /// + /// A tuple containing the SwapStyle and the constructed command string. + internal (SwapStyle?, string) Build() + { + var value = string.Empty; + + if (modifiers.Count > 0) + { + value = modifiers.Cast() + .Select(entry => $"{entry.Key}:{entry.Value}") + .Aggregate((current, next) => $"{current} {next}"); + } + + return (style, value); + } + + /// + /// Adds a modifier to modifiers, overriding existing values if present + /// + /// + /// + private void AddModifier(string modifier, string options) + { + if (modifiers.Contains(modifier)) + modifiers.Remove(modifier); + + modifiers.Add(modifier, options); + } + + /// + /// Adds a boolean modifier to modifiers + /// + /// + /// + private void AddModifier(string modifier, bool option) => AddModifier(modifier, option ? "true" : "false"); +} + +/// +/// Extension methods for the SwapStyle enum to facilitate building swap style commands. +/// +public static class SwapStyleBuilderExtension +{ + /// + /// Modifies the amount of time that htmx will wait after receiving a + /// response to swap the content by including the modifier swap:. + /// + /// + /// will be converted to milliseconds if less than 1000, otherwise seconds, + /// meaning the resulting modifier will be swap:500ms for a of 500 milliseconds + /// or swap:2s for a of 2 seconds.. + /// + /// The swap style. + /// The amount of time htmx should wait after receiving a + /// response to swap the content. + public static SwapStyleBuilder AfterSwapDelay(this SwapStyle style, TimeSpan time) => new SwapStyleBuilder(style).AfterSwapDelay(time); + + /// + /// Modifies the amount of time that htmx will wait between the swap + /// and the settle logic by including the modifier settle:. + /// + /// + /// will be converted to milliseconds if less than 1000, otherwise seconds, + /// meaning the resulting modifier will be settle:500ms for a of 500 milliseconds + /// or settle:2s for a of 2 seconds.. + /// + /// The swap style. + /// The amount of time htmx should wait after receiving a + /// response to swap the content. + public static SwapStyleBuilder AfterSettleDelay(this SwapStyle style, TimeSpan time) => new SwapStyleBuilder(style).AfterSettleDelay(time); + + /// + /// Specifies how to set the content scrollbar position after the swap and appends the modifier scroll:. + /// + /// + /// Sets the swapped content scrollbar position after swapping immediately (without animation). For instance, using + /// will add the modifier scroll:top which sets the scrollbar position to the top of swap content after the swap. + /// If css is present then the page is scrolled to the of the content identified by the css selector. + /// + /// The swap style. + /// The scroll direction after the swap. + /// Optional CSS selector of the target element. + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder Scroll(this SwapStyle style, ScrollDirection direction, string? selector) => new SwapStyleBuilder(style).Scroll(direction, selector); + + /// + /// Sets the content scrollbar position to the top of the swapped content after a swap. + /// + /// + /// This method adds the modifier scroll:top to the swap commands, instructing the page to scroll to + /// the top of the content after content is swapped immediately and without animation. If css + /// is present then the page is scrolled to the top of the content identified by the css selector. + /// + /// Optional CSS selector of the target element. + /// The swap style. + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder ScrollTop(this SwapStyle style, string? selector) => new SwapStyleBuilder(style).ScrollTop(selector); + + /// + /// Sets the content scrollbar position to the bottom of the swapped content after a swap. + /// + /// + /// This method adds the modifier scroll:bottom to the swap commands, instructing the page to scroll to + /// the bottom of the content after content is swapped immediately and without animation. If css + /// is present then the page is scrolled to the bottom of the content identified by the css selector. + /// + /// Optional CSS selector of the target element. + /// The swap style. + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder ScrollBottom(this SwapStyle style, string? selector) => new SwapStyleBuilder(style).ScrollBottom(selector); + + /// + /// Determines whether to ignore the document title in the swap response by appending the modifier + /// ignoreTitle:. + /// + /// + /// When set to true, the document title in the swap response will be ignored by adding the modifier + /// ignoreTitle:true. + /// This keeps the current title unchanged regardless of the incoming swap content's title tag. + /// + /// The swap style. + /// Whether to ignore the title. + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder IgnoreTitle(this SwapStyle style, bool ignore = true) => new SwapStyleBuilder(style).IgnoreTitle(ignore); + + /// + /// Includes the document title from the swap response in the current page. + /// + /// + /// This method ensures the title of the document is updated according to the swap response by removing any + /// ignoreTitle modifiers, effectively setting ignoreTitle:false. + /// + /// The swap style. + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder IncludeTitle(this SwapStyle style) => new SwapStyleBuilder(style).IncludeTitle(); + + /// + /// Enables or disables transition effects for the swap by appending the modifier transition:{show}. + /// + /// + /// Controls the display of transition effects during the swap. For example, setting to true + /// will add the modifier transition:true to enable smooth transitions. + /// + /// The swap style. + /// Whether to show transition effects. + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder Transition(this SwapStyle style, bool show) => new SwapStyleBuilder(style).Transition(show); + + /// + /// Explicitly includes transition effects for the swap. + /// + /// + /// By calling this method, transition effects are enabled for the swap, adding the modifier transition:true. + /// + /// The swap style. + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder IncludeTransition(this SwapStyle style) => new SwapStyleBuilder(style).IncludeTransition(); + + /// + /// Explicitly ignores transition effects for the swap. + /// + /// + /// This method disables transition effects by adding the modifier transition:false to the swap commands. + /// + /// The swap style. + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder IgnoreTransition(this SwapStyle style) => new SwapStyleBuilder(style).IgnoreTransition(); + + /// + /// Allows you to specify that htmx should scroll to the focused element when a request completes. + /// htmx preserves focus between requests for inputs that have a defined id attribute. By + /// default htmx prevents auto-scrolling to focused inputs between requests which can be + /// unwanted behavior on longer requests when the user has already scrolled away. + /// + /// + /// when true will be focus-scroll:true, otherwise when false + /// will be focus-scroll:false + /// + /// The swap style. + /// Whether to scroll to the focus element. + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder ScrollFocus(this SwapStyle style, bool scroll = true) => new SwapStyleBuilder(style).ScrollFocus(scroll); + + /// + /// Explicitly preserves focus between requests for inputs that have a defined id attribute without + /// scrolling. + /// + /// + /// Adds a modifier of focus-scroll:false + /// + /// The swap style. + /// Whether to scroll to current focus or preserve focus + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder PreserveFocus(this SwapStyle style, bool scroll = true) => new SwapStyleBuilder(style).PreserveFocus(); + + /// + /// Specifies a CSS selector to dynamically target for the swap operation, with a scroll direction after the swap. + /// + /// + /// Adds a show modifier with the specified CSS selector and scroll direction. For example, if + /// is ".item" and is , the modifier show:.item:top + /// is added. + /// + /// The swap style. + /// Optional CSS selector of the target element. + /// The scroll direction after swap. + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder ShowOn(this SwapStyle style, ScrollDirection direction, string? selector = null) => new SwapStyleBuilder(style).ShowOn(direction, selector); + + /// + /// Specifies that the swap should show the element matching the CSS selector at the top of the window. + /// + /// + /// This method adds the modifier show:{selector}:top, directing the swap to display the specified element at the top of the window. + /// + /// The swap style. + /// Optional CSS selector of the target element. + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder ShowOnTop(this SwapStyle style, string? selector = null) => new SwapStyleBuilder(style).ShowOnTop(selector); + + /// + /// Specifies that the swap should show the element matching the CSS selector at the bottom of the window. + /// + /// + /// This method adds the modifier show:{selector}:bottom, directing the swap to display the specified element at the bottom of the window. + /// + /// The swap style. + /// The CSS selector of the target element. + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder ShowOnBottom(this SwapStyle style, string? selector = null) => new SwapStyleBuilder(style).ShowOnBottom(selector); + + /// + /// Specifies that the swap should show in the window by smoothly scrolling to either the top or bottom of the window. + /// + /// The direction to scroll the window after the swap. + /// + /// This method adds the modifier show:window:, directing the swap to display the specified + /// element at the bottom of the window. + /// + /// The swap style. + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder ShowWindow(this SwapStyle style, ScrollDirection direction) => new SwapStyleBuilder(style).ShowWindow(direction); + + /// + /// Specifies that the swap should smoothly scroll to the top of the window. + /// + /// + /// This method adds the modifier show:window:top, instructing the content to be displayed + /// at the top of the window following a swap by smoothly animating the scrollbar position. This can be useful + /// for ensuring that important content or notifications at the top of the page are immediately visible to + /// the user after a swap operation. + /// + /// The swap style. + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder ShowWindowTop(this SwapStyle style) => new SwapStyleBuilder(style).ShowWindowTop(); + + /// + /// Specifies that the swap should smoothly scroll to the bottom of the window. + /// + /// + /// This method adds the modifier show:window:bottom, instructing the content to be displayed + /// at the bottom of the window following a swap by smoothly animating the scrollbar position. This positioning + /// can be used for infinite scrolling, footers, or information appended at the end of the page. + /// + /// The swap style. + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder ShowWindowBottom(this SwapStyle style) => new SwapStyleBuilder(style).ShowWindowBottom(); + + /// + /// Turns off scrolling after swap. + /// + /// + /// This method disables automatic scrolling by setting the modifier show:none, ensuring the page + /// position remains unchanged after the swap. + /// + /// The swap style. + /// The SwapStyleBuilder instance for chaining. + public static SwapStyleBuilder ShowNone(this SwapStyle style) => new SwapStyleBuilder(style).ShowNone(); +} \ No newline at end of file diff --git a/src/Htmxor/ScrollDirection.cs b/src/Htmxor/ScrollDirection.cs new file mode 100644 index 0000000..3ff2258 --- /dev/null +++ b/src/Htmxor/ScrollDirection.cs @@ -0,0 +1,10 @@ +namespace Htmxor; + +/// +/// Direction values for scroll and show modifier methods +/// +public enum ScrollDirection +{ + Top, + Bottom +} diff --git a/test/Htmxor.Tests/Http/SwapStyleBuilderTests.cs b/test/Htmxor.Tests/Http/SwapStyleBuilderTests.cs new file mode 100644 index 0000000..6f38856 --- /dev/null +++ b/test/Htmxor.Tests/Http/SwapStyleBuilderTests.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bunit; + +namespace Htmxor.Http; + +public class SwapStyleBuilderTests +{ + [Fact] + public void SwapStyleBuilder_InitializeAndBuild_ReturnsCorrectValues() + { + // Arrange + var swapStyle = SwapStyle.InnerHTML; + + // Act + var builder = new SwapStyleBuilder(swapStyle); + var (resultStyle, modifiers) = builder.Build(); + + // Assert + resultStyle.Should().Be(swapStyle); + modifiers.Should().BeEmpty(); // Expect no modifiers if none are added + } + + [Fact] + public void SwapStyleBuilder_AfterSwap_AddsCorrectDelay() + { + // Arrange + var builder = new SwapStyleBuilder(SwapStyle.InnerHTML); + + // Act + var (_, modifiers) = builder.AfterSwapDelay(TimeSpan.FromSeconds(1)).Build(); + + // Assert + modifiers.Should().Be("swap:1s"); + } + + [Fact] + public void SwapStyleBuilder_AfterSettle_AddsCorrectDelay() + { + // Arrange + var builder = new SwapStyleBuilder(SwapStyle.InnerHTML); + + // Act + var (_, modifiers) = builder.AfterSettleDelay(TimeSpan.FromSeconds(1)).Build(); + + // Assert + modifiers.Should().Be("settle:1s"); + } + + [Fact] + public void SwapStyleBuilder_Scroll_AddsCorrectDirection() + { + // Arrange + var builder = new SwapStyleBuilder(SwapStyle.InnerHTML); + + // Act + var (_, modifiers) = builder.Scroll(ScrollDirection.Bottom).Build(); + + // Assert + modifiers.Should().Be("scroll:bottom"); + } + + [Fact] + public void SwapStyleBuilder_IgnoreTitle_AddsCorrectFlag() + { + // Arrange + var builder = new SwapStyleBuilder(SwapStyle.InnerHTML); + + // Act + var (_, modifiers) = builder.IgnoreTitle(true).Build(); + + // Assert + modifiers.Should().Be("ignoreTitle:true"); + } + + [Fact] + public void SwapStyleBuilder_Transition_AddsCorrectFlag() + { + // Arrange + var builder = new SwapStyleBuilder(SwapStyle.InnerHTML); + + // Act + var (_, modifiers) = builder.Transition(true).Build(); + + // Assert + modifiers.Should().Be("transition:true"); + } + + [Fact] + public void SwapStyleBuilder_ScrollFocus_AddsCorrectFlag() + { + // Arrange + var builder = new SwapStyleBuilder(SwapStyle.InnerHTML); + + // Act + var (_, modifiers) = builder.ScrollFocus(true).Build(); + + // Assert + modifiers.Should().Be("focus-scroll:true"); + } + + [Fact] + public void SwapStyleBuilder_ShowOn_AddsCorrectSelectorAndDirection() + { + // Arrange + var builder = new SwapStyleBuilder(SwapStyle.InnerHTML); + var selector = "#dynamic-area"; + + // Act + var (_, modifiers) = builder.ShowOn(ScrollDirection.Top, selector).Build(); + + // Assert + modifiers.Should().Be("show:#dynamic-area:top"); + } + + [Fact] + public void SwapStyleBuilder_ShowWindow_AddsCorrectWindowAndDirection() + { + // Arrange + var builder = new SwapStyleBuilder(SwapStyle.InnerHTML); + + // Act + var (_, modifiers) = builder.ShowWindow(ScrollDirection.Top).Build(); + + // Assert + modifiers.Should().Be("show:window:top"); + } + + [Fact] + public void SwapStyleBuilder_ChainedOperations_AddsCorrectModifiers() + { + // Arrange + var builder = new SwapStyleBuilder(SwapStyle.InnerHTML); + + // Act + var (_, modifiers) = builder + .AfterSwapDelay(TimeSpan.FromSeconds(1)) + .Scroll(ScrollDirection.Top) + .Transition(true) + .IgnoreTitle(false) + .Build(); + + // Assert + modifiers.Should().Be("swap:1s scroll:top transition:true ignoreTitle:false"); + } + + [Fact] + public void SwapStyleBuilder_After_With250Milliseconds_AddsCorrectDelay() + { + // Arrange + var builder = new SwapStyleBuilder(SwapStyle.InnerHTML); + + // Act + var (_, modifiers) = builder.AfterSwapDelay(TimeSpan.FromMilliseconds(250)).Build(); + + // Assert + modifiers.Should().Be("swap:250ms"); + } + + [Fact] + public void SwapStyleBuilder_ShowOn_BottomDirection_AddsCorrectModifier() + { + // Arrange + var builder = new SwapStyleBuilder(SwapStyle.InnerHTML); + var selector = "#element-id"; + + // Act + var (_, modifiers) = builder.ShowOn(ScrollDirection.Bottom, selector).Build(); + + // Assert + modifiers.Should().Be("show:#element-id:bottom"); + } + + [Fact] + public void SwapStyleBuilder_ShowWindow_BottomDirection_AddsCorrectModifier() + { + // Arrange + var builder = new SwapStyleBuilder(SwapStyle.InnerHTML); + + // Act + var (_, modifiers) = builder.ShowWindow(ScrollDirection.Bottom).Build(); + + // Assert + modifiers.Should().Be("show:window:bottom"); + } + + [Fact] + public void SwapStyleBuilder_NullSwapStyle_ReturnsNullStyle() + { + // Arrange & Act + var builder = new SwapStyleBuilder(null); + var (style, _) = builder.Build(); + + // Assert + style.Should().BeNull(); + } + + [Fact] + public void SwapStyleBuilder_ShowNone_ReturnsCorrectValue() + { + // Arrange + var builder = new SwapStyleBuilder(SwapStyle.InnerHTML); + + // Act + var (_, modifiers) = builder.ShowNone().Build(); + + // Assert + modifiers.Should().Be("show:none"); + } + + [Fact] + public void SwapStyleBuilder_ShowOnTop_ReturnsCorrectValue() + { + // Arrange + var builder = new SwapStyleBuilder(SwapStyle.InnerHTML); + + // Act + var (_, modifiers) = builder.ShowOnTop().Build(); + + // Assert + modifiers.Should().Be("show:top"); + } + + [Fact] + public void SwapStyleBuilder_ShowOnBottom_ReturnsCorrectValue() + { + // Arrange + var builder = new SwapStyleBuilder(SwapStyle.InnerHTML); + + // Act + var (_, modifiers) = builder.ShowOnBottom().Build(); + + // Assert + modifiers.Should().Be("show:bottom"); + } + + [Fact] + public void SwapStyleBuilder_MixedShowOverrides_ReturnsCorrectValue() + { + // Arrange + var builder = new SwapStyleBuilder(SwapStyle.InnerHTML); + + // Act + var (_, modifiers) = builder.ShowOnTop().AfterSettleDelay(TimeSpan.FromMilliseconds(250)).ShowWindowTop().ShowOnBottom().Build(); + + // Assert + modifiers.Should().Be("settle:250ms show:bottom"); + } +}