diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/TagsInput/BitTagsInput.razor b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/TagsInput/BitTagsInput.razor index 2cf38e175c2..c94d57a5eed 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/TagsInput/BitTagsInput.razor +++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/TagsInput/BitTagsInput.razor @@ -10,7 +10,7 @@ var hasLabel = LabelTemplate is not null || Label.HasValue(); var hasDescription = DescriptionTemplate is not null || Description.HasValue(); var isInteractive = IsEnabled && ReadOnly is false; - var showClearButton = ShowClearButton && isInteractive && (tagCount > 0 || _inputText.Length > 0); + var showClearButton = ShowClearButton && isInteractive && (HasRemovableTag() || _inputText.Length > 0); // The tags beyond the MaxDisplayedTags are folded away behind a chip that says how many they are, // which the reader unfolds the list with; the value itself is untouched, only how much of it is drawn. @@ -27,7 +27,14 @@ var displayedTagCount = GetDisplayedTagCount(); var hiddenTagCount = tagCount - displayedTagCount; - var tagHint = GetTagAriaDescription(); + // What the keyboard can do with the tag just reached. A tag the CanRemoveTag predicate holds in place + // answers to one gesture fewer, so it is described by a sentence of its own rather than being promised + // a removal that does nothing; where the two sentences are the same one element serves every chip. + var tagHint = GetTagAriaDescription(true); + var fixedTagHint = CanRemoveTag is null ? tagHint : GetTagAriaDescription(false); + var sameTagHint = string.Equals(tagHint, fixedTagHint, StringComparison.Ordinal); + var tagHintId = tagHint is null ? null : _hintId; + var fixedTagHintId = fixedTagHint is null ? null : (sameTagHint ? _hintId : _fixedHintId); // The element references are what the arrow key navigation moves the focus with; the array is // rebuilt whenever the number of drawn tags changes so that a stale reference is never focused. @@ -48,8 +55,28 @@ var suggestions = GetSuggestions(); - // The same icon on every tag, so it is resolved once per render rather than once per chip. + // The sentence that says why the last tag was refused, and the one that says how many tags the field + // already holds: the first replaces the helper text while it stands, the second is only ever read out. + var invalidMessage = ShowInvalidMessage ? _invalidMessage : null; + var countDescription = GetTagCountDescription(tagCount); + + // A tag picked up with its handle and never put anywhere - because the parent has since taken it + // away, because the fold has closed over it, or because the field no longer offers the reordering at + // all - is put back down: a chip that is lifted with nowhere to land is only a chip drawn askew. + if (_pickedUpTagIndex >= displayedTagCount || (_pickedUpTagIndex >= 0 && (isInteractive is false || AllowReorder is false))) + { + SetPickedUpTag(-1); + } + + // The tag that is currently being carried, read once rather than once per chip: while it is in the + // air, every other handle is labelled by it rather than by the tag it belongs to. + var carriedTag = _pickedUpTagIndex >= 0 && tags is not null && _pickedUpTagIndex < tags.Count + ? tags[_pickedUpTagIndex] + : null; + + // The same icons on every tag, so they are resolved once per render rather than once per chip. var dismissIcon = BitIconInfo.From(DismissIcon, DismissIconName ?? "Cancel"); + var reorderIcon = AllowReorder ? BitIconInfo.From(ReorderIcon, ReorderIconName ?? "GripperBarVertical") : null; }
@Prefix
+
@Prefix
} @if (tagCount > 0) @@ -107,6 +134,7 @@ var tag = tags![index]; var isTagFocused = _focusedTagIndex == index; var isTagEditing = _editingTagIndex == index; + var isTagRemovable = CanRemove(tag); @* The tag itself is the focusable element rather than its dismiss button: a single roving tab stop covers the whole list, which the arrow keys then walk through, @@ -131,12 +159,44 @@ @ondrop:preventDefault="true" role="listitem" draggable="@(CanDragTag(index) ? "true" : null)" + @* The chip is named by the tag it stands for rather than by everything it holds: + the contents of a chip are the handle that moves it and the button that removes + it as well as the word itself, so a chip reading "blazor" would be read out as + "Move blazor, blazor, Remove blazor". A chip a TagTemplate draws as something + other than its value - "Ada Lovelace" over ada@@example.com - is named through + GetTagName, the template being markup nothing can read a name off. *@ + aria-label="@TagName(tag)" aria-posinset="@(index + 1)" aria-setsize="@tagCount" - aria-describedby="@(tagHint is null ? null : _hintId)" + aria-describedby="@(isTagRemovable ? tagHintId : fixedTagHintId)" tabindex="@(isTagEditing ? "-1" : GetTagTabIndex(index, displayedTagCount))" - style="@BuildTagStyle(tag, isTagFocused)" - class="@BuildTagClass(tag, index, isTagFocused)"> + style="@BuildTagStyle(tag, index, isTagFocused, isTagRemovable)" + class="@BuildTagClass(tag, index, isTagFocused, isTagRemovable)"> + @* Drawn only where there is somewhere to put the tag down: a handle on the only + chip in the field picks a tag up and hands it back, which is chrome that does + nothing on every chip that stands alone. *@ + @if (isInteractive && isTagEditing is false && AllowReorder && displayedTagCount > 1) + { + @* The handle that reorders a tag without a drag. Dragging a chip is the quick + gesture, but it is one no touch screen, no switch and no head pointer can + make, so the very same move is offered as two taps: one on the handle picks + the tag up, one on the tag whose place it should take puts it down, and a + second tap on the handle puts it back. Out of the tab order like the dismiss + button, the keyboard reordering being Alt with the arrow keys. *@ +