Refactor receiver input and improve UI/UX

Replaced receiver list with a tag-based `DxTagBox` for better flexibility and dynamic updates. Updated receiver management logic to synchronize `_receivers` and `_receiverTags`. Enhanced accessibility with ARIA attributes for the suggestion list.

Redesigned the "Add Receiver" button with custom styles, hover effects, and an inline SVG icon. Improved the suggestion list with horizontal scrolling and nowrap styling. Styled tags and added a remove button for each tag.

Increased recent receiver suggestions fetch limit to 20. Added validation to ensure default envelope type selection. Refactored empty state handling and cleaned up redundant code for better maintainability.
This commit is contained in:
2026-10-05 21:58:51 +02:00
parent ece676fbcf
commit e89d471c6f
3 changed files with 151 additions and 28 deletions

View File

@@ -114,7 +114,7 @@
}
else if (_recentReceiverSuggestions.Count > 0)
{
<div class="sender-create-wizard__suggestion-list">
<div class="sender-create-wizard__suggestion-list" role="listbox" aria-label="Recent receivers">
@foreach (var suggestion in _recentReceiverSuggestions)
{
<button type="button"
@@ -154,11 +154,15 @@
</div>
<div class="sender-create-wizard__receiver-actions">
<DxButton Text='@L("AddReceiver", "Empfänger hinzufügen")'
RenderStyle="ButtonRenderStyle.Primary"
CssClass="sender-create-wizard__add-receiver-btn"
Click="AddReceiver"
Enabled="@(!IsActionLocked)" />
<button type="button"
class="sender-create-wizard__add-receiver-btn"
@onclick="AddReceiver"
disabled="@IsActionLocked">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
<path d="M8 4a.5.5 0 0 1 .5.5V7.5H11.5a.5.5 0 0 1 0 1H8.5v3a.5.5 0 0 1-1 0v-3H4.5a.5.5 0 0 1 0-1H7.5v-3A.5.5 0 0 1 8 4z" />
</svg>
<span>@L("AddReceiver", "Empfänger hinzufügen")</span>
</button>
</div>
@if (_receivers.Count == 0)
@@ -169,22 +173,47 @@
}
else
{
<div class="sender-create-wizard__receiver-list">
@foreach (var receiver in _receivers)
<div class="sender-create-wizard__receiver-tagbox-wrap">
<label class="sender-create-wizard__label" for="wizardReceiversTagBox">
@L("Receiver", "Empfänger")
</label>
<DxTagBox Data="@_receivers"
TData="CreateEnvelopeWizardReceiver"
TValue="CreateEnvelopeWizardReceiver"
AllowCustomTags="true"
@bind-Tags="@_receiverTags"
@bind-Tags:after="OnReceiverTagsChanged"
HideSelectedItems="false"
TextFieldName="@nameof(CreateEnvelopeWizardReceiver.EmailAddress)"
ListRenderMode="ListRenderMode.Virtual"
SearchMode="ListSearchMode.AutoSearch"
SearchFilterCondition="ListSearchFilterCondition.Contains"
SearchTextParseMode="ListSearchTextParseMode.GroupWordsByAnd"
CssClass="sender-create-wizard__receiver-tagbox"
InputId="wizardReceiversTagBox"
Enabled="@(!IsActionLocked)">
<TagDisplayTemplate Context="tagContext">
<div class="sender-create-wizard__receiver-tag">
@if (tagContext.DataItem is CreateEnvelopeWizardReceiver receiver)
{
<div class="sender-create-wizard__receiver-item">
<div>
<div class="sender-create-wizard__receiver-name">@receiver.FullName</div>
<div class="sender-create-wizard__receiver-email">@receiver.EmailAddress</div>
</div>
<button type="button"
class="sender-create-wizard__receiver-remove"
@onclick="() => RemoveReceiver(receiver)"
disabled="@IsActionLocked">
@L("Delete", "Löschen")
</button>
</div>
<span class="sender-create-wizard__receiver-tag-name">@receiver.FullName</span>
<span class="sender-create-wizard__receiver-tag-email">@receiver.EmailAddress</span>
}
else
{
<span class="sender-create-wizard__receiver-tag-email">@tagContext.DisplayText</span>
}
<button type="button"
class="sender-create-wizard__receiver-tag-remove"
@onclick="tagContext.RemoveTagAction"
aria-label="Remove receiver"
disabled="@IsActionLocked">x</button>
</div>
</TagDisplayTemplate>
<EmptyDataAreaTemplate Context="emptyContext">
<div class="sender-create-wizard__tagbox-empty"></div>
</EmptyDataAreaTemplate>
</DxTagBox>
</div>
}
}
@@ -289,6 +318,7 @@
private string _receiverName = string.Empty;
private string _receiverEmail = string.Empty;
private readonly List<CreateEnvelopeWizardReceiver> _receivers = [];
private IEnumerable<string> _receiverTags = [];
private readonly List<RecentReceiverSuggestionDto> _recentReceiverSuggestions = [];
private bool _isLoadingRecentSuggestions;
private bool _recentSuggestionsLoaded;
@@ -330,6 +360,9 @@
{
_isVisible = Visible;
if (_selectedEnvelopeTypeId is null && EnvelopeTypes.Count > 0)
_selectedEnvelopeTypeId = GetDefaultEnvelopeTypeId();
if (Visible && !_previousVisible)
ResetWizardState();
@@ -411,6 +444,7 @@
}
_receivers.Add(new CreateEnvelopeWizardReceiver(fullName, email));
_receiverTags = _receivers.Select(x => x.EmailAddress).ToList();
_receiverName = string.Empty;
_receiverEmail = string.Empty;
_validationMessage = null;
@@ -423,6 +457,17 @@
return Task.CompletedTask;
_receivers.Remove(receiver);
_receiverTags = _receivers.Select(x => x.EmailAddress).ToList();
_validationMessage = null;
return Task.CompletedTask;
}
private Task OnReceiverTagsChanged()
{
var selectedEmails = _receiverTags.ToHashSet(StringComparer.OrdinalIgnoreCase);
_receivers.RemoveAll(x => !selectedEmails.Contains(x.EmailAddress));
_receiverTags = _receivers.Select(x => x.EmailAddress).ToList();
_validationMessage = null;
return Task.CompletedTask;
}
@@ -613,7 +658,7 @@
_isLoadingRecentSuggestions = true;
try
{
var suggestions = await ReceiverService.GetRecentReceiverSuggestionsAsync();
var suggestions = await ReceiverService.GetRecentReceiverSuggestionsAsync(20);
_recentReceiverSuggestions.Clear();
_recentReceiverSuggestions.AddRange(suggestions);
_recentSuggestionsLoaded = true;
@@ -644,6 +689,9 @@
{
if (_currentStep == 0)
{
if (_selectedEnvelopeTypeId is null && EnvelopeTypes.Count > 0)
_selectedEnvelopeTypeId = GetDefaultEnvelopeTypeId();
if (string.IsNullOrWhiteSpace(_title))
{
_validationMessage = L("SenderEditor_Validation_TitleRequired", "Bitte geben Sie einen Titel ein.");
@@ -711,6 +759,7 @@
_receiverName = string.Empty;
_receiverEmail = string.Empty;
_receivers.Clear();
_receiverTags = [];
_recentReceiverSuggestions.Clear();
_isLoadingRecentSuggestions = false;
_recentSuggestionsLoaded = false;

View File

@@ -32,7 +32,7 @@ public class ReceiverService(IHttpClientFactory httpClientFactory)
/// Fetches recent receiver suggestions for the authenticated sender.
/// Calls GET /api/EnvelopeReceiver/recent-suggestions?take={take}.
/// </summary>
public async Task<IReadOnlyList<RecentReceiverSuggestionDto>> GetRecentReceiverSuggestionsAsync(int take = 5, CancellationToken cancel = default)
public async Task<IReadOnlyList<RecentReceiverSuggestionDto>> GetRecentReceiverSuggestionsAsync(int take = 20, CancellationToken cancel = default)
{
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var url = $"/api/EnvelopeReceiver/recent-suggestions?take={take}";

View File

@@ -1271,10 +1271,31 @@
justify-content: flex-end;
}
.sender-create-wizard__add-receiver-btn.dxbl-btn {
min-width: 210px;
.sender-create-wizard__add-receiver-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-height: 36px;
min-width: 220px;
font-weight: 700;
box-shadow: 0 8px 18px rgba(14, 116, 144, 0.22);
font-size: 0.84rem;
border: 1px solid rgba(14, 116, 144, 0.28);
border-radius: 8px;
background: rgba(236, 254, 255, 0.95);
color: #0f766e;
box-shadow: 0 6px 14px rgba(14, 116, 144, 0.15);
padding: 0.45rem 0.9rem;
}
.sender-create-wizard__add-receiver-btn:hover:not(:disabled) {
background: rgba(207, 250, 254, 1);
color: #0f766e;
}
.sender-create-wizard__add-receiver-btn:disabled {
opacity: 0.58;
cursor: not-allowed;
}
.sender-create-wizard__suggestions {
@@ -1291,12 +1312,17 @@
.sender-create-wizard__suggestion-list {
display: flex;
flex-wrap: wrap;
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
gap: 0.45rem;
padding-bottom: 0.25rem;
}
.sender-create-wizard__suggestion-item {
display: inline-flex;
display: flex;
flex: 0 0 auto;
flex-direction: column;
align-items: flex-start;
gap: 0.12rem;
@@ -1308,6 +1334,54 @@
min-width: 0;
}
.sender-create-wizard__receiver-tagbox-wrap {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.sender-create-wizard__receiver-tagbox {
width: 100%;
}
.sender-create-wizard__receiver-tag {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.18rem 0.45rem;
border-radius: 999px;
background: rgba(236, 254, 255, 0.95);
border: 1px solid rgba(14, 116, 144, 0.25);
}
.sender-create-wizard__receiver-tag-name {
font-weight: 700;
}
.sender-create-wizard__receiver-tag-email {
color: #64748b;
font-size: 0.78rem;
}
.sender-create-wizard__receiver-tag-remove {
border: 0;
background: transparent;
color: #0f766e;
font-size: 0.76rem;
font-weight: 700;
line-height: 1;
padding: 0;
width: 1rem;
height: 1rem;
cursor: pointer;
}
.sender-create-wizard__tagbox-empty {
height: 0;
padding: 0;
margin: 0;
}
.sender-create-wizard__suggestion-item:disabled {
opacity: 0.58;
}