Skip to content

Commit

Permalink
[dev-v5] FluentListBase (#2954)
Browse files Browse the repository at this point in the history
* Add first classes

* Add missing features

* Remove FluentOption.OnChange

* Add missing attributes

* Add Unit Tests

* Fix csproj

* Add Unit Tests

* Add Unit Tests

* Fix doc
  • Loading branch information
dvoituron authored Nov 20, 2024
1 parent 99e9d96 commit 1be48a3
Show file tree
Hide file tree
Showing 23 changed files with 853 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<FluentSelect Label="Simple" Items="@Digits" />

<FluentSelect Label="Functions" Items="@Digits"
OptionDisabled="@(item => item == "Two")"
OptionText="@(item => item?.ToUpper())"
OptionValue="@(item => $"value-{item}")"
OptionSelected="@(item => item == "One")" />

<FluentSelect Label="Binding" Items="@Digits" @bind-Value="@Value" />

<FluentSelect Label="Template" Items="@Digits" @bind-Value="@Value">
<OptionTemplate>
@if (!String.IsNullOrEmpty(context))
{
<span>➡️</span>
@context
}
</OptionTemplate>
</FluentSelect>

<FluentSelect Label="Manual" @bind-Value="@Value">
<FluentOption Value="One">One</FluentOption>
<FluentOption Value="Two">Two</FluentOption>
<FluentOption Value="Three">Three</FluentOption>
</FluentSelect>

<FluentSelect Label="Colors" Items="@GetEnumValues()" @bind-Value="@SelectedColor" />

<div>
<FluentButton OnClick="@(e => { Value = "One"; })">All to 'One'</FluentButton>
</div>

<div style="margin-top: 16px;">
<div>Selected value: @Value</div>
<div>Selected color: @SelectedColor</div>
</div>

@code {

private string?[] Digits = new string?[] { null, "One", "Two", "Three" };
private string? Value;
private Color SelectedColor;

public static IEnumerable<Color> GetEnumValues()
{
return Enum.GetValues(typeof(Color)).Cast<Color>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
title: Select
route: /List/Select
---

# Select

The `Select` component allows one option to be selected from multiple options.

View the [Usage Guidance](https://fluent2.microsoft.design/components/web/react/select/usage).

## TEST

{{ SelectDefault }}

## API FluentSelect

{{ API Type=FluentSelect }}

{{ API Type=FluentOption }}

## Migrating to v5

TODO
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
{
var type = DocViewerService.ApiAssembly
?.GetTypes()
?.FirstOrDefault(i => i.Name == name);
?.FirstOrDefault(i => i.Name == name || i.Name.StartsWith($"{name}`1"));

return type is null ? null : new ApiClass(DocViewerService, type);
}
Expand Down
1 change: 1 addition & 0 deletions examples/Tools/FluentUI.Demo.DocViewer/Models/ApiClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal class ApiClass
"ToString",
"Dispose",
"DisposeAsync",
"ValueExpression",
];

private readonly Type _component;
Expand Down
35 changes: 27 additions & 8 deletions src/Core/Components/Base/FluentInputBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.JSInterop;
using Microsoft.FluentUI.AspNetCore.Components.Utilities;
using System.Diagnostics.CodeAnalysis;

namespace Microsoft.FluentUI.AspNetCore.Components;

Expand All @@ -19,6 +20,14 @@ public abstract partial class FluentInputBase<TValue> : InputBase<TValue>, IFlue
{
private FluentJSModule? _jsModule;

/// <summary>
/// Initializes a new instance of the <see cref="FluentInputBase{TValue}"/> class.
/// </summary>
protected FluentInputBase()
{
ValueExpression = () => CurrentValueOrDefault;
}

/// <summary />
[Inject]
private IJSRuntime JSRuntime { get; set; } = default!;
Expand All @@ -29,6 +38,12 @@ public abstract partial class FluentInputBase<TValue> : InputBase<TValue>, IFlue
/// </summary>
internal FluentJSModule JSModule => _jsModule ??= new FluentJSModule(JSRuntime);

/// <summary>
/// Internal usage only: to define the default `ValueExpression`.
/// </summary>
[ExcludeFromCodeCoverage]
internal TValue CurrentValueOrDefault { get => CurrentValue ?? default!; set => CurrentValue = value; }

#region IFluentComponentBase

/// <inheritdoc />
Expand Down Expand Up @@ -114,26 +129,30 @@ public abstract partial class FluentInputBase<TValue> : InputBase<TValue>, IFlue
[Parameter]
public bool Required { get; set; }

/// <summary>
///
/// </summary>
/// <param name="e"></param>
/// <returns></returns>
/// <summary />
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0059:Unnecessary assignment of a value", Justification = "TODO")]
protected virtual Task ChangeHandlerAsync(ChangeEventArgs e)
protected virtual async Task ChangeHandlerAsync(ChangeEventArgs e)
{
var isValid = TryParseValueFromString(e.Value?.ToString(), out var result, out var validationErrorMessage);

if (isValid)
{
CurrentValue = result;
await InvokeAsync(() => CurrentValue = result);
}
else
{
// TODO
}
}

return Task.CompletedTask;
/// <summary>
/// Returns the aria-label attribute value with the label and required indicator.
/// </summary>
/// <returns></returns>
protected virtual string? GetAriaLabelWithRequired()
{
return (AriaLabel ?? Label ?? string.Empty) +
(Required ? $", Required" : string.Empty);
}

#endregion
Expand Down
2 changes: 2 additions & 0 deletions src/Core/Components/Grid/FluentGrid.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------------------

using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components.Utilities;
using Microsoft.JSInterop;
Expand Down Expand Up @@ -102,6 +103,7 @@ public async Task FluentGrid_MediaChangedAsync(string size)
/// <inheritdoc />
/// </summary>
/// <returns></returns>
[ExcludeFromCodeCoverage(Justification = "Tested via integration tests.")]
protected override async ValueTask DisposeAsync(IJSObjectReference? jsModule)
{
if (jsModule != null)
Expand Down
40 changes: 40 additions & 0 deletions src/Core/Components/List/FluentListBase.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@namespace Microsoft.FluentUI.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Rendering
@inherits FluentInputBase<TOption>
@typeparam TOption

@code
{
/// <summary>
/// Internal method to render the options.
/// See the <see cref="RenderOptions"/> method for the public API.
/// </summary>
/// <param name="__builder"></param>
private void InternalRenderOptions(RenderTreeBuilder __builder)
{
if (Items is null)
{
@ChildContent
}
else
{
@foreach (TOption item in Items)
{
<FluentOption Value="@GetOptionValue(item)"
Disabled="@(GetOptionDisabled(item))"
Selected="@GetOptionSelected(item)"
SelectedChanged="@(e => OnSelectedItemChangedHandlerAsync(item))"
aria-selected="@(GetOptionSelected(item) ? "true" : null)">
@if (OptionTemplate is not null)
{
@OptionTemplate(item)
}
else
{
@GetOptionText(item)
}
</FluentOption>
}
}
}
}
136 changes: 136 additions & 0 deletions src/Core/Components/List/FluentListBase.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// ------------------------------------------------------------------------
// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------------------

using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components.Extensions;
using Microsoft.FluentUI.AspNetCore.Components.Utilities;

namespace Microsoft.FluentUI.AspNetCore.Components;

/// <summary />
public abstract partial class FluentListBase<TOption> : FluentInputBase<TOption>
{
/// <summary>
/// Initializes a new instance of the <see cref="FluentListBase{TOption}"/> class.
/// </summary>
protected FluentListBase()
{
Id = Identifier.NewId();
}

/// <summary>
/// Gets or sets the width of the component.
/// </summary>
[Parameter]
public string? Width { get; set; }

/// <summary>
/// Gets or sets the height of the component.
/// </summary>
[Parameter]
public string? Height { get; set; }

/// <summary>
/// Gets or sets the content to be rendered inside the component.
/// </summary>
[Parameter]
public virtual RenderFragment? ChildContent { get; set; }

/// <summary>
/// Gets or sets the content source of all items to display in this list.
/// Each item must be instantiated (cannot be null).
/// </summary>
[Parameter]
public virtual IEnumerable<TOption>? Items { get; set; }

/// <summary>
/// Gets or sets the template for the <see cref="FluentListBase{TOption}.Items"/> items.
/// </summary>
[Parameter]
public virtual RenderFragment<TOption>? OptionTemplate { get; set; }

/// <summary>
/// Gets or sets the function used to determine which value to apply to the option value attribute.
/// </summary>
[Parameter]
public virtual Func<TOption?, string>? OptionValue { get; set; }

/// <summary>
/// Gets or sets the function used to determine which text to display for each option.
/// </summary>
[Parameter]
public virtual Func<TOption?, string>? OptionText { get; set; }

/// <summary>
/// Gets or sets the function used to determine if an option is initially selected.
/// </summary>
[Parameter]
public virtual Func<TOption?, bool>? OptionSelected { get; set; }

/// <summary>
/// Gets or sets the function used to determine if an option is disabled.
/// </summary>
[Parameter]
public virtual Func<TOption?, bool>? OptionDisabled { get; set; }

/// <summary />
protected virtual bool GetOptionSelected(TOption? item)
{
return OptionSelected?.Invoke(item) ?? Equals(item, CurrentValue);
}

/// <summary />
protected virtual string? GetOptionValue(TOption? item)
{
return OptionValue?.Invoke(item) ?? item?.ToString() ?? null;
}

/// <summary />
protected virtual string? GetOptionText(TOption? item)
{
return OptionText?.Invoke(item) ?? item?.ToString() ?? string.Empty;
}

/// <summary />
protected virtual bool GetOptionDisabled(TOption? item)
{
return OptionDisabled?.Invoke(item) ?? false;
}

/// <summary />
protected virtual async Task OnSelectedItemChangedHandlerAsync(TOption? item)
{
if (Disabled || item == null)
{
return;
}

if (!Equals(item, CurrentValue))
{
// Assign the current value and raise the change event
CurrentValue = item;
}

await Task.CompletedTask;
}

/// <summary>
/// Renders the list options.
/// </summary>
/// <returns></returns>
protected virtual RenderFragment? RenderOptions() => InternalRenderOptions;

/// <summary />
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TOption result, [NotNullWhen(false)] out string? validationErrorMessage)
{
return this.TryParseSelectableValueFromString(value, out result, out validationErrorMessage);
}

/// <summary />
internal InternalListContext<TOption> GetCurrentContext()
{
return new InternalListContext<TOption>(this);
}
}
16 changes: 16 additions & 0 deletions src/Core/Components/List/FluentOption.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@namespace Microsoft.FluentUI.AspNetCore.Components
@inherits FluentComponentBase

@* This file must be updated when the `fluent-select` web-component will be available *@

<option id="@Id"
class="@Class"
style="@Style"
disabled="@Disabled"
value="@Value"
selected="@Selected"
aria-selected="@(Selected ? "true" : null)"
@onclick="@OnSelectHandlerAsync"
@attributes="@AdditionalAttributes">
@ChildContent
</option>
Loading

0 comments on commit 1be48a3

Please sign in to comment.