Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
86c0fdf
feat(listbox): add baseline implementation of ListBox and ListBoxItem…
desmondinho Dec 5, 2024
e2abc1b
feat(listbox): add `Color` and `Variant` params to Listbox and Listbo…
desmondinho Dec 5, 2024
78bb3bc
refactor(listbox): move Items list into the context
desmondinho Dec 8, 2024
f92b5a0
feat: add a new `transition-colors-shadow` transition
desmondinho Dec 8, 2024
d6e98a6
feat: defer content rendering to collect the items first
desmondinho Dec 8, 2024
778af3a
feat: allow single and multiple selection
desmondinho Dec 8, 2024
ffc5d07
feat: add start/end content parameters
desmondinho Dec 8, 2024
83ab5bd
feat: add `Description` param to the ListboxItem
desmondinho Dec 8, 2024
83d55ce
feat: add Disabled state
desmondinho Dec 9, 2024
2f4ffd9
feat: add OnClick callback on the ListboxItem component
desmondinho Dec 9, 2024
cc9acae
chore: XML summaries
desmondinho Dec 9, 2024
fac2667
feat(docs): add baseline for the listbox component page
desmondinho Dec 9, 2024
d81cc17
feat(popover): add `MatchRefWidth` parameter to the popover component
desmondinho Dec 11, 2024
c2c2354
feat(popover): add 2-way-bindable `Opened` param to the popover compo…
desmondinho Dec 11, 2024
eee0245
feat(popover): make the `Id` a public param
desmondinho Dec 11, 2024
247e5b2
feat(listbox-item): rename `Id` param to `Value`
desmondinho Dec 11, 2024
afae751
chore(input): nits
desmondinho Dec 11, 2024
183010f
feat(extensions): add baseline implementation of the Select component
desmondinho Dec 11, 2024
6498a3c
refactor: allow binding to a single item or multiple items
desmondinho Dec 21, 2024
7523539
feat: implement slots for the listbox and listbox item components
desmondinho Dec 22, 2024
f4a5ae0
feat(docs): complete the listbox component page
desmondinho Dec 22, 2024
fcb4713
feat(popover): add `MatchRefWidth` parameter to the popover component
desmondinho Dec 11, 2024
1a38eeb
feat(popover): add 2-way-bindable `Opened` param to the popover compo…
desmondinho Dec 11, 2024
8c4777f
feat(popover): make the `Id` a public param
desmondinho Dec 11, 2024
4108557
chore(input): nits
desmondinho Dec 11, 2024
241d519
feat(extensions): add baseline implementation of the Select component
desmondinho Dec 11, 2024
4c97b83
feat: add `TextValue` param to the SelectItem component
desmondinho Dec 13, 2024
2f69757
fix: styles
desmondinho Dec 13, 2024
6f41ac5
chore: rebase feat/select onto feat/listbox
desmondinho Dec 22, 2024
fc080c0
chore(listbox): nits
desmondinho Dec 23, 2024
bbab444
chore: improve rendering logic
desmondinho Dec 24, 2024
083f200
feat(plugin): add 'scrollbar-hide' CSS utility
desmondinho Dec 25, 2024
6186624
feat: implement slots
desmondinho Dec 25, 2024
989c607
feat: add `ListboxMaxHeight` parameter
desmondinho Dec 25, 2024
e42dde3
feat: add `DisabledItems` parameter
desmondinho Dec 25, 2024
7072077
feat: add `ValueContent` parameter
desmondinho Dec 25, 2024
ba9f5e8
chore: nits
desmondinho Dec 25, 2024
d2e5162
chore: add missing XML docs
desmondinho Dec 25, 2024
6900664
fix(listbox): apply CSS classes of an individual listbox item to its …
desmondinho Dec 25, 2024
50f5070
feat: add `PopoverClasses` and `ListboxClasses` parameters
desmondinho Dec 25, 2024
4fea1d4
feat(docs): add the `New` component status badge
desmondinho Dec 25, 2024
853b989
feat(docs): add the Select page
desmondinho Dec 25, 2024
95bb0ae
test(popover): fix broken tests
desmondinho Dec 25, 2024
9fb7378
chore: exclude slots from code coverage; nits
desmondinho Dec 26, 2024
1e4a42a
refactor: move value(s) selection logic into the listbox component
desmondinho Dec 26, 2024
2d37e99
test: add tests
desmondinho Dec 26, 2024
bcf081d
feat(popover): add `MatchRefWidth` parameter to the popover component
desmondinho Dec 11, 2024
308988a
feat(popover): add 2-way-bindable `Opened` param to the popover compo…
desmondinho Dec 11, 2024
b2fbec5
feat(popover): make the `Id` a public param
desmondinho Dec 11, 2024
de83d23
chore(input): nits
desmondinho Dec 11, 2024
fee6d07
feat(extensions): add baseline implementation of the Select component
desmondinho Dec 11, 2024
e98b79d
feat: add `TextValue` param to the SelectItem component
desmondinho Dec 13, 2024
089e42d
fix: styles
desmondinho Dec 13, 2024
4dfcaf3
feat(popover): add `MatchRefWidth` parameter to the popover component
desmondinho Dec 11, 2024
23a85f7
feat(popover): add 2-way-bindable `Opened` param to the popover compo…
desmondinho Dec 11, 2024
ace679f
feat(popover): make the `Id` a public param
desmondinho Dec 11, 2024
5f51855
feat(listbox-item): rename `Id` param to `Value`
desmondinho Dec 11, 2024
4acf515
feat(extensions): add baseline implementation of the Select component
desmondinho Dec 11, 2024
420439b
chore(listbox): nits
desmondinho Dec 23, 2024
2985122
chore: improve rendering logic
desmondinho Dec 24, 2024
0e1ac0a
feat(plugin): add 'scrollbar-hide' CSS utility
desmondinho Dec 25, 2024
29d6817
feat: implement slots
desmondinho Dec 25, 2024
b4b5cce
feat: add `ListboxMaxHeight` parameter
desmondinho Dec 25, 2024
39a48bd
feat: add `DisabledItems` parameter
desmondinho Dec 25, 2024
e14b6a7
feat: add `ValueContent` parameter
desmondinho Dec 25, 2024
90e2cc4
chore: nits
desmondinho Dec 25, 2024
212dc87
chore: add missing XML docs
desmondinho Dec 25, 2024
f6ee4d2
fix(listbox): apply CSS classes of an individual listbox item to its …
desmondinho Dec 25, 2024
98cbc20
feat: add `PopoverClasses` and `ListboxClasses` parameters
desmondinho Dec 25, 2024
b8701af
feat(docs): add the `New` component status badge
desmondinho Dec 25, 2024
b56f5ce
feat(docs): add the Select page
desmondinho Dec 25, 2024
da0baee
test(popover): fix broken tests
desmondinho Dec 25, 2024
8911805
chore: merge feat/listbox into feat/select
desmondinho Dec 26, 2024
46f80c6
chore: fix part of incorrectly merged code
desmondinho Dec 26, 2024
a8f619d
docs(listbox): replace the "Selection" section with "Two-way Data Bin…
desmondinho Dec 26, 2024
b4400e6
docs(select): wording
desmondinho Dec 26, 2024
0743058
chore: nits
desmondinho Dec 27, 2024
1b1f919
test: add tests
desmondinho Dec 27, 2024
318a21b
docs(listbox/select): nits
desmondinho Dec 27, 2024
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
Prev Previous commit
Next Next commit
refactor: allow binding to a single item or multiple items
  • Loading branch information
desmondinho committed Dec 21, 2024
commit 6498a3c33317e788c9fc6893ba82bf7e2423a1d2
10 changes: 6 additions & 4 deletions src/LumexUI/Components/Listbox/ListboxContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ namespace LumexUI;

internal class ListboxContext<T>( LumexListbox<T> owner ) : IComponentContext<LumexListbox<T>>
{
private bool _collectingItems;

public LumexListbox<T> Owner { get; } = owner;
public List<LumexListboxItem<T>> Items { get; } = [];
public bool CollectingItems { get; private set; }
public SelectionMode SelectionMode { get; set; }

public void Register( LumexListboxItem<T> item )
{
if( CollectingItems )
if( _collectingItems )
{
Items.Add( item );
}
Expand All @@ -24,11 +26,11 @@ public void Unregister( LumexListboxItem<T> item )
public void StartCollectingItems()
{
Items.Clear();
CollectingItems = true;
_collectingItems = true;
}

public void FinishCollectingItems()
{
CollectingItems = false;
_collectingItems = false;
}
}
4 changes: 2 additions & 2 deletions src/LumexUI/Components/Listbox/LumexListbox.razor
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
@namespace LumexUI
@inherits LumexComponentBase
@typeparam T
@typeparam TValue

<CascadingValue TValue="ListboxContext<T>" Value="@_context" IsFixed="@true">
<CascadingValue TValue="ListboxContext<TValue>" Value="@_context" IsFixed="@true">
@{
_context.StartCollectingItems();
}
Expand Down
58 changes: 43 additions & 15 deletions src/LumexUI/Components/Listbox/LumexListbox.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ namespace LumexUI;
/// <summary>
/// A component representing a listbox component, used to display a selectable list of items.
/// </summary>
/// <typeparam name="T">The type of the values associated with the items in the listbox.</typeparam>
[CascadingTypeParameter( nameof( T ) )]
public partial class LumexListbox<T> : LumexComponentBase
/// <typeparam name="TValue">The type of the values associated with the items in the listbox.</typeparam>
[CascadingTypeParameter( nameof( TValue ) )]
public partial class LumexListbox<TValue> : LumexComponentBase
{
/// <summary>
/// Gets or sets content to be rendered inside the listbox.
Expand Down Expand Up @@ -43,34 +43,36 @@ public partial class LumexListbox<T> : LumexComponentBase
[Parameter] public ThemeColor Color { get; set; } = ThemeColor.Default;

/// <summary>
/// Gets or sets the selection mode for the listbox, determining how items can be selected.
/// Gets or sets the collection of items currently disabled in the listbox.
/// </summary>
/// <remarks>
/// The default is <see cref="SelectionMode.None"/>
/// </remarks>
[Parameter] public SelectionMode SelectionMode { get; set; }
[Parameter] public ICollection<TValue?>? DisabledItems { get; set; }

/// <summary>
/// Gets or sets an item currently selected in the listbox.
/// </summary>
[Parameter] public TValue? Value { get; set; }

/// <summary>
/// Gets or sets the collection of items currently selected in the listbox.
/// </summary>
[Parameter] public ICollection<T>? SelectedItems { get; set; }
[Parameter] public ICollection<TValue?>? Values { get; set; }

/// <summary>
/// Gets or sets the callback that is invoked when the selection of items in the listbox changes.
/// Gets or sets the callback that is invoked when the selection of item in the listbox changes.
/// </summary>
[Parameter] public EventCallback<ICollection<T>> SelectedItemsChanged { get; set; }
[Parameter] public EventCallback<TValue?> ValueChanged { get; set; }

/// <summary>
/// Gets or sets the collection of items currently disabled in the listbox.
/// Gets or sets the callback that is invoked when the selection of items in the listbox changes.
/// </summary>
[Parameter] public ICollection<T>? DisabledItems { get; set; }
[Parameter] public EventCallback<ICollection<TValue?>> ValuesChanged { get; set; }

private readonly static RenderFragment _emptyContent = builder =>
{
builder.AddContent( 0, "No items." );
};

private readonly ListboxContext<T> _context;
private readonly ListboxContext<TValue> _context;
private readonly RenderFragment _renderItems;
private readonly RenderFragment _renderEmptyContent;

Expand All @@ -81,13 +83,39 @@ public partial class LumexListbox<T> : LumexComponentBase
/// </summary>
public LumexListbox()
{
_context = new ListboxContext<T>( this );
_context = new ListboxContext<TValue>( this );
_renderItems = RenderItems;
_renderEmptyContent = RenderEmptyContent;

As = "ul";
}

/// <inheritdoc />
public override Task SetParametersAsync( ParameterView parameters )
{
parameters.SetParameterProperties( this );

if( parameters.TryGetValue<TValue>( nameof( Value ), out var _ ) &&
parameters.TryGetValue<ICollection<TValue>>( nameof( Values ), out var _ ) )
{
throw new InvalidOperationException(
$"{GetType()} requires one of {nameof( Value )} or {nameof( Values )}, but both were specified." );
}

// Automatically set the SelectionMode depending on
// which of the 2-way bindable parameters are provided.
if( parameters.TryGetValue<TValue>( nameof( Value ), out var _ ) )
{
_context.SelectionMode = SelectionMode.Single;
}
else if( parameters.TryGetValue<ICollection<TValue>>( nameof( Values ), out var _ ) )
{
_context.SelectionMode = SelectionMode.Multiple;
}

return base.SetParametersAsync( ParameterView.Empty );
}

/// <inheritdoc />
protected override void OnParametersSet()
{
Expand Down
6 changes: 3 additions & 3 deletions src/LumexUI/Components/Listbox/LumexListboxItem.razor
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
@namespace LumexUI
@inherits LumexComponentBase
@typeparam T
@typeparam TValue

@{
Context.Register(this);
Context?.Register(this);
}

@code {
Expand Down Expand Up @@ -36,7 +36,7 @@
<span class="@_slots.Title" data-slot="title">@ChildContent</span>
}

@if (Context.Owner.SelectionMode is not SelectionMode.None)
@if (Context?.SelectionMode is not SelectionMode.None)
{
<span class="@_slots.SelectedIcon" data-slot="selected-icon">
@_renderSelectedIcon
Expand Down
93 changes: 44 additions & 49 deletions src/LumexUI/Components/Listbox/LumexListboxItem.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// LumexUI licenses this file to you under the MIT license
// See the license here https://github.com/LumexUI/lumexui/blob/main/LICENSE

using System.Diagnostics;

using LumexUI.Common;
using LumexUI.Styles;

Expand All @@ -13,14 +15,9 @@ namespace LumexUI;
/// <summary>
/// A component representing an item within the <see cref="LumexListbox{T}"/>.
/// </summary>
/// <typeparam name="T">The type of the value associated with the listbox item.</typeparam>
public partial class LumexListboxItem<T> : LumexComponentBase, IDisposable
/// <typeparam name="TValue">The type of the value associated with the listbox item.</typeparam>
public partial class LumexListboxItem<TValue> : LumexComponentBase, IDisposable
{
/// <summary>
/// Gets or sets a unique identifier for the listbox item.
/// </summary>
[Parameter, EditorRequired] public T Id { get; set; } = default!;

/// <summary>
/// Gets or sets content to be rendered inside the listbox item.
/// </summary>
Expand All @@ -36,6 +33,11 @@ public partial class LumexListboxItem<T> : LumexComponentBase, IDisposable
/// </summary>
[Parameter] public RenderFragment? EndContent { get; set; }

/// <summary>
/// Gets or sets a value representing the listbox item.
/// </summary>
[Parameter] public TValue? Value { get; set; }

/// <summary>
/// Gets or sets a description of the listbox item.
/// </summary>
Expand Down Expand Up @@ -67,9 +69,9 @@ public partial class LumexListboxItem<T> : LumexComponentBase, IDisposable
/// </summary>
[Parameter] public EventCallback<MouseEventArgs> OnClick { get; set; }

[CascadingParameter] internal ListboxContext<T> Context { get; set; } = default!;
[CascadingParameter] internal ListboxContext<TValue>? Context { get; set; }

private LumexListbox<T> Listbox => Context.Owner;
private LumexListbox<TValue>? Listbox => Context?.Owner;

private readonly RenderFragment _renderSelectedIcon;

Expand All @@ -90,41 +92,40 @@ public override Task SetParametersAsync( ParameterView parameters )
{
parameters.SetParameterProperties( this );

// Respect the Variant value if provided; otherwise, use the owner's
Variant = parameters.TryGetValue<ListboxVariant>( nameof( Variant ), out var variant )
? variant
: Context.Owner.Variant;

// Respect the Color value if provided; otherwise, use the owner's
Color = parameters.TryGetValue<ThemeColor>( nameof( Color ), out var color )
? color
: Context.Owner.Color;
if( Listbox is not null )
{
// Respect the Variant value if provided; otherwise, use the owner's
Variant = parameters.TryGetValue<ListboxVariant>( nameof( Variant ), out var variant )
? variant
: Listbox.Variant;

// Respect the Color value if provided; otherwise, use the owner's
Color = parameters.TryGetValue<ThemeColor>( nameof( Color ), out var color )
? color
: Listbox.Color;
}

return base.SetParametersAsync( ParameterView.Empty );
}

/// <inheritdoc />
protected override void OnInitialized()
{
ContextNullException.ThrowIfNull( Context, nameof( LumexListboxItem<T> ) );
ContextNullException.ThrowIfNull( Context, nameof( LumexListboxItem<TValue> ) );
}

/// <inheritdoc />
protected override void OnParametersSet()
{
if( Id is null )
{
throw new InvalidOperationException( $"{GetType()} requires a value for parameter '{nameof( Id )}'." );
}

_slots ??= ListboxItem.GetStyles( this, TwMerge );
}

internal bool GetSelectedState() =>
Listbox.SelectedItems is not null && Listbox.SelectedItems.Contains( Id );
Listbox?.Value?.Equals( Value ) is true ||
Listbox?.Values?.Contains( Value ) is true;

internal bool GetDisabledState() =>
Listbox.DisabledItems is not null && Listbox.DisabledItems.Contains( Id );
Listbox?.DisabledItems?.Contains( Value ) is true;

private async Task OnClickAsync( MouseEventArgs args )
{
Expand All @@ -133,45 +134,39 @@ private async Task OnClickAsync( MouseEventArgs args )
return;
}

await OnClick.InvokeAsync( args );

if( Listbox.SelectionMode is not SelectionMode.None )
if( Context?.SelectionMode is not SelectionMode.None )
{
await SelectAsync();
}

await OnClick.InvokeAsync( args );
}

private Task SelectAsync()
{
if( Listbox.SelectedItems is null )
{
return Task.CompletedTask;
}
Debug.Assert( Context is not null && Listbox is not null );

var selectedItems = Listbox.SelectedItems;
if( selectedItems.Remove( Id ) )
if( Context.SelectionMode is SelectionMode.Single )
{
return Listbox.SelectedItemsChanged.InvokeAsync( selectedItems );
return Listbox.ValueChanged.InvokeAsync( Value );
}

switch( Listbox.SelectionMode )
else if( Context.SelectionMode is SelectionMode.Multiple )
{
case SelectionMode.Single:
selectedItems.Clear();
selectedItems.Add( Id );
break;

case SelectionMode.Multiple:
selectedItems.Add( Id );
break;
var selectedItems = Listbox.Values ?? [];
if( !selectedItems.Remove( Value ) )
{
selectedItems.Add( Value );
}

return Listbox.ValuesChanged.InvokeAsync( selectedItems );
}

return Listbox.SelectedItemsChanged.InvokeAsync( selectedItems );
return Task.CompletedTask;
}

/// <inheritdoc />
public void Dispose()
public virtual void Dispose()
{
Context.Unregister( this );
Context?.Unregister( this );
}
}
6 changes: 2 additions & 4 deletions src/LumexUI/Styles/Listbox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,15 @@ internal class ListboxItem

public static ListboxItemSlots GetStyles<T>( LumexListboxItem<T> listboxItem, TwMerge twMerge )
{
var listbox = listboxItem.Context.Owner;

return new ListboxItemSlots()
{
Root = twMerge.Merge(
ElementClass.Empty()
.Add( _base )
.Add( _disabled, when: listboxItem.GetDisabledState() )
.Add( listboxItem.Class )
.Add( GetVariantStyles( listbox.Variant, slot: nameof( _base ) ) )
.Add( GetCompoundStyles( listbox.Variant, listboxItem.Color, slot: nameof( _base ) ) )
.Add( GetVariantStyles( listboxItem.Variant, slot: nameof( _base ) ) )
.Add( GetCompoundStyles( listboxItem.Variant, listboxItem.Color, slot: nameof( _base ) ) )
.ToString() ),

Wrapper = twMerge.Merge(
Expand Down