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
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+";
+
+ 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{element}>");
+ }
+
+ [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"));
+ }
}