Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/EventLogExpert/Shared/Components/ValueSelect.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

<div class="dropdown-input" @onkeydown="HandleKeyDown" @onkeydown:preventDefault="_preventDefault" @ref="_selectComponent">
<input aria-activedescendant="@HighlightedItem?.ItemId" aria-controls="@_itemId"
aria-haspopup="listbox"
aria-label="@(string.IsNullOrWhiteSpace(AriaLabelledBy) ? AriaLabel : null)" aria-labelledby="@AriaLabelledBy"
class="@CssClass" @oninput="OnInputChange" readonly="@(!IsInput || IsMultiSelect)" role="combobox" tabindex="0" type="text" value="@DisplayString" />

<div class="dropdown-list" id="@_itemId" role="listbox" tabindex="-1">
<div class="dropdown-list" id="@_itemId" role="listbox" aria-multiselectable="@(IsMultiSelect ? "true" : null)" tabindex="-1">
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
Expand Down
117 changes: 78 additions & 39 deletions src/EventLogExpert/Shared/Components/ValueSelect.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace EventLogExpert.Shared.Components;

public sealed partial class ValueSelect<T> : BaseComponent<T>
public sealed partial class ValueSelect<T> : BaseComponent<T>, IAsyncDisposable
{
private readonly string _itemId = $"select_{Guid.NewGuid().ToString()[..8]}";
private readonly List<ValueSelectItem<T>> _items = [];
Expand All @@ -30,6 +30,8 @@ public ValueSelectItem<T>? HighlightedItem
get => _highlightedItem;
set
{
if (ReferenceEquals(_highlightedItem, value)) { return; }

_highlightedItem = value;
StateHasChanged();
}
Expand Down Expand Up @@ -62,46 +64,64 @@ private string? DisplayString

[Inject] private IJSRuntime JSRuntime { get; init; } = null!;

public bool AddItem(ValueSelectItem<T> item)
public void AddItem(ValueSelectItem<T> item)
{
if (_items.Contains(item))
if (!_items.Contains(item))
{
return _selectedValues.Contains(item.Value);
_items.Add(item);
}
}

_items.Add(item);
public async Task ClearAll()
{
_selectedValues.Clear();
_highlightedItem = null;

if (IsMultiSelect && Values.Contains(item.Value))
if (IsMultiSelect)
{
_selectedValues.Add(item.Value);
return true;
Values.Clear();
await ValuesChanged.InvokeAsync(Values);
}
else
{
Value = default!;
await ValueChanged.InvokeAsync(Value);
}
}

if (Value?.Equals(item.Value) is not true) { return false; }

_selectedValues.Clear();
_selectedValues.Add(item.Value);
public async Task CloseDropDown() => await JSRuntime.InvokeVoidAsync("closeDropdown", _selectComponent);

return true;
public async ValueTask DisposeAsync()
{
try
{
await JSRuntime.InvokeVoidAsync("unregisterDropdown", _selectComponent);
}
catch (JSDisconnectedException)
{
// Expected during app shutdown
}
}

public void ClearSelected() => _selectedValues.Clear();

public async Task CloseDropDown() => await JSRuntime.InvokeVoidAsync("closeDropdown", _selectComponent);
public bool IsItemSelected(T value) => _selectedValues.Contains(value);

public async Task OpenDropDown() => await JSRuntime.InvokeVoidAsync("openDropdown", _selectComponent);

public void RemoveItem(ValueSelectItem<T> item) => _items.Remove(item);
public void RemoveItem(ValueSelectItem<T> item)
{
_items.Remove(item);

if (ReferenceEquals(_highlightedItem, item))
{
HighlightedItem = null;
}
}

public async Task UpdateValue(T item)
{
if (IsMultiSelect)
{
if (item is null)
{
Values.Clear();
}
else if (_selectedValues.Remove(item))
if (_selectedValues.Remove(item))
{
Values.Remove(item);
}
Expand Down Expand Up @@ -137,8 +157,27 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
await base.OnAfterRenderAsync(firstRender);
}

protected override void OnParametersSet()
{
_selectedValues.Clear();

if (IsMultiSelect)
{
foreach (var v in Values)
{
_selectedValues.Add(v);
}
}
else if (Value is not null)
{
_selectedValues.Add(Value);
}
}

private async Task HandleKeyDown(KeyboardEventArgs args)
{
_preventDefault = false;

switch (args.Code)
{
case "Space":
Expand All @@ -164,10 +203,12 @@ private async Task HandleKeyDown(KeyboardEventArgs args)
{
if (HighlightedItem.ClearItem)
{
ClearSelected();
await ClearAll();
}
else
{
await UpdateValue(HighlightedItem.Value);
}

await UpdateValue(HighlightedItem.Value);
}
else
{
Expand All @@ -180,14 +221,15 @@ private async Task HandleKeyDown(KeyboardEventArgs args)

return;
}

_preventDefault = false;
}

private async Task OnInputChange(ChangeEventArgs args)
{
Value = (T)Convert.ChangeType(args.Value, typeof(T))!;
await ValueChanged.InvokeAsync(Value);
if (BindConverter.TryConvertTo<T>($"{args.Value}", null, out var result))
{
Value = result;
await ValueChanged.InvokeAsync(Value);
}
}

private async Task SelectAdjacentItem(int direction)
Expand All @@ -196,33 +238,30 @@ private async Task SelectAdjacentItem(int direction)

if (IsMultiSelect || IsInput)
{
index = _items.FindIndex(x => x.Equals(_items.FirstOrDefault(item => item.Equals(HighlightedItem))));
index = HighlightedItem is not null ? _items.IndexOf(HighlightedItem) : -1;
}
else
{
index = _items.FindIndex(x => x.Equals(
_items.FirstOrDefault(item => item.Value?.Equals(_selectedValues.FirstOrDefault()) is true)));
index = _items.FindIndex(item => item.Value?.Equals(_selectedValues.FirstOrDefault()) is true);
}

// Need to account for first item being an empty placeholder
if (index < 0) { index = 0; }
if (index < 0)
{
index = direction > 0 ? -1 : _items.Count;
}

for (int i = 0; i < _items.Count; i++)
{
index += direction;

if (index < 0) { index = 0; }

if (index >= _items.Count) { index = _items.Count - 1; }
if (index < 0 || index >= _items.Count) { return; }

if (_items[index].IsDisabled) { continue; }

if (IsMultiSelect || IsInput)
{
HighlightedItem = _items[index];

StateHasChanged();

await JSRuntime.InvokeVoidAsync("scrollToHighlightedItem", _selectComponent);
}
else
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@typeparam T

<div aria-selected="@_isSelected.ToString().ToLower()" class="@CssClass" highlighted="@(_parent.HighlightedItem?.Equals(this) ?? false)"
<div aria-disabled="@(IsDisabled ? "true" : null)" aria-selected="@IsSelected.ToString().ToLower()" class="@CssClass"
highlighted="@(IsHighlighted ? "" : null)"
id="@ItemId" @onmousedown="SelectItem" @onmouseenter="HighlightItem" role="option">

@if (ChildContent is not null)
Expand Down
20 changes: 14 additions & 6 deletions src/EventLogExpert/Shared/Components/ValueSelectItem.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ namespace EventLogExpert.Shared.Components;

public sealed partial class ValueSelectItem<T> : IDisposable
{
private bool _isSelected;
private ValueSelect<T> _parent = null!;

[Parameter]
Expand Down Expand Up @@ -37,22 +36,26 @@ private string? DisplayString
}
}

private bool IsHighlighted => _parent.HighlightedItem?.Equals(this) ?? false;

private bool IsSelected => _parent.IsItemSelected(Value);

[CascadingParameter]
private ValueSelect<T> ValueSelect
{
get => _parent;
set
{
_parent = value;
_isSelected = _parent.AddItem(this);
_parent.AddItem(this);
}
}

public void Dispose() => ValueSelect.RemoveItem(this);

private void HighlightItem()
{
if (!_parent.IsMultiSelect) { return; }
if (_parent is { IsMultiSelect: false, IsInput: false }) { return; }

_parent.HighlightedItem = this;
}
Expand All @@ -61,10 +64,15 @@ private async Task SelectItem()
{
if (IsDisabled) { return; }

if (ClearItem) { ValueSelect.ClearSelected(); }

if (!ValueSelect.IsMultiSelect) { await ValueSelect.CloseDropDown(); }

await ValueSelect.UpdateValue(Value);
if (ClearItem)
{
await ValueSelect.ClearAll();
}
else
{
await ValueSelect.UpdateValue(Value);
}
}
}
Loading
Loading