From bbb6b53b2936f91e009d58e336a0b005f5742fdf Mon Sep 17 00:00:00 2001 From: msynk Date: Mon, 31 Aug 2026 19:42:34 +0330 Subject: [PATCH] apply BitLabel improvements #13081 --- .../Components/Utilities/Label/BitLabel.cs | 373 +++++++++++++++ .../Components/Utilities/Label/BitLabel.razor | 12 - .../Utilities/Label/BitLabel.razor.cs | 32 -- .../Components/Utilities/Label/BitLabel.scss | 64 ++- .../Utilities/Label/BitLabelClassStyles.cs | 32 ++ .../Utilities/Label/BitLabelParams.cs | 181 ++++++++ .../Utilities/Label/BitLabelDemo.razor | 223 ++++++++- .../Utilities/Label/BitLabelDemo.razor.cs | 394 +++++++++++++++- .../Utilities/Label/BitLabelDemo.razor.scss | 10 + .../Labels/BitLabelCascadingParamsTest.razor | 19 + .../Utilities/Labels/BitLabelTests.cs | 428 +++++++++++++++++- 11 files changed, 1687 insertions(+), 81 deletions(-) create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabel.cs delete mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabel.razor delete mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabel.razor.cs create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabelClassStyles.cs create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabelParams.cs create mode 100644 src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Utilities/Labels/BitLabelCascadingParamsTest.razor diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabel.cs b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabel.cs new file mode 100644 index 00000000000..bb4f4600a31 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabel.cs @@ -0,0 +1,373 @@ +using System.Globalization; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components.CompilerServices; + +namespace Bit.BlazorUI; + +/// +/// Labels give a name or a title to a control or to a group of controls, including text fields, check boxes, +/// combo boxes, radio buttons and drop-down menus. +/// +/// +/// The component renders a native "label" element, so the browser does the binding of itself: clicking the label +/// moves the focus to the control it names, and a screen reader announces that control by the label's text. The +/// control is named either by , which points at the id of a control anywhere on the page, or by +/// putting the control inside the label's own content. +///
+/// A native label may only name a single form control, which is why is there: the caption of a +/// group of controls - a set of radio buttons, a pair of range inputs - is not a label element but a plain element +/// referenced by the group's aria-labelledby, and naming another tag here is what renders it as one while keeping +/// the same look. +///
+/// and render the necessity of the field beside the text. Only one of +/// the two is ever rendered - a required field is not an optional one - and the default required asterisk is a +/// decoration that assistive technologies are meant to skip, since what makes a field announced as required is the +/// "required" (or "aria-required") attribute of the control itself, not a star in the text beside it. +///
+public partial class BitLabel : BitComponentBase +{ + /// + /// Gets or sets the cascading parameters for the label component. + /// + /// + /// This property receives its value from an ancestor component via Blazor's cascading parameter mechanism. + ///
+ /// The intended use is to allow shared configuration or settings to be applied to multiple label components through the component. + ///
+ [CascadingParameter(Name = BitLabelParams.ParamName)] + public BitLabelParams? CascadingParameters { get; set; } + + + + /// + /// The content of the label, which can be a text or any custom markup. + /// + /// + /// A form control put inside this content is named by the label without needing , which is the + /// implicit association of HTML and the one way to label a control that has no id of its own. + /// + [Parameter] public RenderFragment? ChildContent { get; set; } + + /// + /// Custom CSS classes for the different parts of the label. + /// + [Parameter, ResetClassBuilder] + public BitLabelClassStyles? Classes { get; set; } + + /// + /// The general color of the label. + /// + /// + /// The label inherits the color of its container while this is not set, which is what keeps it in step with the + /// text around it. Setting it is how a caption is given a meaning of its own - an one + /// beside a field that failed its validation, a one for a caption that + /// should sit back from the content it names. + /// + [Parameter, ResetClassBuilder] + public BitColor? Color { get; set; } + + /// + /// The custom html element used for the root node. The default is "label". + /// + /// + /// A native label may only name one form control, so the caption of a group of controls - a set of radio buttons, + /// a pair of inputs making up a range - is rendered as a plain element ("div", "span", "legend", ...) that the + /// group points at through its own aria-labelledby, rather than as a label that would name only the first control + /// in it. + ///
+ /// The value is used as written and only while it is a name a tag can have: a letter followed by letters, digits + /// and the "-", "_", "." and ":" that join them. Anything else falls back to the default "label", since a name + /// carrying a whitespace or a "<" would write markup of its own rather than name an element. + ///
+ /// is only rendered while the element is a label, since the "for" attribute belongs to the label + /// element alone and means nothing on any other tag. + ///
+ [Parameter] public string? Element { get; set; } + + /// + /// The id of the form control this label is bound to, rendered as the "for" attribute of the label element. + /// + /// + /// This is the explicit association of HTML: the value is the id of the control, not its name, and the control may + /// sit anywhere on the page. It is ignored while renders a tag other than a label. + /// + [Parameter] public string? For { get; set; } + + /// + /// Prevents the text of the label from being selected. + ///
+ /// The default value is false. + ///
+ /// + /// A click on a label is forwarded to the control it names, so a double click on one - whose second click lands on + /// the label rather than on the control - selects the label's text instead of doing anything to the control. This + /// turns that selection off for the labels where it is only ever an accident, such as the one over a checkbox or a + /// toggle that a user is expected to click. + /// + [Parameter, ResetClassBuilder] + public bool NoSelect { get; set; } + + /// + /// Keeps the label on a single line and truncates the overflow with an ellipsis. + ///
+ /// The default value is false. + ///
+ /// + /// The label wraps by default and breaks a word too long for its line rather than overflowing it. Truncation needs + /// a width to overflow: the label is a block element whichever tag renders, so it takes the + /// width of its container, and one given a display of its own through the style needs a width of its own for the + /// ellipsis to appear. + /// + [Parameter, ResetClassBuilder] + public bool NoWrap { get; set; } + + /// + /// Whether the associated field is optional, which renders an indicator after the content of the label. + ///
+ /// The default value is false. + ///
+ /// + /// This is the other half of , for the forms where most of the fields are required and it is + /// the exceptions that are worth marking. It is ignored while is set, and unlike the + /// required asterisk the indicator is a word rather than a symbol, so it is announced by assistive technologies + /// along with the rest of the label. + /// + [Parameter, ResetClassBuilder] + public bool Optional { get; set; } + + /// + /// The custom template of the optional indicator of the label. + /// + /// + /// Takes precedence over , and is only rendered while is set. + /// + [Parameter] public RenderFragment? OptionalTemplate { get; set; } + + /// + /// The text of the optional indicator of the label. The default is "(optional)". + /// + /// + /// This is what a localized form replaces the English default with. It is ignored while + /// is set, and is only rendered while is set. + /// + [Parameter] public string? OptionalText { get; set; } + + /// + /// Whether the associated field is required, which renders an indicator after the content of the label. + ///
+ /// The default value is false. + ///
+ /// + /// The indicator is an asterisk by default, and a default asterisk is hidden from assistive technologies: a star + /// read out in the middle of a caption says nothing about the field, and what makes a field announced as required + /// is the "required" or "aria-required" attribute of the control itself. An indicator given its own + /// or is announced with the rest of the label instead, + /// since a word put there deliberately is meant to be read. + /// + [Parameter, ResetClassBuilder] + public bool Required { get; set; } + + /// + /// The custom template of the required indicator of the label. + /// + /// + /// Takes precedence over , and is only rendered while is set. The + /// indicator it renders is announced by assistive technologies, unlike the default asterisk. + /// + [Parameter] public RenderFragment? RequiredTemplate { get; set; } + + /// + /// The text of the required indicator of the label. The default is "*". + /// + /// + /// This is what turns the asterisk into another mark or into a word ("(required)"). It is ignored while + /// is set, and is only rendered while is set. The indicator + /// it renders is announced by assistive technologies, unlike the default asterisk. + /// + [Parameter] public string? RequiredText { get; set; } + + /// + /// The size of the label. + /// + /// + /// The default is the medium size, which is the one matching the labels the input components of the library render + /// of their own. + /// + [Parameter, ResetClassBuilder] + public BitSize? Size { get; set; } + + /// + /// Custom CSS styles for the different parts of the label. + /// + [Parameter, ResetStyleBuilder] + public BitLabelClassStyles? Styles { get; set; } + + /// + /// Removes the label from the page while keeping it available to assistive technologies. + ///
+ /// The default value is false. + ///
+ /// + /// This is the accessible way to leave a control without a visible caption: the label is still in the accessibility + /// tree and still names its control, which neither a nor a + /// label is. A visible caption is what most users need, so this belongs to + /// the places where the surrounding design already says what the control is - a search box with a magnifier in it, + /// a cell of a table whose column header names the field. + /// + [Parameter, ResetClassBuilder] + public bool VisuallyHidden { get; set; } + + + + protected override string RootElementClass => "bit-lbl"; + + protected override void RegisterCssClasses() + { + ClassBuilder.Register(() => Classes?.Root); + + ClassBuilder.Register(() => Size switch + { + BitSize.Small => "bit-lbl-sm", + BitSize.Medium => "bit-lbl-md", + BitSize.Large => "bit-lbl-lg", + _ => string.Empty + }); + + ClassBuilder.Register(() => Color switch + { + BitColor.Primary => "bit-lbl-pri", + BitColor.Secondary => "bit-lbl-sec", + BitColor.Tertiary => "bit-lbl-ter", + BitColor.Info => "bit-lbl-inf", + BitColor.Success => "bit-lbl-suc", + BitColor.Warning => "bit-lbl-wrn", + BitColor.SevereWarning => "bit-lbl-swr", + BitColor.Error => "bit-lbl-err", + BitColor.PrimaryBackground => "bit-lbl-pbg", + BitColor.SecondaryBackground => "bit-lbl-sbg", + BitColor.TertiaryBackground => "bit-lbl-tbg", + BitColor.PrimaryForeground => "bit-lbl-pfg", + BitColor.SecondaryForeground => "bit-lbl-sfg", + BitColor.TertiaryForeground => "bit-lbl-tfg", + BitColor.PrimaryBorder => "bit-lbl-pbr", + BitColor.SecondaryBorder => "bit-lbl-sbr", + BitColor.TertiaryBorder => "bit-lbl-tbr", + _ => string.Empty + }); + + ClassBuilder.Register(() => Required ? "bit-lbl-req" : (Optional ? "bit-lbl-opt" : string.Empty)); + + ClassBuilder.Register(() => NoWrap ? "bit-lbl-nwr" : string.Empty); + + ClassBuilder.Register(() => NoSelect ? "bit-lbl-nsl" : string.Empty); + + ClassBuilder.Register(() => VisuallyHidden ? "bit-lbl-vhd" : string.Empty); + } + + protected override void RegisterCssStyles() + { + StyleBuilder.Register(() => Styles?.Root); + } + + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitLabelParams))] + protected override void OnParametersSet() + { + CascadingParameters?.UpdateParameters(this); + + base.OnParametersSet(); + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + var element = Element?.Trim(); + if (element.HasNoValue() || IsValidElement(element!) is false) + { + element = "label"; + } + + var isLabelElement = string.Equals(element, "label", StringComparison.OrdinalIgnoreCase); + + builder.OpenElement(0, element!); + builder.AddMultipleAttributes(1, RuntimeHelpers.TypeCheck(HtmlAttributes)); + builder.AddAttribute(2, "id", _Id); + builder.AddAttribute(3, "style", StyleBuilder.Value); + builder.AddAttribute(4, "class", ClassBuilder.Value); + builder.AddAttribute(5, "dir", Dir?.ToString().ToLower(CultureInfo.InvariantCulture)); + builder.AddAttribute(6, "aria-label", AriaLabel); + // A label is not focusable of itself, so the tab index is only ever written by a page that means to reach it - + // a "-1" for a caption the validation of a form moves the focus to, for instance. + builder.AddAttribute(7, "tabindex", TabIndex); + // The "for" attribute is defined on the label element alone: on any other tag it is markup that names nothing, + // and a group caption rendered through Element is pointed at by its group rather than pointing at a control. + builder.AddAttribute(8, "for", isLabelElement ? For : null); + builder.AddElementReferenceCapture(9, v => RootElement = v); + builder.AddContent(10, ChildContent); + + // A field is either required or optional, so the two indicators are the branches of one decision rather than + // two independent ones: a label asked for both renders the required one, which is the stronger statement. + if (Required) + { + builder.OpenElement(11, "span"); + builder.AddAttribute(12, "style", Styles?.RequiredIndicator); + builder.AddAttribute(13, "class", Classes?.RequiredIndicator.HasValue() is true + ? $"bit-lbl-rqi {Classes!.RequiredIndicator}" + : "bit-lbl-rqi"); + // The default asterisk is a decoration - a screen reader that announces it at all announces a "star" in the + // middle of the caption, which says nothing about the field, while the "required" attribute of the control + // is what does. An indicator written by the page is a word put there to be read, so it is left announced. + builder.AddAttribute(14, "aria-hidden", RequiredTemplate is null && RequiredText.HasNoValue() ? "true" : null); + if (RequiredTemplate is not null) + { + builder.AddContent(15, RequiredTemplate); + } + else + { + builder.AddContent(16, RequiredText ?? "*"); + } + builder.CloseElement(); + } + else if (Optional) + { + builder.OpenElement(17, "span"); + builder.AddAttribute(18, "style", Styles?.OptionalIndicator); + builder.AddAttribute(19, "class", Classes?.OptionalIndicator.HasValue() is true + ? $"bit-lbl-opi {Classes!.OptionalIndicator}" + : "bit-lbl-opi"); + if (OptionalTemplate is not null) + { + builder.AddContent(20, OptionalTemplate); + } + else + { + builder.AddContent(21, OptionalText ?? "(optional)"); + } + builder.CloseElement(); + } + + builder.CloseElement(); + + base.BuildRenderTree(builder); + } + + + + // The same reading of a tag name as BitElement's: what a name is made of rather than what it must not contain, + // since a name the browser refuses throws where the element is built and takes the whole render batch with it. + private static bool IsValidElement(string element) + { + if (char.IsAsciiLetter(element[0]) is false) return false; + + foreach (var @char in element) + { + if (char.IsAsciiLetterOrDigit(@char)) continue; + + if (@char is '-' or '_' or '.' or ':') continue; + + if (char.IsAscii(@char) is false && char.IsLetterOrDigit(@char)) continue; + + return false; + } + + return true; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabel.razor b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabel.razor deleted file mode 100644 index 4186ace5997..00000000000 --- a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabel.razor +++ /dev/null @@ -1,12 +0,0 @@ -@namespace Bit.BlazorUI -@inherits BitComponentBase - - diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabel.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabel.razor.cs deleted file mode 100644 index d9af75ef2ed..00000000000 --- a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabel.razor.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Bit.BlazorUI; - -/// -/// Labels give a name or title to a control or group of controls, including text fields, check boxes, combo boxes, radio buttons, and drop-down menus. -/// -public partial class BitLabel : BitComponentBase -{ - /// - /// The content of label, It can be Any custom tag or a text - /// - [Parameter] public RenderFragment? ChildContent { get; set; } - - /// - /// This attribute specifies which form element a label is bound to - /// - [Parameter] public string? For { get; set; } - - /// - /// Whether the associated field is required or not, it shows a star above of it - /// - [Parameter, ResetClassBuilder] - public bool Required { get; set; } - - - - protected override string RootElementClass => "bit-lbl"; - - protected override void RegisterCssClasses() - { - ClassBuilder.Register(() => Required ? "bit-lbl-req" : string.Empty); - } -} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabel.scss b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabel.scss index 685aa2e7b7c..2324afe9757 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabel.scss +++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabel.scss @@ -14,13 +14,67 @@ &.bit-dis { color: $clr-fg-dis; + // The label itself is never disabled - only the control it names is - so the pointer over it would still + // promise a click that the disabled control has nothing to do with. + cursor: default; } } -.bit-lbl-req { - &::after { - content: "*"; - color: $clr-req; - margin-left: spacing(0.625); +// Size classes follow the type ramp of the library: the unset default is the medium one, which is what keeps a +// standalone label in step with the labels the input components render of their own. +.bit-lbl-sm { + font-size: $tg-fs-xs; +} + +.bit-lbl-md { + font-size: $tg-fs-sm; +} + +.bit-lbl-lg { + font-size: $tg-fs-md; +} + +// Role classes are generated from the shared $bit-color-roles map (see color-role-maps.scss). +@each $role, $tokens in $bit-color-roles { + .bit-lbl-#{$role} { + color: role($tokens, main); } } + +// The necessity indicators. The margin is logical rather than left-handed, so the gap between the caption and its +// mark stays after the caption in a right-to-left direction instead of jumping to the other side of it. +.bit-lbl-rqi { + color: $clr-req; + margin-inline-start: spacing(0.625); +} + +.bit-lbl-opi { + color: $clr-fg-sec; + font-weight: $tg-fw-regular; + margin-inline-start: spacing(0.625); +} + +.bit-lbl-nwr { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.bit-lbl-nsl { + user-select: none; + -webkit-user-select: none; // Safari still needs the prefixed property. +} + +// Out of the page but still in the accessibility tree: a zero-sized box would be dropped from it by some screen +// readers, so the label keeps a pixel of its own and is clipped away from it. +.bit-lbl-vhd { + padding: 0; + width: 1px; + height: 1px; + margin: -1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + clip-path: inset(50%); + clip: rect(0 0 0 0); +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabelClassStyles.cs b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabelClassStyles.cs new file mode 100644 index 00000000000..f2ca2621e17 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabelClassStyles.cs @@ -0,0 +1,32 @@ +namespace Bit.BlazorUI; + +/// +/// The custom CSS classes/styles for the different parts of the component. +/// +public class BitLabelClassStyles +{ + /// + /// Custom CSS classes/styles for the root element of the label. + /// + public string? Root { get; set; } + + /// + /// Custom CSS classes/styles for the required indicator of the label. + /// + /// + /// The indicator is the element the asterisk - or whatever or + /// puts in its place - is rendered into, and it only exists while + /// is set. + /// + public string? RequiredIndicator { get; set; } + + /// + /// Custom CSS classes/styles for the optional indicator of the label. + /// + /// + /// The indicator is the element the "(optional)" text - or whatever or + /// puts in its place - is rendered into, and it only exists while + /// is set and is not. + /// + public string? OptionalIndicator { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabelParams.cs b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabelParams.cs new file mode 100644 index 00000000000..3d123bca42d --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Label/BitLabelParams.cs @@ -0,0 +1,181 @@ +namespace Bit.BlazorUI; + +/// +/// The parameters for the component. +/// +public class BitLabelParams : BitComponentBaseParams, IBitComponentParams +{ + /// + /// Represents the parameter name used to identify the BitLabel cascading parameters within BitParams. + /// + /// + /// This constant is typically used when referencing or accessing the BitLabel value in + /// parameterized APIs or configuration settings. Using this constant helps ensure consistency and reduces the risk + /// of typographical errors. + /// + public const string ParamName = $"{nameof(BitParams)}.{nameof(BitLabel)}"; + + + + public string Name => ParamName; + + + + /// + /// Custom CSS classes for the different parts of the label. + /// + public BitLabelClassStyles? Classes { get; set; } + + /// + /// The general color of the label. + /// + public BitColor? Color { get; set; } + + /// + /// The custom html element used for the root node. The default is "label". + /// + public string? Element { get; set; } + + /// + /// Prevents the text of the label from being selected. + /// + public bool? NoSelect { get; set; } + + /// + /// Keeps the label on a single line and truncates the overflow with an ellipsis. + /// + public bool? NoWrap { get; set; } + + /// + /// Whether the associated field is optional, which renders an indicator after the content of the label. + /// + public bool? Optional { get; set; } + + /// + /// The text of the optional indicator of the label. The default is "(optional)". + /// + public string? OptionalText { get; set; } + + /// + /// Whether the associated field is required, which renders an indicator after the content of the label. + /// + public bool? Required { get; set; } + + /// + /// The text of the required indicator of the label. The default is "*". + /// + public string? RequiredText { get; set; } + + /// + /// The size of the label. + /// + public BitSize? Size { get; set; } + + /// + /// Custom CSS styles for the different parts of the label. + /// + public BitLabelClassStyles? Styles { get; set; } + + /// + /// Removes the label from the page while keeping it available to assistive technologies. + /// + public bool? VisuallyHidden { get; set; } + + + + /// + /// Updates the properties of the specified instance with any values that have been set on + /// this object, if those properties have not already been set on the . + /// + /// + /// Only properties that have a value set and have not already been set on the will be updated. + /// This method does not overwrite existing values on . + /// + /// + /// The instance whose properties will be updated. Cannot be null. + /// + public void UpdateParameters(BitLabel bitLabel) + { + if (bitLabel is null) return; + + UpdateBaseParameters(bitLabel); + + if (Classes is not null && bitLabel.HasNotBeenSet(nameof(Classes))) + { + bitLabel.Classes = Classes; + + bitLabel.ClassBuilder.Reset(); + } + + if (Color.HasValue && bitLabel.HasNotBeenSet(nameof(Color))) + { + bitLabel.Color = Color.Value; + + bitLabel.ClassBuilder.Reset(); + } + + if (Element.HasValue() && bitLabel.HasNotBeenSet(nameof(Element))) + { + bitLabel.Element = Element; + } + + if (NoSelect.HasValue && bitLabel.HasNotBeenSet(nameof(NoSelect))) + { + bitLabel.NoSelect = NoSelect.Value; + + bitLabel.ClassBuilder.Reset(); + } + + if (NoWrap.HasValue && bitLabel.HasNotBeenSet(nameof(NoWrap))) + { + bitLabel.NoWrap = NoWrap.Value; + + bitLabel.ClassBuilder.Reset(); + } + + if (Optional.HasValue && bitLabel.HasNotBeenSet(nameof(Optional))) + { + bitLabel.Optional = Optional.Value; + + bitLabel.ClassBuilder.Reset(); + } + + if (OptionalText.HasValue() && bitLabel.HasNotBeenSet(nameof(OptionalText))) + { + bitLabel.OptionalText = OptionalText; + } + + if (Required.HasValue && bitLabel.HasNotBeenSet(nameof(Required))) + { + bitLabel.Required = Required.Value; + + bitLabel.ClassBuilder.Reset(); + } + + if (RequiredText.HasValue() && bitLabel.HasNotBeenSet(nameof(RequiredText))) + { + bitLabel.RequiredText = RequiredText; + } + + if (Size.HasValue && bitLabel.HasNotBeenSet(nameof(Size))) + { + bitLabel.Size = Size.Value; + + bitLabel.ClassBuilder.Reset(); + } + + if (Styles is not null && bitLabel.HasNotBeenSet(nameof(Styles))) + { + bitLabel.Styles = Styles; + + bitLabel.StyleBuilder.Reset(); + } + + if (VisuallyHidden.HasValue && bitLabel.HasNotBeenSet(nameof(VisuallyHidden))) + { + bitLabel.VisuallyHidden = VisuallyHidden.Value; + + bitLabel.ClassBuilder.Reset(); + } + } +} diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Label/BitLabelDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Label/BitLabelDemo.razor index 43abfc4a618..94ab4bb3614 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Label/BitLabelDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Label/BitLabelDemo.razor @@ -2,14 +2,24 @@ + Description="Labels give a name or a title to a control or to a group of controls, including text fields, check boxes, combo boxes, radio buttons and drop-down menus." /> +
+ A label with nothing configured is a label element carrying the caption it is given, at the + same weight and size as the labels the input components of the library render of their own. + IsEnabled set to false dims it to the disabled foreground color of the theme. That is a look and not + a state: a label is never itself disabled, so it still names its control and a click on it still reaches + that control - disabling the control is what stops it from responding. +
I'm a Label
@@ -17,7 +27,141 @@
- + +
+ For is the for attribute of the label element: the value is the id of the control + the label names, not its name, and that control may sit anywhere on the page. This is the explicit + association of HTML, and it is what makes clicking the caption focus the field and a screen reader read the + caption when the field takes focus. A control put inside the label's own content is associated implicitly + instead, which is the one way to name a control that has no id. +
+
+ A Label for an input + +

+ A Label wrapping its own control +
+
+ + +
+ Required renders an indicator after the caption. The default one is an asterisk in the theme's + required color, and it is hidden from assistive technologies: a star announced in the middle of a caption + says nothing about the field, while the required attribute of the control itself is what makes + it announced as required. RequiredText puts another mark or a word in the asterisk's place and + RequiredTemplate puts any markup there - and an indicator written by the page is left announced, + since a word put there deliberately is meant to be read. +
+
+ I'm a required Label +
+ A required Label with a word instead of the asterisk +
+ + A required Label with a custom template + + + + +
+
+ + +
+ Optional is the other half of the same decision, for the forms where most of the fields are required + and it is the exceptions that are worth marking - marking every field of a form is noise whichever half is + marked. The indicator is the word "(optional)" in a lighter color and a regular weight, and unlike the + asterisk it is announced with the rest of the caption. OptionalText and OptionalTemplate + replace it, which is what a form in another language needs. A label asked for both renders the required + indicator, since a required field is not an optional one. +
+
+ I'm an optional Label +
+ An optional Label with its own text +
+ + An optional Label with a custom template + + + + +
+
+ + +
+ A native label may only name a single form control, so the caption of a group of controls - a set of + radio buttons, a pair of inputs making up a range - must not be one: it would name only the first control in + the group. Element renders the caption as any other tag while keeping the same look, so the group can + point at it through its own aria-labelledby - or as the legend of a + fieldset, which is the same association written in plain HTML. The For parameter is + dropped along with the label element, since the for attribute belongs to that element alone. A + tag name that is not one a tag can have falls back to label. +
+
+ Favorite color +
+ + + +
+
+
+ Delivery notes + + +
+
+
+ + +
+ A label wraps by default and breaks a word too long for its line rather than overflowing it. NoWrap + keeps it on one line and truncates what does not fit with an ellipsis. Truncation needs a width to overflow: + a label is a block element whichever tag Element renders, so it takes the width of its container, and + only a label given a display of its own needs a width of its own. +
+
+ A caption long enough to need more than one line at this width +
+ A caption long enough to need more than one line at this width +
+
+ + +
+ A click on a label is forwarded to the control it names, so a double click on one - whose second click lands + on the label rather than on the control - selects the caption's text instead of doing anything to the + control. NoSelect turns that selection off, for the labels over a checkbox or a toggle where it is + only ever an accident. Try double clicking both captions below. +
+
+ Selectable caption +
+ Unselectable caption +
+
+ + +
+ VisuallyHidden clips the label out of the page while leaving it in the accessibility tree, so the + control it names is still announced by its caption. That is what separates it from Visibility below, + which takes the label away from assistive technologies along with the page. A visible caption is what most + users need, so this belongs to the places where the surrounding design already says what the control is. +
+
+ Search the documentation + +
+
+ + +
+ Visibility decides whether the label is rendered, invisible but still taking its space, or out of the + layout altogether. A hidden or a collapsed label is gone from the accessibility tree as well, so it no longer + names its control - VisuallyHidden above is the one that keeps naming it. +
Visible: [ Visible Label ]

@@ -27,31 +171,80 @@
- + +
+ A label inherits the color of its container while Color is not set, which is what keeps it in step + with the text around it. Setting it gives the caption a meaning of its own - an error color beside a field + that failed its validation, a secondary foreground for a caption that should sit back from the content it + names. The required indicator keeps the theme's required color whichever color the caption takes. +
- I'm a Label with Style -
- I'm a Label with Class + Primary + Secondary + Tertiary + Info + Success + Warning + SevereWarning + Error + PrimaryForeground + SecondaryForeground + TertiaryForeground + PrimaryBorder + SecondaryBorder + TertiaryBorder
- + +
+ Size moves the caption along the type ramp of the theme. The unset default is the medium size, which + is the one that matches the labels the input components render of their own, so a standalone label sits next + to them without being resized. +
- I'm a required Label + Small + Medium + Large
- + +
+ Style and Class land on the root element of the label, and Styles and Classes + reach its parts one by one: the root, the required indicator and the optional indicator. That is how the + necessity mark is restyled - a color, a weight, a position - without a selector reaching into the library's + own class names. +
- A Label for An Input +
Component's Style & Class:

+ I'm a Label with Style
- + I'm a Label with Class +


+
Styles & Classes:

+ + I'm a Label with Styles + +
+ + I'm a Label with Classes +
- + +
+ Setting Dir to BitDir.Rtl lays the label out right to left. The gap between the caption and its + necessity indicator is a logical one, so the mark stays after the caption - on its left in this direction - + rather than jumping to the other side of it. +
من یک برچسب هستم +
+ من یک برچسب الزامی هستم +
+ من یک برچسب اختیاری هستم
-
\ No newline at end of file + diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Label/BitLabelDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Label/BitLabelDemo.razor.cs index 553fbc5b943..4fa753fedec 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Label/BitLabelDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Label/BitLabelDemo.razor.cs @@ -9,21 +9,296 @@ public partial class BitLabelDemo Name = "ChildContent", Type = "RenderFragment?", DefaultValue = "null", - Description = "The content of label, It can be Any custom tag or a text.", + Description = "The content of the label, which can be a text or any custom markup. A form control put inside it is named by the label without needing the For parameter.", + }, + new() + { + Name = "Classes", + Type = "BitLabelClassStyles?", + DefaultValue = "null", + Description = "Custom CSS classes for the different parts of the label.", + LinkType = LinkType.Link, + Href = "#class-styles", + }, + new() + { + Name = "Color", + Type = "BitColor?", + DefaultValue = "null", + Description = "The general color of the label. The label inherits the color of its container while this is not set.", + LinkType = LinkType.Link, + Href = "#color-enum", + }, + new() + { + Name = "Element", + Type = "string?", + DefaultValue = "null", + Description = "The custom html element used for the root node. The default is \"label\", and a name that is not one a tag can have falls back to it.", }, new() { Name = "For", Type = "string?", DefaultValue = "null", - Description = "This attribute specifies which form element a label is bound to.", + Description = "The id of the form control this label is bound to, rendered as the \"for\" attribute of the label element. It is ignored while the Element parameter renders another tag.", + }, + new() + { + Name = "NoSelect", + Type = "bool", + DefaultValue = "false", + Description = "Prevents the text of the label from being selected, which is what a double click on a label does instead of reaching the control it names.", + }, + new() + { + Name = "NoWrap", + Type = "bool", + DefaultValue = "false", + Description = "Keeps the label on a single line and truncates the overflow with an ellipsis.", + }, + new() + { + Name = "Optional", + Type = "bool", + DefaultValue = "false", + Description = "Whether the associated field is optional, which renders an indicator after the content of the label. It is ignored while Required is set.", + }, + new() + { + Name = "OptionalTemplate", + Type = "RenderFragment?", + DefaultValue = "null", + Description = "The custom template of the optional indicator of the label. Takes precedence over OptionalText.", + }, + new() + { + Name = "OptionalText", + Type = "string?", + DefaultValue = "null", + Description = "The text of the optional indicator of the label. The default is \"(optional)\".", }, new() { Name = "Required", Type = "bool", DefaultValue = "false", - Description = "Whether the associated field is required or not, it shows a star above of it.", + Description = "Whether the associated field is required, which renders an indicator after the content of the label. The default asterisk is hidden from assistive technologies.", + }, + new() + { + Name = "RequiredTemplate", + Type = "RenderFragment?", + DefaultValue = "null", + Description = "The custom template of the required indicator of the label. Takes precedence over RequiredText.", + }, + new() + { + Name = "RequiredText", + Type = "string?", + DefaultValue = "null", + Description = "The text of the required indicator of the label. The default is \"*\".", + }, + new() + { + Name = "Size", + Type = "BitSize?", + DefaultValue = "null", + Description = "The size of the label. The default is the medium size.", + LinkType = LinkType.Link, + Href = "#size-enum", + }, + new() + { + Name = "Styles", + Type = "BitLabelClassStyles?", + DefaultValue = "null", + Description = "Custom CSS styles for the different parts of the label.", + LinkType = LinkType.Link, + Href = "#class-styles", + }, + new() + { + Name = "VisuallyHidden", + Type = "bool", + DefaultValue = "false", + Description = "Removes the label from the page while keeping it available to assistive technologies, so it still names its control.", + } + ]; + + private readonly List componentSubClasses = + [ + new() + { + Id = "class-styles", + Title = "BitLabelClassStyles", + Description = "The custom CSS classes/styles for the different parts of the label.", + Parameters = + [ + new() + { + Name = "Root", + Type = "string?", + DefaultValue = "null", + Description = "Custom CSS classes/styles for the root element of the label.", + }, + new() + { + Name = "RequiredIndicator", + Type = "string?", + DefaultValue = "null", + Description = "Custom CSS classes/styles for the required indicator of the label, which only exists while Required is set.", + }, + new() + { + Name = "OptionalIndicator", + Type = "string?", + DefaultValue = "null", + Description = "Custom CSS classes/styles for the optional indicator of the label, which only exists while Optional is set and Required is not.", + } + ] + } + ]; + + private readonly List componentSubEnums = + [ + new() + { + Id = "color-enum", + Name = "BitColor", + Description = "Defines the general colors available in the bit BlazorUI.", + Items = + [ + new() + { + Name= "Primary", + Description="Primary general color.", + Value="0", + }, + new() + { + Name= "Secondary", + Description="Secondary general color.", + Value="1", + }, + new() + { + Name= "Tertiary", + Description="Tertiary general color.", + Value="2", + }, + new() + { + Name= "Info", + Description="Info general color.", + Value="3", + }, + new() + { + Name= "Success", + Description="Success general color.", + Value="4", + }, + new() + { + Name= "Warning", + Description="Warning general color.", + Value="5", + }, + new() + { + Name= "SevereWarning", + Description="SevereWarning general color.", + Value="6", + }, + new() + { + Name= "Error", + Description="Error general color.", + Value="7", + }, + new() + { + Name= "PrimaryBackground", + Description="Primary background color.", + Value="8", + }, + new() + { + Name= "SecondaryBackground", + Description="Secondary background color.", + Value="9", + }, + new() + { + Name= "TertiaryBackground", + Description="Tertiary background color.", + Value="10", + }, + new() + { + Name= "PrimaryForeground", + Description="Primary foreground color.", + Value="11", + }, + new() + { + Name= "SecondaryForeground", + Description="Secondary foreground color.", + Value="12", + }, + new() + { + Name= "TertiaryForeground", + Description="Tertiary foreground color.", + Value="13", + }, + new() + { + Name= "PrimaryBorder", + Description="Primary border color.", + Value="14", + }, + new() + { + Name= "SecondaryBorder", + Description="Secondary border color.", + Value="15", + }, + new() + { + Name= "TertiaryBorder", + Description="Tertiary border color.", + Value="16", + } + ] + }, + new() + { + Id = "size-enum", + Name = "BitSize", + Description = "Defines the sizes available in the bit BlazorUI.", + Items = + [ + new() + { + Name= "Small", + Description="The small size.", + Value="0", + }, + new() + { + Name= "Medium", + Description="The medium size.", + Value="1", + }, + new() + { + Name= "Large", + Description="The large size.", + Value="2", + } + ] } ]; @@ -34,30 +309,123 @@ public partial class BitLabelDemo I'm a disabled Label"; private readonly string example2RazorCode = @" +A Label for an input + + + A Label wrapping its own control"; + + private readonly string example3RazorCode = @" +I'm a required Label + +A required Label with a word instead of the asterisk + + + A required Label with a custom template + + + +"; + + private readonly string example4RazorCode = @" +I'm an optional Label + +An optional Label with its own text + + + An optional Label with a custom template + + + +"; + + private readonly string example5RazorCode = @" +Favorite color +
+ + + +
+ +
+ Delivery notes + + +
"; + + private readonly string example6RazorCode = @" +A caption long enough to need more than one line at this width + +A caption long enough to need more than one line at this width"; + + private readonly string example7RazorCode = @" + Selectable caption + + Unselectable caption"; + + private readonly string example8RazorCode = @" +Search the documentation +"; + + private readonly string example9RazorCode = @" Visible: [ Visible Label ] Hidden: [ Hidden Label ] Collapsed: [ Collapsed Label ]"; - private readonly string example3RazorCode = @" + private readonly string example10RazorCode = @" +Primary +Secondary +Tertiary +Info +Success +Warning +SevereWarning +Error +PrimaryForeground +SecondaryForeground +TertiaryForeground +PrimaryBorder +SecondaryBorder +TertiaryBorder"; + + private readonly string example11RazorCode = @" +Small +Medium +Large"; + + private readonly string example12RazorCode = @" -I'm a Label with Style -I'm a Label with Class"; +I'm a Label with Style +I'm a Label with Class - private readonly string example4RazorCode = @" -I'm a required Label"; + + I'm a Label with Styles + - private readonly string example5RazorCode = @" -A Label for An Input -"; + + I'm a Label with Classes +"; - private readonly string example6RazorCode = @" -من یک برچسب هستم"; + private readonly string example13RazorCode = @" +من یک برچسب هستم + +من یک برچسب الزامی هستم +من یک برچسب اختیاری هستم"; } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Label/BitLabelDemo.razor.scss b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Label/BitLabelDemo.razor.scss index 09556387780..4de5c03ceab 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Label/BitLabelDemo.razor.scss +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Label/BitLabelDemo.razor.scss @@ -3,3 +3,13 @@ border: 1px solid red; max-width: max-content; } + +::deep .custom-root { + text-transform: uppercase; + letter-spacing: 0.05rem; +} + +::deep .custom-optional { + color: mediumseagreen; + font-style: italic; +} diff --git a/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Utilities/Labels/BitLabelCascadingParamsTest.razor b/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Utilities/Labels/BitLabelCascadingParamsTest.razor new file mode 100644 index 00000000000..3da35dce284 --- /dev/null +++ b/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Utilities/Labels/BitLabelCascadingParamsTest.razor @@ -0,0 +1,19 @@ +@* Covers BitLabelParams: the cascaded values fill in the parameters the label did not set itself, + and never overwrite the ones it did. *@ + + + cascaded + own + diff --git a/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Utilities/Labels/BitLabelTests.cs b/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Utilities/Labels/BitLabelTests.cs index 7af05b146c9..2a8cc6c1dcf 100644 --- a/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Utilities/Labels/BitLabelTests.cs +++ b/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Utilities/Labels/BitLabelTests.cs @@ -1,4 +1,5 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.AspNetCore.Components; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Bunit; namespace Bit.BlazorUI.Tests.Components.Utilities.Labels; @@ -56,9 +57,13 @@ public void BitLabelShouldRespectRequired(bool required) parameters.Add(p => p.Required, required); }); - var cssClass = required ? "bit-lbl bit-lbl-req" : "bit-lbl"; + // The asterisk is an element rather than a css pseudo element, so that it can be hidden from the assistive + // technologies that would otherwise announce a "star" in the middle of the caption. + var expected = required + ? @"" + : @""; - component.MarkupMatches(@$""); + component.MarkupMatches(expected); } [TestMethod] @@ -73,7 +78,374 @@ public void BitLabelShouldRespectRequiredChangingAfterRender() parameters.Add(p => p.Required, true); }); - component.MarkupMatches(@""); + component.MarkupMatches(@""); + } + + [TestMethod] + public void BitLabelShouldRenderRequiredIndicatorAfterTheContent() + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.Required, true); + parameters.AddChildContent("Bit Blazor UI"); + }); + + component.MarkupMatches(@""); + } + + [TestMethod, + DataRow("(required)"), + DataRow("**") + ] + public void BitLabelShouldRespectRequiredText(string requiredText) + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.Required, true); + parameters.Add(p => p.RequiredText, requiredText); + }); + + // An indicator written by the page is a word put there to be read, so it is left announced. + component.MarkupMatches(@$""); + } + + [TestMethod] + public void BitLabelShouldRespectRequiredTemplate() + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.Required, true); + parameters.Add(p => p.RequiredText, "(required)"); + parameters.Add(p => p.RequiredTemplate, (RenderFragment)(builder => builder.AddMarkupContent(0, "!"))); + }); + + component.MarkupMatches(@""); + } + + [TestMethod] + public void BitLabelShouldNotRenderRequiredIndicatorWhenNotRequired() + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.RequiredText, "(required)"); + parameters.Add(p => p.RequiredTemplate, (RenderFragment)(builder => builder.AddMarkupContent(0, "!"))); + }); + + component.MarkupMatches(@""); + } + + [TestMethod, + DataRow(true), + DataRow(false) + ] + public void BitLabelShouldRespectOptional(bool optional) + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.Optional, optional); + }); + + var expected = optional + ? @"" + : @""; + + component.MarkupMatches(expected); + } + + [TestMethod] + public void BitLabelShouldRespectOptionalChangingAfterRender() + { + var component = RenderComponent(); + + component.MarkupMatches(@""); + + component.Render(parameters => + { + parameters.Add(p => p.Optional, true); + }); + + component.MarkupMatches(@""); + } + + [TestMethod] + public void BitLabelShouldRespectOptionalText() + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.Optional, true); + parameters.Add(p => p.OptionalText, "(اختیاری)"); + }); + + component.MarkupMatches(@""); + } + + [TestMethod] + public void BitLabelShouldRespectOptionalTemplate() + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.Optional, true); + parameters.Add(p => p.OptionalText, "(optional)"); + parameters.Add(p => p.OptionalTemplate, (RenderFragment)(builder => builder.AddMarkupContent(0, "opt"))); + }); + + component.MarkupMatches(@""); + } + + [TestMethod] + public void BitLabelShouldPreferRequiredOverOptional() + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.Required, true); + parameters.Add(p => p.Optional, true); + }); + + component.MarkupMatches(@""); + } + + [TestMethod, + DataRow("div"), + DataRow("span"), + DataRow("legend") + ] + public void BitLabelShouldRespectElement(string element) + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.Element, element); + parameters.AddChildContent("Bit Blazor UI"); + }); + + component.MarkupMatches(@$"<{element} class=""bit-lbl"" id:ignore>Bit Blazor UI"); + } + + [TestMethod, + DataRow("not a tag name"), + DataRow("label!"), + DataRow("1div"), + DataRow(" "), + DataRow("") + ] + public void BitLabelShouldFallBackToLabelForAnInvalidElement(string element) + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.Element, element); + }); + + component.MarkupMatches(@""); + } + + [TestMethod] + public void BitLabelShouldNotRenderForOnANonLabelElement() + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.Element, "div"); + parameters.Add(p => p.For, "test-for"); + }); + + component.MarkupMatches(@"
"); + } + + [TestMethod, + DataRow(true), + DataRow(false) + ] + public void BitLabelShouldRespectNoWrap(bool noWrap) + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.NoWrap, noWrap); + }); + + var cssClass = noWrap ? "bit-lbl bit-lbl-nwr" : "bit-lbl"; + + component.MarkupMatches(@$""); + } + + [TestMethod, + DataRow(true), + DataRow(false) + ] + public void BitLabelShouldRespectNoSelect(bool noSelect) + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.NoSelect, noSelect); + }); + + var cssClass = noSelect ? "bit-lbl bit-lbl-nsl" : "bit-lbl"; + + component.MarkupMatches(@$""); + } + + [TestMethod, + DataRow(true), + DataRow(false) + ] + public void BitLabelShouldRespectVisuallyHidden(bool visuallyHidden) + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.VisuallyHidden, visuallyHidden); + }); + + var cssClass = visuallyHidden ? "bit-lbl bit-lbl-vhd" : "bit-lbl"; + + component.MarkupMatches(@$""); + } + + [TestMethod] + public void BitLabelShouldRespectVisuallyHiddenChangingAfterRender() + { + var component = RenderComponent(); + + component.MarkupMatches(@""); + + component.Render(parameters => + { + parameters.Add(p => p.VisuallyHidden, true); + }); + + component.MarkupMatches(@""); + } + + [TestMethod, + DataRow(BitSize.Small, "bit-lbl-sm"), + DataRow(BitSize.Medium, "bit-lbl-md"), + DataRow(BitSize.Large, "bit-lbl-lg"), + DataRow(null, null) + ] + public void BitLabelShouldRespectSize(BitSize? size, string sizeClass) + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.Size, size); + }); + + var cssClass = sizeClass is null ? "bit-lbl" : $"bit-lbl {sizeClass}"; + + component.MarkupMatches(@$""); + } + + [TestMethod] + public void BitLabelShouldRespectSizeChangingAfterRender() + { + var component = RenderComponent(); + + component.MarkupMatches(@""); + + component.Render(parameters => + { + parameters.Add(p => p.Size, BitSize.Large); + }); + + component.MarkupMatches(@""); + } + + [TestMethod, + DataRow(BitColor.Primary, "bit-lbl-pri"), + DataRow(BitColor.Secondary, "bit-lbl-sec"), + DataRow(BitColor.Tertiary, "bit-lbl-ter"), + DataRow(BitColor.Info, "bit-lbl-inf"), + DataRow(BitColor.Success, "bit-lbl-suc"), + DataRow(BitColor.Warning, "bit-lbl-wrn"), + DataRow(BitColor.SevereWarning, "bit-lbl-swr"), + DataRow(BitColor.Error, "bit-lbl-err"), + DataRow(BitColor.PrimaryBackground, "bit-lbl-pbg"), + DataRow(BitColor.SecondaryBackground, "bit-lbl-sbg"), + DataRow(BitColor.TertiaryBackground, "bit-lbl-tbg"), + DataRow(BitColor.PrimaryForeground, "bit-lbl-pfg"), + DataRow(BitColor.SecondaryForeground, "bit-lbl-sfg"), + DataRow(BitColor.TertiaryForeground, "bit-lbl-tfg"), + DataRow(BitColor.PrimaryBorder, "bit-lbl-pbr"), + DataRow(BitColor.SecondaryBorder, "bit-lbl-sbr"), + DataRow(BitColor.TertiaryBorder, "bit-lbl-tbr"), + DataRow(null, null) + ] + public void BitLabelShouldRespectColor(BitColor? color, string colorClass) + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.Color, color); + }); + + var cssClass = colorClass is null ? "bit-lbl" : $"bit-lbl {colorClass}"; + + component.MarkupMatches(@$""); + } + + [TestMethod] + public void BitLabelShouldRespectColorChangingAfterRender() + { + var component = RenderComponent(); + + component.MarkupMatches(@""); + + component.Render(parameters => + { + parameters.Add(p => p.Color, BitColor.Error); + }); + + component.MarkupMatches(@""); + } + + [TestMethod] + public void BitLabelShouldRespectClasses() + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.Required, true); + parameters.Add(p => p.Classes, new BitLabelClassStyles + { + Root = "custom-root", + RequiredIndicator = "custom-required" + }); + }); + + component.MarkupMatches(@""); + } + + [TestMethod] + public void BitLabelShouldRespectOptionalIndicatorClasses() + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.Optional, true); + parameters.Add(p => p.Classes, new BitLabelClassStyles { OptionalIndicator = "custom-optional" }); + }); + + component.MarkupMatches(@""); + } + + [TestMethod] + public void BitLabelShouldRespectStyles() + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.Required, true); + parameters.Add(p => p.Styles, new BitLabelClassStyles + { + Root = "font-style: italic", + RequiredIndicator = "color: blueviolet" + }); + }); + + component.MarkupMatches(@""); + } + + [TestMethod] + public void BitLabelShouldRespectOptionalIndicatorStyles() + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.Optional, true); + parameters.Add(p => p.Styles, new BitLabelClassStyles { OptionalIndicator = "color: mediumseagreen" }); + }); + + component.MarkupMatches(@""); } [TestMethod, @@ -181,6 +553,28 @@ public void BitLabelShouldRespectFor(string @for) } } + [TestMethod, + DataRow("-1"), + DataRow("0"), + DataRow(null) + ] + public void BitLabelShouldRespectTabIndex(string tabIndex) + { + var component = RenderComponent(parameters => + { + parameters.Add(p => p.TabIndex, tabIndex); + }); + + if (tabIndex.HasValue()) + { + component.MarkupMatches(@$""); + } + else + { + component.MarkupMatches(@""); + } + } + [TestMethod, DataRow(BitDir.Rtl), DataRow(BitDir.Ltr), @@ -304,4 +698,30 @@ public void BitLabelShouldRespectHtmlAttributes() component.MarkupMatches(@""); } + + [TestMethod] + public void BitLabelShouldRespectCascadingParams() + { + var component = RenderComponent(); + + var labels = component.FindAll(".bit-lbl"); + + Assert.AreEqual(2, labels.Count); + + // The first label takes everything from the cascading parameters. + Assert.AreEqual("DIV", labels[0].TagName); + Assert.IsTrue(labels[0].ClassList.Contains("bit-lbl-lg")); + Assert.IsTrue(labels[0].ClassList.Contains("bit-lbl-err")); + Assert.IsTrue(labels[0].ClassList.Contains("bit-lbl-nwr")); + Assert.IsTrue(labels[0].ClassList.Contains("cascaded")); + Assert.IsTrue(labels[0].ClassList.Contains("bit-lbl-req")); + Assert.AreEqual("(required)", labels[0].QuerySelector(".bit-lbl-rqi")!.TextContent); + + // The second one sets its own size and renders a label element, which the cascading parameters must not + // overwrite. + Assert.AreEqual("LABEL", labels[1].TagName); + Assert.IsTrue(labels[1].ClassList.Contains("bit-lbl-sm")); + Assert.IsFalse(labels[1].ClassList.Contains("bit-lbl-lg")); + Assert.IsTrue(labels[1].ClassList.Contains("bit-lbl-err")); + } }