diff --git a/src/MudBlazor.Analyzers/MudBlazor.Analyzers.csproj b/src/MudBlazor.Analyzers/MudBlazor.Analyzers.csproj index 56612da8a2de..dca630339324 100644 --- a/src/MudBlazor.Analyzers/MudBlazor.Analyzers.csproj +++ b/src/MudBlazor.Analyzers/MudBlazor.Analyzers.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/MudBlazor.Docs/Pages/Components/Charts/Examples/BarExample1.razor b/src/MudBlazor.Docs/Pages/Components/Charts/Examples/BarExample1.razor index 0e11f5bfcafa..15027c7814d4 100644 --- a/src/MudBlazor.Docs/Pages/Components/Charts/Examples/BarExample1.razor +++ b/src/MudBlazor.Docs/Pages/Components/Charts/Examples/BarExample1.razor @@ -9,7 +9,7 @@ Selected: @(_index < 0 ? "None" : _series[_index].Name) - + @@ -19,14 +19,8 @@ - Label Extra Height - - - - - - Label Rotation - + XAxis Label Rotation + diff --git a/src/MudBlazor.Docs/Pages/Components/Charts/Examples/DonutCustomGraphicsExample.razor b/src/MudBlazor.Docs/Pages/Components/Charts/Examples/DonutCustomGraphicsExample.razor index 67efb61c2e3f..f2daeb8d9b29 100644 --- a/src/MudBlazor.Docs/Pages/Components/Charts/Examples/DonutCustomGraphicsExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/Charts/Examples/DonutCustomGraphicsExample.razor @@ -2,12 +2,12 @@ - Total - @data.Sum().ToString() + Total + @data.Sum().ToString() @code { public double[] data = { 25, 77, 28, 5 }; public string[] labels = { "Oil", "Coal", "Gas", "Biomass" }; -} \ No newline at end of file +} diff --git a/src/MudBlazor.Docs/Pages/Components/Charts/Examples/DonutExample1.razor b/src/MudBlazor.Docs/Pages/Components/Charts/Examples/DonutExample1.razor index d54c4c947107..ce475a470fae 100644 --- a/src/MudBlazor.Docs/Pages/Components/Charts/Examples/DonutExample1.razor +++ b/src/MudBlazor.Docs/Pages/Components/Charts/Examples/DonutExample1.razor @@ -46,4 +46,4 @@ RandomizeData(); } } -} \ No newline at end of file +} diff --git a/src/MudBlazor.Docs/Pages/Components/Charts/Examples/LineExample1.razor b/src/MudBlazor.Docs/Pages/Components/Charts/Examples/LineExample1.razor index a525f22243f7..4b8ebf1f5508 100644 --- a/src/MudBlazor.Docs/Pages/Components/Charts/Examples/LineExample1.razor +++ b/src/MudBlazor.Docs/Pages/Components/Charts/Examples/LineExample1.razor @@ -8,34 +8,28 @@ Selected: @(_index < 0 ? "None" : _series[_index].Name) - - - - + - Line Width: @_options.LineStrokeWidth.ToString() + - + Line Width: @_options.LineStrokeWidth.ToString() - - Label Extra Height - - + - Label Rotation - + XAxis Label Rotation + diff --git a/src/MudBlazor.Docs/Pages/Components/Charts/Examples/PieExample1.razor b/src/MudBlazor.Docs/Pages/Components/Charts/Examples/PieExample1.razor index 08cca2c9e25f..792df4e1668e 100644 --- a/src/MudBlazor.Docs/Pages/Components/Charts/Examples/PieExample1.razor +++ b/src/MudBlazor.Docs/Pages/Components/Charts/Examples/PieExample1.razor @@ -13,7 +13,7 @@ @code { private int Index = -1; //default value cannot be 0 -> first selectedindex is 0. int dataSize = 4; - double[] data = { 77, 25, 20, 5 }; + double[] data = { 77 }; string[] labels = { "Uranium", "Plutonium", "Thorium", "Caesium", "Technetium", "Promethium", "Polonium", "Astatine", "Radon", "Francium", "Radium", "Actinium", "Protactinium", "Neptunium", "Americium", "Curium", "Berkelium", "Californium", "Einsteinium", "Mudblaznium" }; @@ -46,4 +46,4 @@ RandomizeData(); } } -} \ No newline at end of file +} diff --git a/src/MudBlazor.Docs/Pages/Components/Charts/Examples/StackedBarExample1.razor b/src/MudBlazor.Docs/Pages/Components/Charts/Examples/StackedBarExample1.razor index 665a7bb2ecac..80daba05722e 100644 --- a/src/MudBlazor.Docs/Pages/Components/Charts/Examples/StackedBarExample1.razor +++ b/src/MudBlazor.Docs/Pages/Components/Charts/Examples/StackedBarExample1.razor @@ -9,7 +9,7 @@ Selected: @(_index < 0 ? "None" : _series[_index].Name) - + @@ -19,14 +19,8 @@ - Label Extra Height - - - - - - Label Rotation - + XAxis Label Rotation + diff --git a/src/MudBlazor.Docs/Pages/Components/Charts/Examples/TimeSeriesExample1.razor b/src/MudBlazor.Docs/Pages/Components/Charts/Examples/TimeSeriesExample1.razor index a7fdeebe71f3..1f9a09bcec89 100644 --- a/src/MudBlazor.Docs/Pages/Components/Charts/Examples/TimeSeriesExample1.razor +++ b/src/MudBlazor.Docs/Pages/Components/Charts/Examples/TimeSeriesExample1.razor @@ -19,37 +19,31 @@ Selected: @(_index < 0 ? "None" : _series[_index].Name) - Line Width: @_options.LineStrokeWidth.ToString() + Line Width: @_options.LineStrokeWidth.ToString() - + - - - - + - + - - Label Extra Height - - + - Label Rotation - + XAxis Label Rotation + - + @@ -66,7 +60,7 @@ LineStrokeWidth = 1, }; - private AxisChartOptions _axisChartOptions = new(); + private AxisChartOptions _axisChartOptions = new() { }; private TimeSeriesChartSeries _chart1 = new(); private TimeSeriesChartSeries _chart2 = new(); diff --git a/src/MudBlazor.Docs/Pages/Components/DateRangePicker/DateRangePickerPage.razor b/src/MudBlazor.Docs/Pages/Components/DateRangePicker/DateRangePickerPage.razor index a599cd422907..aa8eba137383 100644 --- a/src/MudBlazor.Docs/Pages/Components/DateRangePicker/DateRangePickerPage.razor +++ b/src/MudBlazor.Docs/Pages/Components/DateRangePicker/DateRangePickerPage.razor @@ -39,7 +39,9 @@ - Defines the smallest and largest number of days that a user can select + Defines the smallest and largest number of days that a user can select.

+ For performance reasons, the component checks up to 50 years ahead, from the user-selected start date, to find valid dates that meet the MaxDays requirement. + To extend this limit, set the MaxDate property to a specific date.
diff --git a/src/MudBlazor.Docs/Pages/Components/Select/Examples/SelectFitContentExample.razor b/src/MudBlazor.Docs/Pages/Components/Select/Examples/SelectFitContentExample.razor new file mode 100644 index 000000000000..5d599d4a4ddb --- /dev/null +++ b/src/MudBlazor.Docs/Pages/Components/Select/Examples/SelectFitContentExample.razor @@ -0,0 +1,51 @@ +@namespace MudBlazor.Docs.Examples +@using MudBlazor + + + @foreach (var state in _states) + { + @state + } + + + + @foreach (var state in _states) + { + @state + } + + + + @foreach (var state in _states) + { + @state + } + + + + + + + +@code { + private bool _fullWidth; + private bool _fitContent; + private string _selectedState; + private readonly string[] _states = + [ + "Alabama", "Alaska", "American Samoa", "Arizona", + "Arkansas", "California", "Colorado", "Connecticut", + "Delaware", "District of Columbia", "Federated States of Micronesia", + "Florida", "Georgia", "Guam", "Hawaii", "Idaho", + "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", + "Louisiana", "Maine", "Marshall Islands", "Maryland", + "Massachusetts", "Michigan", "Minnesota", "Mississippi", + "Missouri", "Montana", "Nebraska", "Nevada", + "New Hampshire", "New Jersey", "New Mexico", "New York", + "North Carolina", "North Dakota", "Northern Mariana Islands", "Ohio", + "Oklahoma", "Oregon", "Palau", "Pennsylvania", "Puerto Rico", + "Rhode Island", "South Carolina", "South Dakota", "Tennessee", + "Texas", "Utah", "Vermont", "Virgin Island", "Virginia", + "Washington", "West Virginia", "Wisconsin", "Wyoming" + ]; +} diff --git a/src/MudBlazor.Docs/Pages/Components/Select/Examples/SelectPlaygroundExample.razor b/src/MudBlazor.Docs/Pages/Components/Select/Examples/SelectPlaygroundExample.razor index 0ad810f62ec8..044cb79150db 100644 --- a/src/MudBlazor.Docs/Pages/Components/Select/Examples/SelectPlaygroundExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/Select/Examples/SelectPlaygroundExample.razor @@ -10,6 +10,7 @@ Label="@variant.ToString()" Margin="_margin" Dense="_dense" + FitContent="_fitContent" Disabled="_disabled" ReadOnly="_readonly" Placeholder="@(_placeholder ? "Placeholder" : null)" @@ -39,21 +40,23 @@ + @code { - string _value; - Margin _margin; - bool _dense; - bool _disabled; - bool _readonly; - bool _placeholder; - bool _helperText; - bool _helperTextOnFocus; - bool _clearable; + private string _value; + private Margin _margin; + private bool _dense; + private bool _disabled; + private bool _readonly; + private bool _placeholder; + private bool _helperText; + private bool _helperTextOnFocus; + private bool _clearable; + private bool _fitContent; - private string[] _states = + private readonly string[] _states = { "Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "Florida", "Georgia", diff --git a/src/MudBlazor.Docs/Pages/Components/Select/SelectPage.razor b/src/MudBlazor.Docs/Pages/Components/Select/SelectPage.razor index aacb293b2ad2..d991bbc55808 100644 --- a/src/MudBlazor.Docs/Pages/Components/Select/SelectPage.razor +++ b/src/MudBlazor.Docs/Pages/Components/Select/SelectPage.razor @@ -77,6 +77,37 @@
+ + + + The MudSelect component provides dynamic sizing capabilities through the FullWidth and FitContent parameters, allowing it to adapt to different layout needs: + + +
+ Default Behavior - FullWidth=False: The component expands to fill the available space within its container, adjusting dynamically based on its surroundings. +
+
+ +
+ Full-Width Mode - FullWidth=True: Forces the component to stretch and occupy 100% of its parent container’s width. +
+
+ +
+ Fit-Content Mode - FitContent=True: Instead of expanding, the component adjusts its width to fit only the necessary space required for its content. +
+
+
+ + Note: When using FitContent, the width of the select component can depend on the Placeholder, Label, Adornment, and Clearable properties. + +
+
+ + + +
+ diff --git a/src/MudBlazor.Docs/Pages/Components/Switch/Examples/SwitchColorExample.razor b/src/MudBlazor.Docs/Pages/Components/Switch/Examples/SwitchColorExample.razor index b8ae09052cbb..0308003d8a00 100644 --- a/src/MudBlazor.Docs/Pages/Components/Switch/Examples/SwitchColorExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/Switch/Examples/SwitchColorExample.razor @@ -3,7 +3,7 @@ - + @code{ public bool Basic_Switch1 { get; set; } = false; diff --git a/src/MudBlazor.SourceGenerator/MudBlazor.SourceGenerator.csproj b/src/MudBlazor.SourceGenerator/MudBlazor.SourceGenerator.csproj index 2c541a23a85e..515a320e41bf 100644 --- a/src/MudBlazor.SourceGenerator/MudBlazor.SourceGenerator.csproj +++ b/src/MudBlazor.SourceGenerator/MudBlazor.SourceGenerator.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -8,7 +8,7 @@ - + diff --git a/src/MudBlazor.UnitTests.Docs/MudBlazor.UnitTests.Docs.csproj b/src/MudBlazor.UnitTests.Docs/MudBlazor.UnitTests.Docs.csproj index e7a372cd1255..dad4dd97875a 100644 --- a/src/MudBlazor.UnitTests.Docs/MudBlazor.UnitTests.Docs.csproj +++ b/src/MudBlazor.UnitTests.Docs/MudBlazor.UnitTests.Docs.csproj @@ -67,7 +67,7 @@ - + diff --git a/src/MudBlazor.UnitTests.Viewer/Program.cs b/src/MudBlazor.UnitTests.Viewer/Program.cs index 6d5939361867..40b47bfa326e 100644 --- a/src/MudBlazor.UnitTests.Viewer/Program.cs +++ b/src/MudBlazor.UnitTests.Viewer/Program.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using MudBlazor.Examples.Data; using MudBlazor.Services; namespace MudBlazor.UnitTests; @@ -12,6 +13,7 @@ public static Task Main(string[] args) builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddMudServices(); + builder.Services.AddSingleton(); return builder.Build().RunAsync(); } diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridEventCallbacksTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridEventCallbacksTest.razor index 66d1f955e8a0..0238423d5501 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridEventCallbacksTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridEventCallbacksTest.razor @@ -1,29 +1,33 @@ - + + RowClick="@OnRowClick" + RowContextMenuClick="@OnRowContextMenuClick" + SelectedItemChanged="@OnSelectedItemChanged" + SelectedItemsChanged="@OnSelectedItemsChanged" + CommittedItemChanges="@OnCommittedItemChanges" + StartedEditingItem="@OnStartedEditingItem" + CanceledEditingItem="@OnCanceledEditingItem" + ReadOnly="false"> @code { + public static string __description__ = "Used for Unit Tests to verify SelectedItem/SelectedItems Changed"; + private IEnumerable _items = new List() { - new Item("A"), - new Item("B"), + new Item("A"), + new Item("B"), new Item("C") }; public bool RowClicked { get; set; } public bool RowContextMenuClicked { get; set; } public bool SelectedItemChanged { get; set; } + public bool SelectedItemsChanged { get; set; } public bool CommittedItemChanges { get; set; } public bool StartedEditingItem { get; set; } public bool CanceledEditingItem { get; set; } @@ -32,7 +36,7 @@ { RowClicked = true; } - + private void OnRowContextMenuClick(DataGridRowClickEventArgs args) { RowContextMenuClicked = true; @@ -43,6 +47,11 @@ SelectedItemChanged = true; } + private void OnSelectedItemsChanged(HashSet items) + { + SelectedItemsChanged = true; + } + private void OnCommittedItemChanges(Item item) { CommittedItemChanges = true; diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridEventSelectionTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridEventSelectionTest.razor new file mode 100644 index 000000000000..b648e63d4c36 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridEventSelectionTest.razor @@ -0,0 +1,39 @@ + + + + + + + + + + @_message + + +@code { +#nullable enable + public static string __description__ = "A test viewer for visibility testing of row Selection"; + private List _selectedDocuments = []; + private string _message = "Click select column to select the item"; + + private void OnSelectedItemsChanged(HashSet documents) + { + _selectedDocuments = documents.ToList(); + _message = "OnSelectedItemsChanged was called. " + _selectedDocuments.Count() + " items selected"; + } + + private static Task> ServerReload(GridState state) + { + List items = + [ + new("1", "Test"), + new("2", "Test2") + ]; + + return Task.FromResult(new GridData { Items = items, TotalItems = items.Count }); + } + + public record DocumentChecklistItem(string Id, string Name); +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/SimpleMudDatePickerTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/SimpleMudDatePickerTest.razor index 4303fb8840b3..53038742b4f2 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/SimpleMudDatePickerTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/SimpleMudDatePickerTest.razor @@ -70,6 +70,7 @@ else [Parameter] public Func? AdditionalDateClassesFunc { get; set; } + [Parameter] public DateTime? Date { get; set; } private async Task HandleDateChanged(DateTime? input) diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/InlineDialogDuplicateTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/InlineDialogDuplicateTest.razor new file mode 100644 index 000000000000..f198ba370ae3 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/InlineDialogDuplicateTest.razor @@ -0,0 +1,27 @@ +
+ + Open dialog + +
+ + + + + Edit rating + + + +

There should only be one dialog

+
+ + +
+ +@code { + private bool _visible; + + private void OpenDialog() { + _visible = true; + StateHasChanged(); + } +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Overlay/OverlayNestedFreezeTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Overlay/OverlayNestedFreezeTest.razor index f71903a09dd5..20544efd674b 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Overlay/OverlayNestedFreezeTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Overlay/OverlayNestedFreezeTest.razor @@ -6,7 +6,7 @@ - I am a button + AutoComplete button @@ -18,7 +18,7 @@ - I am a button + Select button @@ -37,7 +37,7 @@ - I am a button + Autocomplete button @@ -50,7 +50,7 @@ - I am a button + Select button @@ -70,8 +70,8 @@ @code { public static string __description__ = "Testing MudOverlay impact on MudAutoComplete when by itself, nested in mudmenu and nested in mudmenu in mudappbar"; - string _value = string.Empty; - private string[] _states = + private string _value = string.Empty; + private readonly string[] _states = { "Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "Florida", "Georgia", diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectCustomizedTextTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectCustomizedTextTest.razor index e3ce26d97317..c78ffb181e95 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectCustomizedTextTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectCustomizedTextTest.razor @@ -38,4 +38,4 @@ { public required string A { get; init; } } -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectTest2.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectTest2.razor index 13e017cde297..b0f5a227baaf 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectTest2.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectTest2.razor @@ -26,4 +26,4 @@ base.OnInitialized(); } -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectTest5.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectTest5.razor index 728b5598f907..b779b7d0ba87 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectTest5.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectTest5.razor @@ -1,4 +1,4 @@ -@if (_posts is not null) +@if (_posts is not null) { @foreach (var role in _posts) @@ -22,4 +22,4 @@ base.OnInitialized(); } -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectTest7.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectTest7.razor index 74bcfb52df0c..19132ff75dda 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectTest7.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectTest7.razor @@ -33,4 +33,4 @@ base.OnInitialized(); } -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectTestRequiredValue.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectTestRequiredValue.razor index ce786096b5e0..b230851b8d55 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectTestRequiredValue.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectTestRequiredValue.razor @@ -29,7 +29,6 @@ Value: @_objectSelect?.Value?.Name Selected values: @((_objectSelect?.SelectedValues is null) ? null : string.Join(", ", _objectSelect.SelectedValues.Select(x => x!.Name))) - @code { public static string __description__ = "Multi-Select Required Should Recognize Values"; diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectWithInitialValues.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectWithInitialValuesTest.razor similarity index 94% rename from src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectWithInitialValues.razor rename to src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectWithInitialValuesTest.razor index 1d00cc0d7c58..73a0a08933d5 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectWithInitialValues.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectWithInitialValuesTest.razor @@ -12,7 +12,7 @@ @code { - private readonly List _allItems = []; + private readonly List _allItems = []; private readonly HashSet _selectedItems = []; protected override async Task OnInitializedAsync() @@ -32,4 +32,4 @@ { public required string A { get; init; } } -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectWithValueContainZeroTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectWithValueContainZeroTest.razor index ff7b27dfae2d..a881606f81f8 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectWithValueContainZeroTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectWithValueContainZeroTest.razor @@ -40,4 +40,4 @@ Value2, Value3 } -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/ReloadSelectItemsTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/ReloadSelectItemsTest.razor index 69ff9c777b6a..9065e13659b2 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/ReloadSelectItemsTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/ReloadSelectItemsTest.razor @@ -18,7 +18,7 @@ @code { public static string __description__ = "After clicking 'load new states' selecting items should result in the correct values being selected."; - + private string _value = "Nothing selected"; private readonly List _states2 = ["Alabama", "Alaska", "American Samoa"]; private List _states = @@ -44,4 +44,4 @@ _value = "Nothing selected"; _states = _states2; } -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/ReselectValueTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/ReselectValueTest.razor index 0d66375be005..ea3b0bd78dc9 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/ReselectValueTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/ReselectValueTest.razor @@ -16,4 +16,4 @@ { ChangeCount++; } -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectFitContentTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectFitContentTest.razor new file mode 100644 index 000000000000..0ddd543b7166 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectFitContentTest.razor @@ -0,0 +1,47 @@ + + @foreach (var state in _states) + { + @state + } + + + + @foreach (var state in _states) + { + @state + } + + + + + + + +@code { + public static string __description__ = "FullWidth should override FitContent"; + + [Parameter] + public bool FullWidth { get; set; } + + [Parameter] + public bool FitContent { get; set; } + + private string? _selectedState; + private readonly string[] _states = + [ + "Alabama", "Alaska", "American Samoa", "Arizona", + "Arkansas", "California", "Colorado", "Connecticut", + "Delaware", "District of Columbia", "Federated States of Micronesia", + "Florida", "Georgia", "Guam", "Hawaii", "Idaho", + "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", + "Louisiana", "Maine", "Marshall Islands", "Maryland", + "Massachusetts", "Michigan", "Minnesota", "Mississippi", + "Missouri", "Montana", "Nebraska", "Nevada", + "New Hampshire", "New Jersey", "New Mexico", "New York", + "North Carolina", "North Dakota", "Northern Mariana Islands", "Ohio", + "Oklahoma", "Oregon", "Palau", "Pennsylvania", "Puerto Rico", + "Rhode Island", "South Carolina", "South Dakota", "Tennessee", + "Texas", "Utah", "Vermont", "Virgin Island", "Virginia", + "Washington", "West Virginia", "Wisconsin", "Wyoming" + ]; +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectFocusAndTypeTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectFocusAndTypeTest.razor index 233f52bacecd..04d66ca27837 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectFocusAndTypeTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectFocusAndTypeTest.razor @@ -23,4 +23,4 @@ "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming" }; -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectRequiredTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectRequiredTest.razor index 396c05854b62..4b82d3f67c38 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectRequiredTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectRequiredTest.razor @@ -12,4 +12,4 @@ public static string __description__ = "The Select should not show required-error initially. The first (empty) item should."; private string? _value; -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectScrollDrawerTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectScrollDrawerTest.razor index d611002a8eb4..64188d106e19 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectScrollDrawerTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectScrollDrawerTest.razor @@ -6,7 +6,7 @@ My App - + diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectTest1.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectTest1.razor index 5ec486b56f29..c111e4fc59d2 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectTest1.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectTest1.razor @@ -10,18 +10,18 @@ @code { - public static string __description__ = "Click should open the Menu and selecting a value should update the bindable value."; + public static string __description__ = "Click should open the Menu and selecting a value should update the bindable value."; - private string? _value = null; - private bool _focused; + private string? _value = null; + private bool _focused; - private void Focused() - { - _focused = true; - } + private void Focused() + { + _focused = true; + } - private void Blurred() - { - _focused = false; - } + private void Blurred() + { + _focused = false; + } } diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectValidationDataAttrTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectValidationDataAttrTest.razor index 6c1174b7b8e8..c3576328023f 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectValidationDataAttrTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectValidationDataAttrTest.razor @@ -15,6 +15,7 @@ + @code { public static string __description__ = "Test use of data attributes accessible using `For` bound expression."; @@ -28,4 +29,4 @@ [StringLength(3, ErrorMessage = "Should not be longer than 3")] public string? Value { get; set; } } -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectVariantsTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectVariantsTest.razor index faf42ba3b6ab..fc0d841256cc 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectVariantsTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectVariantsTest.razor @@ -27,4 +27,4 @@ @code { public static string __description__ = "The first two selects are Strict and should not crash in WASM on navigating the list with arrows."; -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Table/TableSortLabelTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Table/TableSortLabelTest.razor index f00c0446b3a5..141a789e4cc1 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Table/TableSortLabelTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Table/TableSortLabelTest.razor @@ -1,27 +1,33 @@ - +@using System.Net.Http.Json +@using MudBlazor.Examples.Data +@using MudBlazor.Examples.Data.Models +@inject IPeriodicTableService _periodicTableService + + - Sortable Header + Nr + Unsortable Nr + Position + Mass - @context + @context.Number + @context.Number + @context.Position + @context.Molar + + + -Sort Enabled - @code { - public static string __description__ = "Table sortable header styles should not be applied when sorting is disabled."; - - private string[] _data = ["A", "B", "C"]; - private bool _sortEnabled; - - [Parameter] - public bool SortEnabled { get; set; } = true; + private IEnumerable _elements = new List(); - protected override void OnParametersSet() + protected override async Task OnInitializedAsync() { - base.OnParametersSet(); - - _sortEnabled = SortEnabled; + _elements = await _periodicTableService.GetElements(); } } + + diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/TextField/TextFieldRenderTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/TextField/TextFieldRenderTest.razor new file mode 100644 index 000000000000..aeb35e9ef6db --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/TextField/TextFieldRenderTest.razor @@ -0,0 +1,16 @@ + + + + diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipDurationDelayTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipDurationDelayTest.razor new file mode 100644 index 000000000000..34a60bbacd0e --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipDurationDelayTest.razor @@ -0,0 +1,17 @@ + + Button With Tooltip + + +@code { + public static string __description__ = "Used to test Tooltip Duration and Delay"; + + [Parameter] + public double Duration { get; set; } = 1000; + + [Parameter] + public double Delay { get; set; } = 500; + + [Parameter] + public bool ShowOnHover { get; set; } = true; +} + diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipInMenuTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipInMenuTest.razor new file mode 100644 index 000000000000..48eaf94298c7 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipInMenuTest.razor @@ -0,0 +1,20 @@ +MudTooltip is @Text + + + + Some button with flashing tooltip + + + + + + + Some button with normal tooltip + + + +@code { + public static string __description__ = "Shows tooltips in a menu"; + + private const string Text = "Testing"; +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/TreeView/TreeViewHeterogeneous.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/TreeView/TreeViewHeterogeneous.razor new file mode 100644 index 000000000000..99793bf9ad68 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/TreeView/TreeViewHeterogeneous.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/_README.txt b/src/MudBlazor.UnitTests.Viewer/TestComponents/_README.txt index b8b67a4c02d6..76f5386c7830 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/_README.txt +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/_README.txt @@ -5,5 +5,5 @@ It is advised to name the test component exactly the same as the bUnit testcase In order to have the test description show up, make sure the component implements a static field __description__ in the @code section: @code { - public static string __description__ = " ... "; + public static string __description__ = "..."; } \ No newline at end of file diff --git a/src/MudBlazor.UnitTests/Analyzers/ValidAttributeTests.cs b/src/MudBlazor.UnitTests/Analyzers/ValidAttributeTests.cs index 289b4651172d..76f21c833ad3 100644 --- a/src/MudBlazor.UnitTests/Analyzers/ValidAttributeTests.cs +++ b/src/MudBlazor.UnitTests/Analyzers/ValidAttributeTests.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using FluentAssertions; +using FluentAssertions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; @@ -136,7 +134,6 @@ public void Cleanup() } [Test] - [Ignore("https://github.com/MudBlazor/MudBlazor/issues/10869")] public void AllowLowerCaseAttributes() { var diagnostics = LowerCaseAttributesDiagnostics.FilterToClass(typeof(AttributeTest).FullName); @@ -161,7 +158,6 @@ public void AllowLowerCaseAttributes() } [Test] - [Ignore("https://github.com/MudBlazor/MudBlazor/issues/10869")] public void AllowDefaultListAttributes() { var diagnostics = DefaultAttributesListDiagnostics.FilterToClass(typeof(AttributeTest).FullName); @@ -190,7 +186,6 @@ public void AllowDefaultListAttributes() } [Test] - [Ignore("https://github.com/MudBlazor/MudBlazor/issues/10869")] public void AllowCustomListAttributes() { var diagnostics = CustomAttributesListDiagnostics.FilterToClass(typeof(AttributeTest).FullName); @@ -218,7 +213,6 @@ public void AllowCustomListAttributes() } [Test] - [Ignore("https://github.com/MudBlazor/MudBlazor/issues/10869")] public void AllowDataAndAriaAttributes() { var diagnostics = DataAndAriaAttributesDiagnostics.FilterToClass(typeof(AttributeTest).FullName); @@ -248,7 +242,6 @@ public void AllowDataAndAriaAttributes() } [Test] - [Ignore("https://github.com/MudBlazor/MudBlazor/issues/10869")] public void AllowNoAttributes() { var diagnostics = NoAttributesDiagnostics.FilterToClass(typeof(AttributeTest).FullName); @@ -281,7 +274,6 @@ public void AllowNoAttributes() } [Test] - [Ignore("https://github.com/MudBlazor/MudBlazor/issues/10869")] public void AllowAnyAttributes() { var diagnostics = AnyAttributesDiagnostics.FilterToClass(typeof(AttributeTest).FullName); diff --git a/src/MudBlazor.UnitTests/Components/Charts/BarChartTests.cs b/src/MudBlazor.UnitTests/Components/Charts/BarChartTests.cs index 4b5741348b9a..bc2bfa990ead 100644 --- a/src/MudBlazor.UnitTests/Components/Charts/BarChartTests.cs +++ b/src/MudBlazor.UnitTests/Components/Charts/BarChartTests.cs @@ -86,16 +86,17 @@ public void BarChartExampleData() Contain("United States").And.Contain("Germany").And.Contain("Sweden"); } - if (chartSeries.Count == 3 && chartSeries.Any(x => x.Data.Contains(40))) + var bars = comp.FindAll("path.mud-chart-bar"); + if (chartSeries.TryGetIndexOfDataValue(0, 40, out var index)) { - comp.Markup.Should() - .Contain("d=\"M 30 265 L 30 145\""); + bars[index].OuterHtml.Should() + .Contain("d=\"M 34 261 L 34 143\""); } - if (chartSeries.Count == 3 && chartSeries.Any(x => x.Data.Contains(80))) + if (chartSeries.TryGetIndexOfDataValue(0, 80, out index)) { - comp.Markup.Should() - .Contain("d=\"M 546.25 265 L 546.25 25\""); + bars[index].OuterHtml.Should() + .Contain("d=\"M 525.75 261 L 525.75 25\""); } comp.SetParametersAndRender(parameters => parameters @@ -141,16 +142,17 @@ public void BarChartExampleSingleXAxis() Contain("United States").And.Contain("Germany").And.Contain("Sweden"); } - if (chartSeries.Count == 3 && chartSeries.Any(x => x.Data.Contains(40))) + var bars = comp.FindAll("path.mud-chart-bar"); + if (chartSeries.TryGetIndexOfDataValue(0, 40, out var index)) { - comp.Markup.Should() - .Contain("d=\"M 30 265 L 30 145\""); + bars[index].OuterHtml.Should() + .Contain("d=\"M 34 261 L 34 143\""); } - if (chartSeries.Count == 3 && chartSeries.Any(x => x.Data.Contains(80))) + if (chartSeries.TryGetIndexOfDataValue(0, 80, out index)) { - comp.Markup.Should() - .Contain("d=\"M 546.25 265 L 546.25 25\""); + bars[index].OuterHtml.Should() + .Contain("d=\"M 525.75 261 L 525.75 25\""); } comp.SetParametersAndRender(parameters => parameters.Add(p => p.ChartOptions, new ChartOptions() { ChartPalette = _modifiedPalette })); diff --git a/src/MudBlazor.UnitTests/Components/Charts/ChartSeriesExtensions.cs b/src/MudBlazor.UnitTests/Components/Charts/ChartSeriesExtensions.cs new file mode 100644 index 000000000000..be0e7f77e3c9 --- /dev/null +++ b/src/MudBlazor.UnitTests/Components/Charts/ChartSeriesExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MudBlazor.UnitTests.Components +{ + internal static class ChartSeriesExtensions + { + internal static bool TryGetIndexOfDataValue(this ChartSeries chartSeries, int seriesIndex, double value, out int dataIndex) + { + dataIndex = -1; + + for (var i = 0; i < chartSeries.Data.Length; i++) + { + if (chartSeries.Data[i] == value) + { + dataIndex = i; + return true; + } + } + + return false; + } + + internal static bool TryGetIndexOfDataValue(this IEnumerable chartSeries, int seriesIndex, double value, out int dataIndex) => TryGetIndexOfDataValue(chartSeries.ElementAt(seriesIndex), seriesIndex, value, out dataIndex); + } +} diff --git a/src/MudBlazor.UnitTests/Components/Charts/ChartToolTipTests.cs b/src/MudBlazor.UnitTests/Components/Charts/ChartToolTipTests.cs new file mode 100644 index 000000000000..17c7060d9564 --- /dev/null +++ b/src/MudBlazor.UnitTests/Components/Charts/ChartToolTipTests.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using FluentAssertions; +using MudBlazor.Charts; +using NUnit.Framework; + +namespace MudBlazor.UnitTests.Charts +{ + public class ChartToolTipTests : BunitTest + { + [SetUp] + public void Init() + { + + } + + [SetCulture("ru")] + [Test] + public void BarChartEmptyData() + { + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Title, "Some Title") + .Add(p => p.Subtitle, "Some Subtitle") + .Add(p => p.X, 10.05) + .Add(p => p.Y, 20.02) + .Add(p => p.Color, "red") + ); + + comp.Markup.Should().Contain(""); + comp.Markup.Should().Contain(""); + comp.Markup.Should().Contain(""); + comp.Markup.Should().Contain(""); + comp.Markup.Should().Contain("Some TitleSome Subtitle"); + } + } +} diff --git a/src/MudBlazor.UnitTests/Components/Charts/DonutChartTests.cs b/src/MudBlazor.UnitTests/Components/Charts/DonutChartTests.cs index b99570e92066..2f4393ef63f4 100644 --- a/src/MudBlazor.UnitTests/Components/Charts/DonutChartTests.cs +++ b/src/MudBlazor.UnitTests/Components/Charts/DonutChartTests.cs @@ -164,5 +164,17 @@ public void DonutChartColoring() } } } + + [Test] + public void DonutChart100Percent() + { + double[] data = { 50, 0, 0 }; + + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.ChartType, ChartType.Donut) + .Add(p => p.InputData, data)); + + comp.Markup.Should().Contain("d=\"M 0 -140 A 140 140 0 1 1 0 140 A 140 140 0 1 1 -0 -140 L -0 -105 A 105 105 0 1 0 0 105 A 105 105 0 1 0 0 -105 Z\""); + } } } diff --git a/src/MudBlazor.UnitTests/Components/Charts/LineChartTests.cs b/src/MudBlazor.UnitTests/Components/Charts/LineChartTests.cs index 6b37f263141b..b2f69e22194e 100644 --- a/src/MudBlazor.UnitTests/Components/Charts/LineChartTests.cs +++ b/src/MudBlazor.UnitTests/Components/Charts/LineChartTests.cs @@ -83,28 +83,32 @@ public void LineChartExampleData(InterpolationOption opt) if (chartSeries.FirstOrDefault(x => x.Name == "Series 1") is not null) { + var path = comp.Find("path.mud-chart-line"); + var d = path.GetAttribute("d"); + switch (opt) { case InterpolationOption.NaturalSpline: - comp.Markup.Should().Contain("d=\"M 30 36.5385 L 37.375 30.7688 L 44.75 25.4257 L 52.125 20.9358 L 59.5 17.7257 L 66.875 16.222 L 74.25 16.8514 L 81.625 20.0403 L 89 26.2154 L 96.375 35.8034 L 103.75 49.2308 L 111.125 66.6591 L 118.5 87.1897 L 125.875 109.6587 L 133.25 132.9024 L 140.625 155.757 L 148 177.0588 L 155.375 195.6439 L 162.75 210.3485 L 170.125 220.009 L 177.5 223.4615 L 184.875 219.9425 L 192.25 210.2895 L 199.625 195.7403 L 207 177.5324 L 214.375 156.9038 L 221.75 135.0921 L 229.125 113.335 L 236.5 92.8704 L 243.875 74.9359 L 251.25 60.7692 L 258.625 51.2784 L 266 46.0522 L 273.375 44.3495 L 280.75 45.4294 L 288.125 48.5509 L 295.5 52.9729 L 302.875 57.9545 L 310.25 62.7545 L 317.625 66.6321 L 325 68.8462 L 332.375 68.8881 L 339.75 67.1787 L 347.125 64.3713 L 354.5 61.1191 L 361.875 58.0753 L 369.25 55.8932 L 376.625 55.226 L 384 56.7269 L 391.375 61.0492 L 398.75 68.8462 L 406.125 80.4932 L 413.5 95.2546 L 420.875 112.1169 L 428.25 130.0666 L 435.625 148.0903 L 443 165.1744 L 450.375 180.3055 L 457.75 192.4702 L 465.125 200.6549 L 472.5 203.8462 L 479.875 201.3577 L 487.25 193.8123 L 494.625 182.1597 L 502 167.35 L 509.375 150.3329 L 516.75 132.0584 L 524.125 113.4765 L 531.5 95.5369 L 538.875 79.1897 L 546.25 65.3846 L 553.625 54.8552 L 561 47.4686 L 568.375 42.8758 L 575.75 40.7274 L 583.125 40.6743 L 590.5 42.3672 L 597.875 45.457 L 605.25 49.5945 L 612.625 54.4303 L 620 59.6154\""); + d.Should().Contain("M 30 36.3462 L 37.375 30.6726 L 44.75 25.4186 L 52.125 21.0035 L 59.5 17.8469 L 66.875 16.3683 L 74.25 16.9872 L 81.625 20.123 L 89 26.1952 L 96.375 35.6233 L 103.75 48.8269 L 111.125 65.9648 L 118.5 86.1532 L 125.875 108.2477 L 133.25 131.104 L 140.625 153.5777 L 148 174.5244 L 155.375 192.7998 L 162.75 207.2594 L 170.125 216.7589 L 177.5 220.1538 L 184.875 216.6935 L 192.25 207.2014 L 199.625 192.8946 L 207 174.9902 L 214.375 154.7054 L 221.75 133.2572 L 229.125 111.8628 L 236.5 91.7392 L 243.875 74.1036 L 251.25 60.1731 L 258.625 50.8404 L 266 45.7013 L 273.375 44.027 L 280.75 45.0889 L 288.125 48.1584 L 295.5 52.5067 L 302.875 57.4052 L 310.25 62.1253 L 317.625 65.9382 L 325 68.1154 L 332.375 68.1566 L 339.75 66.4757 L 347.125 63.7151 L 354.5 60.5171 L 361.875 57.524 L 369.25 55.3783 L 376.625 54.7222 L 384 56.1981 L 391.375 60.4484 L 398.75 68.1154 L 406.125 79.5683 L 413.5 94.0837 L 420.875 110.6649 L 428.25 128.3155 L 435.625 146.0388 L 443 162.8382 L 450.375 177.7171 L 457.75 189.679 L 465.125 197.7273 L 472.5 200.8654 L 479.875 198.4184 L 487.25 190.9987 L 494.625 179.5404 L 502 164.9775 L 509.375 148.244 L 516.75 130.2741 L 524.125 112.0019 L 531.5 94.3613 L 538.875 78.2865 L 546.25 64.7115 L 553.625 54.3576 L 561 47.0941 L 568.375 42.5778 L 575.75 40.4653 L 583.125 40.4131 L 590.5 42.0778 L 597.875 45.1161 L 605.25 49.1846 L 612.625 53.9398 L 620 59.0385"); break; case InterpolationOption.Straight: - comp.Markup.Should() - .Contain("d=\"M 30 36.5385 L 103.75 49.2308 L 177.5 223.4615 L 251.25 60.7692 L 325 68.8462 L 398.75 68.8462 L 472.5 203.8462 L 546.25 65.3846 L 620 59.6154\""); + d.Should().Contain("M 30 36.3462 L 103.75 48.8269 L 177.5 220.1538 L 251.25 60.1731 L 325 68.1154 L 398.75 68.1154 L 472.5 200.8654 L 546.25 64.7115 L 620 59.0385"); break; case InterpolationOption.EndSlope: - comp.Markup.Should().Contain("d=\"M 30 36.5385 L 37.375 35.6406 L 44.75 33.4025 L 52.125 30.5074 L 59.5 27.6384 L 66.875 25.4787 L 74.25 24.7115 L 81.625 26.0199 L 89 30.0871 L 96.375 37.5964 L 103.75 49.2308 L 111.125 65.3542 L 118.5 85.0535 L 125.875 107.0958 L 133.25 130.2487 L 140.625 153.2795 L 148 174.9557 L 155.375 194.0446 L 162.75 209.3136 L 170.125 219.5301 L 177.5 223.4615 L 184.875 220.2901 L 192.25 210.8575 L 199.625 196.4201 L 207 178.2344 L 214.375 157.557 L 221.75 135.6442 L 229.125 113.7525 L 236.5 93.1385 L 243.875 75.0586 L 251.25 60.7692 L 258.625 51.193 L 266 45.9166 L 273.375 44.193 L 280.75 45.2751 L 288.125 48.4156 L 295.5 52.8676 L 302.875 57.8837 L 310.25 62.7169 L 317.625 66.6201 L 325 68.8462 L 332.375 68.8822 L 339.75 67.1529 L 347.125 64.3174 L 354.5 61.0344 L 361.875 57.9631 L 369.25 55.7625 L 376.625 55.0915 L 384 56.6091 L 391.375 60.9743 L 398.75 68.8462 L 406.125 80.602 L 413.5 95.4931 L 420.875 112.4891 L 428.25 130.5596 L 435.625 148.6741 L 443 165.8025 L 450.375 180.9142 L 457.75 192.979 L 465.125 200.9664 L 472.5 203.8462 L 479.875 200.9281 L 487.25 192.8838 L 494.625 180.7247 L 502 165.4628 L 509.375 148.1095 L 516.75 129.6768 L 524.125 111.1762 L 531.5 93.6195 L 538.875 78.0184 L 546.25 65.3846 L 553.625 56.4646 L 561 50.9441 L 568.375 48.2435 L 575.75 47.7833 L 583.125 48.9839 L 590.5 51.2658 L 597.875 54.0494 L 605.25 56.7553 L 612.625 58.8038 L 620 59.6154\""); + d.Should().Contain("M 30 36.3462 L 37.375 35.4633 L 44.75 33.2625 L 52.125 30.4156 L 59.5 27.5944 L 66.875 25.4707 L 74.25 24.7163 L 81.625 26.0029 L 89 30.0023 L 96.375 37.3864 L 103.75 48.8269 L 111.125 64.6817 L 118.5 84.0526 L 125.875 105.7275 L 133.25 128.4946 L 140.625 151.1415 L 148 172.4564 L 155.375 191.2272 L 162.75 206.2417 L 170.125 216.2879 L 177.5 220.1538 L 184.875 217.0353 L 192.25 207.7599 L 199.625 193.5631 L 207 175.6805 L 214.375 155.3477 L 221.75 133.8001 L 229.125 112.2733 L 236.5 92.0029 L 243.875 74.2243 L 251.25 60.1731 L 258.625 50.7564 L 266 45.568 L 273.375 43.8732 L 280.75 44.9372 L 288.125 48.0254 L 295.5 52.4031 L 302.875 57.3356 L 310.25 62.0883 L 317.625 65.9265 L 325 68.1154 L 332.375 68.1508 L 339.75 66.4504 L 347.125 63.6621 L 354.5 60.4338 L 361.875 57.4137 L 369.25 55.2498 L 376.625 54.59 L 384 56.0823 L 391.375 60.3747 L 398.75 68.1154 L 406.125 79.6753 L 413.5 94.3182 L 420.875 111.0309 L 428.25 128.8002 L 435.625 146.6129 L 443 163.4558 L 450.375 178.3157 L 457.75 190.1794 L 465.125 198.0337 L 472.5 200.8654 L 479.875 197.996 L 487.25 190.0857 L 494.625 178.1293 L 502 163.1217 L 509.375 146.0577 L 516.75 127.9322 L 524.125 109.7399 L 531.5 92.4759 L 538.875 77.1348 L 546.25 64.7115 L 553.625 55.9402 L 561 50.5117 L 568.375 47.8561 L 575.75 47.4035 L 583.125 48.5841 L 590.5 50.828 L 597.875 53.5652 L 605.25 56.226 L 612.625 58.2404 L 620 59.0385"); break; case InterpolationOption.Periodic: - comp.Markup.Should().Contain("d=\"M 30 36.5385 L 37.375 36.3538 L 44.75 34.5703 L 52.125 31.9087 L 59.5 29.0896 L 66.875 26.8338 L 74.25 25.8621 L 81.625 26.8952 L 89 30.6538 L 96.375 37.8588 L 103.75 49.2308 L 111.125 65.1633 L 118.5 84.7409 L 125.875 106.7209 L 133.25 129.8605 L 140.625 152.9172 L 148 174.6482 L 155.375 193.8109 L 162.75 209.1624 L 170.125 219.4602 L 177.5 223.4615 L 184.875 220.3407 L 192.25 210.94 L 199.625 196.5187 L 207 178.3359 L 214.375 157.6511 L 221.75 135.7234 L 229.125 113.8121 L 236.5 93.1765 L 243.875 75.0758 L 251.25 60.7692 L 258.625 51.1816 L 266 45.8991 L 273.375 44.1738 L 280.75 45.2573 L 288.125 48.4014 L 295.5 52.8581 L 302.875 57.8791 L 310.25 62.7163 L 317.625 66.6213 L 325 68.8462 L 332.375 68.8773 L 339.75 67.1404 L 347.125 64.296 L 354.5 61.0043 L 361.875 57.9258 L 369.25 55.721 L 376.625 55.0502 L 384 56.5738 L 391.375 60.9524 L 398.75 68.8462 L 406.125 80.6331 L 413.5 95.5607 L 420.875 112.5939 L 428.25 130.6979 L 435.625 148.8376 L 443 165.9779 L 450.375 181.0839 L 457.75 193.1207 L 465.125 201.0531 L 472.5 203.8462 L 479.875 200.8089 L 487.25 192.6262 L 494.625 180.3267 L 502 164.9395 L 509.375 147.4931 L 516.75 129.0166 L 524.125 110.5387 L 531.5 93.0881 L 538.875 77.6938 L 546.25 65.3846 L 553.625 56.9106 L 561 51.907 L 568.375 49.7307 L 575.75 49.7381 L 583.125 51.2861 L 590.5 53.7311 L 597.875 56.4299 L 605.25 58.7391 L 612.625 60.0154 L 620 59.6154\""); + d.Should().Contain("M 30 36.3462 L 37.375 36.1646 L 44.75 34.4108 L 52.125 31.7935 L 59.5 29.0214 L 66.875 26.8032 L 74.25 25.8477 L 81.625 26.8636 L 89 30.5596 L 96.375 37.6445 L 103.75 48.8269 L 111.125 64.4939 L 118.5 83.7452 L 125.875 105.3589 L 133.25 128.1129 L 140.625 150.7853 L 148 172.1541 L 155.375 190.9974 L 162.75 206.093 L 170.125 216.2192 L 177.5 220.1538 L 184.875 217.085 L 192.25 207.841 L 199.625 193.66 L 207 175.7803 L 214.375 155.4402 L 221.75 133.878 L 229.125 112.3319 L 236.5 92.0402 L 243.875 74.2412 L 251.25 60.1731 L 258.625 50.7452 L 266 45.5508 L 273.375 43.8542 L 280.75 44.9196 L 288.125 48.0114 L 295.5 52.3938 L 302.875 57.3311 L 310.25 62.0877 L 317.625 65.9276 L 325 68.1154 L 332.375 68.146 L 339.75 66.4381 L 347.125 63.641 L 354.5 60.4042 L 361.875 57.3771 L 369.25 55.209 L 376.625 54.5494 L 384 56.0476 L 391.375 60.3532 L 398.75 68.1154 L 406.125 79.7058 L 413.5 94.3846 L 420.875 111.134 L 428.25 128.9363 L 435.625 146.7736 L 443 163.6283 L 450.375 178.4825 L 457.75 190.3186 L 465.125 198.1188 L 472.5 200.8654 L 479.875 197.8788 L 487.25 189.8324 L 494.625 177.738 L 502 162.6071 L 509.375 145.4516 L 516.75 127.283 L 524.125 109.113 L 531.5 91.9533 L 538.875 76.8156 L 546.25 64.7115 L 553.625 56.3787 L 561 51.4586 L 568.375 49.3185 L 575.75 49.3258 L 583.125 50.848 L 590.5 53.2522 L 597.875 55.9061 L 605.25 58.1768 L 612.625 59.4318 L 620 59.0385"); break; } } if (comp.Instance.ChartOptions.InterpolationOption == InterpolationOption.Straight && chartSeries.FirstOrDefault(x => x.Name == "Series 2") is not null) { - comp.Markup.Should() - .Contain("d=\"M 30 128.8462 L 103.75 93.0769 L 177.5 100 L 251.25 81.5385 L 325 83.8462 L 398.75 68.8462 L 472.5 220 L 546.25 35.3846 L 620 311.1538\""); + var path = comp.FindAll("path.mud-chart-line").Skip(1).First(); + var d = path.GetAttribute("d"); + + d.Should().Contain("M 30 127.1154 L 103.75 91.9423 L 177.5 98.75 L 251.25 80.5962 L 325 82.8654 L 398.75 68.1154 L 472.5 216.75 L 546.25 35.2115 L 620 306.3846"); } comp.SetParametersAndRender(parameters => parameters @@ -169,20 +173,22 @@ public void LineChartExampleZeroValues(InterpolationOption opt) comp.Markup.Should().Contain("mud-chart-legend-item"); comp.Markup.Should().Contain("Series 1"); + var path = comp.Find("path.mud-chart-line"); + var d = path.GetAttribute("d"); + switch (opt) { case InterpolationOption.NaturalSpline: - comp.Markup.Should().Contain("d=\"M 30 325 L 37.375 325 L 44.75 325 L 52.125 325 L 59.5 325 L 66.875 325 L 74.25 325 L 81.625 325 L 89 325 L 96.375 325 L 103.75 325 L 111.125 325 L 118.5 325 L 125.875 325 L 133.25 325 L 140.625 325 L 148 325 L 155.375 325 L 162.75 325 L 170.125 325 L 177.5 325 L 184.875 325 L 192.25 325 L 199.625 325 L 207 325 L 214.375 325 L 221.75 325 L 229.125 325 L 236.5 325 L 243.875 325 L 251.25 325 L 258.625 325 L 266 325 L 273.375 325 L 280.75 325 L 288.125 325 L 295.5 325 L 302.875 325 L 310.25 325 L 317.625 325 L 325 325 L 332.375 325 L 339.75 325 L 347.125 325 L 354.5 325 L 361.875 325 L 369.25 325 L 376.625 325 L 384 325 L 391.375 325 L 398.75 325 L 406.125 325 L 413.5 325 L 420.875 325 L 428.25 325 L 435.625 325 L 443 325 L 450.375 325 L 457.75 325 L 465.125 325 L 472.5 325 L 479.875 325 L 487.25 325 L 494.625 325 L 502 325 L 509.375 325 L 516.75 325 L 524.125 325 L 531.5 325 L 538.875 325 L 546.25 325 L 553.625 325 L 561 325 L 568.375 325 L 575.75 325 L 583.125 325 L 590.5 325 L 597.875 325 L 605.25 325 L 612.625 325 L 620 325\""); + d.Should().Contain("M 30 320 L 37.375 320 L 44.75 320 L 52.125 320 L 59.5 320 L 66.875 320 L 74.25 320 L 81.625 320 L 89 320 L 96.375 320 L 103.75 320 L 111.125 320 L 118.5 320 L 125.875 320 L 133.25 320 L 140.625 320 L 148 320 L 155.375 320 L 162.75 320 L 170.125 320 L 177.5 320 L 184.875 320 L 192.25 320 L 199.625 320 L 207 320 L 214.375 320 L 221.75 320 L 229.125 320 L 236.5 320 L 243.875 320 L 251.25 320 L 258.625 320 L 266 320 L 273.375 320 L 280.75 320 L 288.125 320 L 295.5 320 L 302.875 320 L 310.25 320 L 317.625 320 L 325 320 L 332.375 320 L 339.75 320 L 347.125 320 L 354.5 320 L 361.875 320 L 369.25 320 L 376.625 320 L 384 320 L 391.375 320 L 398.75 320 L 406.125 320 L 413.5 320 L 420.875 320 L 428.25 320 L 435.625 320 L 443 320 L 450.375 320 L 457.75 320 L 465.125 320 L 472.5 320 L 479.875 320 L 487.25 320 L 494.625 320 L 502 320 L 509.375 320 L 516.75 320 L 524.125 320 L 531.5 320 L 538.875 320 L 546.25 320 L 553.625 320 L 561 320 L 568.375 320 L 575.75 320 L 583.125 320 L 590.5 320 L 597.875 320 L 605.25 320 L 612.625 320 L 620 320"); break; case InterpolationOption.Straight: - comp.Markup.Should() - .Contain("d=\"M 30 325 L 103.75 325 L 177.5 325 L 251.25 325 L 325 325 L 398.75 325 L 472.5 325 L 546.25 325 L 620 325\""); + d.Should().Contain("M 30 320 L 103.75 320 L 177.5 320 L 251.25 320 L 325 320 L 398.75 320 L 472.5 320 L 546.25 320 L 620 320"); break; case InterpolationOption.EndSlope: - comp.Markup.Should().Contain("d=\"M 30 325 L 37.375 325 L 44.75 325 L 52.125 325 L 59.5 325 L 66.875 325 L 74.25 325 L 81.625 325 L 89 325 L 96.375 325 L 103.75 325 L 111.125 325 L 118.5 325 L 125.875 325 L 133.25 325 L 140.625 325 L 148 325 L 155.375 325 L 162.75 325 L 170.125 325 L 177.5 325 L 184.875 325 L 192.25 325 L 199.625 325 L 207 325 L 214.375 325 L 221.75 325 L 229.125 325 L 236.5 325 L 243.875 325 L 251.25 325 L 258.625 325 L 266 325 L 273.375 325 L 280.75 325 L 288.125 325 L 295.5 325 L 302.875 325 L 310.25 325 L 317.625 325 L 325 325 L 332.375 325 L 339.75 325 L 347.125 325 L 354.5 325 L 361.875 325 L 369.25 325 L 376.625 325 L 384 325 L 391.375 325 L 398.75 325 L 406.125 325 L 413.5 325 L 420.875 325 L 428.25 325 L 435.625 325 L 443 325 L 450.375 325 L 457.75 325 L 465.125 325 L 472.5 325 L 479.875 325 L 487.25 325 L 494.625 325 L 502 325 L 509.375 325 L 516.75 325 L 524.125 325 L 531.5 325 L 538.875 325 L 546.25 325 L 553.625 325 L 561 325 L 568.375 325 L 575.75 325 L 583.125 325 L 590.5 325 L 597.875 325 L 605.25 325 L 612.625 325 L 620 325\""); + d.Should().Contain("M 30 320 L 37.375 320 L 44.75 320 L 52.125 320 L 59.5 320 L 66.875 320 L 74.25 320 L 81.625 320 L 89 320 L 96.375 320 L 103.75 320 L 111.125 320 L 118.5 320 L 125.875 320 L 133.25 320 L 140.625 320 L 148 320 L 155.375 320 L 162.75 320 L 170.125 320 L 177.5 320 L 184.875 320 L 192.25 320 L 199.625 320 L 207 320 L 214.375 320 L 221.75 320 L 229.125 320 L 236.5 320 L 243.875 320 L 251.25 320 L 258.625 320 L 266 320 L 273.375 320 L 280.75 320 L 288.125 320 L 295.5 320 L 302.875 320 L 310.25 320 L 317.625 320 L 325 320 L 332.375 320 L 339.75 320 L 347.125 320 L 354.5 320 L 361.875 320 L 369.25 320 L 376.625 320 L 384 320 L 391.375 320 L 398.75 320 L 406.125 320 L 413.5 320 L 420.875 320 L 428.25 320 L 435.625 320 L 443 320 L 450.375 320 L 457.75 320 L 465.125 320 L 472.5 320 L 479.875 320 L 487.25 320 L 494.625 320 L 502 320 L 509.375 320 L 516.75 320 L 524.125 320 L 531.5 320 L 538.875 320 L 546.25 320 L 553.625 320 L 561 320 L 568.375 320 L 575.75 320 L 583.125 320 L 590.5 320 L 597.875 320 L 605.25 320 L 612.625 320 L 620 320"); break; case InterpolationOption.Periodic: - comp.Markup.Should().Contain("d=\"M 30 325 L 37.375 325 L 44.75 325 L 52.125 325 L 59.5 325 L 66.875 325 L 74.25 325 L 81.625 325 L 89 325 L 96.375 325 L 103.75 325 L 111.125 325 L 118.5 325 L 125.875 325 L 133.25 325 L 140.625 325 L 148 325 L 155.375 325 L 162.75 325 L 170.125 325 L 177.5 325 L 184.875 325 L 192.25 325 L 199.625 325 L 207 325 L 214.375 325 L 221.75 325 L 229.125 325 L 236.5 325 L 243.875 325 L 251.25 325 L 258.625 325 L 266 325 L 273.375 325 L 280.75 325 L 288.125 325 L 295.5 325 L 302.875 325 L 310.25 325 L 317.625 325 L 325 325 L 332.375 325 L 339.75 325 L 347.125 325 L 354.5 325 L 361.875 325 L 369.25 325 L 376.625 325 L 384 325 L 391.375 325 L 398.75 325 L 406.125 325 L 413.5 325 L 420.875 325 L 428.25 325 L 435.625 325 L 443 325 L 450.375 325 L 457.75 325 L 465.125 325 L 472.5 325 L 479.875 325 L 487.25 325 L 494.625 325 L 502 325 L 509.375 325 L 516.75 325 L 524.125 325 L 531.5 325 L 538.875 325 L 546.25 325 L 553.625 325 L 561 325 L 568.375 325 L 575.75 325 L 583.125 325 L 590.5 325 L 597.875 325 L 605.25 325 L 612.625 325 L 620 325\""); + d.Should().Contain("M 30 320 L 37.375 320 L 44.75 320 L 52.125 320 L 59.5 320 L 66.875 320 L 74.25 320 L 81.625 320 L 89 320 L 96.375 320 L 103.75 320 L 111.125 320 L 118.5 320 L 125.875 320 L 133.25 320 L 140.625 320 L 148 320 L 155.375 320 L 162.75 320 L 170.125 320 L 177.5 320 L 184.875 320 L 192.25 320 L 199.625 320 L 207 320 L 214.375 320 L 221.75 320 L 229.125 320 L 236.5 320 L 243.875 320 L 251.25 320 L 258.625 320 L 266 320 L 273.375 320 L 280.75 320 L 288.125 320 L 295.5 320 L 302.875 320 L 310.25 320 L 317.625 320 L 325 320 L 332.375 320 L 339.75 320 L 347.125 320 L 354.5 320 L 361.875 320 L 369.25 320 L 376.625 320 L 384 320 L 391.375 320 L 398.75 320 L 406.125 320 L 413.5 320 L 420.875 320 L 428.25 320 L 435.625 320 L 443 320 L 450.375 320 L 457.75 320 L 465.125 320 L 472.5 320 L 479.875 320 L 487.25 320 L 494.625 320 L 502 320 L 509.375 320 L 516.75 320 L 524.125 320 L 531.5 320 L 538.875 320 L 546.25 320 L 553.625 320 L 561 320 L 568.375 320 L 575.75 320 L 583.125 320 L 590.5 320 L 597.875 320 L 605.25 320 L 612.625 320 L 620 320"); break; } diff --git a/src/MudBlazor.UnitTests/Components/Charts/PieChartTests.cs b/src/MudBlazor.UnitTests/Components/Charts/PieChartTests.cs index 821e8ca5803a..f103e6a86a90 100644 --- a/src/MudBlazor.UnitTests/Components/Charts/PieChartTests.cs +++ b/src/MudBlazor.UnitTests/Components/Charts/PieChartTests.cs @@ -78,17 +78,16 @@ public void PieChartExampleData(double[] data) comp.Markup.Should() .Contain("Technetium"); } - if (data.Length == 4 && data.Contains(77)) { comp.Markup.Should() - .Contain("d=\"M 0 -140 A 140 140 0 1 1 -86.7071 109.9176 L 0 0\""); + .Contain("d=\"M 0 -140 A 140 140 0 1 1 -86.7071 109.9176 L 0 0 Z\""); } if (data.Length == 4 && data.Contains(5)) { comp.Markup.Should() - .Contain("d=\"M -34.2796 -135.7384 A 140 140 0 0 1 -0 -140 L 0 0\""); + .Contain("d=\"M -34.2796 -135.7384 A 140 140 0 0 1 -0 -140 L 0 0 Z\""); } comp.SetParametersAndRender(parameters => parameters @@ -133,5 +132,17 @@ public void PieChartColoring() } } } + + [Test] + public void PieChart100Percent() + { + double[] data = { 50, 0, 0 }; + + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.ChartType, ChartType.Pie) + .Add(p => p.InputData, data)); + + comp.Markup.Should().Contain("d=\"M 0 -140 A 140 140 0 1 1 0 140 A 140 140 0 1 1 -0 -140 L 0 0 Z\""); + } } } diff --git a/src/MudBlazor.UnitTests/Components/Charts/StackedBarChartTests.cs b/src/MudBlazor.UnitTests/Components/Charts/StackedBarChartTests.cs index 5a4c481838ea..2de131cf4fe6 100644 --- a/src/MudBlazor.UnitTests/Components/Charts/StackedBarChartTests.cs +++ b/src/MudBlazor.UnitTests/Components/Charts/StackedBarChartTests.cs @@ -88,10 +88,10 @@ public void BarChartExampleData() } comp.Markup.Should() - .Contain("d=\"M 62.9 325 L 62.9 224.5\""); + .Contain("d=\"M 62.9 320 L 62.9 221.1667\""); comp.Markup.Should() - .Contain("d=\"M 587.7 210 L 587.7 164.5\""); + .Contain("d=\"M 587.7 206.9167 L 587.7 162.1667\""); comp.SetParametersAndRender(parameters => parameters .Add(p => p.ChartOptions, new ChartOptions() { ChartPalette = _modifiedPalette })); diff --git a/src/MudBlazor.UnitTests/Components/Charts/TimeSeriesChartTests.cs b/src/MudBlazor.UnitTests/Components/Charts/TimeSeriesChartTests.cs index 52507598734a..e6cb5f7eb8ec 100644 --- a/src/MudBlazor.UnitTests/Components/Charts/TimeSeriesChartTests.cs +++ b/src/MudBlazor.UnitTests/Components/Charts/TimeSeriesChartTests.cs @@ -37,10 +37,12 @@ public void TimeSeriesChartBasicExample() .Add(p => p.TimeLabelSpacing, TimeSpan.FromHours(1))); // check the line path - comp.Markup.Should().ContainEquivalentOf(""); + comp.Markup.Should().ContainEquivalentOf(""); // check the axis - comp.Markup.Should().ContainEquivalentOf("\n 1000\n 23:0000:0001:00"); + comp.Markup.Should().ContainEquivalentOf(""); + comp.Markup.Should().ContainEquivalentOf("1000"); + comp.Markup.Should().ContainEquivalentOf("23:0000:0001:0002:00"); } [Test] @@ -88,7 +90,8 @@ public void TimeSeriesChartTimeLabelSpacingRounding() .Add(p => p.TimeLabelSpacingRounding, true)); // check the axis - comp.Markup.Should().ContainEquivalentOf("\n 1000\n 00:0001:00"); + comp.Markup.Should().ContainEquivalentOf("1000"); + comp.Markup.Should().ContainEquivalentOf("00:0001:0002:00"); } [Test] @@ -112,10 +115,11 @@ public void TimeSeriesChartTimeLabelSpacingRoundingPadSeries() .Add(p => p.TimeLabelSpacingRoundingPadSeries, true)); // check the axis - comp.Markup.Should().ContainEquivalentOf("\n 1000\n 23:0000:0001:0002:00"); + comp.Markup.Should().ContainEquivalentOf("1000"); + comp.Markup.Should().ContainEquivalentOf("23:0000:0001:0002:0003:00"); // check the line path - comp.Markup.Should().ContainEquivalentOf(""); + comp.Markup.Should().ContainEquivalentOf("d=\"M 60.8333 320 L 245.8333 320 L 430.8333 320 L 615.8333 320\""); } [Test] diff --git a/src/MudBlazor.UnitTests/Components/DataGridTests.cs b/src/MudBlazor.UnitTests/Components/DataGridTests.cs index 408a51609d29..3e07b0a7750d 100644 --- a/src/MudBlazor.UnitTests/Components/DataGridTests.cs +++ b/src/MudBlazor.UnitTests/Components/DataGridTests.cs @@ -5103,5 +5103,78 @@ public async Task DataGrid_TwoWayBind_SelectedItem_SelectedItems() selectedItems.Count().Should().Be(2); selectedItem.Should().Be(4); } + + [Test] + public async Task DataGridSelectedItemEventsTest() + { + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + + // Test single selection mode + dataGrid.SetParam(x => x.MultiSelection, false); + comp.Render(); + + // Select an item + var firstItem = dataGrid.Instance.Items.First(); + await comp.InvokeAsync(async () => await dataGrid.Instance.SetSelectedItemAsync(true, firstItem)); + + // Verify events + comp.Instance.SelectedItemChanged.Should().BeTrue(); + comp.Instance.SelectedItemsChanged.Should().BeTrue(); + + // Reset event flags + comp.Instance.SelectedItemChanged = false; + comp.Instance.SelectedItemsChanged = false; + + // Deselect the item + await comp.InvokeAsync(async () => await dataGrid.Instance.SetSelectedItemAsync(false, firstItem)); + + // Verify events for deselection + comp.Instance.SelectedItemChanged.Should().BeTrue(); + comp.Instance.SelectedItemsChanged.Should().BeTrue(); + + // Reset event flags + comp.Instance.SelectedItemChanged = false; + comp.Instance.SelectedItemsChanged = false; + + // Test multi-selection mode + dataGrid.SetParam(x => x.MultiSelection, true); + comp.Render(); + + // Select all items + await comp.InvokeAsync(async () => await dataGrid.Instance.SetSelectAllAsync(true)); + + // Verify events + comp.Instance.SelectedItemChanged.Should().BeFalse(); + comp.Instance.SelectedItemsChanged.Should().BeTrue(); + + // Reset event flags + comp.Instance.SelectedItemChanged = false; + comp.Instance.SelectedItemsChanged = false; + + // Deselect all items + await comp.InvokeAsync(async () => await dataGrid.Instance.SetSelectAllAsync(false)); + + // Verify events for deselection + comp.Instance.SelectedItemChanged.Should().BeFalse(); + comp.Instance.SelectedItemsChanged.Should().BeTrue(); + + // Reset event flags + comp.Instance.SelectedItemChanged = false; + comp.Instance.SelectedItemsChanged = false; + + // Test row click select + // find first mud-table-row and second mud-table-cell + var firstRow = dataGrid.FindAll(".mud-table-row")[1]; + firstRow.Click(); + + // Verify events for row click + comp.WaitForAssertion(() => comp.Instance.SelectedItemChanged.Should().BeTrue()); + comp.Instance.SelectedItemsChanged.Should().BeTrue(); + + // Reset event flags + comp.Instance.SelectedItemChanged = false; + comp.Instance.SelectedItemsChanged = false; + } } } diff --git a/src/MudBlazor.UnitTests/Components/DatePickerTests.cs b/src/MudBlazor.UnitTests/Components/DatePickerTests.cs index 983ee7282f52..105a83b123dc 100644 --- a/src/MudBlazor.UnitTests/Components/DatePickerTests.cs +++ b/src/MudBlazor.UnitTests/Components/DatePickerTests.cs @@ -284,6 +284,20 @@ public void Check_Initial_Date_Format() picker.Text.Should().Be("13/01/2021"); } + [Test] + public void Check_DateTime_MaxValue() + { + DateTime? date = DateTime.MaxValue; + + var comp = OpenPicker(Parameter("Date", date)); + + comp.Instance.Date.Should().Be(DateTime.MaxValue); + + comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("31")).ToMarkup().Should().Contain("mud-selected"); + comp.Find("button.mud-button-date .mud-button-label").InnerHtml.Should().Be("Fri, 31 Dec"); + comp.Find("button.mud-button-year .mud-button-label").InnerHtml.Should().Be("9999"); + } + private IRenderedComponent OpenPicker(ComponentParameter parameter) { return OpenPicker(new[] { parameter }); diff --git a/src/MudBlazor.UnitTests/Components/DateRangePickerTests.cs b/src/MudBlazor.UnitTests/Components/DateRangePickerTests.cs index b7e249cb2c94..1baeadf8075f 100644 --- a/src/MudBlazor.UnitTests/Components/DateRangePickerTests.cs +++ b/src/MudBlazor.UnitTests/Components/DateRangePickerTests.cs @@ -1183,7 +1183,25 @@ public async Task DateRangePicker_MinMaxDays() await comp.FindAll("button.mud-picker-calendar-day").Where(x => x.TrimmedText().Equals("24")).First().ClickAsync(new MouseEventArgs()); comp.Instance.DateRange.Should().Be(new DateRange(new DateTime(2025, 1, 16).Date, new DateTime(2025, 1, 24).Date)); //max valid range 9 days + } + + [Test] + public async Task DateRangePicker_MaxSelectableDateTest() + { + var comp = Context.RenderComponent(); + comp.SetParametersAndRender(parameters => parameters.Add(picker => picker.MaxDays, 30) + .Add(picker => picker.PickerVariant, PickerVariant.Static) + .Add(picker => picker.IsDateDisabledFunc, x => x.Date > DateTime.Today)); + + var today = DateTime.Today; + + await comp.FindAll("button.mud-picker-calendar-day") + .Where(x => x.TrimmedText().Equals(today.Day.ToString())).First().ClickAsync(new MouseEventArgs()); + await comp.FindAll("button.mud-picker-calendar-day") + .Where(x => x.TrimmedText().Equals(today.Day.ToString())).First().ClickAsync(new MouseEventArgs()); + + comp.Instance.DateRange.Start.Should().Be(comp.Instance.DateRange.End); } } } diff --git a/src/MudBlazor.UnitTests/Components/DialogTests.cs b/src/MudBlazor.UnitTests/Components/DialogTests.cs index 6a855894d84a..d76adfcd0a9b 100644 --- a/src/MudBlazor.UnitTests/Components/DialogTests.cs +++ b/src/MudBlazor.UnitTests/Components/DialogTests.cs @@ -1325,6 +1325,22 @@ public async Task DialogWithNestedDialogOptionShouldNotReset() // Close button should still be visible closeButton().Should().NotBeNull(); } + + /// + /// Ensures dialog will only be shown once + /// + [Test] + public async Task DuplicateDialogTest() + { + var comp = Context.RenderComponent(); + var service = Context.Services.GetRequiredService(); + service.Should().NotBe(null); + var comp1 = Context.RenderComponent(); + // open the dialog + comp1.Find("button").Click(); + await Task.Delay(1000); + comp.FindComponents().Count.Should().Be(1); + } } internal class CustomDialogService : DialogService diff --git a/src/MudBlazor.UnitTests/Components/MenuTests.cs b/src/MudBlazor.UnitTests/Components/MenuTests.cs index 53c807cdef7e..9528e2321221 100644 --- a/src/MudBlazor.UnitTests/Components/MenuTests.cs +++ b/src/MudBlazor.UnitTests/Components/MenuTests.cs @@ -583,5 +583,75 @@ public void ClickingMenuItem_ClosesNestedMenu() // Ensure all popovers are closed. comp.FindAll("div.mud-popover-open").Count.Should().Be(0); } + + [Test] + public async Task IActivatable_Activate_Should_ToggleMenu_Except_For_InputAdornmentIconButton() + { + // Arrange + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.ActivatorContent, builder => + { + builder.OpenComponent(0); + builder.AddAttribute(1, "Class", "mud-no-activator"); + builder.CloseComponent(); + })); + var menu = comp.Instance; + var activatable = (Interfaces.IActivatable)menu; + + // Normal case + activatable.Activate(new object(), new MouseEventArgs()); + comp.WaitForAssertion(() => menu.GetState(x => x.Open).Should().BeTrue("Menu should open when Activate is called with a regular object")); + + // Close Menu + activatable.Activate(new object(), new MouseEventArgs()); + comp.WaitForAssertion(() => menu.GetState(x => x.Open).Should().BeFalse("Menu should be closed after calling Activate again")); + + // Special case with icon button + var classButton = comp.FindComponent().Instance; + activatable.Activate(classButton, new MouseEventArgs()); + comp.WaitForAssertion(() => menu.GetState(x => x.Open).Should().BeFalse("Menu should not open when Activate is called with an mud-no-activator selector")); + + // Special case with regular button + var compButton = Context.RenderComponent(p => p.Add(p => p.Class, "mud-no-activator")).Instance; + activatable.Activate(compButton, new MouseEventArgs()); + comp.WaitForAssertion(() => menu.GetState(x => x.Open).Should().BeFalse("Menu should not open when Activate is called with an mud-no-activator selector")); + + // Clean up + await menu.CloseMenuAsync(); + } + + [Test] + public void Menu_ButtonActivator() + { + var provider = Context.RenderComponent(); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.ActivatorContent, builder => + { + builder.OpenComponent(0); + builder.CloseComponent(); + })); + + provider.FindAll("div.mud-popover-open").Count.Should().Be(0); + + // Click the MudButton inside the ActivatorContent + var button = comp.Find("button.mud-button-root"); + button.Click(); + provider.FindAll("div.mud-popover-open").Count.Should().Be(1); + button.Click(); + // Render component with MudIconButton inside ActivatorContent + comp = Context.RenderComponent(parameters => parameters + .Add(p => p.ActivatorContent, builder => + { + builder.OpenComponent(0); + builder.AddAttribute(1, "Class", "mud-icon-button-activator"); + builder.CloseComponent(); + })); + + provider.FindAll("div.mud-popover-open").Count.Should().Be(0); + + // Click the MudIconButton inside the ActivatorContent + comp.Find("button.mud-icon-button-activator").Click(); + provider.FindAll("div.mud-popover-open").Count.Should().Be(1); + } } } diff --git a/src/MudBlazor.UnitTests/Components/SelectTests.cs b/src/MudBlazor.UnitTests/Components/SelectTests.cs index a0a16fd4038d..a3f99c3c9d05 100644 --- a/src/MudBlazor.UnitTests/Components/SelectTests.cs +++ b/src/MudBlazor.UnitTests/Components/SelectTests.cs @@ -674,7 +674,7 @@ public void SingleSelect_Should_CallValidationFunc() [Test] public void MultiSelect_Initial_Values() { - var comp = Context.RenderComponent(); + var comp = Context.RenderComponent(); // print the generated html // select the input of the select @@ -1524,7 +1524,7 @@ public void ReadOnlyShouldNotHaveClearButton() } [Test] - public async Task SelectFullWidthTest() + public async Task SelectPopoverFullWidthTest() { var comp = Context.RenderComponent(); @@ -1548,6 +1548,41 @@ public async Task SelectFullWidthTest() comp.Find(".expanded").ClassList.Should().Contain("mud-popover-open").And.NotContain("mud-popover-relative-width"); } + [Test] + public void SelectFitContentTest() + { + var comp = Context.RenderComponent(); + + //default values + comp.Instance.FullWidth.Should().BeFalse(); + comp.Instance.FitContent.Should().BeFalse(); + + var select = comp.Find(".mud-select"); + + select.ClassList.Should().NotContain("mud-width-content"); + + //set fit content + comp.SetParametersAndRender(parameters => parameters.Add(c => c.FitContent, true)); + + comp.Instance.FullWidth.Should().BeFalse(); + comp.Instance.FitContent.Should().BeTrue(); + + select.ClassList.Should().Contain("mud-width-content"); + + var filler = comp.Find(".mud-select-filler"); + + filler.ClassList.Should().Contain("d-inline-block").And.Contain("mx-4"); + filler.TextContent.Trim().Should().Be("Federated States of Micronesia"); + + //set full width + comp.SetParametersAndRender(parameters => parameters.Add(c => c.FullWidth, true)); + + comp.Instance.FullWidth.Should().BeTrue(); + comp.Instance.FitContent.Should().BeTrue(); + + select.ClassList.Should().NotContain("mud-width-content"); + } + [TestCaseSource(typeof(MouseEventArgsTestCase), nameof(MouseEventArgsTestCase.AllCombinations))] [Test] public async Task Select_HandleMouseDown(MouseEventArgs args) diff --git a/src/MudBlazor.UnitTests/Components/TableTests.cs b/src/MudBlazor.UnitTests/Components/TableTests.cs index de3b20cb4e09..2b0cbcc8bd28 100644 --- a/src/MudBlazor.UnitTests/Components/TableTests.cs +++ b/src/MudBlazor.UnitTests/Components/TableTests.cs @@ -115,24 +115,6 @@ public async Task TableDisabledSort() comp.FindAll("td")[2].TextContent.Trim().Should().Be("A"); } - [Theory] - [TestCase(true)] - [TestCase(false)] - public void TableSortLabel(bool sortEnabled) - { - // Arrange - var comp = Context.RenderComponent( - parameters => parameters.Add(x => x.SortEnabled, sortEnabled)); - var tableSortLabel = comp.FindComponent>(); - - // Assert - tableSortLabel - .Find("span") - .GetAttribute("class") - .Contains("mud-button-root") - .Should().Be(sortEnabled); - } - /// /// Check if the loading parameter is adding a supplementary row. /// @@ -1315,25 +1297,25 @@ public void TableServerSideDataTest6() var comp = Context.RenderComponent(); comp.Find("#counter").TextContent.Should().Be("1"); //initial counter - comp.Find("span.mud-button-root.mud-table-sort-label").Click(); // sort + comp.Find("span.mud-clickable.mud-table-sort-label").Click(); // sort comp.Find("#counter").TextContent.Should().Be("2"); - comp.Find("span.mud-button-root.mud-table-sort-label").Click(); // sort + comp.Find("span.mud-clickable.mud-table-sort-label").Click(); // sort comp.Find("#counter").TextContent.Should().Be("3"); - comp.Find("span.mud-button-root.mud-table-sort-label").Click(); // sort + comp.Find("span.mud-clickable.mud-table-sort-label").Click(); // sort comp.Find("#counter").TextContent.Should().Be("4"); comp.Find("#reseter").Click(); //reset counter and test again comp.Find("#counter").TextContent.Should().Be("0"); - comp.Find("span.mud-button-root.mud-table-sort-label").Click(); // sort + comp.Find("span.mud-clickable.mud-table-sort-label").Click(); // sort comp.Find("#counter").TextContent.Should().Be("1"); - comp.Find("span.mud-button-root.mud-table-sort-label").Click(); // sort + comp.Find("span.mud-clickable.mud-table-sort-label").Click(); // sort comp.Find("#counter").TextContent.Should().Be("2"); - comp.Find("span.mud-button-root.mud-table-sort-label").Click(); // sort + comp.Find("span.mud-clickable.mud-table-sort-label").Click(); // sort comp.Find("#counter").TextContent.Should().Be("3"); } @@ -2537,5 +2519,22 @@ public async Task TestCurrentPageParameterTwoWayBinding() comp.WaitForAssertion(() => table.CurrentPage.Should().Be(2)); comp.WaitForAssertion(() => comp.Find(".mud-table-body .mud-table-row .mud-table-cell").TextContent.Should().Be("3")); } + + [Test] + [TestCase(SortDirection.None)] + [TestCase(SortDirection.Ascending)] + [TestCase(SortDirection.Descending)] + public void TableSortLabelDirectionClasses(SortDirection direction) + { + var comp = Context.RenderComponent>(parameters => parameters + .Add(p => p.SortDirection, direction) + ); + + var icon = comp.Find(".mud-table-sort-label-icon"); + + icon.ClassList.Should().Contain("mud-table-sort-label-icon"); + icon.ClassList.Contains("mud-direction-asc").Should().Be(direction == SortDirection.Ascending); + icon.ClassList.Contains("mud-direction-desc").Should().Be(direction == SortDirection.Descending); + } } } diff --git a/src/MudBlazor.UnitTests/Components/TextFieldTests.cs b/src/MudBlazor.UnitTests/Components/TextFieldTests.cs index c8cf92ada31b..e3100de88b80 100644 --- a/src/MudBlazor.UnitTests/Components/TextFieldTests.cs +++ b/src/MudBlazor.UnitTests/Components/TextFieldTests.cs @@ -1042,7 +1042,7 @@ public async Task DebouncedTextFieldRerenderTest() // trigger first value change await Task.Delay(comp.Instance.DebounceInterval); // trigger delayed re-render - comp.Find("#re-render-button").Click(); + await comp.InvokeAsync(() => comp.Find("#re-render-button").Click()); // imitate "typing in progress" by extending the debounce interval until component re-renders var elapsedTime = 0; var currentText = "test"; diff --git a/src/MudBlazor.UnitTests/Components/ToolTipTests.cs b/src/MudBlazor.UnitTests/Components/ToolTipTests.cs index 86c49681a601..fb112497bba3 100644 --- a/src/MudBlazor.UnitTests/Components/ToolTipTests.cs +++ b/src/MudBlazor.UnitTests/Components/ToolTipTests.cs @@ -356,5 +356,83 @@ public async Task Tooltip_Disabled_Button_OnPointerEnter_NoPopover() await button.ParentElement.TriggerEventAsync("onpointerenter", new PointerEventArgs()); comp.FindAll("div.mud-popover-open").Count.Should().Be(0); } + + [TestCase(0, 0)] + [TestCase(500, 500)] + [Test] + public void Tooltip_Debouncer_Initials(double duration, double delay) + { + var comp = Context.RenderComponent(p => + { + p.Add(x => x.Delay, delay); + p.Add(x => x.Duration, duration); + }); + + var tooltipComp = comp.FindComponent().Instance; + tooltipComp.Delay.Should().Be(delay); + tooltipComp.Duration.Should().Be(duration); + tooltipComp._previousDelay.Should().Be(delay); + tooltipComp._previousDuration.Should().Be(duration); + var button = comp.Find("button"); + button.Should().NotBeNull(); + } + + [TestCase(0, 0)] + [TestCase(500, 500)] + [Test] + public async Task Tooltip_Debouncer_Duration_and_Delay(double duration, double delay) + { + var comp = Context.RenderComponent(p => + { + p.Add(x => x.Delay, delay); + p.Add(x => x.Duration, duration); + }); + + var tooltipComp = comp.FindComponent().Instance; + + // cannot await or it waits until the debounce happens + var eventTask = tooltipComp.HandlePointerEnterAsync(); + if (delay > 0) + tooltipComp.ShowToolTip().Should().BeFalse(); + + await Task.Delay((int)delay + 50); + tooltipComp.ShowToolTip().Should().BeTrue(); + + await eventTask; // ensure all completed + + // cannot await or it waits until the debounce happens + eventTask = tooltipComp.HandlePointerLeaveAsync(); + if (duration > 0) + tooltipComp.ShowToolTip().Should().BeTrue(); + + await Task.Delay((int)(duration + delay) + 50); + tooltipComp.ShowToolTip().Should().BeFalse(); + + await eventTask; + } + + [TestCase(true)] + [TestCase(false)] + [Test] + public async Task Tooltip_ShowOnHover(bool showOnHover) + { + var comp = Context.RenderComponent(p => + { + p.Add(x => x.ShowOnHover, showOnHover); + }); + // we don't need to await Task.Delay to account for Delay/Duration since the await Handle takes care of it. + var tooltipComp = comp.FindComponent().Instance; + tooltipComp.ShowOnHover.Should().Be(showOnHover); + if (!showOnHover) + { + await tooltipComp.HandlePointerEnterAsync(); + tooltipComp.ShowToolTip().Should().BeFalse(); + } + else + { + await tooltipComp.HandlePointerEnterAsync(); + tooltipComp.ShowToolTip().Should().BeTrue(); + } + } } } diff --git a/src/MudBlazor.UnitTests/Components/TreeViewTests.cs b/src/MudBlazor.UnitTests/Components/TreeViewTests.cs index b884ccc078b5..2aafad19930b 100644 --- a/src/MudBlazor.UnitTests/Components/TreeViewTests.cs +++ b/src/MudBlazor.UnitTests/Components/TreeViewTests.cs @@ -1286,5 +1286,14 @@ public void TreeView_ClickItemWhileDisabled_DoesNotChangeSelectionAndExpanded() parentItemButton.Click(); GetItemExpandedValue().Should().Be(false); } + + [Test] + public void TreeView_ClickHeterogeneousTreeElement_ShouldNotThrow() + { + var comp = Context.RenderComponent(); + var l2 = comp.Find(".L2 > div.mud-treeview-item-content"); + var act = () => l2.Click(); + act.Should().NotThrow(); + } } } diff --git a/src/MudBlazor.UnitTests/MudBlazor.UnitTests.csproj b/src/MudBlazor.UnitTests/MudBlazor.UnitTests.csproj index 310e20ceeb6c..6eee73c9de8f 100644 --- a/src/MudBlazor.UnitTests/MudBlazor.UnitTests.csproj +++ b/src/MudBlazor.UnitTests/MudBlazor.UnitTests.csproj @@ -28,7 +28,7 @@ - +
diff --git a/src/MudBlazor.UnitTests/TestData/MouseEventArgsTestCase.cs b/src/MudBlazor.UnitTests/TestData/MouseEventArgsTestCase.cs index 9a9395878570..a8cb6c97d5bf 100644 --- a/src/MudBlazor.UnitTests/TestData/MouseEventArgsTestCase.cs +++ b/src/MudBlazor.UnitTests/TestData/MouseEventArgsTestCase.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.Web; using NUnit.Framework; namespace MudBlazor.UnitTests.TestData @@ -13,11 +8,11 @@ public static class MouseEventArgsTestCase public static TestCaseData[] AllCombinations() { return - [ - new TestCaseData(new MouseEventArgs { Button = 0}), - new TestCaseData (new MouseEventArgs { Button = 1 }), - new TestCaseData (new MouseEventArgs { Button = 2 }), - ]; + [ + new TestCaseData(new MouseEventArgs { Button = 0 }), + new TestCaseData(new MouseEventArgs { Button = 1 }), + new TestCaseData(new MouseEventArgs { Button = 2 }), + ]; } } } diff --git a/src/MudBlazor.UnitTests/Utilities/ObserverManager/ObserverManagerTests.cs b/src/MudBlazor.UnitTests/Utilities/ObserverManager/ObserverManagerTests.cs index 496dd93901cd..a0a7beff5b34 100644 --- a/src/MudBlazor.UnitTests/Utilities/ObserverManager/ObserverManagerTests.cs +++ b/src/MudBlazor.UnitTests/Utilities/ObserverManager/ObserverManagerTests.cs @@ -65,6 +65,42 @@ public void TryGetSubscription_ReturnsFalseAndDefault_WhenObserverDoesNotExist() retrievedObserver.Should().BeNull(); } + [Test] + public void TryGetOrAddSubscription_ReturnsTrueAndUpdatesObserver_WhenObserverExists() + { + // Arrange + var id = 1; + var observer1 = "Observer1"; + var observer2 = "Observer2"; + _observerManager.Subscribe(id, observer1); + + // Act + var result = _observerManager.TryGetOrAddSubscription(id, observer2, out var newObserver); + + // Assert + result.Should().BeTrue(); + newObserver.Should().Be(observer2); + _observerManager.Count.Should().Be(1); + _observerManager.Observers[id].Should().Be(observer2); + } + + [Test] + public void TryGetOrAddSubscription_ReturnsFalseAndAddsObserver_WhenObserverDoesNotExist() + { + // Arrange + var id = 1; + var observer = "Observer1"; + + // Act + var result = _observerManager.TryGetOrAddSubscription(id, observer, out var newObserver); + + // Assert + result.Should().BeFalse(); + newObserver.Should().Be(observer); + _observerManager.Count.Should().Be(1); + _observerManager.Observers[id].Should().Be(observer); + } + [Test] public void FindObserverIdentities_ReturnsMatchingIdentities() { diff --git a/src/MudBlazor/Base/MudBaseButton.cs b/src/MudBlazor/Base/MudBaseButton.cs index 2b59d943c89c..e4a850eb3897 100644 --- a/src/MudBlazor/Base/MudBaseButton.cs +++ b/src/MudBlazor/Base/MudBaseButton.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using MudBlazor.Interfaces; using static System.String; diff --git a/src/MudBlazor/Components/Autocomplete/MudAutocomplete.razor b/src/MudBlazor/Components/Autocomplete/MudAutocomplete.razor index 02f53e307b9b..6c0db0442c1c 100644 --- a/src/MudBlazor/Components/Autocomplete/MudAutocomplete.razor +++ b/src/MudBlazor/Components/Autocomplete/MudAutocomplete.razor @@ -3,7 +3,7 @@ @typeparam T -
+
: MudBaseInput private int _elementKey = 0; private int _returnedItemsCount; private bool _open; + private bool _opening; private MudInput _elementReference = null!; private CancellationTokenSource? _cancellationTokenSrc; private Task? _currentSearchTask; @@ -665,11 +666,8 @@ public async Task CloseMenuAsync() } /// - /// Opens the drop-down of items. + /// Opens the drop-down of items, or refreshes the list if it is already open. /// - /// - /// Will have no effect if the autocomplete is disabled or read-only. - /// public async Task OpenMenuAsync() { if (MinCharacters > 0 && (string.IsNullOrWhiteSpace(Text) || Text.Length < MinCharacters)) @@ -679,6 +677,8 @@ public async Task OpenMenuAsync() return; } + _opening = true; + var searchedItems = Array.Empty(); CancelToken(); @@ -744,6 +744,7 @@ public async Task OpenMenuAsync() Open = true; } + _opening = false; StateHasChanged(); } @@ -964,17 +965,12 @@ private async Task OnInputActivationAsync(bool openMenu) { _isFocused = true; - if (Open || GetDisabledState() || GetReadOnlyState()) - { - return; - } - - if (SelectOnActivation) + if (SelectOnActivation && !GetDisabledState() && !GetReadOnlyState()) { await SelectAsync(); } - if (openMenu) + if (openMenu && !Open && !_opening) { await OpenMenuAsync(); } diff --git a/src/MudBlazor/Components/Chart/Charts/Bar.razor b/src/MudBlazor/Components/Chart/Charts/Bar.razor index c2f9536e9b41..3d784760ca35 100644 --- a/src/MudBlazor/Components/Chart/Charts/Bar.razor +++ b/src/MudBlazor/Components/Chart/Charts/Bar.razor @@ -15,7 +15,7 @@ } - @if (MudChartParent?.ChartOptions.XAxisLines==true) + @if (MudChartParent?.ChartOptions.XAxisLines == true) { @foreach (var verticalLine in _verticalLines) @@ -25,19 +25,19 @@ } - + @foreach (var horizontalLineValue in _horizontalValues) { @((MarkupString)$"{horizontalLineValue.Value?.ToString(CultureInfo.InvariantCulture)}") } - + @for (var i = 0; i < _verticalValues.Count; i++) { var verticalLineValue = _verticalValues[i]; var x = verticalLineValue.X.ToString(CultureInfo.InvariantCulture); var y = verticalLineValue.Y.ToString(CultureInfo.InvariantCulture); - var rotation = (-AxisChartOptions.LabelRotation).ToString(CultureInfo.InvariantCulture); + var rotation = (-AxisChartOptions.XAxisLabelRotation).ToString(CultureInfo.InvariantCulture); @((MarkupString)$"{verticalLineValue.Value?.ToString(CultureInfo.InvariantCulture)}") } @@ -50,7 +50,7 @@ @onclick="() => SelectedIndex = bar.Index" fill="@color" stroke="@color" - stroke-width="8" + stroke-width="@BarStroke" d="@bar.Data" @onmouseover="(e) => OnBarMouseOver(e, bar)" @onmouseout="OnBarMouseOut"> @@ -58,12 +58,12 @@ } - @MudChartParent?.CustomGraphics + @MudChartParent?.CustomGraphics @* Render the tooltip as an SVG group when a bar is hovered *@ - @if (_hoveredBar is not null) + @if (_hoveredBar is not null && MudChartParent?.ChartOptions.ShowToolTips == true) { - var color = MudChartParent?.ChartOptions.ChartPalette.GetValue(_hoveredBar.Index % MudChartParent.ChartOptions.ChartPalette.Length)?.ToString() ?? string.Empty; + var color = MudChartParent.ChartOptions.ChartPalette.GetValue(_hoveredBar.Index % MudChartParent.ChartOptions.ChartPalette.Length)?.ToString() ?? string.Empty; var series = _series[_hoveredBar.Index]; if (!string.IsNullOrWhiteSpace(series.DataMarkerTooltipTitleFormat)) diff --git a/src/MudBlazor/Components/Chart/Charts/Bar.razor.cs b/src/MudBlazor/Components/Chart/Charts/Bar.razor.cs index d90055770d86..08969cb6ac03 100644 --- a/src/MudBlazor/Components/Chart/Charts/Bar.razor.cs +++ b/src/MudBlazor/Components/Chart/Charts/Bar.razor.cs @@ -13,18 +13,22 @@ namespace MudBlazor.Charts /// partial class Bar : MudCategoryAxisChartBase { - private List _horizontalLines = []; - private List _horizontalValues = []; + private readonly List _horizontalLines = []; + private readonly List _horizontalValues = []; - private List _verticalLines = []; - private List _verticalValues = []; + private readonly List _verticalLines = []; + private readonly List _verticalValues = []; - private List _legends = []; + private readonly List _legends = []; private List _series = []; - private List _bars = []; + private readonly List _bars = []; private SvgPath? _hoveredBar; + private const double BarStroke = 8; + private const double BarGap = 10; + private double BarGroupWidth => (_series.Count - 1) * BarGap + BarStroke; // number of gaps of 10 + the stroke width + /// protected override void OnParametersSet() { @@ -41,8 +45,8 @@ protected override void RebuildChart() SetBounds(); ComputeUnitsAndNumberOfLines(out var gridXUnits, out var gridYUnits, out var numHorizontalLines, out var lowestHorizontalLine, out var numVerticalLines); - var horizontalSpace = (_boundWidth - HorizontalStartSpace - HorizontalEndSpace) / Math.Max(1, numVerticalLines - 1); - var verticalSpace = (_boundHeight - VerticalStartSpace - VerticalEndSpace - AxisChartOptions.LabelExtraHeight) / Math.Max(1, numHorizontalLines - 1); + var horizontalSpace = (_boundWidth - HorizontalStartSpace - HorizontalEndSpace - BarGroupWidth) / Math.Max(1, numVerticalLines - 1); + var verticalSpace = (_boundHeight - VerticalStartSpace - VerticalEndSpace) / Math.Max(1, numHorizontalLines - 1); GenerateHorizontalGridLines(numHorizontalLines, lowestHorizontalLine, gridYUnits, verticalSpace); GenerateVerticalGridLines(numVerticalLines, gridXUnits, horizontalSpace); @@ -66,7 +70,7 @@ private void ComputeUnitsAndNumberOfLines(out double gridXUnits, out double grid numHorizontalLines = highestHorizontalLine - lowestHorizontalLine + 1; // this is a safeguard against millions of gridlines which might arise with very high values - var maxYTicks = MudChartParent?.ChartOptions.MaxNumYAxisTicks ?? 100; + var maxYTicks = MudChartParent?.ChartOptions.MaxNumYAxisTicks ?? 20; while (numHorizontalLines > maxYTicks) { gridYUnits *= 2; @@ -96,7 +100,7 @@ private void GenerateHorizontalGridLines(int numHorizontalLines, int lowestHoriz var line = new SvgPath() { Index = i, - Data = $"M {ToS(HorizontalStartSpace)} {ToS(_boundHeight - AxisChartOptions.LabelExtraHeight - y)} L {ToS(_boundWidth - HorizontalEndSpace)} {ToS(_boundHeight - AxisChartOptions.LabelExtraHeight - y)}" + Data = $"M {ToS(HorizontalStartSpace)} {ToS(_boundHeight - y)} L {ToS(_boundWidth - HorizontalEndSpace)} {ToS(_boundHeight - y)}" }; _horizontalLines.Add(line); @@ -104,7 +108,7 @@ private void GenerateHorizontalGridLines(int numHorizontalLines, int lowestHoriz var lineValue = new SvgText() { X = HorizontalStartSpace - 10, - Y = _boundHeight - AxisChartOptions.LabelExtraHeight - y + 5, + Y = _boundHeight - y + 5, Value = ToS(startGridY, MudChartParent?.ChartOptions.YAxisFormat) }; _horizontalValues.Add(lineValue); @@ -122,15 +126,15 @@ private void GenerateVerticalGridLines(int numVerticalLines, double gridXUnits, var line = new SvgPath() { Index = i, - Data = $"M {ToS(x)} {ToS(_boundHeight - VerticalStartSpace - AxisChartOptions.LabelExtraHeight)} L {ToS(x)} {ToS(VerticalEndSpace)}" + Data = $"M {ToS(x)} {ToS(_boundHeight - VerticalStartSpace)} L {ToS(x)} {ToS(VerticalEndSpace)}" }; _verticalLines.Add(line); var xLabels = i < XAxisLabels.Length ? XAxisLabels[i] : ""; var lineValue = new SvgText() { - X = x, - Y = _boundHeight - (AxisChartOptions.LabelExtraHeight / 2) - 10, + X = x + (BarGroupWidth / 2), + Y = _boundHeight - 10, Value = xLabels }; _verticalValues.Add(lineValue); @@ -144,21 +148,22 @@ private void GenerateBars(int lowestHorizontalLine, double gridYUnits, double ho for (var i = 0; i < _series.Count; i++) { - var data = _series[i].Data; + var series = _series[i]; + var data = series.Data; for (var j = 0; j < data.Length; j++) { - var gridValueX = HorizontalStartSpace + (i * 10) + (j * horizontalSpace); - var gridValueY = _boundHeight - VerticalStartSpace - AxisChartOptions.LabelExtraHeight + (lowestHorizontalLine * verticalSpace); + var gridValueX = HorizontalStartSpace + (BarStroke / 2) + (i * BarGap) + (j * horizontalSpace); + var gridValueY = _boundHeight - VerticalStartSpace + (lowestHorizontalLine * verticalSpace); var dataValue = ((data[j] / gridYUnits) - lowestHorizontalLine) * verticalSpace; - var gridValue = _boundHeight - VerticalStartSpace - AxisChartOptions.LabelExtraHeight - dataValue; + var gridValue = _boundHeight - VerticalStartSpace - dataValue; var bar = new SvgPath() { Index = i, Data = $"M {ToS(gridValueX)} {ToS(gridValueY)} L {ToS(gridValueX)} {ToS(gridValue)}", LabelXValue = XAxisLabels.Length > j ? XAxisLabels[j] : string.Empty, - LabelYValue = dataValue.ToString(), + LabelYValue = dataValue.ToString(series.DataMarkerTooltipYValueFormat), LabelX = gridValueX, LabelY = gridValue }; @@ -168,7 +173,7 @@ private void GenerateBars(int lowestHorizontalLine, double gridYUnits, double ho var legend = new SvgLegend() { Index = i, - Labels = _series[i].Name + Labels = series.Name }; _legends.Add(legend); } diff --git a/src/MudBlazor/Components/Chart/Charts/Donut.razor b/src/MudBlazor/Components/Chart/Charts/Donut.razor index 0bf8a059f55c..f5ce3b234a7f 100644 --- a/src/MudBlazor/Components/Chart/Charts/Donut.razor +++ b/src/MudBlazor/Components/Chart/Charts/Donut.razor @@ -1,5 +1,4 @@ @namespace MudBlazor.Charts -@using System.Globalization @inherits MudCategoryChartBase diff --git a/src/MudBlazor/Components/Chart/Charts/Line.razor b/src/MudBlazor/Components/Chart/Charts/Line.razor index c543f9cae536..cd07d877e190 100644 --- a/src/MudBlazor/Components/Chart/Charts/Line.razor +++ b/src/MudBlazor/Components/Chart/Charts/Line.razor @@ -31,19 +31,19 @@ } - + @foreach (var horizontalLineValue in _horizontalValues) { @((MarkupString)$"{horizontalLineValue.Value?.ToString(CultureInfo.InvariantCulture)}") } - + @for (var i = 0; i < _verticalValues.Count; i++) { var verticalLineValue = _verticalValues[i]; var x = verticalLineValue.X.ToString(CultureInfo.InvariantCulture); var y = verticalLineValue.Y.ToString(CultureInfo.InvariantCulture); - var rotation = (-AxisChartOptions.LabelRotation).ToString(CultureInfo.InvariantCulture); + var rotation = (-AxisChartOptions.XAxisLabelRotation).ToString(CultureInfo.InvariantCulture); @((MarkupString)$"{verticalLineValue.Value?.ToString(CultureInfo.InvariantCulture)}") } @@ -82,7 +82,7 @@ cx="@ToS(item.CX)" cy="@ToS(item.CY)" r="@ToS(dataPointHoverRadius)" - @onmouseover="(e) => OnDataPointMouseOver(e, item)" + @onmouseover="(e) => OnDataPointMouseOver(e, item, chartLine)" @onmouseout="OnDataPointMouseOut"> } @@ -92,11 +92,11 @@ @MudChartParent?.CustomGraphics @* Render the tooltip as an SVG group when a bar is hovered *@ - @if (_hoveredDataPoint is not null && _hoverDataPointChartLine is not null) + @if (_hoveredDataPoint is not null && _hoverDataPointChartLine is not null && MudChartParent?.ChartOptions.ShowToolTips == true) { var seriesIndex = _hoverDataPointChartLine.Index; - var color = MudChartParent?.ChartOptions.ChartPalette.GetValue(seriesIndex % MudChartParent.ChartOptions.ChartPalette.Length)?.ToString() ?? string.Empty; + var color = MudChartParent.ChartOptions.ChartPalette.GetValue(seriesIndex % MudChartParent.ChartOptions.ChartPalette.Length)?.ToString() ?? string.Empty; var series = _series[seriesIndex]; if (!string.IsNullOrWhiteSpace(series.DataMarkerTooltipTitleFormat)) diff --git a/src/MudBlazor/Components/Chart/Charts/Line.razor.cs b/src/MudBlazor/Components/Chart/Charts/Line.razor.cs index 3ddabf2caf41..8e9a1efef6b4 100644 --- a/src/MudBlazor/Components/Chart/Charts/Line.razor.cs +++ b/src/MudBlazor/Components/Chart/Charts/Line.razor.cs @@ -16,18 +16,18 @@ namespace MudBlazor.Charts /// partial class Line : MudCategoryAxisChartBase { - private List _horizontalLines = []; - private List _horizontalValues = []; + private readonly List _horizontalLines = []; + private readonly List _horizontalValues = []; - private List _verticalLines = []; - private List _verticalValues = []; + private readonly List _verticalLines = []; + private readonly List _verticalValues = []; - private List _legends = []; + private readonly List _legends = []; private List _series = []; - private List _chartLines = []; - private Dictionary _chartAreas = []; - private Dictionary> _chartDataPoints = []; + private readonly List _chartLines = []; + private readonly Dictionary _chartAreas = []; + private readonly Dictionary> _chartDataPoints = []; private SvgCircle? _hoveredDataPoint; private SvgPath? _hoverDataPointChartLine; @@ -47,7 +47,7 @@ protected override void RebuildChart() ComputeUnitsAndNumberOfLines(out var gridXUnits, out var gridYUnits, out var numHorizontalLines, out var lowestHorizontalLine, out var numVerticalLines); var horizontalSpace = (_boundWidth - HorizontalStartSpace - HorizontalEndSpace) / Math.Max(1, numVerticalLines - 1); - var verticalSpace = (_boundHeight - VerticalStartSpace - VerticalEndSpace - AxisChartOptions.LabelExtraHeight) / Math.Max(1, numHorizontalLines - 1); + var verticalSpace = (_boundHeight - VerticalStartSpace - VerticalEndSpace) / Math.Max(1, numHorizontalLines - 1); GenerateHorizontalGridLines(numHorizontalLines, lowestHorizontalLine, gridYUnits, verticalSpace); GenerateVerticalGridLines(numVerticalLines, gridXUnits, horizontalSpace); @@ -109,7 +109,7 @@ private void GenerateHorizontalGridLines(int numHorizontalLines, int lowestHoriz var line = new SvgPath() { Index = i, - Data = $"M {ToS(HorizontalStartSpace)} {ToS(_boundHeight - AxisChartOptions.LabelExtraHeight - y)} L {ToS(_boundWidth - HorizontalEndSpace)} {ToS(_boundHeight - AxisChartOptions.LabelExtraHeight - y)}" + Data = $"M {ToS(HorizontalStartSpace)} {ToS(_boundHeight - y)} L {ToS(_boundWidth - HorizontalEndSpace)} {ToS(_boundHeight - y)}" }; _horizontalLines.Add(line); @@ -117,7 +117,7 @@ private void GenerateHorizontalGridLines(int numHorizontalLines, int lowestHoriz var lineValue = new SvgText() { X = HorizontalStartSpace - 10, - Y = _boundHeight - AxisChartOptions.LabelExtraHeight - y + 5, + Y = _boundHeight - y + 5, Value = ToS(startGridY, MudChartParent?.ChartOptions.YAxisFormat) }; _horizontalValues.Add(lineValue); @@ -143,7 +143,7 @@ private void GenerateVerticalGridLines(int numVerticalLines, double gridXUnits, var lineValue = new SvgText() { X = x, - Y = _boundHeight - (AxisChartOptions.LabelExtraHeight / 2) - 10, + Y = _boundHeight - 10, Value = xLabels }; _verticalValues.Add(lineValue); @@ -159,130 +159,138 @@ private void GenerateChartLines(int lowestHorizontalLine, double gridYUnits, dou for (var i = 0; i < _series.Count; i++) { - var chartLine = new StringBuilder(); - var series = _series[i]; - var data = series.Data; - var chartDataCirlces = _chartDataPoints[i] = []; - (double x, double y) GetXYForDataPoint(int index) - { - var x = HorizontalStartSpace + (index * horizontalSpace); - var gridValue = ((data[index] / gridYUnits) - lowestHorizontalLine) * verticalSpace; - var y = _boundHeight - VerticalStartSpace - AxisChartOptions.LabelExtraHeight - gridValue; - return (x, y); - } - double GetYForZeroPoint() + if (series.Visible) { - var gridValue = (0 / gridYUnits - lowestHorizontalLine) * verticalSpace; - var y = _boundHeight - VerticalStartSpace - AxisChartOptions.LabelExtraHeight - gridValue; + var chartLine = new StringBuilder(); + var data = series.Data; + var chartDataCircles = _chartDataPoints[i] = []; - return y; - } - - var zeroPointY = GetYForZeroPoint(); - double firstPointX = 0; - double firstPointY = 0; - double lastPointX = 0; - - var interpolationEnabled = MudChartParent != null && MudChartParent.ChartOptions.InterpolationOption != InterpolationOption.Straight; - if (interpolationEnabled) - { - var interpolationResolution = 10; - var XValues = new double[data.Length]; - var YValues = new double[data.Length]; - for (var j = 0; j < data.Length; j++) + (double x, double y) GetXYForDataPoint(int index) { - var (x, y) = (XValues[j], YValues[j]) = GetXYForDataPoint(j); - - var dataValue = data[j]; - chartDataCirlces.Add(new() - { - Index = j, - CX = x, - CY = y, - LabelX = x, - LabelXValue = XAxisLabels[j / interpolationResolution], - LabelY = y, - LabelYValue = dataValue.ToString(), - }); + var x = HorizontalStartSpace + (index * horizontalSpace); + var gridValue = ((data[index] / gridYUnits) - lowestHorizontalLine) * verticalSpace; + var y = _boundHeight - VerticalStartSpace - gridValue; + return (x, y); } - - ILineInterpolator interpolator = MudChartParent?.ChartOptions.InterpolationOption switch + double GetYForZeroPoint() { - InterpolationOption.NaturalSpline => new NaturalSpline(XValues, YValues, interpolationResolution), - InterpolationOption.EndSlope => new EndSlopeSpline(XValues, YValues, interpolationResolution), - InterpolationOption.Periodic => new PeriodicSpline(XValues, YValues, interpolationResolution), - _ => throw new NotImplementedException("Interpolation option not implemented yet") - }; + var gridValue = (0 / gridYUnits - lowestHorizontalLine) * verticalSpace; + var y = _boundHeight - VerticalStartSpace - gridValue; + + return y; + } - var horizontalSpaceInterpolated = (_boundWidth - HorizontalStartSpace - HorizontalEndSpace) / (interpolator.InterpolatedXs.Length - 1); + var zeroPointY = GetYForZeroPoint(); + double firstPointX = 0; + double firstPointY = 0; + double lastPointX = 0; - for (var j = 0; j < interpolator.InterpolatedYs.Length; j++) + var interpolationEnabled = MudChartParent != null && MudChartParent.ChartOptions.InterpolationOption != InterpolationOption.Straight; + if (interpolationEnabled) { - var x = HorizontalStartSpace + (j * horizontalSpaceInterpolated); - var y = interpolator.InterpolatedYs[j]; - - if (j == 0) + var interpolationResolution = 10; + var XValues = new double[data.Length]; + var YValues = new double[data.Length]; + for (var j = 0; j < data.Length; j++) { - chartLine.Append("M "); - firstPointX = x; - firstPointY = y; + var (x, y) = (XValues[j], YValues[j]) = GetXYForDataPoint(j); + + var dataValue = data[j]; + + if (MudChartParent?.ChartOptions.ShowToolTips != true) + { + continue; + } + + chartDataCircles.Add(new() + { + Index = j, + CX = x, + CY = y, + LabelX = x, + LabelXValue = XAxisLabels[j / interpolationResolution], + LabelY = y, + LabelYValue = dataValue.ToString(series.DataMarkerTooltipYValueFormat), + }); } - else - chartLine.Append(" L "); - if (j == interpolator.InterpolatedYs.Length - 1) + ILineInterpolator interpolator = MudChartParent?.ChartOptions.InterpolationOption switch { - lastPointX = x; - } + InterpolationOption.NaturalSpline => new NaturalSpline(XValues, YValues, interpolationResolution), + InterpolationOption.EndSlope => new EndSlopeSpline(XValues, YValues, interpolationResolution), + InterpolationOption.Periodic => new PeriodicSpline(XValues, YValues, interpolationResolution), + _ => throw new NotImplementedException("Interpolation option not implemented yet") + }; - chartLine.Append(ToS(x)); - chartLine.Append(' '); - chartLine.Append(ToS(y)); - } - } - else - { - for (var j = 0; j < data.Length; j++) - { - var (x, y) = GetXYForDataPoint(j); + var horizontalSpaceInterpolated = (_boundWidth - HorizontalStartSpace - HorizontalEndSpace) / (interpolator.InterpolatedXs.Length - 1); - if (j == 0) + for (var j = 0; j < interpolator.InterpolatedYs.Length; j++) { - chartLine.Append("M "); - firstPointX = x; - firstPointY = y; + var x = HorizontalStartSpace + (j * horizontalSpaceInterpolated); + var y = interpolator.InterpolatedYs[j]; + + if (j == 0) + { + chartLine.Append("M "); + firstPointX = x; + firstPointY = y; + } + else + chartLine.Append(" L "); + + if (j == interpolator.InterpolatedYs.Length - 1) + { + lastPointX = x; + } + + chartLine.Append(ToS(x)); + chartLine.Append(' '); + chartLine.Append(ToS(y)); } - else - chartLine.Append(" L "); - - if (j == data.Length - 1) + } + else + { + for (var j = 0; j < data.Length; j++) { - lastPointX = x; + var (x, y) = GetXYForDataPoint(j); + + if (j == 0) + { + chartLine.Append("M "); + firstPointX = x; + firstPointY = y; + } + else + chartLine.Append(" L "); + + if (j == data.Length - 1) + { + lastPointX = x; + } + + chartLine.Append(ToS(x)); + chartLine.Append(' '); + chartLine.Append(ToS(y)); + + var dataValue = data[j]; + + if (MudChartParent?.ChartOptions.ShowToolTips == true) + { + chartDataCircles.Add(new() + { + Index = j, + CX = x, + CY = y, + LabelX = x, + LabelXValue = XAxisLabels.Length > j ? XAxisLabels[j] : string.Empty, + LabelY = y, + LabelYValue = dataValue.ToString(series.DataMarkerTooltipYValueFormat), + }); + } } - - chartLine.Append(ToS(x)); - chartLine.Append(' '); - chartLine.Append(ToS(y)); - - var dataValue = data[j]; - - chartDataCirlces.Add(new() - { - Index = j, - CX = x, - CY = y, - LabelX = x, - LabelXValue = XAxisLabels.Length > j ? XAxisLabels[j] : string.Empty, - LabelY = y, - LabelYValue = dataValue.ToString(), - }); } - } - - if (series.Visible) - { var line = new SvgPath() { Index = i, @@ -324,6 +332,7 @@ double GetYForZeroPoint() _chartAreas.Add(i, area); } } + var legend = new SvgLegend() { Index = i, @@ -342,11 +351,10 @@ private void HandleLegendVisibilityChanged(SvgLegend legend) RebuildChart(); } - private void OnDataPointMouseOver(MouseEventArgs _, SvgCircle dataPoint) + private void OnDataPointMouseOver(MouseEventArgs _, SvgCircle dataPoint, SvgPath seriesPath) { _hoveredDataPoint = dataPoint; - var seriesIndex = _chartDataPoints.First(x => x.Value.Contains(_hoveredDataPoint)).Key; - _hoverDataPointChartLine = _chartLines[seriesIndex]; + _hoverDataPointChartLine = seriesPath; } private void OnDataPointMouseOut(MouseEventArgs _) diff --git a/src/MudBlazor/Components/Chart/Charts/Pie.razor b/src/MudBlazor/Components/Chart/Charts/Pie.razor index 95a1ad3eddb6..b4ffab83c8c5 100644 --- a/src/MudBlazor/Components/Chart/Charts/Pie.razor +++ b/src/MudBlazor/Components/Chart/Charts/Pie.razor @@ -6,9 +6,9 @@ var chartClass = CircleDonutRatio < 1 ? "mud-chart-donut" : "mud-chart-pie"; } - + - + @foreach (var item in _paths) { + } } diff --git a/src/MudBlazor/Components/Chart/Charts/Pie.razor.cs b/src/MudBlazor/Components/Chart/Charts/Pie.razor.cs index e2fa0aa4825b..e9f23b705ea0 100644 --- a/src/MudBlazor/Components/Chart/Charts/Pie.razor.cs +++ b/src/MudBlazor/Components/Chart/Charts/Pie.razor.cs @@ -1,5 +1,5 @@ -using System.Diagnostics.Metrics; -using System.Globalization; +using System.Globalization; +using System.Text; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using MudBlazor.Extensions; @@ -35,8 +35,8 @@ partial class Pie : MudCategoryChartBase [Category(CategoryTypes.Chart.Appearance)] public double CircleDonutRatio { get; set; } = 1; - private List _paths = []; - private List _legends = []; + private readonly List _paths = []; + private readonly List _legends = []; private SvgPath? _hoveredSegment; protected override void OnParametersSet() @@ -60,12 +60,16 @@ protected override void OnParametersSet() var data = normalizedData[i]; var startx = Math.Cos(cumulativeRadians); var starty = Math.Sin(cumulativeRadians); - cumulativeRadians += 2 * Math.PI * data; + cumulativeRadians += 2 * Math.PI * data / 2; + var midx = Math.Cos(cumulativeRadians); + var midy = Math.Sin(cumulativeRadians); + cumulativeRadians += 2 * Math.PI * data / 2; var endx = Math.Cos(cumulativeRadians); var endy = Math.Sin(cumulativeRadians); var largeArcFlag = data > 0.5 ? 1 : 0; SvgPath path; + var pathStringBuilder = new StringBuilder(); if (donutRadiusRatio < 1) { // Calculate inner radius with a hole. @@ -74,32 +78,55 @@ protected override void OnParametersSet() // Outer coordinates var outerStartX = startx * Radius; var outerStartY = starty * Radius; + var outerMidX = midx * Radius; + var outerMidY = midy * Radius; var outerEndX = endx * Radius; var outerEndY = endy * Radius; // Inner coordinates (for the hole) var innerStartX = startx * innerRadius; var innerStartY = starty * innerRadius; + var innerMidX = midx * innerRadius; + var innerMidY = midy * innerRadius; var innerEndX = endx * innerRadius; var innerEndY = endy * innerRadius; + + pathStringBuilder.Append($"M {ToS(outerStartX)} {ToS(outerStartY)} "); // Move to the start point + if (data >= 1) + { + pathStringBuilder.Append($"A {ToS(Radius)} {ToS(Radius)} 0 {ToS(largeArcFlag)} 1 {ToS(outerMidX)} {ToS(outerMidY)} "); // Add an arc to a mid point half way through the slice (outer) to support 100% donuts + } + pathStringBuilder.Append($"A {ToS(Radius)} {ToS(Radius)} 0 {ToS(largeArcFlag)} 1 {ToS(outerEndX)} {ToS(outerEndY)} "); // Add an arc to the end point (outer) + pathStringBuilder.Append($"L {ToS(innerEndX)} {ToS(innerEndY)} "); // Line to the end point of the inner arc + if (data >= 1) + { + pathStringBuilder.Append($"A {ToS(innerRadius)} {ToS(innerRadius)} 0 {ToS(largeArcFlag)} 0 {ToS(innerMidX)} {ToS(innerMidY)} "); // Add an arc to a mid point half way through the slice to support 100% donuts + } + pathStringBuilder.Append($"A {ToS(innerRadius)} {ToS(innerRadius)} 0 {ToS(largeArcFlag)} 0 {ToS(innerStartX)} {ToS(innerStartY)} Z"); // Add an arc to the start point (inner) + // Build a compound path: outer arc -> line to inner arc -> inner arc -> close path = new SvgPath { Index = i, - Data = $"M {ToS(outerStartX)} {ToS(outerStartY)} " + - $"A {ToS(Radius)} {ToS(Radius)} 0 {ToS(largeArcFlag)} 1 {ToS(outerEndX)} {ToS(outerEndY)} " + - $"L {ToS(innerEndX)} {ToS(innerEndY)} " + - $"A {ToS(innerRadius)} {ToS(innerRadius)} 0 {ToS(largeArcFlag)} 0 {ToS(innerStartX)} {ToS(innerStartY)} Z" + Data = pathStringBuilder.ToString() }; } else { + pathStringBuilder.Append($"M {ToS(startx * Radius)} {ToS(starty * Radius)} "); // Move to the start point + if (data >= 1) + { + pathStringBuilder.Append($"A {ToS(Radius)} {ToS(Radius)} 0 {ToS(largeArcFlag)} 1 {ToS(midx * Radius)} {ToS(midy * Radius)} "); // Add an arc to a mid point half way through the slice to support 100% pies + } + pathStringBuilder.Append($"A {ToS(Radius)} {ToS(Radius)} 0 {ToS(largeArcFlag)} 1 {ToS(endx * Radius)} {ToS(endy * Radius)} "); // Add an arc to the end point + pathStringBuilder.Append("L 0 0 Z"); // Line to the center + // Standard pie slice path going to the center. path = new SvgPath() { Index = i, - Data = $"M {ToS(startx * Radius)} {ToS(starty * Radius)} A {Radius} {Radius} 0 {ToS(largeArcFlag)} 1 {ToS(endx * Radius)} {ToS(endy * Radius)} L 0 0" + Data = pathStringBuilder.ToString() }; } @@ -107,9 +134,15 @@ protected override void OnParametersSet() var midAngle = cumulativeRadians - Math.PI * data; var midRadius = Radius * (1 - donutRadiusRatio / 2); - // Calculate the midpoint coordinates at half the radius - var midX = Math.Cos(midAngle) * midRadius; - var midY = Math.Sin(midAngle) * midRadius; + var midX = 0d; + var midY = 0d; + + if (donutRadiusRatio < 1 || data < 1) // don't find mid point when donut is 100% and data is 100%, just use the 0,0 point. + { + // Calculate the midpoint coordinates at half the radius + midX = Math.Cos(midAngle) * midRadius; + midY = Math.Sin(midAngle) * midRadius; + } path.LabelX = midX; path.LabelY = midY; @@ -123,6 +156,10 @@ protected override void OnParametersSet() { var percent = normalizedData[i] * 100; var labels = i < InputLabels.Length ? InputLabels[i] : ""; + + if (labels.Length == 0) + continue; + var legend = new SvgLegend() { Index = i, diff --git a/src/MudBlazor/Components/Chart/Charts/StackedBar.razor b/src/MudBlazor/Components/Chart/Charts/StackedBar.razor index 512c03282da9..2492b6d21110 100644 --- a/src/MudBlazor/Components/Chart/Charts/StackedBar.razor +++ b/src/MudBlazor/Components/Chart/Charts/StackedBar.razor @@ -1,6 +1,5 @@ @namespace MudBlazor.Charts @using System.Globalization; -@using Microsoft.JSInterop @inherits MudCategoryAxisChartBase @{ @@ -26,19 +25,19 @@ } - + @foreach (var horizontalLineValue in _horizontalValues) { - @((MarkupString)$"{horizontalLineValue.Value?.ToString(CultureInfo.InvariantCulture)}") + @((MarkupString)$"{horizontalLineValue.Value?.ToString(CultureInfo.InvariantCulture)}") } - + @for (var i = 0; i < _verticalValues.Count; i++) { var verticalLineValue = _verticalValues[i]; - var x = verticalLineValue.X.ToString(CultureInfo.InvariantCulture); - var y = verticalLineValue.Y.ToString(CultureInfo.InvariantCulture); - var rotation = (-AxisChartOptions.LabelRotation).ToString(CultureInfo.InvariantCulture); + var x = ToS(verticalLineValue.X); + var y = ToS(verticalLineValue.Y); + var rotation = ToS(-AxisChartOptions.XAxisLabelRotation); @((MarkupString)$"{verticalLineValue.Value?.ToString(CultureInfo.InvariantCulture)}") } @@ -63,9 +62,9 @@ @MudChartParent?.CustomGraphics @* Render the tooltip as an SVG group when a bar is hovered *@ - @if (_hoveredBar is not null) + @if (_hoveredBar is not null && MudChartParent?.ChartOptions.ShowToolTips == true) { - var color = MudChartParent?.ChartOptions.ChartPalette.GetValue(_hoveredBar.Index % MudChartParent.ChartOptions.ChartPalette.Length)?.ToString() ?? string.Empty; + var color = MudChartParent.ChartOptions.ChartPalette.GetValue(_hoveredBar.Index % MudChartParent.ChartOptions.ChartPalette.Length)?.ToString() ?? string.Empty; var series = _series[_hoveredBar.Index]; if (!string.IsNullOrWhiteSpace(series.DataMarkerTooltipTitleFormat)) diff --git a/src/MudBlazor/Components/Chart/Charts/StackedBar.razor.cs b/src/MudBlazor/Components/Chart/Charts/StackedBar.razor.cs index 39b3a580e3ec..3ee2840e4ad8 100644 --- a/src/MudBlazor/Components/Chart/Charts/StackedBar.razor.cs +++ b/src/MudBlazor/Components/Chart/Charts/StackedBar.razor.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.Web; using MudBlazor.Extensions; #nullable enable @@ -17,16 +16,16 @@ partial class StackedBar : MudCategoryAxisChartBase { private const double BarOverlapAmountFix = 0.5; // used to trigger slight overlap so the bars don't have gaps due to floating point rounding - private List _horizontalLines = []; - private List _horizontalValues = []; + private readonly List _horizontalLines = []; + private readonly List _horizontalValues = []; - private List _verticalLines = []; - private List _verticalValues = []; + private readonly List _verticalLines = []; + private readonly List _verticalValues = []; - private List _legends = []; + private readonly List _legends = []; private List _series = []; - private List _bars = []; + private readonly List _bars = []; private double _barWidth; private double _barWidthStroke; private SvgPath? _hoveredBar; @@ -52,7 +51,7 @@ protected override void RebuildChart() // Calculate spacing – note the horizontal space is computed so that the vertical grid lines line up double horizontalSpace = Math.Round((_boundWidth - HorizontalStartSpace - HorizontalEndSpace) / (numVerticalLines > 1 ? (numVerticalLines) : 1), 1); - double verticalSpace = (_boundHeight - VerticalStartSpace - VerticalEndSpace - AxisChartOptions.LabelExtraHeight) / (numHorizontalLines > 1 ? (numHorizontalLines) : 1); + double verticalSpace = (_boundHeight - VerticalStartSpace - VerticalEndSpace) / (numHorizontalLines > 1 ? (numHorizontalLines) : 1); GenerateHorizontalGridLines(numHorizontalLines, gridYUnits, verticalSpace); GenerateVerticalGridLines(numVerticalLines, horizontalSpace); @@ -106,6 +105,16 @@ private void ComputeStackedUnitsAndNumberOfLines( } var maxY = stackedTotals.Any() ? stackedTotals.Max() : 0; numHorizontalLines = (int)(maxY / gridYUnits) + 1; + + // this is a safeguard against millions of gridlines which might arise with very high values + var maxYTicks = MudChartParent?.ChartOptions.MaxNumYAxisTicks ?? 20; + while (numHorizontalLines > maxYTicks) + { + gridYUnits *= 2; + var lowestHorizontalLine = Math.Min((int)Math.Floor(0 / gridYUnits), 0); + var highestHorizontalLine = Math.Max((int)Math.Ceiling(maxY / gridYUnits), 0); + numHorizontalLines = highestHorizontalLine - lowestHorizontalLine + 1; + } } /// @@ -124,14 +133,14 @@ private void GenerateHorizontalGridLines(int numHorizontalLines, double gridYUni var line = new SvgPath() { Index = i, - Data = $"M {ToS(HorizontalStartSpace)} {ToS(_boundHeight - AxisChartOptions.LabelExtraHeight - y)} L {ToS(_boundWidth - HorizontalEndSpace)} {ToS(_boundHeight - AxisChartOptions.LabelExtraHeight - y)}" + Data = $"M {ToS(HorizontalStartSpace)} {ToS(_boundHeight - y)} L {ToS(_boundWidth - HorizontalEndSpace)} {ToS(_boundHeight - y)}" }; _horizontalLines.Add(line); var text = new SvgText() { X = HorizontalStartSpace - 10, - Y = _boundHeight - AxisChartOptions.LabelExtraHeight - y + 5, + Y = _boundHeight - y + 5, Value = ToS(lineValue, MudChartParent?.ChartOptions.YAxisFormat) }; _horizontalValues.Add(text); @@ -163,7 +172,7 @@ private void GenerateVerticalGridLines(int numVerticalLines, double horizontalSp var text = new SvgText() { X = x, - Y = _boundHeight - (AxisChartOptions.LabelExtraHeight / 2) - 10, + Y = _boundHeight - XAxisLabelOffset, Value = label, }; _verticalValues.Add(text); @@ -186,7 +195,7 @@ private void GenerateStackedBars(double gridYUnits, double horizontalSpace, doub { double x = HorizontalStartSpace + startPadding + (j * horizontalSpace); - var yStart = _boundHeight - VerticalStartSpace - AxisChartOptions.LabelExtraHeight; + var yStart = _boundHeight - VerticalStartSpace; for (int i = 0; i < _series.Count; i++) { var series = _series[i]; @@ -206,7 +215,7 @@ private void GenerateStackedBars(double gridYUnits, double horizontalSpace, doub Index = i, Data = $"M {ToS(x)} {ToS(yStart)} L {ToS(x)} {ToS(yEnd - BarOverlapAmountFix)}", LabelXValue = XAxisLabels.Length > j ? XAxisLabels[j] : string.Empty, - LabelYValue = dataValue.ToString(), + LabelYValue = dataValue.ToString(series.DataMarkerTooltipYValueFormat), LabelX = x, LabelY = yEnd }; diff --git a/src/MudBlazor/Components/Chart/Charts/TimeSeries.razor b/src/MudBlazor/Components/Chart/Charts/TimeSeries.razor index 63d7a69c9ecc..352f7e3761a2 100644 --- a/src/MudBlazor/Components/Chart/Charts/TimeSeries.razor +++ b/src/MudBlazor/Components/Chart/Charts/TimeSeries.razor @@ -30,7 +30,7 @@ } - + @foreach (var horizontalLineValue in _horizontalValues) { @((MarkupString)$"{horizontalLineValue.Value?.ToString(CultureInfo.InvariantCulture)}") @@ -38,19 +38,17 @@ @if (YAxisTitle is not null) { - - @YAxisTitle - + @YAxisTitle } - + @for (var i = 0; i < _verticalValues.Count; i++) { var verticalLineValue = _verticalValues[i]; var x = verticalLineValue.X.ToString(CultureInfo.InvariantCulture); var y = verticalLineValue.Y.ToString(CultureInfo.InvariantCulture); - var rotation = (-AxisChartOptions.LabelRotation).ToString(CultureInfo.InvariantCulture); + var rotation = (-AxisChartOptions.XAxisLabelRotation).ToString(CultureInfo.InvariantCulture); @((MarkupString)$"{verticalLineValue.Value?.ToString(CultureInfo.InvariantCulture)}") } @@ -89,7 +87,7 @@ cx="@ToS(item.CX)" cy="@ToS(item.CY)" r="@ToS(dataPointHoverRadius)" - @onmouseover="(e) => OnDataPointMouseOver(e, item)" + @onmouseover="(e) => OnDataPointMouseOver(e, item, chartLine)" @onmouseout="OnDataPointMouseOut"> } @@ -99,11 +97,11 @@ @MudChartParent?.CustomGraphics @* Render the tooltip as an SVG group when a bar is hovered *@ - @if (_hoveredDataPoint is not null && _hoverDataPointChartLine is not null) + @if (_hoveredDataPoint is not null && _hoverDataPointChartLine is not null && MudChartParent?.ChartOptions.ShowToolTips == true) { var seriesIndex = _chartDataPoints.First(x => x.Value.Contains(_hoveredDataPoint)).Key; - var color = MudChartParent?.ChartOptions.ChartPalette.GetValue(seriesIndex % MudChartParent.ChartOptions.ChartPalette.Length)?.ToString() ?? string.Empty; + var color = MudChartParent.ChartOptions.ChartPalette.GetValue(seriesIndex % MudChartParent.ChartOptions.ChartPalette.Length)?.ToString() ?? string.Empty; var series = _series[seriesIndex]; if (!string.IsNullOrWhiteSpace(series.DataMarkerTooltipTitleFormat)) diff --git a/src/MudBlazor/Components/Chart/Charts/TimeSeries.razor.cs b/src/MudBlazor/Components/Chart/Charts/TimeSeries.razor.cs index e8fbb44f0c38..677477b84bf2 100644 --- a/src/MudBlazor/Components/Chart/Charts/TimeSeries.razor.cs +++ b/src/MudBlazor/Components/Chart/Charts/TimeSeries.razor.cs @@ -12,23 +12,24 @@ namespace MudBlazor.Charts /// /// A chart which displays values over time. /// - /// - /// - /// - /// - /// partial class TimeSeries : MudTimeSeriesChartBase, IDisposable { private const double Epsilon = 1e-6; private const double BoundWidthDefault = 800; private const double BoundHeightDefault = 350; - private const double HorizontalStartSpace = 80.0; // needs space to have the full label visible and be even to the end space - private const double HorizontalEndSpace = 80.0; // needs space to have the full label visible and be even to the start space - private const double VerticalStartSpace = 25.0; + private const double HorizontalStartSpaceBuffer = 10.0; + protected double HorizontalStartSpace => Math.Max(HorizontalStartSpaceBuffer + (_yAxisLabelSize?.Width ?? 0), 30); + private const double HorizontalEndSpace = 30.0; + private const double VerticalStartSpaceBuffer = 10.0; + protected double VerticalStartSpace => Math.Max(VerticalStartSpaceBuffer + (_xAxisLabelSize?.Height ?? 0), 30); private const double VerticalEndSpace = 25.0; + protected double XAxisLabelOffset => Math.Ceiling(_xAxisLabelSize?.Height ?? 20) / 2; + private double _boundWidth = BoundWidthDefault; private double _boundHeight = BoundHeightDefault; private ElementSize? _elementSize = null; + private ElementSize? _yAxisLabelSize; + private ElementSize? _xAxisLabelSize; [Inject] private IJSRuntime JsRuntime { get; set; } = null!; @@ -36,26 +37,28 @@ partial class TimeSeries : MudTimeSeriesChartBase, IDisposable [CascadingParameter] public MudTimeSeriesChartBase? MudChartParent { get; set; } - private List _horizontalLines = []; - private List _horizontalValues = []; + private readonly List _horizontalLines = []; + private readonly List _horizontalValues = []; - private List _verticalLines = []; - private List _verticalValues = []; + private readonly List _verticalLines = []; + private readonly List _verticalValues = []; - private List _legends = []; + private readonly List _legends = []; private List _series = []; - private List _chartLines = []; - private Dictionary _chartAreas = []; - private Dictionary> _chartDataPoints = []; + private readonly List _chartLines = []; + private readonly Dictionary _chartAreas = []; + private readonly Dictionary> _chartDataPoints = []; private SvgCircle? _hoveredDataPoint; private SvgPath? _hoverDataPointChartLine; private DateTime _minDateTime; private DateTime _maxDateTime; private TimeSpan _minDateLabelOffset; - private DotNetObjectReference _dotNetObjectReference; + private readonly DotNetObjectReference _dotNetObjectReference; private ElementReference _elementReference; + protected ElementReference? _xAxisGroupElementReference; + protected ElementReference? _yAxisGroupElementReference; public TimeSeries() { @@ -75,9 +78,32 @@ protected override async Task OnAfterRenderAsync(bool firstRender) if (firstRender) { - _elementSize = await JsRuntime.InvokeAsync("mudObserveElementSize", _dotNetObjectReference, _elementReference); + var elementSize = await JsRuntime.InvokeAsync("mudObserveElementSize", _dotNetObjectReference, _elementReference); + + OnElementSizeChanged(elementSize); + } - OnElementSizeChanged(_elementSize); + var yAxisLabelSize = _yAxisGroupElementReference != null ? await JsRuntime.InvokeAsync("mudGetSvgBBox", _yAxisGroupElementReference) : null; + var xAxisLabelSize = _xAxisGroupElementReference != null ? await JsRuntime.InvokeAsync("mudGetSvgBBox", _xAxisGroupElementReference) : null; + + var axisChanged = false; + if (yAxisLabelSize != null && (_yAxisLabelSize == null || !DoubleEpsilonEqualityComparer.Default.Equals(yAxisLabelSize.Height, _yAxisLabelSize.Height))) + { + _yAxisLabelSize = yAxisLabelSize; + axisChanged = true; + } + + if (xAxisLabelSize != null && (_xAxisLabelSize == null || !DoubleEpsilonEqualityComparer.Default.Equals(xAxisLabelSize.Width, _xAxisLabelSize.Width))) + { + _xAxisLabelSize = xAxisLabelSize; + axisChanged = true; + } + + // maybe there should be some kind of cancellation token here to prevent multiple rebuilds when the invokeasync takes time in server mode and subsequent renders have started to take place + if (axisChanged) + { + RebuildChart(); + StateHasChanged(); } } @@ -90,8 +116,8 @@ private void RebuildChart() ComputeMinAndMaxDateTimes(); ComputeUnitsAndNumberOfLines(out var gridXUnits, out var gridYUnits, out var numHorizontalLines, out var lowestHorizontalLine, out var numVerticalLines); - var horizontalSpace = (_boundWidth - HorizontalStartSpace - HorizontalEndSpace) / Math.Max(1, numVerticalLines - 1); - var verticalSpace = (_boundHeight - VerticalStartSpace - VerticalEndSpace - AxisChartOptions.LabelExtraHeight) / Math.Max(1, numHorizontalLines - 1); + var horizontalSpace = (_boundWidth - HorizontalStartSpace - HorizontalEndSpace) / Math.Max(1, (_maxDateTime - _minDateTime) / TimeLabelSpacing); + var verticalSpace = (_boundHeight - VerticalStartSpace - VerticalEndSpace) / Math.Max(1, numHorizontalLines - 1); GenerateHorizontalGridLines(numHorizontalLines, lowestHorizontalLine, gridYUnits, verticalSpace); GenerateVerticalGridLines(numVerticalLines, gridXUnits, horizontalSpace); @@ -122,9 +148,9 @@ private void SetBounds() } [JSInvokable] - public void OnElementSizeChanged(ElementSize elementSize) + public void OnElementSizeChanged(ElementSize? elementSize) { - if (elementSize == null) + if (elementSize == null || elementSize.Timestamp <= _elementSize?.Timestamp) return; _elementSize = elementSize; @@ -215,7 +241,7 @@ private void ComputeUnitsAndNumberOfLines(out double gridXUnits, out double grid var labelSpacing = TimeLabelSpacing; - numVerticalLines = (int)Math.Ceiling((_maxDateTime - _minDateTime) / labelSpacing); + numVerticalLines = (int)((_maxDateTime - _minDateTime) / labelSpacing) + 1; } else { @@ -236,7 +262,7 @@ private void GenerateHorizontalGridLines(int numHorizontalLines, int lowestHoriz var line = new SvgPath() { Index = i, - Data = $"M {ToS(HorizontalStartSpace)} {ToS((_boundHeight - AxisChartOptions.LabelExtraHeight - y))} L {ToS((_boundWidth - HorizontalEndSpace))} {ToS((_boundHeight - AxisChartOptions.LabelExtraHeight - y))}" + Data = $"M {ToS(HorizontalStartSpace)} {ToS(_boundHeight - y)} L {ToS(_boundWidth - HorizontalEndSpace)} {ToS(_boundHeight - y)}" }; _horizontalLines.Add(line); @@ -244,7 +270,7 @@ private void GenerateHorizontalGridLines(int numHorizontalLines, int lowestHoriz var lineValue = new SvgText() { X = HorizontalStartSpace - 10, - Y = _boundHeight - AxisChartOptions.LabelExtraHeight - y + 5, + Y = _boundHeight - y + 5, Value = ToS(startGridY, MudChartParent?.ChartOptions.YAxisFormat) }; _horizontalValues.Add(lineValue); @@ -280,7 +306,7 @@ private void GenerateVerticalGridLines(int numVerticalLines, double gridXUnits, var line = new SvgPath() { Index = i, - Data = $"M {ToS(x)} {ToS((_boundHeight - VerticalStartSpace))} L {ToS(x)} {ToS(VerticalEndSpace)}" + Data = $"M {ToS(x)} {ToS(_boundHeight - VerticalStartSpace)} L {ToS(x)} {ToS(VerticalEndSpace)}" }; _verticalLines.Add(line); @@ -289,7 +315,7 @@ private void GenerateVerticalGridLines(int numVerticalLines, double gridXUnits, var lineValue = new SvgText() { X = x, - Y = _boundHeight - (AxisChartOptions.LabelExtraHeight / 2) - 10, + Y = _boundHeight - XAxisLabelOffset, Value = xLabels.ToString(TimeLabelFormat), }; _verticalValues.Add(lineValue); @@ -310,83 +336,86 @@ private void GenerateChartLines(int lowestHorizontalLine, double gridYUnits, dou for (var i = 0; i < _series.Count; i++) { - var chartLine = new StringBuilder(); - var series = _series[i]; - var data = series.Data; - var chartDataCirlces = _chartDataPoints[i] = []; - - if (data.Count <= 0) - continue; - (double x, double y) GetXYForDataPoint(int index) + if (series.IsVisible) { - var dateTime = data[index].DateTime; + var chartLine = new StringBuilder(); + var data = series.Data; + var chartDataCirlces = _chartDataPoints[i] = []; - var diffFromMin = dateTime - _minDateTime; + if (data.Count <= 0) + continue; - var gridValue = (data[index].Value / gridYUnits - lowestHorizontalLine) * verticalSpace; - var y = _boundHeight - VerticalStartSpace - AxisChartOptions.LabelExtraHeight - gridValue; + (double x, double y) GetXYForDataPoint(int index) + { + var dateTime = data[index].DateTime; - if (fullDateTimeDiff.TotalMilliseconds == 0) - return (HorizontalStartSpace, y); + var diffFromMin = dateTime - _minDateTime; - var x = HorizontalStartSpace + diffFromMin.TotalMilliseconds / fullDateTimeDiff.TotalMilliseconds * (_boundWidth - HorizontalStartSpace - HorizontalEndSpace); + var gridValue = (data[index].Value / gridYUnits - lowestHorizontalLine) * verticalSpace; + var y = _boundHeight - VerticalStartSpace - gridValue; - return (x, y); - } - double GetYForZeroPoint() - { - var gridValue = (0 / gridYUnits - lowestHorizontalLine) * verticalSpace; - var y = _boundHeight - VerticalStartSpace - AxisChartOptions.LabelExtraHeight - gridValue; + if (fullDateTimeDiff.TotalMilliseconds == 0) + return (HorizontalStartSpace, y); - return y; - } + var x = HorizontalStartSpace + (diffFromMin.TotalMilliseconds / fullDateTimeDiff.TotalMilliseconds * (_boundWidth - HorizontalStartSpace - HorizontalEndSpace)); - bool interpolationEnabled = MudChartParent != null && MudChartParent.ChartOptions.InterpolationOption != InterpolationOption.Straight; - if (interpolationEnabled) - { - // TODO this is not simple to implement, as the x values are not linearly spaced - // and the interpolation should be done based on the datetime - // so we need to find a way to interpolate the x values based on the datetime - // and then interpolate the y values based on the x values - // this is not trivial and needs to be done in a separate PR - - throw new NotImplementedException("Interpolation not implemented yet for timeseries charts"); - } - else - { - for (var j = 0; j < data.Count; j++) + return (x, y); + } + double GetYForZeroPoint() { - var (x, y) = GetXYForDataPoint(j); - - if (j == 0) - { - chartLine.Append("M "); - } - else - chartLine.Append(" L "); + var gridValue = (0 / gridYUnits - lowestHorizontalLine) * verticalSpace; + var y = _boundHeight - VerticalStartSpace - gridValue; - chartLine.Append(ToS(x)); - chartLine.Append(' '); - chartLine.Append(ToS(y)); + return y; + } - var dataValue = data[j]; + bool interpolationEnabled = MudChartParent != null && MudChartParent.ChartOptions.InterpolationOption != InterpolationOption.Straight; + if (interpolationEnabled) + { + // TODO this is not simple to implement, as the x values are not linearly spaced + // and the interpolation should be done based on the datetime + // so we need to find a way to interpolate the x values based on the datetime + // and then interpolate the y values based on the x values + // this is not trivial and needs to be done in a separate PR - chartDataCirlces.Add(new() + throw new NotImplementedException("Interpolation not implemented yet for timeseries charts"); + } + else + { + for (var j = 0; j < data.Count; j++) { - Index = j, - CX = x, - CY = y, - LabelX = x, - LabelXValue = dataValue.DateTime.ToString(MudChartParent?.DataMarkerTooltipTimeLabelFormat ?? "{0}"), - LabelY = y, - LabelYValue = dataValue.Value.ToString(), - }); + var (x, y) = GetXYForDataPoint(j); + + if (j == 0) + { + chartLine.Append("M "); + } + else + chartLine.Append(" L "); + + chartLine.Append(ToS(x)); + chartLine.Append(' '); + chartLine.Append(ToS(y)); + + var dataValue = data[j]; + + if (MudChartParent?.ChartOptions.ShowToolTips != true) + continue; + + chartDataCirlces.Add(new() + { + Index = j, + CX = x, + CY = y, + LabelX = x, + LabelXValue = dataValue.DateTime.ToString(MudChartParent?.DataMarkerTooltipTimeLabelFormat ?? "{0}"), + LabelY = y, + LabelYValue = dataValue.Value.ToString(series.DataMarkerTooltipYValueFormat), + }); + } } - } - if (_series[i].IsVisible) - { var line = new SvgPath() { Index = i, @@ -436,8 +465,8 @@ double GetYForZeroPoint() var legend = new SvgLegend() { Index = i, - Labels = _series[i].Name, - Visible = _series[i].IsVisible, + Labels = series.Name, + Visible = series.IsVisible, OnVisibilityChanged = EventCallback.Factory.Create(this, HandleLegendVisibilityChanged) }; _legends.Add(legend); @@ -451,11 +480,10 @@ private void HandleLegendVisibilityChanged(SvgLegend legend) RebuildChart(); } - private void OnDataPointMouseOver(MouseEventArgs _, SvgCircle dataPoint) + private void OnDataPointMouseOver(MouseEventArgs _, SvgCircle dataPoint, SvgPath seriesPath) { _hoveredDataPoint = dataPoint; - var seriesIndex = _chartDataPoints.First(x => x.Value.Contains(_hoveredDataPoint)).Key; - _hoverDataPointChartLine = _chartLines[seriesIndex]; + _hoverDataPointChartLine = seriesPath; } private void OnDataPointMouseOut(MouseEventArgs _) diff --git a/src/MudBlazor/Components/Chart/Models/AxisChartOptions.cs b/src/MudBlazor/Components/Chart/Models/AxisChartOptions.cs index f511d12ae06a..b96c9ec5e089 100644 --- a/src/MudBlazor/Components/Chart/Models/AxisChartOptions.cs +++ b/src/MudBlazor/Components/Chart/Models/AxisChartOptions.cs @@ -11,11 +11,18 @@ public class AxisChartOptions /// /// Rotation angle to rotate the labels in degrees. /// - public int LabelRotation { get; set; } + [Obsolete("Renamed to XAxisLabelRotation. This will be removed in a future major version.", false)] + public int LabelRotation { get => XAxisLabelRotation; set => XAxisLabelRotation = value; } /// - /// Extra height to fit rotated labels. + /// Rotation angle to rotate the labels in degrees. + /// + public int XAxisLabelRotation { get; set; } + + /// + /// Extra height to fit XAxis rotated labels. /// + [Obsolete("No longer required, labels are now calculated automatically. This will be removed in a future major version.", false)] public int LabelExtraHeight { get; set; } /// diff --git a/src/MudBlazor/Components/Chart/Models/ChartOptions.cs b/src/MudBlazor/Components/Chart/Models/ChartOptions.cs index 40e0d93acad9..5efea1147a47 100644 --- a/src/MudBlazor/Components/Chart/Models/ChartOptions.cs +++ b/src/MudBlazor/Components/Chart/Models/ChartOptions.cs @@ -120,7 +120,7 @@ public class ChartOptions public YAxisLabelPosition YAxisLabelPosition { get; set; } = YAxisLabelPosition.Left; /// - /// Enables tooltips for values in a + /// Enables tooltips for values /// Defaults to true /// public bool ShowToolTips { get; set; } = true; diff --git a/src/MudBlazor/Components/Chart/Models/ChartSeries.cs b/src/MudBlazor/Components/Chart/Models/ChartSeries.cs index ce0c04876856..84be0a71a295 100644 --- a/src/MudBlazor/Components/Chart/Models/ChartSeries.cs +++ b/src/MudBlazor/Components/Chart/Models/ChartSeries.cs @@ -50,6 +50,11 @@ public class ChartSeries /// public string? DataMarkerTooltipSubtitleFormat { get; set; } + /// + /// Tooltip YValue format for the series. It is used to format the {{Y_VALUE}} tag. + /// + public string? DataMarkerTooltipYValueFormat { get; set; } + public LineDisplayType LineDisplayType { get; set; } public double FillOpacity { get; set; } = 0.4; diff --git a/src/MudBlazor/Components/Chart/Models/TimeSeriesChartSeries.cs b/src/MudBlazor/Components/Chart/Models/TimeSeriesChartSeries.cs index e60e63459dbe..1da1bd5f1b02 100644 --- a/src/MudBlazor/Components/Chart/Models/TimeSeriesChartSeries.cs +++ b/src/MudBlazor/Components/Chart/Models/TimeSeriesChartSeries.cs @@ -35,5 +35,10 @@ public record TimeValue(DateTime DateTime, double Value); /// Tooltip subtitle format for the series. Supported tags are {{SERIES_NAME}}, {{X_VALUE}} and {{Y_VALUE}}. /// public string? DataMarkerTooltipSubtitleFormat { get; set; } + + /// + /// Tooltip YValue format for the series. It is used to format the {{Y_VALUE}} tag. + /// + public string? DataMarkerTooltipYValueFormat { get; set; } } } diff --git a/src/MudBlazor/Components/Chart/MudCategoryAxisChartBase.cs b/src/MudBlazor/Components/Chart/MudCategoryAxisChartBase.cs index b1c94bbad398..c0afacf1fcc9 100644 --- a/src/MudBlazor/Components/Chart/MudCategoryAxisChartBase.cs +++ b/src/MudBlazor/Components/Chart/MudCategoryAxisChartBase.cs @@ -18,19 +18,26 @@ public abstract class MudCategoryAxisChartBase : MudCategoryChartBase, IDisposab public MudChart? MudChartParent { get; set; } private const double Epsilon = 1e-6; - protected const double HorizontalStartSpace = 30.0; + protected const double BoundWidthDefault = 650.0; + protected const double BoundHeightDefault = 350.0; + protected const double HorizontalStartSpaceBuffer = 10.0; + protected double HorizontalStartSpace => Math.Max(HorizontalStartSpaceBuffer + Math.Ceiling(_yAxisLabelSize?.Width ?? 0), 30); protected const double HorizontalEndSpace = 30.0; - protected const double VerticalStartSpace = 25.0; + protected const double VerticalStartSpaceBuffer = 10.0; + protected double VerticalStartSpace => Math.Max(VerticalStartSpaceBuffer + (_xAxisLabelSize?.Height ?? 0), 30); protected const double VerticalEndSpace = 25.0; + protected double XAxisLabelOffset => Math.Ceiling(_xAxisLabelSize?.Height ?? 20) / 2; - protected const double BoundWidthDefault = 650.0; - protected const double BoundHeightDefault = 350.0; protected double _boundWidth = 650.0; protected double _boundHeight = 350.0; - private ElementSize _elementSize = new() { Width = BoundWidthDefault, Height = BoundHeightDefault }; + private ElementSize? _elementSize; + private ElementSize? _yAxisLabelSize; + private ElementSize? _xAxisLabelSize; private readonly DotNetObjectReference _dotNetObjectReference; - protected ElementReference _elementReference = new(); + protected ElementReference _elementReference; + protected ElementReference? _xAxisGroupElementReference; + protected ElementReference? _yAxisGroupElementReference; [DynamicDependency(nameof(OnElementSizeChanged))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ElementSize))] @@ -45,29 +52,65 @@ protected override async Task OnAfterRenderAsync(bool firstRender) if (firstRender) { - _elementSize = await JsRuntime.InvokeAsync("mudObserveElementSize", _dotNetObjectReference, _elementReference); + var elementSize = await JsRuntime.InvokeAsync("mudObserveElementSize", _dotNetObjectReference, _elementReference); + + OnElementSizeChanged(elementSize); + } + + var yAxisLabelSize = _yAxisGroupElementReference != null ? await JsRuntime.InvokeAsync("mudGetSvgBBox", _yAxisGroupElementReference) : null; + var xAxisLabelSize = _xAxisGroupElementReference != null ? await JsRuntime.InvokeAsync("mudGetSvgBBox", _xAxisGroupElementReference) : null; + + var axisChanged = false; + var comparer = new DoubleEpsilonEqualityComparer(0.01); + if (yAxisLabelSize != null && (_yAxisLabelSize == null || !comparer.Equals(yAxisLabelSize.Width, _yAxisLabelSize.Width))) + { + _yAxisLabelSize = yAxisLabelSize; + axisChanged = true; + } + + if (xAxisLabelSize != null && (_xAxisLabelSize == null || !comparer.Equals(xAxisLabelSize.Height, _xAxisLabelSize.Height))) + { + _xAxisLabelSize = xAxisLabelSize; + axisChanged = true; + } - OnElementSizeChanged(_elementSize); + // maybe there should be some kind of cancellation token here to prevent multiple rebuilds when the invokeasync takes time in server mode and subsequent renders have started to take place + if (axisChanged) + { + RebuildChart(); + StateHasChanged(); } } - protected virtual void SetBounds() + protected void SetBounds() { _boundWidth = BoundWidthDefault; _boundHeight = BoundHeightDefault; -#pragma warning disable CS0618 - if (MudChartParent != null && AxisChartOptions.MatchBoundsToSize) + if (MudChartParent != null && (MudChartParent.AxisChartOptions.MatchBoundsToSize)) // backwards compatibilitly to the mudchartparent approach { - _boundWidth = _elementSize.Width; - _boundHeight = _elementSize.Height; + if (_elementSize != null) + { + _boundWidth = _elementSize.Width; + _boundHeight = _elementSize.Height; + } + else if (MudChartParent.Width.EndsWith("px") + && MudChartParent.Height.EndsWith("px") + && double.TryParse(MudChartParent.Width.AsSpan(0, MudChartParent.Width.Length - 2), out var width) + && double.TryParse(MudChartParent.Height.AsSpan(0, MudChartParent.Height.Length - 2), out var height)) + { + _boundWidth = width; + _boundHeight = height; + } } -#pragma warning restore CS0618 } [JSInvokable] public void OnElementSizeChanged(ElementSize elementSize) { + if (elementSize == null || elementSize.Timestamp <= _elementSize?.Timestamp) + return; + _elementSize = elementSize; if (!AxisChartOptions.MatchBoundsToSize) diff --git a/src/MudBlazor/Components/Chart/MudChartBase.cs b/src/MudBlazor/Components/Chart/MudChartBase.cs index a2399d500209..f9b8e778bcc9 100644 --- a/src/MudBlazor/Components/Chart/MudChartBase.cs +++ b/src/MudBlazor/Components/Chart/MudChartBase.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System.Globalization; -using System.Runtime.InteropServices.JavaScript; using Microsoft.AspNetCore.Components; using MudBlazor.Utilities; @@ -133,14 +132,6 @@ internal void SetSelectedIndex(int index) [Category(CategoryTypes.Chart.Behavior)] public EventCallback SelectedIndexChanged { get; set; } - protected string ToS(double d, string? format = null) - { - if (string.IsNullOrEmpty(format)) - return Math.Round(d, 4).ToString(CultureInfo.InvariantCulture); - - return Math.Round(d, 4).ToString(format); - } - /// /// Allows series to be hidden when is . /// diff --git a/src/MudBlazor/Components/Chart/Parts/ChartTooltip.razor b/src/MudBlazor/Components/Chart/Parts/ChartTooltip.razor index cf9a0819d0be..cf2bccf50ce4 100644 --- a/src/MudBlazor/Components/Chart/Parts/ChartTooltip.razor +++ b/src/MudBlazor/Components/Chart/Parts/ChartTooltip.razor @@ -7,24 +7,24 @@ } - - + - - - @Title + + @Title @if (hasSubtitle) { - @Subtitle + @Subtitle } diff --git a/src/MudBlazor/Components/Chart/Parts/ChartTooltip.razor.cs b/src/MudBlazor/Components/Chart/Parts/ChartTooltip.razor.cs index 7981ee786063..2ed28bdf3e9b 100644 --- a/src/MudBlazor/Components/Chart/Parts/ChartTooltip.razor.cs +++ b/src/MudBlazor/Components/Chart/Parts/ChartTooltip.razor.cs @@ -2,11 +2,12 @@ using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; +#nullable enable namespace MudBlazor.Charts; public partial class ChartTooltip : ComponentBase { - private double _boxWidth = -1; + private double _boxWidth = 40; private ElementReference? _hoverTextTitle = null; [Inject] @@ -62,7 +63,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) // Uses interop to get the bounding box of the title text to determine the width of the tooltip box var bboxTitle = await JsRuntime.InvokeAsync("mudGetSvgBBox", _hoverTextTitle); - _boxWidth = Math.Max(bboxTitle.Width, 30) + 10; // Minimum width for the text of 30px with 10px padding (5px each side) + _boxWidth = Math.Max(bboxTitle?.Width ?? 0, 30) + 10; // Minimum width for the text of 30px with 10px padding (5px each side) StateHasChanged(); } diff --git a/src/MudBlazor/Components/DataGrid/HeaderCell.razor.cs b/src/MudBlazor/Components/DataGrid/HeaderCell.razor.cs index c7b8c43c4c0b..ced148ee1d38 100644 --- a/src/MudBlazor/Components/DataGrid/HeaderCell.razor.cs +++ b/src/MudBlazor/Components/DataGrid/HeaderCell.razor.cs @@ -356,7 +356,7 @@ internal async Task SortChangedAsync(MouseEventArgs args) DataGrid.DropContainerHasChanged(); - if (args.CtrlKey && DataGrid.SortMode == SortMode.Multiple) + if ((args.MetaKey || args.CtrlKey) && DataGrid.SortMode == SortMode.Multiple) await InvokeAsync(() => DataGrid.ExtendSortAsync(Column.PropertyName, SortDirection, Column.GetLocalSortFunc(), Column.Comparer)); else await InvokeAsync(() => DataGrid.SetSortAsync(Column.PropertyName, SortDirection, Column.GetLocalSortFunc(), Column.Comparer)); diff --git a/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs b/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs index 24186d8b8b1e..88ebfdf74f6d 100644 --- a/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs +++ b/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs @@ -1269,6 +1269,8 @@ private async Task OnSelectedItemChangedAsync(ParameterChangedEventArgs args) } await _selectedItemsState.SetValueAsync(Selection); + // doesn't fire due to hashset reference not changing, so fire it manually + await SelectedItemsChanged.InvokeAsync(Selection); } private void OnSelectedItemsChanged(ParameterChangedEventArgs> args) @@ -1529,11 +1531,31 @@ internal async Task SetSelectedItemAsync(bool value, T item) } await _selectedItemsState.SetValueAsync(Selection); + // manually invoke due to ParameterState not seeing state change with HashSet + await InvokeAsync(async () => await SelectedItemsChanged.InvokeAsync(Selection)); await InvokeAsync(() => SelectedItemsChangedEvent?.Invoke(Selection)); await InvokeAsync(StateHasChanged); } + /// + /// Set the currently selected item in the data grid. + /// + /// The item to select. + /// + /// When is true and is true, the are updated. The is also updated. + /// + public Task SetSelectedItemAsync(T item) + { + if (!SelectOnRowClick) + { + return Task.CompletedTask; + } + + var isSelected = Selection.Contains(item); + return SetSelectedItemAsync(!isSelected, item); + } + internal async Task SetSelectAllAsync(bool value) { // nothing should happen if multiselection is false @@ -1544,12 +1566,15 @@ internal async Task SetSelectAllAsync(bool value) ? ServerItems : FilteredItems; + Selection.Clear(); if (value) - Selection = new HashSet(items, Comparer); - else - Selection.Clear(); + { + Selection.UnionWith(items); + } await InvokeAsync(async () => await _selectedItemsState.SetValueAsync(Selection)); + // manually invoke due to ParameterState not seeing state change with HashSet + await InvokeAsync(async () => await SelectedItemsChanged.InvokeAsync(Selection)); await InvokeAsync(() => SelectedItemsChangedEvent?.Invoke(Selection)); await InvokeAsync(() => SelectedAllItemsChangedEvent?.Invoke(value)); @@ -1636,9 +1661,9 @@ internal async Task OnRowClickedAsync(MouseEventArgs args, T item, int rowIndex) await SetSelectedItemAsync(item); } - internal async Task OnContextMenuClickedAsync(MouseEventArgs args, T item, int rowIndex) + internal Task OnContextMenuClickedAsync(MouseEventArgs args, T item, int rowIndex) { - await RowContextMenuClick.InvokeAsync(new DataGridRowClickEventArgs(args, item, rowIndex)); + return RowContextMenuClick.InvokeAsync(new DataGridRowClickEventArgs(args, item, rowIndex)); } /// @@ -1831,43 +1856,6 @@ private void VirtualItemsProviderInitialize() }; } - /// - /// Set the currently selected item in the data grid. - /// - /// The item to select. - /// - /// When is true and is true, the are updated. The is also updated. - /// - public async Task SetSelectedItemAsync(T item) - { - if (!SelectOnRowClick) - return; - - // this is toggle logic (unselect if selected) - if (!Selection.Remove(item)) - { - Selection.Add(item); - } - else if (!MultiSelection) - { - await _selectedItemState.SetValueAsync(default); - return; - } - - if (MultiSelection) - { - await _selectedItemsState.SetValueAsync(Selection); - SelectedItemsChangedEvent?.Invoke(Selection); - } - else - { - Selection.Remove(_selectedItemState.Value); - } - - await _selectedItemState.SetValueAsync(item); - await _selectedItemsState.SetValueAsync(Selection); - } - /// /// Starts editing for the specified item. /// diff --git a/src/MudBlazor/Components/DatePicker/MudBaseDatePicker.razor b/src/MudBlazor/Components/DatePicker/MudBaseDatePicker.razor index 465232ff0da1..75787bb7349d 100644 --- a/src/MudBlazor/Components/DatePicker/MudBaseDatePicker.razor +++ b/src/MudBlazor/Components/DatePicker/MudBaseDatePicker.razor @@ -113,11 +113,12 @@ @GetWeekNumber(tempMonth, tempWeek)
} + var wasMaxValue = false; @foreach (var day in GetWeek(tempMonth, tempWeek)) { var tempId = ++dayId; - @if (tempId != 0 || !firstMonthFirstYear) + @if ((tempId != 0 || !firstMonthFirstYear) && !wasMaxValue) { var selectedDay = !firstMonthFirstYear ? day : day.AddDays(-1); } + wasMaxValue = day == Culture.Calendar.MaxSupportedDateTime; } }
diff --git a/src/MudBlazor/Components/DatePicker/MudBaseDatePicker.razor.cs b/src/MudBlazor/Components/DatePicker/MudBaseDatePicker.razor.cs index 06cb4f7e1163..f454afbe38f6 100644 --- a/src/MudBlazor/Components/DatePicker/MudBaseDatePicker.razor.cs +++ b/src/MudBlazor/Components/DatePicker/MudBaseDatePicker.razor.cs @@ -342,6 +342,11 @@ protected DateTime GetMonthStart(int month) { return Culture.Calendar.MinSupportedDateTime; } + + if (_picker_month.HasValue && _picker_month.Value.Year == 9999 && _picker_month.Value.Month == 12 && month >= 1) + { + return Culture.Calendar.MaxSupportedDateTime; + } return Culture.Calendar.AddMonths(monthStartDate, month); } @@ -371,9 +376,17 @@ protected IEnumerable GetWeek(int month, int index) if (index is < 0 or > 5) throw new ArgumentException("Index must be between 0 and 5"); var month_first = GetMonthStart(month); - var week_first = month_first.AddDays(index * 7).StartOfWeek(GetFirstDayOfWeek()); - for (var i = 0; i < 7; i++) - yield return week_first.AddDays(i); + if ((Culture.Calendar.MaxSupportedDateTime - month_first).Days >= index * 7) + { + var week_first = month_first.AddDays(index * 7).StartOfWeek(GetFirstDayOfWeek()); + for (var i = 0; i < 7; i++) + { + if ((Culture.Calendar.MaxSupportedDateTime - week_first).Days >= i) + yield return week_first.AddDays(i); + else + yield return Culture.Calendar.MaxSupportedDateTime; + } + } } private string GetWeekNumber(int month, int index) diff --git a/src/MudBlazor/Components/DatePicker/MudDateRangePicker.razor.cs b/src/MudBlazor/Components/DatePicker/MudDateRangePicker.razor.cs index c097137ed443..9be9b1091b4c 100644 --- a/src/MudBlazor/Components/DatePicker/MudDateRangePicker.razor.cs +++ b/src/MudBlazor/Components/DatePicker/MudDateRangePicker.razor.cs @@ -285,20 +285,30 @@ private static bool IsDateOutOfRange(DateTime date, DateTime selectedDate, DateR private DateTime GetMaxSelectableDate(DateTime startDate, int maxDays) { var validDayCount = 1; + var lastValidDate = startDate; var maxDate = startDate.AddDays(1); while (validDayCount < maxDays) { if (!IsDateDisabledFunc(maxDate)) + { validDayCount++; + lastValidDate = maxDate; + } if (validDayCount == maxDays) break; + if (maxDate.Date > MaxDate.GetValueOrDefault(startDate.AddYears(50)).Date) + break; + + if (maxDate.Date == DateTime.MaxValue.Date) + break; + maxDate = maxDate.AddDays(1); } - return maxDate; + return lastValidDate; } /// diff --git a/src/MudBlazor/Components/Dialog/MudDialog.razor.cs b/src/MudBlazor/Components/Dialog/MudDialog.razor.cs index 3acfe99230c8..683bd7b335a4 100644 --- a/src/MudBlazor/Components/Dialog/MudDialog.razor.cs +++ b/src/MudBlazor/Components/Dialog/MudDialog.razor.cs @@ -2,6 +2,7 @@ // Copyright (c) 2019 Blazored - See https://github.com/Blazored // Copyright (c) 2020 MudBlazor Contributors +using System.Threading; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using MudBlazor.Interfaces; @@ -25,6 +26,7 @@ public partial class MudDialog : MudComponentBase { private IDialogReference? _reference; private readonly ParameterState _visibleState; + private SemaphoreSlim _showLock = new SemaphoreSlim(1, 1); /// /// Creates a new instance. @@ -195,44 +197,49 @@ public MudDialog() /// The reference to the displayed instance of this dialog. public async Task ShowAsync(string? title = null, DialogOptions? options = null) { - if (!IsInline) + await _showLock.WaitAsync(); + try { - throw new InvalidOperationException("You can only show an inlined dialog."); - } + if (!IsInline) + { + throw new InvalidOperationException("You can only show an inlined dialog."); + } - if (_reference is not null) - { - await CloseAsync(); - } + if (_reference is not null) + return _reference; - var parameters = new DialogParameters - { - [nameof(Class)] = Class, - [nameof(Style)] = Style, - [nameof(Tag)] = Tag, - [nameof(UserAttributes)] = UserAttributes, - [nameof(TitleContent)] = TitleContent, - [nameof(DialogContent)] = DialogContent, - [nameof(DialogActions)] = DialogActions, - [nameof(OnBackdropClick)] = OnBackdropClick, - [nameof(Gutters)] = Gutters, - [nameof(TitleClass)] = TitleClass, - [nameof(ContentClass)] = ContentClass, - [nameof(ActionsClass)] = ActionsClass, - [nameof(ContentStyle)] = ContentStyle, - [nameof(DefaultFocus)] = DefaultFocus, - }; - - _reference = await DialogService.ShowAsync(title, parameters, options ?? Options); - - await _visibleState.SetValueAsync(true); - - // Do not await this! - _reference.Result.ContinueWith(t => + var parameters = new DialogParameters + { + [nameof(Class)] = Class, + [nameof(Style)] = Style, + [nameof(Tag)] = Tag, + [nameof(UserAttributes)] = UserAttributes, + [nameof(TitleContent)] = TitleContent, + [nameof(DialogContent)] = DialogContent, + [nameof(DialogActions)] = DialogActions, + [nameof(OnBackdropClick)] = OnBackdropClick, + [nameof(Gutters)] = Gutters, + [nameof(TitleClass)] = TitleClass, + [nameof(ContentClass)] = ContentClass, + [nameof(ActionsClass)] = ActionsClass, + [nameof(ContentStyle)] = ContentStyle, + [nameof(DefaultFocus)] = DefaultFocus, + }; + + _reference = await DialogService.ShowAsync(title, parameters, options ?? Options); + + await _visibleState.SetValueAsync(true); + + // Do not await this! + _reference.Result.ContinueWith(t => + { + return InvokeAsync(() => _visibleState.SetValueAsync(false)); + }).CatchAndLog(); + } + finally { - return InvokeAsync(() => _visibleState.SetValueAsync(false)); - }).CatchAndLog(); - + _showLock.Release(); + } return _reference; } diff --git a/src/MudBlazor/Components/Input/MudInput.razor.cs b/src/MudBlazor/Components/Input/MudInput.razor.cs index 784d0f280d74..c44408f9bb22 100644 --- a/src/MudBlazor/Components/Input/MudInput.razor.cs +++ b/src/MudBlazor/Components/Input/MudInput.razor.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Components; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.JSInterop; using MudBlazor.Utilities; @@ -18,6 +19,7 @@ public partial class MudInput : MudBaseInput private ElementReference _elementReference1; private readonly Lazy>> _dotNetReferenceLazy; + [DynamicDependency(nameof(CallOnBlurredAsync))] public MudInput() { _dotNetReferenceLazy = new Lazy>>(DotNetObjectReference.Create(this)); @@ -44,6 +46,7 @@ public MudInput() .AddClass("mud-icon-button-edge-end", Adornment == Adornment.End && HideSpinButtons) .AddClass("me-6", Adornment != Adornment.End && HideSpinButtons == false) .AddClass("mud-icon-button-edge-margin-end", Adornment != Adornment.End && HideSpinButtons) + .AddClass("mud-no-activator") .Build(); internal override InputType GetInputType() => InputType; @@ -371,9 +374,13 @@ or InputType.Time /// protected override async ValueTask DisposeAsyncCore() { - if (AutoGrow && IsJSRuntimeAvailable) + if (IsJSRuntimeAvailable) { - await JsRuntime.InvokeVoidAsyncWithErrorHandling("mudInputAutoGrow.destroy", ElementReference); + await JsRuntime.InvokeVoidAsyncWithErrorHandling("mudElementRef.removeOnBlurEvent", ElementReference, _dotNetReferenceLazy); + if (AutoGrow) + { + await JsRuntime.InvokeVoidAsyncWithErrorHandling("mudInputAutoGrow.destroy", ElementReference); + } } await base.DisposeAsyncCore(); @@ -386,7 +393,6 @@ public async Task CallOnBlurredAsync() if (!_isFocused) return; - StateHasChanged(); await OnBlurredAsync(new FocusEventArgs { Type = "jsBlur.OnBlur" }); } } diff --git a/src/MudBlazor/Components/Input/MudInputAdornment.razor b/src/MudBlazor/Components/Input/MudInputAdornment.razor index 3eedade1fdb3..00e5c944c8fc 100644 --- a/src/MudBlazor/Components/Input/MudInputAdornment.razor +++ b/src/MudBlazor/Components/Input/MudInputAdornment.razor @@ -14,7 +14,7 @@ { @if (AdornmentClick.HasDelegate) { - void IActivatable.Activate(object activator, MouseEventArgs args) { - _ = ToggleMenuAsync(args); + if (activator is MudBaseButton activatorButton && + (activatorButton.Class?.Contains("mud-no-activator") ?? false)) + { + return; + } + ToggleMenuAsync(args).CatchAndLog(); } + /// /// Disposes managed and unmanaged resources. /// diff --git a/src/MudBlazor/Components/Select/MudSelect.razor b/src/MudBlazor/Components/Select/MudSelect.razor index 018a0573b68d..865e66b85c82 100644 --- a/src/MudBlazor/Components/Select/MudSelect.razor +++ b/src/MudBlazor/Components/Select/MudSelect.razor @@ -3,7 +3,7 @@ @inherits MudBaseInput -
+
+ @if (FitContent) + { + + @{ + var value = _longestItem is { Value: not null } ? ToStringFunc ?.Invoke(_longestItem.Value) ?? Converter.Set(_longestItem.Value) : string.Empty; + var useLabel = Label is not null && Placeholder is null && !ShrinkLabel + && (Adornment != Adornment.Start || (AdornmentIcon is null && AdornmentText is null)); + + var defaultValue = useLabel ? Label : Placeholder; + } + @if (defaultValue?.Length > value!.Length) + { + @(MultiSelectionTextFunc?.Invoke([defaultValue]) ?? defaultValue) + } + else if (_longestItem?.ChildContent is null || MultiSelectionTextFunc is not null) + { + @(MultiSelectionTextFunc?.Invoke([value!]) ?? value!) + } + else + { + @_longestItem.ChildContent + } + @AdornmentText + @if (AdornmentText is null) + { + + } + @if (Clearable) + { + + } + + } - diff --git a/src/MudBlazor/Components/Select/MudSelect.razor.cs b/src/MudBlazor/Components/Select/MudSelect.razor.cs index 864bbc97ca76..5ccc0430a147 100644 --- a/src/MudBlazor/Components/Select/MudSelect.razor.cs +++ b/src/MudBlazor/Components/Select/MudSelect.razor.cs @@ -23,6 +23,8 @@ public partial class MudSelect : MudBaseInput, IMudSelect, IMudShadowSelec private string? _activeItemId; private bool? _selectAllChecked; private string? _multiSelectionText; + private int _longestItemLength; + private MudSelectItem? _longestItem; private IEqualityComparer? _comparer; private TaskCompletionSource? _renderComplete; private MudInput _elementReference = null!; @@ -36,6 +38,7 @@ public partial class MudSelect : MudBaseInput, IMudSelect, IMudShadowSelec protected string OuterClassname => new CssBuilder("mud-select") .AddClass("mud-width-full", FullWidth) + .AddClass("mud-width-content", FitContent && !FullWidth) .AddClass(OuterClass) .Build(); @@ -49,6 +52,14 @@ public partial class MudSelect : MudBaseInput, IMudSelect, IMudShadowSelec .AddClass(InputClass) .Build(); + protected string FillerClassname => + new CssBuilder("mud-select-filler") + .AddClass("d-inline-block") + .AddClass("invisible") + .AddClass("mx-2", Variant == Variant.Text) + .AddClass("mx-4", Variant != Variant.Text) + .Build(); + [Inject] private IKeyInterceptorService KeyInterceptorService { get; set; } = null!; @@ -224,6 +235,16 @@ private async Task SelectLastItem() [Category(CategoryTypes.Popover.Appearance)] public DropdownWidth RelativeWidth { get; set; } = DropdownWidth.Relative; + /// + /// Sets the container width to match its contents. + /// + /// + /// Defaults to false. Requires FullWidth to be false + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Appearance)] + public bool FitContent { get; set; } + /// /// The CSS classes applied to the outer div. /// @@ -1283,7 +1304,20 @@ public void RegisterShadowItem(MudSelectItem? item) { if (item == null || item.Value == null) return; + _shadowLookup[item.Value] = item; + + if (!FitContent) return; + + var stringValue = ToStringFunc?.Invoke(item.Value) ?? Converter.Set(item.Value); + + if (_longestItem is null || stringValue?.Length > _longestItemLength) + { + _longestItem = item; + _longestItemLength = stringValue?.Length ?? 0; + + StateHasChanged(); + } } /// diff --git a/src/MudBlazor/Components/Table/MudTableSortLabel.razor b/src/MudBlazor/Components/Table/MudTableSortLabel.razor index 8254c9f07ba7..9c72cd030eb0 100644 --- a/src/MudBlazor/Components/Table/MudTableSortLabel.razor +++ b/src/MudBlazor/Components/Table/MudTableSortLabel.razor @@ -3,21 +3,14 @@ @inherits MudComponentBase @implements IDisposable - + @if (!AppendIcon) { @ChildContent } @if (Enabled) { - @if (_direction != SortDirection.None) - { - - } - else - { - - } + } @if (AppendIcon) { diff --git a/src/MudBlazor/Components/Table/MudTableSortLabel.razor.cs b/src/MudBlazor/Components/Table/MudTableSortLabel.razor.cs index 5023c5de88f0..e2cf711ed040 100644 --- a/src/MudBlazor/Components/Table/MudTableSortLabel.razor.cs +++ b/src/MudBlazor/Components/Table/MudTableSortLabel.razor.cs @@ -1,6 +1,4 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Components; using MudBlazor.Utilities; @@ -16,10 +14,18 @@ public partial class MudTableSortLabel<[DynamicallyAccessedMembers(DynamicallyAc { private SortDirection _direction = SortDirection.None; - protected string Classname => new CssBuilder("mud-table-sort-label") - .AddClass("mud-button-root", Enabled) - .AddClass(Class) - .Build(); + protected string Classname => + new CssBuilder("mud-table-sort-label") + .AddClass("mud-clickable", Enabled) + .AddClass(Class) + .Build(); + + protected string SortIconClassname => + new CssBuilder("mud-table-sort-label-icon") + .AddClass("mud-direction-none", _direction == SortDirection.None) + .AddClass("mud-direction-asc", _direction == SortDirection.Ascending) + .AddClass("mud-direction-desc", _direction == SortDirection.Descending) + .Build(); /// /// The current state of the containing this sort label. @@ -173,20 +179,5 @@ internal void SetSortDirection(SortDirection dir) _direction = dir; StateHasChanged(); } - - private string GetSortIconClass() - { - if (_direction == SortDirection.Descending) - { - return "mud-table-sort-label-icon mud-direction-desc"; - } - - if (_direction == SortDirection.Ascending) - { - return "mud-table-sort-label-icon mud-direction-asc"; - } - - return "mud-table-sort-label-icon"; - } } } diff --git a/src/MudBlazor/Components/Tooltip/MudTooltip.razor b/src/MudBlazor/Components/Tooltip/MudTooltip.razor index b51243294822..3a9f2a6a2449 100644 --- a/src/MudBlazor/Components/Tooltip/MudTooltip.razor +++ b/src/MudBlazor/Components/Tooltip/MudTooltip.razor @@ -5,7 +5,7 @@ @ChildContent @if (ShowToolTip()) { - + @if (TooltipContent is not null) {
diff --git a/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs b/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs index 4138b507013b..56f438cee950 100644 --- a/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs +++ b/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Components; using MudBlazor.State; using MudBlazor.Utilities; +using MudBlazor.Utilities.Debounce; namespace MudBlazor { @@ -10,9 +11,16 @@ public partial class MudTooltip : MudComponentBase private readonly ParameterState _visibleState; private Origin _anchorOrigin; private Origin _transformOrigin; - + internal DebounceDispatcher _showDebouncer; + internal DebounceDispatcher _hideDebouncer; + internal double _previousDelay; + internal double _previousDuration; public MudTooltip() { + _previousDelay = Delay; + _showDebouncer = new DebounceDispatcher(TimeSpan.FromMilliseconds(Delay)); + _previousDuration = Duration; + _hideDebouncer = new DebounceDispatcher(TimeSpan.FromMilliseconds(Duration)); using var registerScope = CreateRegisterScope(); _visibleState = registerScope.RegisterParameter(nameof(Visible)) .WithParameter(() => Visible) @@ -162,21 +170,50 @@ public MudTooltip() /// /// Register and Show the Popover for the tooltip if it is not disabled, set to be visible, the content or Text is not empty or null /// - private bool ShowToolTip() + internal bool ShowToolTip() { if (_anchorOrigin == Origin.TopLeft || _transformOrigin == Origin.TopLeft) ConvertPlacement(); return !Disabled && _visibleState.Value && (TooltipContent is not null || !string.IsNullOrEmpty(Text)); } - private Task HandlePointerEnterAsync() + protected override void OnParametersSet() + { + base.OnParametersSet(); + + if (Math.Abs(_previousDelay - Delay) > .001) + { + _showDebouncer = new DebounceDispatcher(TimeSpan.FromMilliseconds(Delay)); + _previousDelay = Delay; + } + + if (Math.Abs(_previousDuration - Duration) > .001) + { + _hideDebouncer = new DebounceDispatcher(TimeSpan.FromMilliseconds(Duration)); + _previousDuration = Duration; + } + } + + internal Task HandlePointerEnterAsync() { - return ShowOnHover ? _visibleState.SetValueAsync(true) : Task.CompletedTask; + if (!ShowOnHover) + { + return Task.CompletedTask; + } + + _hideDebouncer.Cancel(); + return _showDebouncer.DebounceAsync(() => _visibleState.SetValueAsync(true)); } - private Task HandlePointerLeaveAsync() + internal Task HandlePointerLeaveAsync() { - return ShowOnHover ? _visibleState.SetValueAsync(false) : Task.CompletedTask; + if (!ShowOnHover) + { + return Task.CompletedTask; + } + + _showDebouncer.Cancel(); + return _hideDebouncer.DebounceAsync(() => _visibleState.SetValueAsync(false)); } private Task HandleFocusInAsync() diff --git a/src/MudBlazor/Components/TreeView/MudTreeViewItem.razor.cs b/src/MudBlazor/Components/TreeView/MudTreeViewItem.razor.cs index 584c9f6607af..a90040c2dbf7 100644 --- a/src/MudBlazor/Components/TreeView/MudTreeViewItem.razor.cs +++ b/src/MudBlazor/Components/TreeView/MudTreeViewItem.razor.cs @@ -438,8 +438,10 @@ private async Task OnItemClickedAsync(MouseEventArgs ev) } if (!GetReadOnly()) { - Debug.Assert(MudTreeRoot != null); - await MudTreeRoot.OnItemClickAsync(this); + if (MudTreeRoot is not null) + { + await MudTreeRoot.OnItemClickAsync(this); + } } await OnClick.InvokeAsync(ev); } @@ -457,8 +459,10 @@ private async Task OnItemDoubleClickedAsync(MouseEventArgs ev) } if (!GetReadOnly()) { - Debug.Assert(MudTreeRoot != null); - await MudTreeRoot.OnItemClickAsync(this); + if (MudTreeRoot is not null) + { + await MudTreeRoot.OnItemClickAsync(this); + } } await OnDoubleClick.InvokeAsync(ev); } diff --git a/src/MudBlazor/Extensions/ElementReferenceExtensions.cs b/src/MudBlazor/Extensions/ElementReferenceExtensions.cs index ecffbbfc9d49..0327ca85b78f 100644 --- a/src/MudBlazor/Extensions/ElementReferenceExtensions.cs +++ b/src/MudBlazor/Extensions/ElementReferenceExtensions.cs @@ -1,7 +1,5 @@ -using System; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; using MudBlazor.Interop; @@ -71,5 +69,10 @@ public static ValueTask RemoveDefaultPreventingHandlers(this ElementReference el this ElementReference elementReference, DotNetObjectReference obj) where T : class => elementReference.GetJSRuntime()?.InvokeVoidAsync("mudElementRef.addOnBlurEvent", elementReference, obj) ?? ValueTask.CompletedTask; + + public static ValueTask MudDetachBlurEventWithJS<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>( + this ElementReference elementReference, + DotNetObjectReference obj) where T : class => + elementReference.GetJSRuntime()?.InvokeVoidAsync("mudElementRef.removeOnBlurEvent", elementReference, obj) ?? ValueTask.CompletedTask; } } diff --git a/src/MudBlazor/Extensions/ResizeOptionsExtensions.cs b/src/MudBlazor/Extensions/ResizeOptionsExtensions.cs index cc27300c4f1f..3f4219d7d78b 100644 --- a/src/MudBlazor/Extensions/ResizeOptionsExtensions.cs +++ b/src/MudBlazor/Extensions/ResizeOptionsExtensions.cs @@ -2,9 +2,6 @@ // MudBlazor licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; -using System.Linq; using MudBlazor.Services; namespace MudBlazor.Extensions; diff --git a/src/MudBlazor/GlobalUsings.cs b/src/MudBlazor/GlobalUsings.cs new file mode 100644 index 000000000000..e422b2651961 --- /dev/null +++ b/src/MudBlazor/GlobalUsings.cs @@ -0,0 +1 @@ +global using static MudBlazor.Utilities.StringHelpers; diff --git a/src/MudBlazor/Interop/ElementSize.cs b/src/MudBlazor/Interop/ElementSize.cs index 393c93e3db44..6417f40ecef7 100644 --- a/src/MudBlazor/Interop/ElementSize.cs +++ b/src/MudBlazor/Interop/ElementSize.cs @@ -14,4 +14,9 @@ public class ElementSize /// The width of the Element. ///
public required double Width { get; init; } + + /// + /// The timestamp of the size change, used to help filter out of order events for server mode. + /// + public long Timestamp { get; set; } } diff --git a/src/MudBlazor/Services/Browser/BrowserViewportService.cs b/src/MudBlazor/Services/Browser/BrowserViewportService.cs index cbb4c1510d90..16ff3effeb1b 100644 --- a/src/MudBlazor/Services/Browser/BrowserViewportService.cs +++ b/src/MudBlazor/Services/Browser/BrowserViewportService.cs @@ -101,22 +101,16 @@ public async Task SubscribeAsync(IBrowserViewportObserver observer, bool fireImm optionsClone.BreakpointDefinitions = BreakpointGlobalOptions.GetDefaultOrUserDefinedBreakpointDefinition(optionsClone, ResizeOptions); var subscription = await CreateJavaScriptListener(optionsClone, observer.Id); - if (_observerManager.IsSubscribed(subscription)) - { - // Only re-subscribe - _observerManager.Subscribe(subscription, observer); - } - else + + if (!_observerManager.TryGetOrAddSubscription(subscription, observer, out var newObserver)) { - // Subscribe and fire if necessary - _observerManager.Subscribe(subscription, observer); if (fireImmediately) { // Not waiting for Browser Size to change and RaiseOnResized to fire and post event with current breakpoint and browser window size var latestWindowSize = await GetCurrentBrowserWindowSizeAsync(); var latestBreakpoint = await GetCurrentBreakpointAsync(); // Notify only current subscription - await observer.NotifyBrowserViewportChangeAsync(new BrowserViewportEventArgs(subscription.JavaScriptListenerId, latestWindowSize, latestBreakpoint, isImmediate: true)); + await newObserver.NotifyBrowserViewportChangeAsync(new BrowserViewportEventArgs(subscription.JavaScriptListenerId, latestWindowSize, latestBreakpoint, isImmediate: true)); } } } diff --git a/src/MudBlazor/Services/KeyInterceptor/KeyInterceptorService.cs b/src/MudBlazor/Services/KeyInterceptor/KeyInterceptorService.cs index 0308e12b124e..7eecf6389578 100644 --- a/src/MudBlazor/Services/KeyInterceptor/KeyInterceptorService.cs +++ b/src/MudBlazor/Services/KeyInterceptor/KeyInterceptorService.cs @@ -54,17 +54,9 @@ public async Task SubscribeAsync(IKeyInterceptorObserver observer, KeyIntercepto return; } - if (!_observerManager.IsSubscribed(observer.ElementId)) + if (!_observerManager.TryGetOrAddSubscription(observer.ElementId, observer, out var newObserver)) { - var isConnected = await _keyInterceptorInterop.Connect(_dotNetReferenceLazy.Value, observer.ElementId, options); - if (isConnected) - { - _observerManager.Subscribe(observer.ElementId, observer); - } - } - else - { - _observerManager.Subscribe(observer.ElementId, observer); + _ = await _keyInterceptorInterop.Connect(_dotNetReferenceLazy.Value, newObserver.ElementId, options); } } diff --git a/src/MudBlazor/Styles/components/_input.scss b/src/MudBlazor/Styles/components/_input.scss index df260c064e05..b795a92f12dd 100644 --- a/src/MudBlazor/Styles/components/_input.scss +++ b/src/MudBlazor/Styles/components/_input.scss @@ -370,7 +370,7 @@ } &.mud-input-root-margin-dense { - margin-top: 3px; + padding-top: 3px; } &.mud-input-root-type-search { @@ -429,15 +429,15 @@ } &.mud-input-root-adorned-start { - margin-left: 0; - margin-inline-start: 0; - margin-inline-end: 12px; + padding-left: 0; + padding-inline-start: 0; + padding-inline-end: 12px; } &.mud-input-root-adorned-end { - margin-right: 0; - margin-inline-end: unset; - margin-inline-start: 12px; + padding-right: 0; + padding-inline-end: unset; + padding-inline-start: 12px; } } @@ -480,15 +480,15 @@ } &.mud-input-root-adorned-start { - margin-left: 0; - margin-inline-start: 0; - margin-inline-end: 14px; + padding-left: 0; + padding-inline-start: 0; + padding-inline-end: 14px; } &.mud-input-root-adorned-end { - margin-right: 0; - margin-inline-end: 0; - margin-inline-start: 14px; + padding-right: 0; + padding-inline-end: 0; + padding-inline-start: 14px; } } } diff --git a/src/MudBlazor/Styles/components/_popover.scss b/src/MudBlazor/Styles/components/_popover.scss index 8ddd8aa6b43e..8294cff79cea 100644 --- a/src/MudBlazor/Styles/components/_popover.scss +++ b/src/MudBlazor/Styles/components/_popover.scss @@ -23,6 +23,10 @@ transition-delay: 0ms !important; } + &:empty { + box-shadow: none !important; + } + .mud-list { max-height: inherit; overflow-y: auto; @@ -60,7 +64,7 @@ } .mud-select { - .mud-popover-cascading-value { + .mud-popover-cascading-value { z-index: calc(var(--mud-zindex-select) + 5); } } diff --git a/src/MudBlazor/Styles/components/_select.scss b/src/MudBlazor/Styles/components/_select.scss index 9cb64bebc7eb..cd5f8b064281 100644 --- a/src/MudBlazor/Styles/components/_select.scss +++ b/src/MudBlazor/Styles/components/_select.scss @@ -97,3 +97,12 @@ border-bottom: 1px solid lightgray; padding-bottom: 18px; } + +.mud-select-filler { + white-space: nowrap; + height: 0px; +} + +.mud-width-content { + max-width: min-content; +} diff --git a/src/MudBlazor/Styles/components/_switch.scss b/src/MudBlazor/Styles/components/_switch.scss index 0f9238b52f9d..677224940383 100644 --- a/src/MudBlazor/Styles/components/_switch.scss +++ b/src/MudBlazor/Styles/components/_switch.scss @@ -42,7 +42,7 @@ z-index: -1; transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,background-color 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; border-radius: 9px; - background-color: var(--mud-palette-black); + background-color: var(--mud-palette-action-default); } } diff --git a/src/MudBlazor/Styles/components/_table.scss b/src/MudBlazor/Styles/components/_table.scss index 1ba5f58d0704..24613149afb3 100644 --- a/src/MudBlazor/Styles/components/_table.scss +++ b/src/MudBlazor/Styles/components/_table.scss @@ -31,18 +31,6 @@ color: var(--mud-palette-text-primary); font-weight: 500; line-height: 1.5rem; - - & .mud-button-root { - @media(hover: hover) and (pointer: fine) { - &:hover { - color: var(--mud-palette-action-default); - - .mud-table-sort-label-icon { - opacity: 0.8; - } - } - } - } } } @@ -65,15 +53,30 @@ } .mud-table-sort-label { - user-select: auto; display: inline-flex; align-items: center; flex-direction: inherit; justify-content: flex-start; + &.mud-clickable { + cursor: pointer; + user-select: none; + transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + + @media(hover: hover) and (pointer: fine) { + &:hover { + opacity: 0.64; + + .mud-table-sort-label-icon { + opacity: 1; + } + } + } + } + .mud-table-sort-label-icon { font-size: 18px; - transition: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,transform 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,transform 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; margin-left: 4px; user-select: none; margin-right: 4px; @@ -101,7 +104,6 @@ } - .mud-table-cell { display: table-cell; padding: 16px; diff --git a/src/MudBlazor/TScripts/mudElementReference.js b/src/MudBlazor/TScripts/mudElementReference.js index 478b116321b3..a810cde4813e 100644 --- a/src/MudBlazor/TScripts/mudElementReference.js +++ b/src/MudBlazor/TScripts/mudElementReference.js @@ -150,7 +150,8 @@ class MudElementReference { // ios doesn't trigger Blazor/React/Other dom style blur event so add a base event listener here // that will trigger with IOS Done button and regular blur events addOnBlurEvent(element, dotNetReference) { - function onFocusOut(e) { + element._mudBlurHandler = function (e) { + if (!element) return; e.preventDefault(); element.blur(); if (dotNetReference) { @@ -160,7 +161,13 @@ class MudElementReference { console.error("No dotNetReference found for iosKeyboardFocus"); } } - element.addEventListener('blur', onFocusOut); + element.addEventListener('blur', element._mudBlurHandler); + } + // dispose event + removeOnBlurEvent(element, dotnetRef) { + if (!element || !element._mudBlurHandler) return; + element.removeEventListener('blur', element._mudBlurHandler); + delete element._mudBlurHandler; } }; window.mudElementRef = new MudElementReference(); diff --git a/src/MudBlazor/TScripts/mudHelpers.js b/src/MudBlazor/TScripts/mudHelpers.js index f589e7d16579..e997a62732a8 100644 --- a/src/MudBlazor/TScripts/mudHelpers.js +++ b/src/MudBlazor/TScripts/mudHelpers.js @@ -88,6 +88,8 @@ window.serializeParameter = (data, spec) => { // mudGetSvgBBox is a helper function to get the size of an svgElement window.mudGetSvgBBox = (svgElement) => { + if (svgElement == null) return null; + const bbox = svgElement.getBBox(); return { x: bbox.x, @@ -108,13 +110,13 @@ window.mudObserveElementSize = (dotNetReference, element, functionName = 'OnElem // Throttled notification function. const throttledNotify = (width, height) => { - const now = Date.now(); - const timeSinceLast = now - lastNotifiedTime; + const timestamp = Date.now(); + const timeSinceLast = timestamp - lastNotifiedTime; if (timeSinceLast >= debounceMillis) { // Enough time has passed, notify immediately. - lastNotifiedTime = now; + lastNotifiedTime = timestamp; try { - dotNetReference.invokeMethodAsync(functionName, { width, height }); + dotNetReference.invokeMethodAsync(functionName, { width, height, timestamp }); } catch (error) { this.logger("[MudBlazor] Error in mudObserveElementSize:", { error }); @@ -128,7 +130,7 @@ window.mudObserveElementSize = (dotNetReference, element, functionName = 'OnElem lastNotifiedTime = Date.now(); scheduledCall = null; try { - dotNetReference.invokeMethodAsync(functionName, { width, height }); + dotNetReference.invokeMethodAsync(functionName, { width, height, timestamp }); } catch (error) { this.logger("[MudBlazor] Error in mudObserveElementSize:", { error }); @@ -139,6 +141,8 @@ window.mudObserveElementSize = (dotNetReference, element, functionName = 'OnElem // Create the ResizeObserver to notify on size changes. const resizeObserver = new ResizeObserver(entries => { + if (element.isConnected === false) { return; } // Element is no longer in the DOM. + // Use the last entry's contentRect (or element's client dimensions). let width = element.clientWidth; let height = element.clientHeight; diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index 249450c9625f..318c46e43e0c 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -427,6 +427,10 @@ window.mudpopoverHelper = { }, updatePopoverOverlay: function (popoverContentNode) { + // tooltips don't have an overlay + if (popoverContentNode.classList.contains("mud-tooltip")) { + return; + } // set any associated overlay to equal z-index const provider = popoverContentNode.closest('.mud-popover-provider'); if (provider && popoverContentNode.classList.contains("mud-popover")) { @@ -635,11 +639,10 @@ class MudPopover { observer.observe(popoverContentNode, config); - // Optimize resize observer - const throttledResize = window.mudpopoverHelper.rafThrottle(entries => { + const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { const target = entry.target; - for (let childNode of target.childNodes) { + for (const childNode of target.childNodes) { if (childNode.id && childNode.id.startsWith('popover-')) { window.mudpopoverHelper.placePopover(childNode); } @@ -647,16 +650,16 @@ class MudPopover { } }); - const resizeObserver = new ResizeObserver(throttledResize); resizeObserver.observe(popoverNode.parentNode); - const throttledContent = window.mudpopoverHelper.rafThrottle(entries => { + const contentNodeObserver = new ResizeObserver(entries => { for (let entry of entries) { - window.mudpopoverHelper.placePopoverByNode(entry.target); + const target = entry.target; + if (target) + window.mudpopoverHelper.placePopoverByNode(target); } }); - const contentNodeObserver = new ResizeObserver(throttledContent); contentNodeObserver.observe(popoverContentNode); this.map[id] = { diff --git a/src/MudBlazor/Utilities/Debounce/DebounceDispatcher.cs b/src/MudBlazor/Utilities/Debounce/DebounceDispatcher.cs index 7ca3b97cefe1..827bda1c8fd5 100644 --- a/src/MudBlazor/Utilities/Debounce/DebounceDispatcher.cs +++ b/src/MudBlazor/Utilities/Debounce/DebounceDispatcher.cs @@ -2,10 +2,6 @@ // MudBlazor licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Threading; -using System.Threading.Tasks; - namespace MudBlazor.Utilities.Debounce; #nullable enable @@ -15,7 +11,7 @@ namespace MudBlazor.Utilities.Debounce; ///
internal class DebounceDispatcher { - private readonly int _interval; + private readonly TimeSpan _interval; private CancellationTokenSource? _cancellationTokenSource; /// @@ -23,6 +19,15 @@ internal class DebounceDispatcher /// /// The minimum interval in milliseconds between invocations of the debounced function. public DebounceDispatcher(int interval) + : this(TimeSpan.FromMilliseconds(interval)) + { + } + + /// + /// Initializes a new instance of the class with the specified interval. + /// + /// The minimum interval as a between invocations of the debounced function. + public DebounceDispatcher(TimeSpan interval) { _interval = interval; } @@ -39,7 +44,7 @@ public DebounceDispatcher(int interval) /// A Task representing the asynchronous operation with minimal delay. public async Task DebounceAsync(Func action, CancellationToken cancellationToken = default) { - // ReSharper disable MethodHasAsyncOverload (not available in .net7) + // ReSharper disable MethodHasAsyncOverload (we should not use it as it behaves differently) // Cancel the previous debounce task if it exists _cancellationTokenSource?.Cancel(); // ReSharper restore MethodHasAsyncOverload @@ -59,4 +64,9 @@ public async Task DebounceAsync(Func action, CancellationToken cancellatio // If the task was canceled, ignore it } } + + /// + /// Cancel the current debounced task. + /// + public void Cancel() => _cancellationTokenSource?.Cancel(); } diff --git a/src/MudBlazor/Utilities/ObserverManager/ObserverManager.cs b/src/MudBlazor/Utilities/ObserverManager/ObserverManager.cs index ed76188cd1b2..501c2a1aa1ce 100644 --- a/src/MudBlazor/Utilities/ObserverManager/ObserverManager.cs +++ b/src/MudBlazor/Utilities/ObserverManager/ObserverManager.cs @@ -25,15 +25,23 @@ namespace MudBlazor.Utilities.ObserverManager; /// internal class ObserverManager : IEnumerable where TIdentity : notnull { - private readonly ConcurrentDictionary _observers = new(); + private readonly ConcurrentDictionary _observers; private readonly ILogger _log; /// /// Initializes a new instance of the class. /// - public ObserverManager(ILogger log) + public ObserverManager(ILogger log) : this(log, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + public ObserverManager(ILogger log, IEqualityComparer? comparer) { _log = log ?? throw new ArgumentNullException(nameof(log)); + _observers = new(comparer); } /// @@ -84,16 +92,13 @@ public IEnumerable FindObserverIdentities(Func predicate(kvp.Key, kvp.Value.Observer)).Select(kvp => kvp.Key); /// - /// Ensures that the provided is subscribed, renewing its subscription. + /// Tries to get the existing subscription for the specified identity, or subscribes the observer if it does not exist. /// - /// - /// The observer's identity. - /// - /// - /// The observer. - /// - /// A delegate callback throws an exception. - public void Subscribe(TIdentity id, TObserver observer) + /// The identity of the observer. + /// The observer to subscribe if it does not already exist. + /// When this method returns, contains the observer associated with the specified identity, whether it was already subscribed or newly subscribed. + /// True if the observer was already subscribed; otherwise, false. + public bool TryGetOrAddSubscription(TIdentity id, TObserver observer, out TObserver newObserver) { // Add or update the subscription. if (_observers.TryGetValue(id, out var entry)) @@ -103,15 +108,35 @@ public void Subscribe(TIdentity id, TObserver observer) { _log.LogTrace("Updating entry for {Id}/{Observer}. {Count} total observers.", id, observer, _observers.Count); } + + newObserver = entry.Observer; + return true; } - else + + var newEntry = new ObserverEntry(observer); + _observers[id] = newEntry; + if (_log.IsEnabled(LogLevel.Trace)) { - _observers[id] = new ObserverEntry(observer); - if (_log.IsEnabled(LogLevel.Trace)) - { - _log.LogTrace("Adding entry for {Id}/{Observer}. {Count} total observers after add.", id, observer, _observers.Count); - } + _log.LogTrace("Adding entry for {Id}/{Observer}. {Count} total observers after add.", id, observer, _observers.Count); } + + newObserver = newEntry.Observer; + return false; + } + + /// + /// Ensures that the provided is subscribed, renewing its subscription. + /// + /// + /// The observer's identity. + /// + /// + /// The observer. + /// + /// A delegate callback throws an exception. + public void Subscribe(TIdentity id, TObserver observer) + { + _ = TryGetOrAddSubscription(id, observer, out _); } /// diff --git a/src/MudBlazor/Utilities/StringHelpers.cs b/src/MudBlazor/Utilities/StringHelpers.cs new file mode 100644 index 000000000000..2d8451ee405c --- /dev/null +++ b/src/MudBlazor/Utilities/StringHelpers.cs @@ -0,0 +1,20 @@ +using System.Globalization; + +namespace MudBlazor.Utilities; + +#nullable enable +internal static class StringHelpers +{ + /// + /// Converts a double value to its string representation, rounded to 4 decimal places. + /// + /// The double value to convert. + /// An optional format string. + /// The string representation of the double value. + public static string ToS(double value, string? format = null) + { + return string.IsNullOrEmpty(format) + ? Math.Round(value, 4).ToString(CultureInfo.InvariantCulture) + : Math.Round(value, 4).ToString(format); + } +}