diff --git a/src/BlazorUI/Bit.BlazorUI/Components/BitLinkRels.cs b/src/BlazorUI/Bit.BlazorUI/Components/BitLinkRels.cs
index feeefa5af70..78c432b57b2 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/BitLinkRels.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/BitLinkRels.cs
@@ -70,5 +70,36 @@ public enum BitLinkRels
///
/// A tag (keyword) for the current document.
///
- Tag = 4096
+ Tag = 4096,
+
+ ///
+ /// Indicates that the linked document represents the person who owns the current content. (used for identity verification)
+ ///
+ Me = 8192,
+
+ ///
+ /// Requires that any browsing context created by following the hyperlink keeps its opener browsing context.
+ /// (reverses the implicit noopener modern browsers apply to _blank targets)
+ ///
+ Opener = 16384,
+
+ ///
+ /// Links to the privacy policy that applies to the current document. (rendered as privacy-policy)
+ ///
+ PrivacyPolicy = 32768,
+
+ ///
+ /// Marks the link as an advertisement or paid placement, so search engines do not count it as an organic endorsement.
+ ///
+ Sponsored = 65536,
+
+ ///
+ /// Links to the terms of service that apply to the current document. (rendered as terms-of-service)
+ ///
+ TermsOfService = 131072,
+
+ ///
+ /// Marks the link as user-generated content, like forum posts or comments, for search engines.
+ ///
+ Ugc = 262144
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.razor b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.razor
index afd69d8a74d..857a71e3a58 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.razor
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.razor
@@ -1,45 +1,148 @@
@namespace Bit.BlazorUI
@inherits BitComponentBase
-@if (Href.HasValue())
+@{
+ var isAnchor = Href.HasValue();
+
+ @* An in-page link scrolls rather than navigates, so it answers the click itself and never lets the
+ browser move the document. That also keeps the fragment out of the address bar and out of the
+ history, which is what separates a link into the page from a link to it. *@
+ var isHash = isAnchor && Href!.StartsWith('#');
+
+ var icon = BitIconInfo.From(Icon, IconName);
+
+ @* An attribute written after the splat wins over the one the app passed in through HtmlAttributes, and
+ a null one takes it off the element altogether. So every attribute the link writes for itself reads
+ what came in with the attributes first and writes it back when the parameter behind it was left
+ unset - otherwise a plain rel, target or title written on a BitLink would silently do nothing. *@
+ var splattedRel = HtmlAttributes.TryGetValue("rel", out var srl) ? srl?.ToString() : null;
+ var splattedDir = HtmlAttributes.TryGetValue("dir", out var sdr) ? sdr?.ToString() : null;
+ var splattedRole = HtmlAttributes.TryGetValue("role", out var sro) ? sro?.ToString() : null;
+ var splattedTitle = HtmlAttributes.TryGetValue("title", out var stl) ? stl?.ToString() : null;
+ var splattedLabel = HtmlAttributes.TryGetValue("aria-label", out var sal) ? sal?.ToString() : null;
+ var splattedTarget = HtmlAttributes.TryGetValue("target", out var stg) ? stg?.ToString() : null;
+ var splattedTabIndex = HtmlAttributes.TryGetValue("tabindex", out var sti) ? sti?.ToString() : null;
+ var splattedDisabled = HtmlAttributes.TryGetValue("aria-disabled", out var sdd) ? sdd?.ToString() : null;
+ var splattedCurrent = HtmlAttributes.TryGetValue("aria-current", out var scr) ? scr?.ToString() : null;
+ var splattedLabelledBy = HtmlAttributes.TryGetValue("aria-labelledby", out var slb) ? slb?.ToString() : null;
+ var splattedDescribedBy = HtmlAttributes.TryGetValue("aria-describedby", out var sdb) ? sdb?.ToString() : null;
+
+ @* The download attribute is the one of these that is also written without a value, so what came in with
+ the attributes is handed back as the object it arrived as: a bool renders the bare attribute, a string
+ renders the file name it names. *@
+ var splattedDownload = HtmlAttributes.TryGetValue("download", out var sdl) ? sdl : null;
+
+ var dir = Dir?.ToString().ToLower() ?? splattedDir;
+ var title = Title ?? splattedTitle;
+ var tabIndex = _tabIndex ?? splattedTabIndex;
+ var target = Target.HasValue() ? Target : splattedTarget;
+ var role = splattedRole ?? (IsEnabled ? null : "link");
+ var ariaDisabled = IsEnabled ? splattedDisabled : "true";
+ var ariaCurrent = GetAriaCurrent(splattedCurrent);
+
+ @* Neither the target nor the download belongs on a link that cannot be followed, and neither means
+ anything on one that scrolls the page instead of navigating it. Everything downstream reads the
+ target that is actually on the element rather than the parameter, so a link that will not open a new
+ tab is not described as one either. *@
+ var canNavigate = isAnchor && IsEnabled && isHash is false;
+ var download = canNavigate ? (Download is not null ? Download : splattedDownload) : null;
+ var renderedTarget = canNavigate ? target : null;
+
+ @* A rel is document metadata rather than a navigation behaviour - it is what a crawler reads to learn
+ that a link is sponsored, or that the profile it points at is the author's - so a disabled link keeps
+ saying what it is a link to, and keeps the noopener that says what opening it must not hand over. *@
+ var rel = isHash ? splattedRel : BuildRel(_rel ?? splattedRel, target);
+
+ var newTabHint = GetNewTabHint(renderedTarget);
+ var label = AriaLabel ?? splattedLabel;
+
+ @* The new-tab sentence has to land wherever the link's name is coming from, since a name given from
+ somewhere else replaces the content rather than adding to it. An aria-labelledby is the one that wins,
+ and it points at elements rather than holding text, so the sentence is rendered as an element of its
+ own and its id appended to the list; an aria-label holds the text itself, so the sentence is appended
+ to it; a link named by nothing but its own content gets the sentence as a sibling of that content,
+ hidden from the screen but not from a reader. *@
+ var hintId = $"{_Id}-nth";
+ var labelledByHint = newTabHint.HasValue() && splattedLabelledBy.HasValue();
+ var ariaLabelledBy = labelledByHint ? $"{splattedLabelledBy} {hintId}" : splattedLabelledBy;
+ var ariaLabel = label.HasValue() && newTabHint.HasValue() && labelledByHint is false ? $"{label} {newTabHint}" : label;
+ var hintText = label.HasValue() && labelledByHint is false ? null : newTabHint;
+
+ @* A description is read out after the name rather than as part of it, so it is an element of its own that
+ the link points at - and one the link points at beside whatever the app was already pointing it at. *@
+ var describedById = $"{_Id}-dsc";
+ var hasDescription = AriaDescription.HasValue();
+ var ariaDescribedBy = hasDescription
+ ? (splattedDescribedBy.HasValue() ? $"{splattedDescribedBy} {describedById}" : describedById)
+ : splattedDescribedBy;
+
+ var iconAtEnd = IconPosition == BitIconPosition.End;
+
+ RenderFragment body = @
+ @if (icon is not null && iconAtEnd is false)
+ {
+
+ }
+ @ChildContent
+ @if (icon is not null && iconAtEnd)
+ {
+
+ }
+ @if (hintText.HasValue())
+ {
+ @hintText
+ }
+ ;
+}
+
+@if (isAnchor)
{
- if (Href!.StartsWith('#'))
- {
-
- @ChildContent
-
- }
- else
- {
-
- @ChildContent
-
- }
+
+ @body
+
}
else
{
-
-}
\ No newline at end of file
+}
+
+@* Rendered beside the link rather than inside it, so the description drives aria-describedby without becoming
+ part of the accessible name the link's own content already is. *@
+@if (hasDescription)
+{
+ @AriaDescription
+}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.razor.cs
index 860978c7efe..233dce09460 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.razor.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.razor.cs
@@ -1,18 +1,111 @@
-namespace Bit.BlazorUI;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Bit.BlazorUI;
///
/// Links lead to another part of an app, other pages, or help articles. They can also be used to initiate commands.
///
+///
+/// The component renders the element the destination asks for: an anchor when names one, a
+/// button when it does not - so a link that runs a command is a button wearing the link's look, and answers the
+/// keyboard the way a button does. An starting with # is the third case, an in-page
+/// link that scrolls the named element into view and takes the focus with it.
+///
+/// What the browser already gets right is passed through rather than reimplemented: ,
+/// and are the anchor attributes of the same names, and anything else
+/// an anchor accepts goes through the splatted HTML attributes, which the component reads back rather than
+/// overwrites. What it gets wrong for an app is defaulted: a _blank link is given noopener so the
+/// page it opens cannot reach back into the one that opened it, and is announced as opening in a new tab
+/// ().
+///
public partial class BitLink : BitComponentBase
{
+ ///
+ /// The rel values named by , rendered as the space separated list an HTML rel attribute
+ /// holds. The automatic noopener of a new-tab link is not part of it: that one depends on the
+ /// target actually on the element, which may have arrived as a splatted attribute rather than as the
+ /// parameter, and is therefore decided at render time.
+ ///
private string? _rel;
+ private string? _tabIndex;
+
+ ///
+ /// The text a _blank link is announced with when nothing else is said - see .
+ ///
+ private const string DefaultNewTabHint = "(opens in a new tab)";
+
+ private static readonly Dictionary _ariaCurrentMap = new()
+ {
+ [BitNavAriaCurrent.Page] = "page",
+ [BitNavAriaCurrent.Step] = "step",
+ [BitNavAriaCurrent.Location] = "location",
+ [BitNavAriaCurrent.Time] = "time",
+ [BitNavAriaCurrent.Date] = "date",
+ [BitNavAriaCurrent.True] = "true"
+ };
+
[Inject] private IJSRuntime _js { get; set; } = default!;
+ ///
+ /// Gets or sets the cascading parameters for the link 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 link components
+ /// through the component.
+ ///
+ [CascadingParameter(Name = BitLinkParams.ParamName)]
+ public BitLinkParams? CascadingParameters { get; set; }
+
+
+
+ ///
+ /// Keeps the disabled link focusable and discoverable by assistive technologies.
+ /// When enabled, the disabled state is conveyed using the aria-disabled attribute instead of removing
+ /// the element from the tab order, so keyboard and screen reader users can still find the link while its
+ /// navigation and click action stay suppressed.
+ ///
+ [Parameter] public bool AllowDisabledFocus { get; set; }
+
+ ///
+ /// Gives the link the focus as soon as it is rendered, through the autofocus attribute.
+ ///
+ ///
+ /// The browser honors this once per document, on the first element that asks for it, so only the one
+ /// element a page opens on should carry it. Being taken somewhere unasked is a jump the reader did not
+ /// make, so reserve it for a destination that is the point of the page.
+ ///
+ [Parameter] public bool AutoFocus { get; set; }
+
+ ///
+ /// A longer description of the link for the benefit of screen readers, rendered as visually hidden text the
+ /// link points at through aria-describedby.
+ ///
+ ///
+ /// A description is read out after the name and is not part of it, which is what makes it the place for what
+ /// the reader would want to know before following the link but not in the words on the page: the size of a
+ /// file, the format it is in, what the page it leads to is going to ask for. Unlike a ,
+ /// which the browser only ever shows to a mouse, this reaches everyone a screen reader is reading to.
+ ///
+ [Parameter] public string? AriaDescription { get; set; }
+
+ ///
+ /// Reports the link as the current item of the set it belongs to, through the aria-current attribute.
+ ///
+ ///
+ /// A menu draws the link to the page already open differently from the rest, and a color is all that says
+ /// so - which is nothing at all to a reader who is not looking at it. This is the same statement made in a
+ /// way a screen reader announces: in a navigation menu,
+ /// in a wizard, in a
+ /// breadcrumb. Only one link of a set is ever the current one.
+ ///
+ [Parameter] public BitNavAriaCurrent? AriaCurrent { get; set; }
///
/// The content of the link, can be any custom tag or a text.
///
@@ -25,44 +118,173 @@ public partial class BitLink : BitComponentBase
public BitColor? Color { get; set; }
///
- /// URL the link points to.
+ /// The value of the download attribute of the link when the Href parameter is provided.
+ /// Instructs the browser to download the linked resource instead of navigating to it, using the provided value
+ /// (if any) as the suggested file name (only works for same-origin, blob: and data: URLs).
+ ///
+ [Parameter] public string? Download { get; set; }
+
+ ///
+ /// URL the link points to. If provided, the component renders an anchor tag, otherwise a button.
+ /// A value starting with the # character makes the link smooth-scroll the element with that id into view
+ /// and move the focus to it, while a bare # renders an inert link that navigates nowhere.
///
[Parameter]
[CallOnSet(nameof(OnSetHrefAndRel))]
public string? Href { get; set; }
///
- /// Removes the applying any foreground color to the link content.
+ /// Gets or sets the icon rendered beside the link content, using custom CSS classes for external icon libraries.
+ /// Takes precedence over when both are set.
+ ///
+ ///
+ /// The glyph is normalized back to the size of the text beside it, so an icon from any set sits on the same
+ /// line as the words whatever type scale it came with.
+ ///
+ ///
+ /// Bootstrap: Icon="BitIconInfo.Bi("gear-fill")"
+ /// FontAwesome: Icon="BitIconInfo.Fa("solid house")"
+ /// Custom CSS: Icon="BitIconInfo.Css("my-icon-class")"
+ ///
+ [Parameter] public BitIconInfo? Icon { get; set; }
+
+ ///
+ /// Gets or sets the name of the icon rendered beside the link content, from the built-in Fluent UI icons.
+ ///
+ ///
+ /// Browse available names in BitIconName of the Bit.BlazorUI.Icons nuget package or the gallery:
+ /// .
+ ///
+ /// For external icon libraries, use instead.
+ ///
+ [Parameter] public string? IconName { get; set; }
+
+ ///
+ /// Gets or sets the position of the icon relative to the link content.
+ ///
+ ///
+ /// The icon goes in front of the text by default, the way it does everywhere else in the library.
+ /// puts it after the text instead, which is where the two glyphs a link
+ /// carries most often - the arrow of a link opening a new tab and the tray of a download - belong.
+ ///
+ /// The icon is drawn as decoration and hidden from assistive technologies, so whatever it says has to be
+ /// said by the link text or by an as well.
+ ///
+ [Parameter, ResetClassBuilder]
+ public BitIconPosition? IconPosition { get; set; }
+
+ ///
+ /// Replaces the text a new-tab link is announced with, for translating it or for saying it another way.
+ ///
+ ///
+ /// A link opening a new tab takes the reader somewhere the back button no longer returns from, which is a
+ /// change of context nothing on the page predicts on its own. So a _blank link carries the sentence
+ /// saying so - "(opens in a new tab)" unless this replaces it - as visually hidden text after its
+ /// content, or appended to its when it has one, since an aria-label
+ /// replaces the content rather than adding to it.
+ ///
+ /// An empty value takes the announcement off, the same as does.
+ ///
+ [Parameter] public string? NewTabHint { get; set; }
+
+ ///
+ /// Removes applying any foreground color to the link content, letting it keep its own color.
///
[Parameter, ResetClassBuilder]
public bool NoColor { get; set; }
+ ///
+ /// Stops a new-tab link from announcing that it opens in a new tab.
+ ///
+ ///
+ /// Only set this where the announcement would be made twice - beside a visible "opens in a new tab" label of
+ /// your own, or inside a list whose heading already says that every link in it opens a new tab. See
+ /// for what is being taken off.
+ ///
+ [Parameter] public bool NoNewTabHint { get; set; }
+
///
/// Styles the link to have no underline at any state.
///
+ ///
+ /// This wins over when both are set.
+ ///
[Parameter, ResetClassBuilder]
public bool NoUnderline { get; set; }
///
- /// Callback for when the link clicked.
+ /// Callback for when the link is clicked.
+ /// It is invoked in every render mode of the link: on anchor links it runs alongside the navigation,
+ /// and on button links (no Href) it is the sole click action.
///
[Parameter] public EventCallback OnClick { get; set; }
+ ///
+ /// Suppresses the navigation a click on the link would otherwise perform, leaving as
+ /// the whole of what the click does.
+ ///
+ ///
+ /// The anchor keeps its , so the destination is still what the status bar shows, what a
+ /// middle click opens in a new tab and what "copy link address" copies - only the plain click is answered by
+ /// the handler instead of by the browser. That is what a link needs to confirm before leaving, or to save a
+ /// draft first and navigate from the handler afterwards.
+ ///
+ /// An in-page (#) link always suppresses its own navigation, since it scrolls rather than navigates,
+ /// so this changes nothing there.
+ ///
+ [Parameter] public bool PreventDefault { get; set; }
+
///
/// If Href provided, specifies the relationship between the current document and the linked document.
+ /// Ignored for empty or hash-only (#) hrefs.
+ ///
+ /// When is set to _blank and no opener-related rel
+ /// (NoOpener, NoReferrer or Opener) is provided, noopener is added automatically.
///
[Parameter]
[CallOnSet(nameof(OnSetHrefAndRel))]
public BitLinkRels? Rel { get; set; }
///
- /// If Href provided, specifies how to open the link.
+ /// Sets the preset size of the link text.
+ ///
+ ///
+ /// A link is a piece of text before it is a control, so with nothing set here it takes the font size of
+ /// whatever it sits in - which is what keeps a link inside a paragraph the same size as the sentence around
+ /// it. A size is for the link that stands on its own, where there is no surrounding text to take one from.
+ ///
+ [Parameter, ResetClassBuilder]
+ public BitSize? Size { get; set; }
+
+ ///
+ /// If true, stops the propagation of the click event to the parent elements.
+ /// Useful when the link is placed inside clickable containers like rows or cards.
+ ///
+ [Parameter] public bool StopPropagation { get; set; }
+
+ ///
+ /// If Href provided, specifies how to open the link (e.g. _blank to open it in a new tab).
+ ///
+ /// When set to _blank and no opener-related is provided, noopener is added to the rel attribute automatically.
///
[Parameter] public string? Target { get; set; }
+ ///
+ /// The tooltip to show when the mouse is placed on the link.
+ ///
+ ///
+ /// A title is shown by the browser only after a hover long enough to count as one, and is reached by neither
+ /// touch nor the keyboard, so nothing the reader has to have belongs only here. It is the place for what a
+ /// full URL, or a longer wording of the link text, adds to the words already on screen.
+ ///
+ [Parameter] public string? Title { get; set; }
+
///
/// Styles the link with a fixed underline at all states.
///
+ ///
+ /// wins over this when both are set.
+ ///
[Parameter, ResetClassBuilder]
public bool Underlined { get; set; }
@@ -74,10 +296,21 @@ protected override void RegisterCssClasses()
{
ClassBuilder.Register(() => NoUnderline ? "bit-lnk-nun" : string.Empty);
- ClassBuilder.Register(() => Underlined ? "bit-lnk-und" : string.Empty);
+ // The two underline parameters ask for opposite things, and a stylesheet can only answer with whichever
+ // rule it happens to declare last. Deciding it here instead makes the answer the same wherever the link
+ // is used: taking the underline off is the narrower request, so it is the one that wins.
+ ClassBuilder.Register(() => Underlined && NoUnderline is false ? "bit-lnk-und" : string.Empty);
ClassBuilder.Register(() => NoColor ? "bit-lnk-ncl" : string.Empty);
+ ClassBuilder.Register(() => Size switch
+ {
+ BitSize.Small => "bit-lnk-sm",
+ BitSize.Medium => "bit-lnk-md",
+ BitSize.Large => "bit-lnk-lg",
+ _ => string.Empty
+ });
+
ClassBuilder.Register(() => Color switch
{
BitColor.Primary => "bit-lnk-pri",
@@ -101,28 +334,120 @@ protected override void RegisterCssClasses()
});
}
- protected virtual async Task HandleClick(MouseEventArgs e)
+ ///
+ /// Gives focus to the root element of the link.
+ ///
+ ///
+ /// A disabled link is only focusable when keeps it in the tab order;
+ /// otherwise the browser ignores the call.
+ ///
+ ///
+ /// A ValueTask that represents the asynchronous focus operation.
+ ///
+ public ValueTask FocusAsync() => RootElement.FocusAsync();
+
+ ///
+ /// Gives focus to the root element of the link, optionally without scrolling it into view.
+ ///
+ ///
+ /// True to leave the page scrolled where it is instead of bringing the link into view.
+ ///
+ ///
+ /// A ValueTask that represents the asynchronous focus operation.
+ ///
+ public ValueTask FocusAsync(bool preventScroll) => RootElement.FocusAsync(preventScroll);
+
+
+
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitLinkParams))]
+ protected override void OnParametersSet()
{
- if (IsEnabled is false) return;
+ if (CascadingParameters is not null)
+ {
+ CascadingParameters.UpdateParameters(this);
- await OnClick.InvokeAsync(e);
+ // The rel string is built as the Rel parameter is set, which has already happened by now, so a rel
+ // arriving from the cascade instead would otherwise never reach the attribute.
+ OnSetHrefAndRel();
+ }
+
+ _tabIndex = IsEnabled
+ ? TabIndex
+ : AllowDisabledFocus
+ ? (TabIndex ?? (Href.HasValue() ? "0" : null))
+ : Href.HasValue() ? null : "-1";
+
+ base.OnParametersSet();
}
- private async Task ScrollIntoView()
+
+
+ protected virtual async Task HandleClick(MouseEventArgs e)
{
if (IsEnabled is false) return;
- await _js.BitUtilsScrollElementIntoView(Href![1..]);
+ await OnClick.InvokeAsync(e);
+
+ if (Href.HasValue() && Href!.StartsWith('#') && Href!.Length > 1)
+ {
+ // The scroll takes the focus with it: an in-page link that only scrolls leaves the keyboard where
+ // it was, so the next Tab carries on from the link rather than from what it pointed at, and a
+ // screen reader is never told the page moved at all.
+ //
+ // The browser percent-decodes a fragment before matching it against an id, so an href of
+ // "#section%201" points at the element with the id "section 1"; the lookup is given the same
+ // decoded name rather than the escaped one, which would match nothing.
+ await _js.BitUtilsScrollElementIntoView(Uri.UnescapeDataString(Href![1..]), true);
+ }
}
private void OnSetHrefAndRel()
{
- if (Rel.HasValue is false || Href.HasNoValue() || Href!.StartsWith('#'))
+ _rel = Href.HasNoValue() || Href!.StartsWith('#') || Rel.HasValue is false
+ ? null
+ : BitLinkRelUtils.GetRels(Rel!.Value);
+ }
+
+ ///
+ /// Merges the rel values the link was given with the one a new-tab link is not safe without.
+ ///
+ ///
+ /// The page a _blank link opens is handed a reference back to the one that opened it, which it can
+ /// navigate somewhere else; noopener is what severs that. It is added unless the rel list already
+ /// says what the opener relationship should be - an author asking for opener back means it, and
+ /// noreferrer already implies noopener.
+ ///
+ private static string? BuildRel(string? rel, string? target)
+ {
+ if (target is not "_blank") return rel;
+
+ var tokens = rel?.Split(' ', StringSplitOptions.RemoveEmptyEntries) ?? [];
+
+ foreach (var token in tokens)
{
- _rel = null;
- return;
+ if (token is "noopener" or "noreferrer" or "opener") return rel;
}
- _rel = BitLinkRelUtils.GetRels(Rel.Value);
+ return tokens.Length > 0 ? $"{rel} noopener" : "noopener";
+ }
+
+ ///
+ /// The sentence a new-tab link is announced with, or null where there is nothing to announce.
+ ///
+ private string? GetNewTabHint(string? target)
+ {
+ if (NoNewTabHint || target is not "_blank") return null;
+
+ var hint = NewTabHint ?? DefaultNewTabHint;
+
+ return hint.HasValue() ? hint : null;
+ }
+
+ ///
+ /// The value of the aria-current attribute, or null where the link is not the current item.
+ ///
+ private string? GetAriaCurrent(string? splattedAriaCurrent)
+ {
+ return AriaCurrent.HasValue ? _ariaCurrentMap[AriaCurrent.Value] : splattedAriaCurrent;
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.scss b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.scss
index bee952b6949..7d0b342d485 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.scss
@@ -5,13 +5,21 @@
padding: 0;
outline: none;
cursor: pointer;
- font-size: inherit;
font-weight: inherit;
text-decoration: none;
color: var(--bit-lnk-clr);
font-family: $tg-font-family;
background-color: transparent;
border-radius: $shp-radius-control;
+ // A link is a piece of text before it is a control, so it takes the type of the sentence it sits in
+ // rather than a size of its own - which is also what the four properties below are for. A button
+ // brings its own centered text, its own line box and its own baseline, and a link that renders as
+ // one has to be talked out of all three to sit on the same line as the words around it the way the
+ // anchor does. Size overrides the font size from the ramp; nothing overrides the rest.
+ font-size: inherit;
+ line-height: inherit;
+ text-align: inherit;
+ vertical-align: baseline;
&:hover,
&:active,
@@ -39,10 +47,10 @@
}
&:focus-visible {
- // Cancel both layers of the focus-ring mixin applied above: the box-shadow ring and
- // the forced-colors Highlight outline, so a disabled link never shows a focus indicator.
- outline: none;
- box-shadow: none;
+ // AllowDisabledFocus keeps a disabled link in the tab order, so the indicator has to stay
+ // visible - but drawn in the disabled color, since the vivid role ring of the enabled state
+ // contradicts the inert look and reads as actionable.
+ @include focus-ring(var(--bit-lnk-clr-dis));
}
&:hover,
@@ -82,6 +90,54 @@
}
}
+.bit-lnk-icn {
+ // Whatever type scale the icon set came with, the glyph is normalized back to the size of the text
+ // beside it, and lowered by a fraction of that size so its optical center sits on the text's own
+ // center rather than on the baseline the letters stand on.
+ line-height: 1;
+ font-size: 1em;
+ vertical-align: -0.125em;
+}
+
+.bit-lnk-sic {
+ margin-inline-end: spacing(0.375);
+}
+
+.bit-lnk-eic {
+ margin-inline-start: spacing(0.375);
+}
+
+// The two pieces of text a link carries for a screen reader and for nobody else: the sentence saying that
+// it opens a new tab, and the description it is pointed at through aria-describedby. The visual cue for
+// either is the icon or the wording an author puts on the page themselves. Both are taken out of the flow
+// rather than hidden, since display:none and visibility:hidden are also hidden from the readers they are
+// written for.
+.bit-lnk-hnt,
+.bit-lnk-dsc {
+ border: 0;
+ padding: 0;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ position: absolute;
+ white-space: nowrap;
+ clip-path: inset(50%);
+}
+
+// The size classes are for the link that stands on its own; a link inside a sentence is left inheriting.
+.bit-lnk-sm {
+ font-size: $tg-fs-xs;
+}
+
+.bit-lnk-md {
+ font-size: $tg-fs-sm;
+}
+
+.bit-lnk-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-lnk-#{$role} {
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLinkParams.cs b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLinkParams.cs
new file mode 100644
index 00000000000..f3c31ca0422
--- /dev/null
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLinkParams.cs
@@ -0,0 +1,206 @@
+namespace Bit.BlazorUI;
+
+///
+/// The parameters for the component.
+///
+///
+/// What a subtree of links can share is how they look and what they are allowed to do, never where they go:
+/// an Href, an icon or a title shared by every link of a page would be one link written many times
+/// over, which is never what was meant. The one worth having above all the others is
+/// - the sentence a new-tab link is announced with is English until an app says
+/// otherwise, and an app says it once here rather than at every link it writes.
+///
+public class BitLinkParams : BitComponentBaseParams, IBitComponentParams
+{
+ ///
+ /// Represents the parameter name used to identify the BitLink cascading parameters within BitParams.
+ ///
+ ///
+ /// This constant is typically used when referencing or accessing the BitLink 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(BitLink)}";
+
+
+
+ public string Name => ParamName;
+
+
+
+ ///
+ /// Keeps the disabled link focusable and discoverable by assistive technologies.
+ ///
+ /// .
+ ///
+ public bool? AllowDisabledFocus { get; set; }
+
+ ///
+ /// The general color of the link.
+ ///
+ /// .
+ ///
+ public BitColor? Color { get; set; }
+
+ ///
+ /// The position of the icon relative to the link content.
+ ///
+ /// .
+ ///
+ public BitIconPosition? IconPosition { get; set; }
+
+ ///
+ /// Replaces the text a new-tab link is announced with, for translating it or for saying it another way.
+ ///
+ /// .
+ ///
+ public string? NewTabHint { get; set; }
+
+ ///
+ /// Removes applying any foreground color to the link content, letting it keep its own color.
+ ///
+ /// .
+ ///
+ public bool? NoColor { get; set; }
+
+ ///
+ /// Stops a new-tab link from announcing that it opens in a new tab.
+ ///
+ /// .
+ ///
+ public bool? NoNewTabHint { get; set; }
+
+ ///
+ /// Styles the link to have no underline at any state.
+ ///
+ /// .
+ ///
+ public bool? NoUnderline { get; set; }
+
+ ///
+ /// The relationship between the current document and the linked document.
+ ///
+ /// .
+ ///
+ public BitLinkRels? Rel { get; set; }
+
+ ///
+ /// The preset size of the link text.
+ ///
+ /// .
+ ///
+ public BitSize? Size { get; set; }
+
+ ///
+ /// Stops the propagation of the click event to the parent elements.
+ ///
+ /// .
+ ///
+ public bool? StopPropagation { get; set; }
+
+ ///
+ /// How to open the link, for example _blank to open it in a new tab.
+ ///
+ /// .
+ ///
+ public string? Target { get; set; }
+
+ ///
+ /// Styles the link with a fixed underline at all states.
+ ///
+ /// .
+ ///
+ public bool? Underlined { 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(BitLink bitLink)
+ {
+ if (bitLink is null) return;
+
+ UpdateBaseParameters(bitLink);
+
+ if (AllowDisabledFocus.HasValue && bitLink.HasNotBeenSet(nameof(AllowDisabledFocus)))
+ {
+ bitLink.AllowDisabledFocus = AllowDisabledFocus.Value;
+ }
+
+ if (Color.HasValue && bitLink.HasNotBeenSet(nameof(Color)))
+ {
+ bitLink.Color = Color.Value;
+
+ bitLink.ClassBuilder.Reset();
+ }
+
+ if (IconPosition.HasValue && bitLink.HasNotBeenSet(nameof(IconPosition)))
+ {
+ bitLink.IconPosition = IconPosition.Value;
+
+ bitLink.ClassBuilder.Reset();
+ }
+
+ if (NewTabHint is not null && bitLink.HasNotBeenSet(nameof(NewTabHint)))
+ {
+ bitLink.NewTabHint = NewTabHint;
+ }
+
+ if (NoColor.HasValue && bitLink.HasNotBeenSet(nameof(NoColor)))
+ {
+ bitLink.NoColor = NoColor.Value;
+
+ bitLink.ClassBuilder.Reset();
+ }
+
+ if (NoNewTabHint.HasValue && bitLink.HasNotBeenSet(nameof(NoNewTabHint)))
+ {
+ bitLink.NoNewTabHint = NoNewTabHint.Value;
+ }
+
+ if (NoUnderline.HasValue && bitLink.HasNotBeenSet(nameof(NoUnderline)))
+ {
+ bitLink.NoUnderline = NoUnderline.Value;
+
+ bitLink.ClassBuilder.Reset();
+ }
+
+ if (Rel.HasValue && bitLink.HasNotBeenSet(nameof(Rel)))
+ {
+ bitLink.Rel = Rel.Value;
+ }
+
+ if (Size.HasValue && bitLink.HasNotBeenSet(nameof(Size)))
+ {
+ bitLink.Size = Size.Value;
+
+ bitLink.ClassBuilder.Reset();
+ }
+
+ if (StopPropagation.HasValue && bitLink.HasNotBeenSet(nameof(StopPropagation)))
+ {
+ bitLink.StopPropagation = StopPropagation.Value;
+ }
+
+ if (Target.HasValue() && bitLink.HasNotBeenSet(nameof(Target)))
+ {
+ bitLink.Target = Target;
+ }
+
+ if (Underlined.HasValue && bitLink.HasNotBeenSet(nameof(Underlined)))
+ {
+ bitLink.Underlined = Underlined.Value;
+
+ bitLink.ClassBuilder.Reset();
+ }
+ }
+}
diff --git a/src/BlazorUI/Bit.BlazorUI/Extensions/JsInterop/UtilsJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI/Extensions/JsInterop/UtilsJsRuntimeExtensions.cs
index 6343cf8e83d..cfbaf07e800 100644
--- a/src/BlazorUI/Bit.BlazorUI/Extensions/JsInterop/UtilsJsRuntimeExtensions.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Extensions/JsInterop/UtilsJsRuntimeExtensions.cs
@@ -205,9 +205,9 @@ internal static ValueTask BitUtilsDisposePreventDefaultKeys(this IJSRuntime jsRu
}
- internal static ValueTask BitUtilsScrollElementIntoView(this IJSRuntime jsRuntime, string targetElementId)
+ internal static ValueTask BitUtilsScrollElementIntoView(this IJSRuntime jsRuntime, string targetElementId, bool focus = false)
{
- return jsRuntime.InvokeVoid("BitBlazorUI.Utils.scrollElementIntoView", targetElementId);
+ return jsRuntime.InvokeVoid("BitBlazorUI.Utils.scrollElementIntoView", targetElementId, focus);
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Scripts/Utils.ts b/src/BlazorUI/Bit.BlazorUI/Scripts/Utils.ts
index 37df26e2ae8..73ea050c689 100644
--- a/src/BlazorUI/Bit.BlazorUI/Scripts/Utils.ts
+++ b/src/BlazorUI/Bit.BlazorUI/Scripts/Utils.ts
@@ -734,16 +734,38 @@
} catch (e) { console.error("BitBlazorUI.Utils.scrollToChild:", e); }
}
- public static scrollElementIntoView(targetElementId: string) {
+ // Brings the element with the given id into view. The smooth scroll is a courtesy rather than a
+ // requirement, so it is dropped for a reader who has asked for less motion - a page that slides
+ // under someone with a vestibular disorder is worse than one that simply arrives. Passing focus
+ // moves the keyboard along with the viewport, which is what an in-page link owes a reader who is
+ // not looking at the scrollbar.
+ public static scrollElementIntoView(targetElementId: string, focus: boolean = false) {
const element = document.getElementById(targetElementId);
if (!element) return;
try {
+ const reduced = typeof window.matchMedia === "function"
+ && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+
element.scrollIntoView({
- behavior: "smooth",
+ behavior: reduced ? "auto" : "smooth",
block: "start",
inline: "nearest"
});
+
+ if (!focus) return;
+
+ // An element that cannot take focus of its own is given a tab stop that only code can
+ // reach, so the destination becomes focusable without becoming one more stop for everyone
+ // tabbing through the page. One that is already focusable, or that was already given a
+ // tabindex of its own, is left exactly as it is.
+ if (element.tabIndex < 0 && !element.hasAttribute("tabindex")) {
+ element.setAttribute("tabindex", "-1");
+ }
+
+ // The scroll above has already put the element where it belongs; letting the focus scroll
+ // to it as well would undo the alignment it was just given.
+ element.focus({ preventScroll: true });
} catch (e) { console.error("BitBlazorUI.Utils.scrollElementIntoView:", e); }
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Utils/Enums/BitLinkRelUtils.cs b/src/BlazorUI/Bit.BlazorUI/Utils/Enums/BitLinkRelUtils.cs
index 6791370c43d..43cb735649c 100644
--- a/src/BlazorUI/Bit.BlazorUI/Utils/Enums/BitLinkRelUtils.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Utils/Enums/BitLinkRelUtils.cs
@@ -1,11 +1,19 @@
namespace Bit.BlazorUI;
-internal class BitLinkRelUtils
+internal static class BitLinkRelUtils
{
internal static readonly BitLinkRels[] AllRels = Enum.GetValues();
internal static string GetRels(BitLinkRels rel)
{
- return string.Join(" ", AllRels.Where(r => rel.HasFlag(r)).Select(r => r.ToString().ToLower()));
+ return string.Join(" ", AllRels.Where(r => rel.HasFlag(r)).Select(GetRelName));
}
+
+ private static string GetRelName(BitLinkRels rel) => rel switch
+ {
+ // The multi-word rel values are hyphenated in HTML, which a plain lowercasing of the member name cannot produce.
+ BitLinkRels.PrivacyPolicy => "privacy-policy",
+ BitLinkRels.TermsOfService => "terms-of-service",
+ _ => rel.ToString().ToLowerInvariant()
+ };
}
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ActionButton/BitActionButtonDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ActionButton/BitActionButtonDemo.razor.cs
index 293baca31de..d0fb65002ea 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ActionButton/BitActionButtonDemo.razor.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ActionButton/BitActionButtonDemo.razor.cs
@@ -582,6 +582,42 @@ public partial class BitActionButtonDemo
Name = "Tag",
Value = "4096",
Description = "A tag (keyword) for the current document."
+ },
+ new()
+ {
+ Name = "Me",
+ Value = "8192",
+ Description = "Indicates that the linked document represents the person who owns the current content. (used for identity verification)"
+ },
+ new()
+ {
+ Name = "Opener",
+ Value = "16384",
+ Description = "Requires that any browsing context created by following the hyperlink keeps its opener browsing context. (reverses the implicit noopener modern browsers apply to _blank targets)"
+ },
+ new()
+ {
+ Name = "PrivacyPolicy",
+ Value = "32768",
+ Description = "Links to the privacy policy that applies to the current document. (rendered as privacy-policy)"
+ },
+ new()
+ {
+ Name = "Sponsored",
+ Value = "65536",
+ Description = "Marks the link as an advertisement or paid placement, so search engines do not count it as an organic endorsement."
+ },
+ new()
+ {
+ Name = "TermsOfService",
+ Value = "131072",
+ Description = "Links to the terms of service that apply to the current document. (rendered as terms-of-service)"
+ },
+ new()
+ {
+ Name = "Ugc",
+ Value = "262144",
+ Description = "Marks the link as user-generated content, like forum posts or comments, for search engines."
}
]
},
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/Button/BitButtonDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/Button/BitButtonDemo.razor.cs
index 9f68ed6cd3e..cc08a794195 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/Button/BitButtonDemo.razor.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/Button/BitButtonDemo.razor.cs
@@ -747,6 +747,42 @@ public partial class BitButtonDemo
Name = "Tag",
Value = "4096",
Description = "A tag (keyword) for the current document."
+ },
+ new()
+ {
+ Name = "Me",
+ Value = "8192",
+ Description = "Indicates that the linked document represents the person who owns the current content. (used for identity verification)"
+ },
+ new()
+ {
+ Name = "Opener",
+ Value = "16384",
+ Description = "Requires that any browsing context created by following the hyperlink keeps its opener browsing context. (reverses the implicit noopener modern browsers apply to _blank targets)"
+ },
+ new()
+ {
+ Name = "PrivacyPolicy",
+ Value = "32768",
+ Description = "Links to the privacy policy that applies to the current document. (rendered as privacy-policy)"
+ },
+ new()
+ {
+ Name = "Sponsored",
+ Value = "65536",
+ Description = "Marks the link as an advertisement or paid placement, so search engines do not count it as an organic endorsement."
+ },
+ new()
+ {
+ Name = "TermsOfService",
+ Value = "131072",
+ Description = "Links to the terms of service that apply to the current document. (rendered as terms-of-service)"
+ },
+ new()
+ {
+ Name = "Ugc",
+ Value = "262144",
+ Description = "Marks the link as user-generated content, like forum posts or comments, for search engines."
}
]
},
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Badge/BitBadgeDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Badge/BitBadgeDemo.razor.cs
index ad76b3157ba..d5e20b7fe35 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Badge/BitBadgeDemo.razor.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Badge/BitBadgeDemo.razor.cs
@@ -598,6 +598,42 @@ public partial class BitBadgeDemo
Name = "Tag",
Value = "4096",
Description = "A tag (keyword) for the current document."
+ },
+ new()
+ {
+ Name = "Me",
+ Value = "8192",
+ Description = "Indicates that the linked document represents the person who owns the current content. (used for identity verification)"
+ },
+ new()
+ {
+ Name = "Opener",
+ Value = "16384",
+ Description = "Requires that any browsing context created by following the hyperlink keeps its opener browsing context. (reverses the implicit noopener modern browsers apply to _blank targets)"
+ },
+ new()
+ {
+ Name = "PrivacyPolicy",
+ Value = "32768",
+ Description = "Links to the privacy policy that applies to the current document. (rendered as privacy-policy)"
+ },
+ new()
+ {
+ Name = "Sponsored",
+ Value = "65536",
+ Description = "Marks the link as an advertisement or paid placement, so search engines do not count it as an organic endorsement."
+ },
+ new()
+ {
+ Name = "TermsOfService",
+ Value = "131072",
+ Description = "Links to the terms of service that apply to the current document. (rendered as terms-of-service)"
+ },
+ new()
+ {
+ Name = "Ugc",
+ Value = "262144",
+ Description = "Marks the link as user-generated content, like forum posts or comments, for search engines."
}
]
},
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Tag/BitTagDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Tag/BitTagDemo.razor.cs
index 91c49ba5418..9a5b7e5ebb3 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Tag/BitTagDemo.razor.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Tag/BitTagDemo.razor.cs
@@ -815,6 +815,42 @@ public partial class BitTagDemo
Name = "Tag",
Value = "4096",
Description = "A tag (keyword) for the current document."
+ },
+ new()
+ {
+ Name = "Me",
+ Value = "8192",
+ Description = "Indicates that the linked document represents the person who owns the current content. (used for identity verification)"
+ },
+ new()
+ {
+ Name = "Opener",
+ Value = "16384",
+ Description = "Requires that any browsing context created by following the hyperlink keeps its opener browsing context. (reverses the implicit noopener modern browsers apply to _blank targets)"
+ },
+ new()
+ {
+ Name = "PrivacyPolicy",
+ Value = "32768",
+ Description = "Links to the privacy policy that applies to the current document. (rendered as privacy-policy)"
+ },
+ new()
+ {
+ Name = "Sponsored",
+ Value = "65536",
+ Description = "Marks the link as an advertisement or paid placement, so search engines do not count it as an organic endorsement."
+ },
+ new()
+ {
+ Name = "TermsOfService",
+ Value = "131072",
+ Description = "Links to the terms of service that apply to the current document. (rendered as terms-of-service)"
+ },
+ new()
+ {
+ Name = "Ugc",
+ Value = "262144",
+ Description = "Marks the link as user-generated content, like forum posts or comments, for search engines."
}
]
},
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Card/BitCardDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Card/BitCardDemo.razor.cs
index 9df2731867b..50ef08e2e0d 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Card/BitCardDemo.razor.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Card/BitCardDemo.razor.cs
@@ -840,6 +840,42 @@ public partial class BitCardDemo
Name = "Tag",
Value = "4096",
Description = "A tag (keyword) for the current document."
+ },
+ new()
+ {
+ Name = "Me",
+ Value = "8192",
+ Description = "Indicates that the linked document represents the person who owns the current content. (used for identity verification)"
+ },
+ new()
+ {
+ Name = "Opener",
+ Value = "16384",
+ Description = "Requires that any browsing context created by following the hyperlink keeps its opener browsing context. (reverses the implicit noopener modern browsers apply to _blank targets)"
+ },
+ new()
+ {
+ Name = "PrivacyPolicy",
+ Value = "32768",
+ Description = "Links to the privacy policy that applies to the current document. (rendered as privacy-policy)"
+ },
+ new()
+ {
+ Name = "Sponsored",
+ Value = "65536",
+ Description = "Marks the link as an advertisement or paid placement, so search engines do not count it as an organic endorsement."
+ },
+ new()
+ {
+ Name = "TermsOfService",
+ Value = "131072",
+ Description = "Links to the terms of service that apply to the current document. (rendered as terms-of-service)"
+ },
+ new()
+ {
+ Name = "Ugc",
+ Value = "262144",
+ Description = "Marks the link as user-generated content, like forum posts or comments, for search engines."
}
]
},
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Link/BitLinkDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Link/BitLinkDemo.razor
index bfb95103d39..deaf0a75a99 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Link/BitLinkDemo.razor
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Link/BitLinkDemo.razor
@@ -1,49 +1,189 @@
-@page "/components/link"
+@page "/components/link"
+ Description="Links lead to another part of an app, other pages, or help articles. They can also be used to initiate commands, rendering an anchor or a button while keeping the link look, and hardening new-tab links with an automatic noopener and an opens-in-a-new-tab announcement." />
+
+ A link only needs an Href and its text, and it takes the font of whatever it sits in, so a link
+ inside a sentence stays the same size and weight as the words around it. A disabled link keeps its
+ place in the layout but loses its href, so it can no longer be navigated or clicked.
+
+ Basic Link
Disabled Link
+
+ By default the underline only appears on hover. The Underlined parameter keeps it visible at all
+ states, maximizing the visual cue that the text is interactive, especially when the link sits inside body text.
+
+ Underlined link
+
+ The NoUnderline parameter removes the underline at every state, including hover and active, and
+ wins over Underlined when both are set. Reserve it for places where the surrounding context
+ already marks the text as a link (like a navigation menu), since inside body text color alone is not
+ enough of a cue for every reader.
+
+ NoUnderline link
-
- Blank target link
+
+
+ The IconName parameter puts a glyph beside the link text, normalized back to the size of that
+ text so the two sit on the same line. IconPosition moves it from the front of the text to after
+ it, which is where the two glyphs a link carries most often, the arrow of a link opening a new tab and
+ the tray of a download, belong. The glyph is drawn as decoration and hidden from screen readers, so
+ whatever it says has to be in the link text as well.
+
+
+ Link with a leading icon
+
+ Link with a trailing icon
+
+ Icon on an underlined link
+
+
+
+
+ The Target parameter controls the browsing context the link opens in; the BitLinkTarget
+ class provides the standard values, but any plain string (like a frame name) works too. A _blank
+ target is hardened twice over: it automatically gets a noopener rel attribute so the opened
+ page cannot reach back into the app (see the Rel section below), and it announces that it opens in a new
+ tab (see the next section).
+
+
+ Blank target link
- Parent target link
+ Parent target link
- Self target link
+ Self target link
- Top target link
+ Top target link
-
- Click to navigate to the bit platform GitHub repo!
+
+
+ A new tab is a change of context nothing on the page predicts, and the back button no longer returns
+ from it, so a _blank link carries the sentence saying so as visually hidden text after its
+ content. Nothing changes on screen; a screen reader reads "opens in a new tab" with the link.
+ NewTabHint replaces that sentence, for translating it or wording it another way, and
+ NoNewTabHint takes it off where the page already says it, next to a visible label of your own or
+ a trailing icon whose meaning the text already carries. When an AriaLabel is set, the sentence is
+ appended to it instead, since an aria-label replaces the content rather than adding to it. An app that
+ is not written in English says the sentence once for all of its links, by cascading a
+ BitLinkParams with a NewTabHint of its own through BitParams.
+
+
+ Announced as opening in a new tab
+
+ Announced with a translated sentence
+
+
+ Opens in a new tab (said in the text already)
+
-
-
If you start the Href parameter with a # character, it'll look for the an element with that id and tries to scroll the view into that element.
+
+
+ When the link points to a file, the Download parameter tells the browser to save it instead of
+ navigating to it. Pass an empty string to keep the server-provided file name, or a value to suggest your own.
+ Browsers honor this only for same-origin, blob:, and data: URLs, so a cross-origin
+ link will simply navigate as usual.
+
+
+ Download the bit logo
- Go To End of this Article
+ Download with a custom file name
+
+
+
+
+ The Title parameter is the browser's own tooltip, shown after a hover long enough to count as one.
+ Neither touch nor the keyboard reaches it, so nothing the reader has to have belongs only here: it is the
+ place for what a full URL, or a longer wording of the link text, adds to the words already on screen.
+
+
+ Hover to see the full address
+
+
+
+
+ AriaDescription is a longer sentence about the link, rendered as visually hidden text the link
+ points at through aria-describedby. A description is read out after the name and is not
+ part of it, which makes it the place for what the reader would want to know before following the link
+ but not in the words on the page: the size of a file, the format it is in, what the page it leads to
+ will ask for. Unlike a Title, which the browser only ever shows to a mouse, this reaches
+ everyone a screen reader is reading to.
+
+
+
+ Download the brand guidelines
+
+
+
+
+
+ Without an Href, the link renders as a button styled like a link and OnClick is its whole action.
+ With an Href, OnClick still fires alongside the navigation, which is handy for tracking or
+ for extra work right before leaving. The StopPropagation parameter keeps the click from bubbling up
+ to clickable containers around the link.
+
+
+ Click to navigate to the bit platform GitHub repo!
+
+ Link with both Href and OnClick
+
OnClick count: @clickCount
+
+
containerClickCount++">
+ A clickable container (clicked @containerClickCount times):
+ Link with StopPropagation (clicked @linkClickCount times)
+
+
+
+
+
+ PreventDefault suppresses the navigation a click would otherwise perform, leaving OnClick as
+ the whole of what the click does. The anchor keeps its Href, so the destination is still what the
+ status bar shows, what a middle click opens in a new tab and what "copy link address" copies, which is
+ what separates this from a link with no href at all. That is what a link needs to confirm before leaving,
+ or to save a draft first and navigate from the handler afterwards.
+
+
+ Ask before leaving
+
@guardMessage
+
+
+
+
+ An Href starting with the # character smooth-scrolls the element with that id into
+ view and moves the keyboard focus to it, so the next Tab carries on from the destination rather than from
+ the link, without pushing the fragment into the browser's address bar. The scroll drops to an instant jump
+ for a reader who has asked for reduced motion, and a scroll-margin style on the target element
+ keeps it clear of any fixed header once it arrives.
+
+
+ Go To End of this Article
Once upon a time, stories wove connections between people, a symphony of voices crafting shared dreams.
Each word carried meaning, each pause brought understanding. Placeholder text reminds us of that moment
@@ -83,16 +223,65 @@
what this blank slate can become. Whether it’s a story, an idea, or a message that matters, this is your
starting point. The possibilities are endless, and the journey begins now.
- Go To Start of this Article
+ Go To Start of this Article
-
+
+
+ The Rel parameter describes the relationship between the current document and the linked one, using
+ the BitLinkRels flags enum, so multiple values combine with the | operator. It
+ covers the full set of HTML rel values, including the newer sponsored, ugc,
+ opener, privacy-policy and terms-of-service.
+ When the Target is _blank and no opener-related rel (NoOpener,
+ NoReferrer or Opener) is provided, noopener is added automatically
+ to keep the opened page from reaching back into the app.
+
+ Link with a rel attribute (nofollow)
Link with a rel attribute (nofollow & noreferrer)
+
+ Link with a rel attribute (sponsored & ugc)
+
+ Blank target link with an automatic noopener rel
+
+
+
+
+ A disabled link is normally removed from the tab order, which means keyboard and screen reader users can miss
+ that it exists at all. Setting AllowDisabledFocus conveys the disabled state through
+ aria-disabled instead, so the link stays focusable and discoverable while its navigation and
+ click action remain suppressed. Tab through the two links below to feel the difference.
+
+
+ Disabled link (skipped by Tab)
+
+ Disabled link with AllowDisabledFocus (focusable)
+
+
+
+
+ In a set of links, exactly one of them leads to where the reader already is, and drawing it
+ differently says so only to whoever is looking at it. The AriaCurrent parameter says the same
+ thing in a way a screen reader announces, through the aria-current attribute: use
+ Page in a navigation menu, Step in a wizard, Location in a
+ breadcrumb, and True where the kind of set does not have a name. Only one link of a set
+ is ever the current one.
+
+
+ Link (this page)
+
+ Button
+
+ Image
-
+
+
+ The NoColor parameter stops the link from applying any foreground color, letting the content keep
+ its own color, useful when the link wraps rich content that brings its own styling.
+
+ Link with default color!this text color is coming from the link itself.
@@ -103,7 +292,7 @@
-
+
Offering a range of specialized color variants with Primary being the default, providing visual cues for specific actions or states within your application.
@@ -191,15 +380,73 @@
-
+
+
+ Use icons from external libraries like FontAwesome and Bootstrap Icons with the Icon parameter and
+ BitIconInfo. Whatever type scale the set came with, the glyph is normalized back to the size of the
+ link text beside it, so it sits on the same line as the words and follows the link's own color.
+
+
+
+
+
+
+
FontAwesome:
+
+ bit platform on GitHub
+
+ Opens in a new tab
+
+
+
+
+
+
Bootstrap:
+
+ bit platform on GitHub
+
+ Opens in a new tab
+
+
+
+
+ With nothing set, a link takes the font size of whatever it sits in, which is what keeps a link inside a
+ paragraph the same size as the sentence around it. The Size parameter is for the link that stands
+ on its own, where there is no surrounding text to take a size from.
+
+
+ Small link
+
+ Medium link
+
+ Large link
+
+ The icon follows the size
+
+
+
+
+ The link is one element - the anchor or the button it renders as - so the root-level Style and
+ Class parameters are all it takes to restyle it, on top of the theme tokens it reads its colors
+ from. An icon inside it follows the color and the size the link is given, since it is drawn in the
+ link's own type.
+
+ Link with style
Link with class
-
+
+
+ Use the Dir parameter to render the link in right-to-left direction for RTL languages. An icon
+ follows the direction too, so a leading glyph stays on the leading side of the text.
+
+
پیوند راست به چپ
+
+ پیوند راست به چپ با آیکن
-
\ No newline at end of file
+
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Link/BitLinkDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Link/BitLinkDemo.razor.cs
index 2fc5fa31eb1..dc8bb312971 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Link/BitLinkDemo.razor.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Link/BitLinkDemo.razor.cs
@@ -7,6 +7,36 @@ public partial class BitLinkDemo
private readonly List componentParameters =
[
+ new()
+ {
+ Name = "AllowDisabledFocus",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "Keeps the disabled link focusable and discoverable by assistive technologies, conveying the disabled state using the aria-disabled attribute.",
+ },
+ new()
+ {
+ Name = "AriaCurrent",
+ Type = "BitNavAriaCurrent?",
+ DefaultValue = "null",
+ Description = "Reports the link as the current item of the set it belongs to, through the aria-current attribute. Only one link of a set is ever the current one.",
+ LinkType = LinkType.Link,
+ Href = "#nav-aria-current-enum",
+ },
+ new()
+ {
+ Name = "AriaDescription",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "A longer description of the link for the benefit of screen readers, rendered as visually hidden text the link points at through aria-describedby. It is read out after the name rather than as part of it.",
+ },
+ new()
+ {
+ Name = "AutoFocus",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "Gives the link the focus as soon as it is rendered, through the autofocus attribute. The browser honors it once per document, on the first element that asks for it.",
+ },
new()
{
Name = "ChildContent",
@@ -24,56 +54,148 @@ public partial class BitLinkDemo
Href = "#color-enum",
},
new()
+ {
+ Name = "Download",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "The value of the download attribute of the link when the Href parameter is provided. Instructs the browser to download the linked resource instead of navigating to it, using the provided value (if any) as the suggested file name.",
+ },
+ new()
{
Name = "Href",
Type = "string?",
DefaultValue = "null",
- Description = "URL the link points to.",
+ Description = "URL the link points to. If provided, the component renders an anchor tag, otherwise a button. A value starting with the # character makes the link smooth-scroll the element with that id into view and move the focus to it.",
+ },
+ new()
+ {
+ Name = "Icon",
+ Type = "BitIconInfo?",
+ DefaultValue = "null",
+ Description = "The icon rendered beside the link content, using custom CSS classes for external icon libraries. Takes precedence over IconName when both are set.",
+ },
+ new()
+ {
+ Name = "IconName",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "The name of the icon rendered beside the link content, from the built-in Fluent UI icons. The glyph is decorative and hidden from assistive technologies.",
+ },
+ new()
+ {
+ Name = "IconPosition",
+ Type = "BitIconPosition?",
+ DefaultValue = "null",
+ Description = "The position of the icon relative to the link content. The icon goes in front of the text by default; End puts it after the text.",
+ LinkType = LinkType.Link,
+ Href = "#icon-position-enum",
+ },
+ new()
+ {
+ Name = "NewTabHint",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Replaces the text a new-tab link is announced with. A _blank link carries \"(opens in a new tab)\" as visually hidden text after its content, or appended to its AriaLabel when it has one. An empty value takes the announcement off.",
},
new()
{
Name = "NoColor",
Type = "bool",
DefaultValue = "false",
- Description = "Removes the applying any foreground color to the link content.",
+ Description = "Removes applying any foreground color to the link content, letting it keep its own color.",
+ },
+ new()
+ {
+ Name = "NoNewTabHint",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "Stops a new-tab link from announcing that it opens in a new tab. Only set it where the page already says so.",
},
new()
{
Name = "NoUnderline",
Type = "bool",
DefaultValue = "false",
- Description = "Styles the link to have no underline at any state.",
+ Description = "Styles the link to have no underline at any state. It wins over Underlined when both are set.",
},
new()
{
Name = "OnClick",
Type = "EventCallback",
- Description = "Callback for when the link clicked.",
+ Description = "Callback for when the link is clicked. It is invoked in every render mode of the link: on anchor links it runs alongside the navigation, and on button links (no Href) it is the sole click action.",
+ },
+ new()
+ {
+ Name = "PreventDefault",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "Suppresses the navigation a click on the link would otherwise perform, leaving OnClick as the whole of what the click does. The anchor keeps its Href, so a middle click and \"copy link address\" still reach the destination.",
},
new()
{
Name = "Rel",
Type = "BitLinkRels?",
DefaultValue = "null",
- Description = "If Href provided, specifies the relationship between the current document and the linked document.",
+ Description = "If Href provided, specifies the relationship between the current document and the linked document. Ignored for empty or hash-only (#) hrefs. When Target is _blank and no opener-related rel (NoOpener, NoReferrer or Opener) is provided, noopener is added automatically.",
LinkType = LinkType.Link,
Href = "#link-rels",
},
new()
+ {
+ Name = "Size",
+ Type = "BitSize?",
+ DefaultValue = "null",
+ Description = "Sets the preset size of the link text. With nothing set the link takes the font size of whatever it sits in.",
+ LinkType = LinkType.Link,
+ Href = "#size-enum",
+ },
+ new()
+ {
+ Name = "StopPropagation",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "If true, stops the propagation of the click event to the parent elements. Useful when the link is placed inside clickable containers like rows or cards.",
+ },
+ new()
{
Name = "Target",
Type = "string?",
DefaultValue = "null",
- Description = "If Href provided, specifies how to open the link.",
+ Description = "If Href provided, specifies how to open the link (e.g. _blank to open it in a new tab). When set to _blank and no opener-related Rel is provided, noopener is added to the rel attribute automatically.",
LinkType = LinkType.Link,
Href = "#link-target",
},
new()
+ {
+ Name = "Title",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "The tooltip to show when the mouse is placed on the link. Neither touch nor the keyboard reaches it, so nothing the reader has to have belongs only here.",
+ },
+ new()
{
Name = "Underlined",
Type = "bool",
DefaultValue = "false",
- Description = "Styles the link with a fixed underline at all states.",
+ Description = "Styles the link with a fixed underline at all states. NoUnderline wins over it when both are set.",
+ },
+ ];
+
+ private readonly List componentPublicMembers =
+ [
+ new()
+ {
+ Name = "FocusAsync",
+ Type = "ValueTask",
+ DefaultValue = "",
+ Description = "Gives focus to the root element of the link. A disabled link is only focusable when AllowDisabledFocus keeps it in the tab order.",
+ },
+ new()
+ {
+ Name = "FocusAsync(bool preventScroll)",
+ Type = "ValueTask",
+ DefaultValue = "",
+ Description = "Gives focus to the root element of the link. Passing true keeps the page scrolled where it is; passing false lets the browser scroll the link into view.",
},
];
@@ -233,6 +355,99 @@ public partial class BitLinkDemo
]
},
new()
+ {
+ Id = "nav-aria-current-enum",
+ Name = "BitNavAriaCurrent",
+ Description = "Defines the value of the aria-current attribute reported by the current link of a set.",
+ Items =
+ [
+ new()
+ {
+ Name = "Page",
+ Description = "Represents the current page within a set of pages.",
+ Value = "0",
+ },
+ new()
+ {
+ Name = "Step",
+ Description = "Represents the current step within a process.",
+ Value = "1",
+ },
+ new()
+ {
+ Name = "Location",
+ Description = "Represents the current location within an environment or context.",
+ Value = "2",
+ },
+ new()
+ {
+ Name = "Date",
+ Description = "Represents the current date within a collection of dates.",
+ Value = "3",
+ },
+ new()
+ {
+ Name = "Time",
+ Description = "Represents the current time within a set of times.",
+ Value = "4",
+ },
+ new()
+ {
+ Name = "True",
+ Description = "Represents the current item within a set, without saying which kind of set it is.",
+ Value = "5",
+ }
+ ]
+ },
+ new()
+ {
+ Id = "icon-position-enum",
+ Name = "BitIconPosition",
+ Description = "Describes the placement of an icon relative to other content.",
+ Items =
+ [
+ new()
+ {
+ Name = "Start",
+ Description = "Icon renders before the content (default).",
+ Value = "0",
+ },
+ new()
+ {
+ Name = "End",
+ Description = "Icon renders after the content.",
+ Value = "1",
+ }
+ ]
+ },
+ new()
+ {
+ Id = "size-enum",
+ Name = "BitSize",
+ Description = "Defines the preset 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",
+ }
+ ]
+ },
+ new()
{
Id = "link-rels",
Name = "BitLinkRels",
@@ -316,6 +531,42 @@ public partial class BitLinkDemo
Name = "Tag",
Value = "4096",
Description = "A tag (keyword) for the current document."
+ },
+ new()
+ {
+ Name = "Me",
+ Value = "8192",
+ Description = "Indicates that the linked document represents the person who owns the current content. (used for identity verification)"
+ },
+ new()
+ {
+ Name = "Opener",
+ Value = "16384",
+ Description = "Requires that any browsing context created by following the hyperlink keeps its opener browsing context. (reverses the implicit noopener modern browsers apply to _blank targets)"
+ },
+ new()
+ {
+ Name = "PrivacyPolicy",
+ Value = "32768",
+ Description = "Links to the privacy policy that applies to the current document. (rendered as privacy-policy)"
+ },
+ new()
+ {
+ Name = "Sponsored",
+ Value = "65536",
+ Description = "Marks the link as an advertisement or paid placement, so search engines do not count it as an organic endorsement."
+ },
+ new()
+ {
+ Name = "TermsOfService",
+ Value = "131072",
+ Description = "Links to the terms of service that apply to the current document. (rendered as terms-of-service)"
+ },
+ new()
+ {
+ Name = "Ugc",
+ Value = "262144",
+ Description = "Marks the link as user-generated content, like forum posts or comments, for search engines."
}
]
}
@@ -323,8 +574,20 @@ public partial class BitLinkDemo
+ private int clickCount;
+ private int linkClickCount;
+ private int containerClickCount;
+ private string? guardMessage;
+
private void HandleOnClick()
{
Navigation.NavigateTo("https://github.com/bitfoundation/bitplatform");
}
+
+ private void HandleGuardedClick()
+ {
+ // The browser did not navigate, so what happens next is entirely up to this handler:
+ // confirm, save a draft, track the click, and then navigate from here if it should happen.
+ guardMessage = "The navigation was suppressed. This is where a confirmation would go.";
+ }
}
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Link/BitLinkDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Link/BitLinkDemo.razor.samples.cs
index 21b2016c55b..7f896f6d854 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Link/BitLinkDemo.razor.samples.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Link/BitLinkDemo.razor.samples.cs
@@ -14,23 +14,77 @@ public partial class BitLinkDemo
NoUnderline link";
private readonly string example4RazorCode = @"
-Blank target link
-Parent target link
-Self target link
-Top target link";
+Link with a leading icon
+Link with a trailing icon
+Icon on an underlined link";
private readonly string example5RazorCode = @"
-Click to navigate to the bitplatform GitHub repo!";
- private readonly string example5CsharpCode = @"
+Blank target link
+Parent target link
+Self target link
+Top target link";
+
+ private readonly string example6RazorCode = @"
+Announced as opening in a new tab
+
+Announced with a translated sentence
+
+
+ Opens in a new tab (said in the text already)
+";
+
+ private readonly string example7RazorCode = @"
+Download the bit logo
+Download with a custom file name";
+
+ private readonly string example8RazorCode = @"
+Hover to see the full address";
+
+ private readonly string example9RazorCode = @"
+
+ Download the brand guidelines
+";
+
+ private readonly string example10RazorCode = @"
+Click to navigate to the bit platform GitHub repo!
+
+ clickCount++"" Target=""_blank"" Href=""https://github.com/bitfoundation/bitplatform"">Link with both Href and OnClick
+
OnClick count: @clickCount
+
+
containerClickCount++"">
+ A clickable container (clicked @containerClickCount times):
+ linkClickCount++"">Link with StopPropagation (clicked @linkClickCount times)
+
";
+ private readonly string example10CsharpCode = @"
[Inject] private NavigationManager Navigation { get; set; } = default!;
+private int clickCount;
+private int linkClickCount;
+private int containerClickCount;
+
private void HandleOnClick()
{
Navigation.NavigateTo(""https://github.com/bitfoundation/bitplatform"");
}";
- private readonly string example6RazorCode = @"
-Go To End of this Article
+ private readonly string example11RazorCode = @"
+Ask before leaving
+
@guardMessage
";
+ private readonly string example11CsharpCode = @"
+private string? guardMessage;
+
+private void HandleGuardedClick()
+{
+ // The browser did not navigate, so what happens next is entirely up to this handler:
+ // confirm, save a draft, track the click, and then navigate from here if it should happen.
+ guardMessage = ""The navigation was suppressed. This is where a confirmation would go."";
+}";
+
+ private readonly string example12RazorCode = @"
+Go To End of this Article
Once upon a time, stories wove connections between people, a symphony of voices crafting shared dreams.
Each word carried meaning, each pause brought understanding. Placeholder text reminds us of that moment
@@ -57,26 +111,37 @@ each word has the power to transform into something extraordinary. Here lies the
idea that sparks change, these lines are yours to fill, to shape, and to make uniquely yours. The journey
begins here, in this quiet moment where everything is possible.
-Imagine this space as a window into the future empty yet alive with the energy of endless possibilities.
-These words stand as temporary guides, placeholders that whisper of what is to come.
-They hold the promise of stories waiting to unfold, ideas eager to take shape, and
-connections that will soon emerge to inspire and resonate. This is not an empty page;
+Imagine this space as a window into the future empty yet alive with the energy of endless possibilities.
+These words stand as temporary guides, placeholders that whisper of what is to come.
+They hold the promise of stories waiting to unfold, ideas eager to take shape, and
+connections that will soon emerge to inspire and resonate. This is not an empty page;
it is a canvas, rich with potential and ready to transform into something meaningful.
-For now, these lines are here to remind you of the beauty of beginnings. They are the quiet before the symphony,
-the foundation upon which your creativity will build. Soon, this space will hold your thoughts, your visions,
-and your voice a reflection of who you are and what you wish to share with the world. Every sentence will carry
-purpose, every word will invite others to connect, to think, to feel. So take a moment to dream, to imagine
-what this blank slate can become. Whether it’s a story, an idea, or a message that matters, this is your
+For now, these lines are here to remind you of the beauty of beginnings. They are the quiet before the symphony,
+the foundation upon which your creativity will build. Soon, this space will hold your thoughts, your visions,
+and your voice a reflection of who you are and what you wish to share with the world. Every sentence will carry
+purpose, every word will invite others to connect, to think, to feel. So take a moment to dream, to imagine
+what this blank slate can become. Whether it’s a story, an idea, or a message that matters, this is your
starting point. The possibilities are endless, and the journey begins now.
-Go To Start of this Article";
+Go To Start of this Article";
- private readonly string example7RazorCode = @"
+ private readonly string example13RazorCode = @"
Link with a rel attribute (nofollow)
-Link with a rel attribute (nofollow & noreferrer)";
+Link with a rel attribute (nofollow & noreferrer)
+Link with a rel attribute (sponsored & ugc)
+Blank target link with an automatic noopener rel";
- private readonly string example8RazorCode = @"
+ private readonly string example14RazorCode = @"
+Disabled link (skipped by Tab)
+Disabled link with AllowDisabledFocus (focusable)";
+
+ private readonly string example15RazorCode = @"
+Link (this page)
+Button
+Image";
+
+ private readonly string example16RazorCode = @"
Link with default color!this text color is coming from the link itself.
@@ -86,8 +151,8 @@ and your voice a reflection of who you are and what you wish to share with the w
Link with NoColor!";
- private readonly string example9RazorCode = @"
- Primary Color Link
+ private readonly string example17RazorCode = @"
+Primary Color Link (default)Secondary Color LinkTertiary Color LinkInfo Color Link
@@ -96,9 +161,11 @@ and your voice a reflection of who you are and what you wish to share with the w
SevereWarning Color LinkError Color Link
-PrimaryBackground Color Link
-SecondaryBackground Color Link
-TertiaryBackground Color Link
+
+ PrimaryBackground Color Link
+ SecondaryBackground Color Link
+ TertiaryBackground Color Link
+
PrimaryForeground Color LinkSecondaryForeground Color Link
@@ -132,7 +199,25 @@ and your voice a reflection of who you are and what you wish to share with the w
SecondaryBorderTertiaryBorder";
- private readonly string example10RazorCode = @"
+ private readonly string example18RazorCode = @"
+
+
+bit platform on GitHub
+Opens in a new tab
+
+
+
+
+bit platform on GitHub
+Opens in a new tab";
+
+ private readonly string example19RazorCode = @"
+Small link
+Medium link
+Large link
+The icon follows the size";
+
+ private readonly string example20RazorCode = @"