TagInput
Overview
Section titled “Overview”TagInput folds the web contract’s five parts (Root/List/Field/Tag/TagRemove) into one lookless
Control with two named template parts: PART_Input (a TextBox) and PART_Chips (an
ItemsControl of TagChipVm). It owns the tag collection, the commit pipeline (trim, transform,
duplicate/max/validate checks), delimiter-based chip creation, and a chip-navigation highlight that
is a real WPF focus target rather than a virtual one.
xmlns:taginput="clr-namespace:Navius.Wpf.Primitives.Controls.TagInput;assembly=Navius.Wpf.Primitives"
<taginput:NaviusTagInput x:Name="Tags" Value="{Binding SelectedTags, Mode=TwoWay}" Placeholder="Add a tag" />Properties
Section titled “Properties”| Property | Type | Default | From | Description |
|---|---|---|---|---|
Value |
IReadOnlyList<string>? |
null |
NaviusTagInput |
The committed tags, in order. Two-way bindable by default (BindsTwoWayByDefault). |
DefaultValue |
IReadOnlyList<string>? |
null |
NaviusTagInput |
Uncontrolled initial tags, applied once on Loaded when Value is unset. |
Delimiters |
IReadOnlyList<TagDelimiter>? |
null |
NaviusTagInput |
Which keys/chars commit a chip. null resolves to EffectiveDelimiters = [Enter, Comma]. |
AllowDuplicates |
bool |
false |
NaviusTagInput |
When false, a duplicate candidate is rejected. |
MaxTags |
int? |
null |
NaviusTagInput |
Caps the tag count; a candidate past the cap is rejected. |
Validate |
Func<string, bool>? |
null |
NaviusTagInput |
Returning false blocks the candidate and raises TagRejected. |
Transform |
Func<string, string>? |
null |
NaviusTagInput |
Normalizes a candidate after trim (e.g. lowercase). |
AddOnBlur |
bool |
false |
NaviusTagInput |
Commits the field’s current text when the field loses focus. |
Placeholder |
string? |
null |
NaviusTagInput |
Field placeholder text. |
HighlightedIndex |
int |
-1 |
NaviusTagInput |
The chip-navigation highlight; -1 means the field holds focus. |
Chips |
IReadOnlyList<TagChipVm>? (read-only) |
null |
NaviusTagInput |
The chip view-models PART_Chips renders, one per committed tag. |
IsTagListEmpty |
bool (read-only) |
true |
NaviusTagInput |
True when there are no tags. |
EffectiveDelimiters |
IReadOnlyList<TagDelimiter> (read-only) |
- |
NaviusTagInput |
Delimiters, or [Enter, Comma] when unset. |
Methods
Section titled “Methods”| Method | Description |
|---|---|
CommitText(string raw) |
Commits raw through the pipeline (trim, transform, empty-silent, duplicate, max, validate, in that order); fires TagAdded or TagRejected. Returns a TagCommitStatus. No-op (Empty) when disabled. Directly unit-testable without a template. |
RemoveTagAt(int index) |
Removes the chip at index; highlight (and focus, if a chip had it) moves to the next adjacent chip, or back to the field if the list empties. |
RemoveHighlighted() |
The empty-field Backspace second press: removes the highlighted chip and returns focus to the field. |
Highlight(int index, bool focus) |
Sets the highlight (-1 = field); when focus is true, also moves real keyboard focus. |
Events
Section titled “Events”| Event | Signature | Fires when |
|---|---|---|
TagAdded |
EventHandler<string> |
A chip is successfully committed (the web’s OnAdd). |
TagRemoved |
EventHandler<string> |
A chip is removed by any path: RemoveTagCommand, chip Delete/Backspace, or the field’s Backspace two-step (the web’s OnRemove). |
TagRejected |
EventHandler<string> |
A candidate is blocked by duplicate check, MaxTags, or Validate (the web’s OnInvalid). |
ValueChanged |
EventHandler<IReadOnlyList<string>> |
Every committed mutation, add or remove (the web’s ValueChanged). |
Commands
Section titled “Commands”| Command | Description |
|---|---|
NaviusTagInput.RemoveTagCommand |
Removes one chip; the chip’s TagChipVm is the command parameter. Bound by the remove-button template part inside each chip. |
Keyboard interactions
Section titled “Keyboard interactions”| Key | Behavior |
|---|---|
Enter (field, if Enter in EffectiveDelimiters) |
Commits current field text as a chip when non-empty. |
Tab (field, if Tab in EffectiveDelimiters) |
Same as Enter; Tab itself is not consumed, so focus still moves. |
| Comma / Space typed or pasted (field, if in delimiters) | Detected on TextChanged; text is split at the delimiter, each completed segment committed, remainder stays in the field. |
Backspace (field, empty) |
First press highlights the last chip (no focus move); a second press (with a chip already highlighted) removes it. |
ArrowLeft (field, empty) |
Enters chip navigation: highlights and focuses the last chip. |
ArrowLeft (chip) |
Highlight and focus the previous chip; no-op at index 0. |
ArrowRight (chip) |
Highlight and focus the next chip, or return to the field (-1) if already at the last chip. |
Home / End (chip) |
Highlight and focus the first/last chip. |
Delete / Backspace (chip) |
Remove the highlighted chip. |
After a chip removal, highlight (and focus, if a chip had it) moves to the next adjacent chip, or back to the field if the list becomes empty.
UIA mechanism
Section titled “UIA mechanism”NaviusTagInputAutomationPeer (a FrameworkElementAutomationPeer) reports
AutomationControlType.Group, since the interactive field inside is a real TextBox with its own
native Edit peer. Because the committed tags live in template text (chips), not in a single control
value, the root peer also implements a read-only IValueProvider: Value returns the comma-joined
tag list, and SetValue throws (NaviusTagInput is read-only over ValuePattern), following the
same precedent as the Select family’s value-in-template-text peer. Each remove button carries
AutomationProperties.Name = “Remove {value}”. The web contract wires no ARIA roles at all for
this family, so this UIA design is a fresh minimal mapping rather than a 1:1 port. The unit suite
(tests/Navius.Wpf.Tests/TagInputTests.cs) exercises CommitText’s full rule order and precedence
(duplicate checked before max), delimiter splitting, the Backspace two-step, and, added during the
M6 audit, real PreviewKeyDown tests for field ArrowLeft chip-navigation entry and chip
ArrowLeft/Home/End/ArrowRight/Delete handling.
Web deltas
Section titled “Web deltas”- Five web parts (Root/List/Field/Tag/TagRemove) fold into one
Controlwith two template parts (PART_Input,PART_Chips);NaviusTag/NaviusTagRemovebecome aTagChipVmplus aDataTemplate(the Combobox chip idiom) rather than CLR control types. - The chip highlight is a real WPF focus target: the web’s one-shot
PendingChipFocus/PendingFieldFocusrender-cycle dance collapses into synchronousFocus()calls, since WPF has no re-render round-trip to wait on. - Accessibility is a fresh design, not a port: the web source wires no ARIA roles for this family at
all. The field gets a native TextBox Edit peer for free; the root peer adds a read-only
ValuePattern(comma-joined tags) since the committed tags otherwise live only in template text, following the Select-peer precedent that such values must be exposed over UIA. Valueis a two-way dependency property with immutable list snapshots (the ComboboxValuesconvention), with CLR eventsTagAdded/TagRemoved/TagRejected/ValueChangedstanding in for the web’sOnAdd/OnRemove/OnInvalid/ValueChangedcallbacks.- Paste-splitting on comma/space is detected on
TextBox.TextChanged(which fires once with the full pasted text) rather than the web’s@oninput; both land on the same split points.
Captures
Section titled “Captures”The TagInput Gallery page rendered at the pinned commit, in each theme.


