diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index ae6152391c67..99c8c75b39b2 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,8 +1,8 @@ # These are supported funding model platforms +open_collective: mudblazor github: mudblazor patreon: # Replace with a single Patreon username -open_collective: mudblazor ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry diff --git a/.github/workflows/deploy-trymudblazor.yml b/.github/workflows/deploy-trymudblazor.yml index d20aa7995fac..2814d314d6ad 100644 --- a/.github/workflows/deploy-trymudblazor.yml +++ b/.github/workflows/deploy-trymudblazor.yml @@ -28,6 +28,7 @@ jobs: web-app-slot-name: 'staging' swap-slots: true project-directory: './src/TryMudBlazor.Server' + second-project-directory: './src/TryMudBlazor.Client' secrets: publish-profile: ${{ secrets.PUBLISH_TRY_MUDBLAZOR }} azure-cred: ${{ secrets.AZURE_CREDENTIALS_TRY_PROD }} diff --git a/src/MudBlazor.Docs/Components/LandingPage/MiniApp/MiniApp.razor b/src/MudBlazor.Docs/Components/LandingPage/MiniApp/MiniApp.razor index c69a79dd5262..11f2c272fe8e 100644 --- a/src/MudBlazor.Docs/Components/LandingPage/MiniApp/MiniApp.razor +++ b/src/MudBlazor.Docs/Components/LandingPage/MiniApp/MiniApp.razor @@ -150,7 +150,7 @@ RBMK-1000RBMK-1500RBMKP-2400
- Read More + Read more
@if(IsMobile == false) diff --git a/src/MudBlazor.Docs/Components/MudTeamCard.razor b/src/MudBlazor.Docs/Components/MudTeamCard.razor index cb6afe179b3b..6af2f3c928e0 100644 --- a/src/MudBlazor.Docs/Components/MudTeamCard.razor +++ b/src/MudBlazor.Docs/Components/MudTeamCard.razor @@ -14,8 +14,14 @@ } + @if(Member.GitHubSponsor) + { + + + + } - + diff --git a/src/MudBlazor.Docs/Components/SectionContent.razor.cs b/src/MudBlazor.Docs/Components/SectionContent.razor.cs index 07861486e024..4e31a65baa81 100644 --- a/src/MudBlazor.Docs/Components/SectionContent.razor.cs +++ b/src/MudBlazor.Docs/Components/SectionContent.razor.cs @@ -20,6 +20,7 @@ public partial class SectionContent { [Inject] protected IJsApiService JsApiService { get; set; } [Inject] protected IDocsJsApiService DocsJsApiService { get; set; } + [Inject] protected ISnackbar SnackbarService { get; set; } protected string Classname => new CssBuilder("docs-section-content") @@ -108,6 +109,7 @@ private async Task CopyTextToClipboard() var code = Snippets.GetCode(Code); code ??= await DocsJsApiService.GetInnerTextByIdAsync(_snippetId); await JsApiService.CopyToClipboardAsync(code ?? $"Snippet '{Code}' not found!"); + SnackbarService.Add("Copied to clipboard"); } RenderFragment CodeComponent(string code) => builder => diff --git a/src/MudBlazor.Docs/Enums/DarkLightMode.cs b/src/MudBlazor.Docs/Enums/DarkLightMode.cs index 7cc9944df7a6..5fe72ab3ea01 100644 --- a/src/MudBlazor.Docs/Enums/DarkLightMode.cs +++ b/src/MudBlazor.Docs/Enums/DarkLightMode.cs @@ -1,8 +1,22 @@ namespace MudBlazor.Docs.Enums; +/// +/// Represents the theme preference for dark or light mode. +/// public enum DarkLightMode { + /// + /// The theme is determined by the operating system or browser. + /// System = 0, + + /// + /// Light theme is used. + /// Light = 1, + + /// + /// Dark theme is used. + /// Dark = 2 } diff --git a/src/MudBlazor.Docs/Models/TeamMember.cs b/src/MudBlazor.Docs/Models/TeamMember.cs index fe782e78458b..303692b855d3 100644 --- a/src/MudBlazor.Docs/Models/TeamMember.cs +++ b/src/MudBlazor.Docs/Models/TeamMember.cs @@ -9,6 +9,7 @@ public class TeamMember public string Name { get; set; } public string From { get; set; } public string GitHub { get; set; } + public bool GitHubSponsor { get; set; } public string Avatar { get; set; } public string LinkedIn { get; set; } } diff --git a/src/MudBlazor.Docs/Pages/Components/Alert/Examples/AlertCloseExample.razor b/src/MudBlazor.Docs/Pages/Components/Alert/Examples/AlertCloseExample.razor index 71f03087ca81..9093c6cdea69 100644 --- a/src/MudBlazor.Docs/Pages/Components/Alert/Examples/AlertCloseExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/Alert/Examples/AlertCloseExample.razor @@ -11,7 +11,7 @@ @if (!showLeaveAlert && !showCallAlert) {
- Show Alerts + Show alerts
} @@ -36,4 +36,4 @@ showCallAlert = true; showLeaveAlert = true; } -} \ No newline at end of file +} diff --git a/src/MudBlazor.Docs/Pages/Components/Autocomplete/AutocompletePage.razor b/src/MudBlazor.Docs/Pages/Components/Autocomplete/AutocompletePage.razor index dbdd11ad007b..973c65e53645 100644 --- a/src/MudBlazor.Docs/Pages/Components/Autocomplete/AutocompletePage.razor +++ b/src/MudBlazor.Docs/Pages/Components/Autocomplete/AutocompletePage.razor @@ -5,10 +5,6 @@ It is great for searching a value from a long list of options. In comparison to a Select, the Autocomplete doesn't need to know the complete item list, it only calls a search function which will return matching items. The search function can even run asynchronously, i.e. for database queries. - - Note: Blazor static rendering is not supported. - See install guide for more info. - diff --git a/src/MudBlazor.Docs/Pages/Components/Autocomplete/Examples/AutocompletePresentationExtrasExample.razor b/src/MudBlazor.Docs/Pages/Components/Autocomplete/Examples/AutocompletePresentationExtrasExample.razor index bf758fde6fce..e7a133d99d43 100644 --- a/src/MudBlazor.Docs/Pages/Components/Autocomplete/Examples/AutocompletePresentationExtrasExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/Autocomplete/Examples/AutocompletePresentationExtrasExample.razor @@ -38,7 +38,7 @@ SearchFunc="@Search" ToStringFunc="@(e=> e==null?null : $"{e.Name} ({e.Sign})")">
- Add Item(does nothing) + Add item (does nothing)
diff --git a/src/MudBlazor.Docs/Pages/Components/Button/Examples/ButtonCustomizedExample.razor b/src/MudBlazor.Docs/Pages/Components/Button/Examples/ButtonCustomizedExample.razor index b2ce31dc44dc..d5c60aa1387e 100644 --- a/src/MudBlazor.Docs/Pages/Components/Button/Examples/ButtonCustomizedExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/Button/Examples/ButtonCustomizedExample.razor @@ -3,5 +3,5 @@ - Download Now - \ No newline at end of file + Download now + diff --git a/src/MudBlazor.Docs/Pages/Components/Card/Examples/CardHeaderExample.razor b/src/MudBlazor.Docs/Pages/Components/Card/Examples/CardHeaderExample.razor index 85f73002c66c..8be17217367c 100644 --- a/src/MudBlazor.Docs/Pages/Components/Card/Examples/CardHeaderExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/Card/Examples/CardHeaderExample.razor @@ -14,6 +14,6 @@ The quick, brown fox jumps over a lazy dog. - Read More + Read more diff --git a/src/MudBlazor.Docs/Pages/Components/Card/Examples/CardMediaExample.razor b/src/MudBlazor.Docs/Pages/Components/Card/Examples/CardMediaExample.razor index 4842c7dbbd37..585760f5be17 100644 --- a/src/MudBlazor.Docs/Pages/Components/Card/Examples/CardMediaExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/Card/Examples/CardMediaExample.razor @@ -9,6 +9,6 @@ Share - Learn More + Learn more \ No newline at end of file diff --git a/src/MudBlazor.Docs/Pages/Components/Card/Examples/CardOutlinedExample.razor b/src/MudBlazor.Docs/Pages/Components/Card/Examples/CardOutlinedExample.razor index 7fadf0fa384f..c1246733b0c1 100644 --- a/src/MudBlazor.Docs/Pages/Components/Card/Examples/CardOutlinedExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/Card/Examples/CardOutlinedExample.razor @@ -6,6 +6,6 @@ The quick, brown fox jumps over a lazy dog. - Learn More + Learn more \ No newline at end of file diff --git a/src/MudBlazor.Docs/Pages/Components/Card/Examples/CardSimpleExample.razor b/src/MudBlazor.Docs/Pages/Components/Card/Examples/CardSimpleExample.razor index 846ae90dd3b9..035ef4b3e93c 100644 --- a/src/MudBlazor.Docs/Pages/Components/Card/Examples/CardSimpleExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/Card/Examples/CardSimpleExample.razor @@ -6,6 +6,6 @@ The quick, brown fox jumps over a lazy dog. - Learn More + Learn more - \ No newline at end of file + diff --git a/src/MudBlazor.Docs/Pages/Components/Charts/Examples/LineExampleHideLines.razor b/src/MudBlazor.Docs/Pages/Components/Charts/Examples/LineExampleHideLines.razor index f23e3fb28277..e6ac36919396 100644 --- a/src/MudBlazor.Docs/Pages/Components/Charts/Examples/LineExampleHideLines.razor +++ b/src/MudBlazor.Docs/Pages/Components/Charts/Examples/LineExampleHideLines.razor @@ -2,7 +2,7 @@
- Randomize Data + Randomize data
@code { @@ -30,4 +30,4 @@ Series = newSeries; StateHasChanged(); } -} \ No newline at end of file +} diff --git a/src/MudBlazor.Docs/Pages/Components/Checkbox/CheckboxPage.razor b/src/MudBlazor.Docs/Pages/Components/Checkbox/CheckboxPage.razor index 9ad233c9bc81..9c4c173c2aa2 100644 --- a/src/MudBlazor.Docs/Pages/Components/Checkbox/CheckboxPage.razor +++ b/src/MudBlazor.Docs/Pages/Components/Checkbox/CheckboxPage.razor @@ -3,7 +3,7 @@ - If you want to let the user turn a setting on or off on demand, a Switch component is recommended instead. + Use checkboxes (instead of switches or radio buttons) if multiple options can be selected from a list. diff --git a/src/MudBlazor.Docs/Pages/Components/Chip/ChipPage.razor b/src/MudBlazor.Docs/Pages/Components/Chip/ChipPage.razor index a654f109083e..2cd70908d256 100644 --- a/src/MudBlazor.Docs/Pages/Components/Chip/ChipPage.razor +++ b/src/MudBlazor.Docs/Pages/Components/Chip/ChipPage.razor @@ -2,7 +2,10 @@ -
See also: ChipSet
+ + Use chips to show options for a specific context. +
See also: ChipSet +
diff --git a/src/MudBlazor.Docs/Pages/Components/ColorPicker/ColorPickerPage.razor b/src/MudBlazor.Docs/Pages/Components/ColorPicker/ColorPickerPage.razor index cfe17c6497d7..28d8c94697ed 100644 --- a/src/MudBlazor.Docs/Pages/Components/ColorPicker/ColorPickerPage.razor +++ b/src/MudBlazor.Docs/Pages/Components/ColorPicker/ColorPickerPage.razor @@ -15,7 +15,7 @@ - All parts of the color picker can be removed individually if you just want certain functionality. + All parts of the color picker can be removed individually if you only want certain features. @@ -24,7 +24,7 @@ - The default view can be changed with ColorPickerView and can be useful when you want a minimalistic color picker or force the use of certain colors. + The default view can be changed with ColorPickerView. This is useful if you want a minimal color picker or want to restrict the available colors. @@ -33,7 +33,7 @@ - You can set the default mode of the colorpicker with the ColorPickerMode prop. This can be useful combined with ShowModeSwitch="false" to force the usage of a specific color mode. + You can set the default mode of the color picker with the ColorPickerMode property. Combine this with ShowModeSwitch="false" to force a specific color mode. @@ -42,7 +42,7 @@ - The default palette can be changed with Palette property. The first five colors show up in the quick view when clicking the color preview dot. + You can change the default palette with the Palette property. The first five colors appear in the quick view when clicking the color preview dot. @@ -51,7 +51,7 @@ - You can disable the alpha slider and its textfield counterpart by setting ShowAlpha="false". Doing so will remove any transparency from the color and output RGB, HSL, HEX instead of RGBA, HSLA and HEXA. + Disable the alpha slider and its text field by setting ShowAlpha="false". This removes transparency and outputs RGB, HSL, or HEX instead of RGBA, HSLA, or HEXA. @@ -60,7 +60,7 @@ - Switch Mode lets the user switch betwen RGB, HSL and HEX the button can be disabled with ShowModeSwitch="false", this can be useful if you want to control what type of input/output should be used. + Switch Mode lets users switch between RGB, HSL, and HEX. You can disable the button with ShowModeSwitch="false" if you want to control the input/output type. @@ -88,8 +88,8 @@ - By default, the color picker is modal, preventing interaction with other elements while it is open. - To allow interactions with other elements, set Modal="false". + By default, the color picker is modal, preventing interaction with other elements while open. + To allow interaction with other elements, set Modal="false". @@ -108,7 +108,7 @@ - You can change the elevation with the Elevation parameter. The default level is 8 for Inline, and 0 for Static or Dialog. + You can change the elevation with the Elevation parameter. The default is 8 for Inline, and 0 for Static or Dialog. @@ -126,8 +126,8 @@ - The ThrottleInterval property controls how long to wait until the color is updated while dragging the pointer in the spectrum view. - If the DragEffect property is set to false, the color selector (circle) will no longer follow the pointer. + The ThrottleInterval property controls how long to wait before updating the color while dragging in the spectrum view. + If DragEffect is set to false, the color selector (circle) will not follow the pointer. diff --git a/src/MudBlazor.Docs/Pages/Components/DataGrid/DataGridPage.razor b/src/MudBlazor.Docs/Pages/Components/DataGrid/DataGridPage.razor index 39604c05a081..acfe972321dd 100644 --- a/src/MudBlazor.Docs/Pages/Components/DataGrid/DataGridPage.razor +++ b/src/MudBlazor.Docs/Pages/Components/DataGrid/DataGridPage.razor @@ -125,6 +125,16 @@ In both active modes, single columns can be excluded from sorting by setting the Sortable property to false for that column. When SortMode is set to SortMode.None sorting is disabled globally and setting the Sortable property on a column has no effect.

You can also use the SortBy parameter on a TemplateColumn to allow sorting on that column. +

+ When using PropertyColumn, if the Property lambda expression returns a primitive type, + the SortDefinition's SortBy will return the Property's nameof value. + Instead, if the Property lamda expression contains a complex value, the SortBy will return the column GUID. + In this case, if you need to retrieve the column, you have several options: + + - Embed a CellTemplate within the PropertyColumn: Use the lambda expression of the PropertyColumn to return the primitive Property value, while implementing the more complex, ad-hoc logic in the embedded CellTemplate. + - Title lookup via GUID: Assign a Title to the TemplateColumn, and a reference to the MudDataGrid using @@ref. Iterate over the RenderedColumns to find the TemplateColumn by matching its unique identifier (where PropertyName == SortDefinition.SortBy), and extract its Title. + - Create a custom TemplateColumn: Inherit from the Column base class and override the PropertyName's default behavior and implement your own custom logic instead of relying on a randomly generated identifier + Changing the SortMode at runtime will reset any former sort orders on all columns. diff --git a/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridColumnsPanelExample.razor b/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridColumnsPanelExample.razor index 4363b75ea13f..da404212c814 100644 --- a/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridColumnsPanelExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridColumnsPanelExample.razor @@ -33,8 +33,8 @@
- Show All Columns - Hide All Columns + Show all columns + Hide all columns
diff --git a/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridDetailRowExample.razor b/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridDetailRowExample.razor index af055bf87817..997ff2976f3c 100644 --- a/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridDetailRowExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridDetailRowExample.razor @@ -32,10 +32,10 @@
Read Only - Expand All - Collapse All - - + Expand all + Collapse all + +
@code { diff --git a/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridGroupingExample.razor b/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridGroupingExample.razor index 7a743cc20421..90ebc3988392 100644 --- a/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridGroupingExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridGroupingExample.razor @@ -36,8 +36,8 @@
Customize Group Template Customize Group By - Expand All - Collapse All + Expand all + Collapse all
@code { diff --git a/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridGroupingMultiLevelExample.razor b/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridGroupingMultiLevelExample.razor index fb75f668d0f1..bfb969022314 100644 --- a/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridGroupingMultiLevelExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridGroupingMultiLevelExample.razor @@ -126,8 +126,8 @@ Customize Group Template Customize Group By Industry Customize Group Style - Expand All - Collapse All + Expand all + Collapse all
@code { diff --git a/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridServerSideExample.razor b/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridServerSideExample.razor index 235559fbf09f..26b7c8399349 100644 --- a/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridServerSideExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridServerSideExample.razor @@ -1,5 +1,4 @@ @using System.Net.Http.Json -@using System.Threading @using MudBlazor.Examples.Data.Models @namespace MudBlazor.Docs.Examples @inject HttpClient httpClient @@ -14,10 +13,18 @@ - + + + @(context.Item.Name ?? "-") + + - + + + @(context.Item.Group ?? "-") + + @@ -66,12 +73,6 @@ o => o.Sign ); break; - case nameof(Element.Name): - data = data.OrderByDirection( - sortDefinition.Descending ? SortDirection.Descending : SortDirection.Ascending, - o => o.Name - ); - break; case nameof(Element.Position): data = data.OrderByDirection( sortDefinition.Descending ? SortDirection.Descending : SortDirection.Ascending, @@ -84,6 +85,24 @@ o => o.Molar ); break; + case nameof(Element.Group): + data = data.OrderByDirection( + sortDefinition.Descending ? SortDirection.Descending : SortDirection.Ascending, + o => o.Group + ); + break; + default: + var sortByColumn = dataGrid.RenderedColumns.First(c => c.PropertyName == sortDefinition.SortBy); + switch (sortByColumn.Title) + { + case nameof(Element.Name): + data = data.OrderByDirection( + sortDefinition.Descending ? SortDirection.Descending : SortDirection.Ascending, + o => o.Name + ); + break; + } + break; } } diff --git a/src/MudBlazor.Docs/Pages/Components/DatePicker/Examples/DatePickerActionButtonsExample.razor b/src/MudBlazor.Docs/Pages/Components/DatePicker/Examples/DatePickerActionButtonsExample.razor index c3573f6aadda..163e8221a3b6 100644 --- a/src/MudBlazor.Docs/Pages/Components/DatePicker/Examples/DatePickerActionButtonsExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/DatePicker/Examples/DatePickerActionButtonsExample.razor @@ -4,7 +4,7 @@ Clear Cancel - Ok + OK AutoClose diff --git a/src/MudBlazor.Docs/Pages/Components/DatePicker/Examples/DateRangePickerUsageExample.razor b/src/MudBlazor.Docs/Pages/Components/DatePicker/Examples/DateRangePickerUsageExample.razor index 2525b1a06bf1..0f92784b961a 100644 --- a/src/MudBlazor.Docs/Pages/Components/DatePicker/Examples/DateRangePickerUsageExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/DatePicker/Examples/DateRangePickerUsageExample.razor @@ -11,7 +11,7 @@ Clear Cancel - Ok + OK AutoClose diff --git a/src/MudBlazor.Docs/Pages/Components/DateRangePicker/Examples/DateRangePickerActionButtonsExample.razor b/src/MudBlazor.Docs/Pages/Components/DateRangePicker/Examples/DateRangePickerActionButtonsExample.razor index 97ecf557ea8d..b37ab3bc1902 100644 --- a/src/MudBlazor.Docs/Pages/Components/DateRangePicker/Examples/DateRangePickerActionButtonsExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/DateRangePicker/Examples/DateRangePickerActionButtonsExample.razor @@ -6,7 +6,7 @@ Clear Cancel - Ok + OK AutoClose diff --git a/src/MudBlazor.Docs/Pages/Components/Dialog/DialogPage.razor b/src/MudBlazor.Docs/Pages/Components/Dialog/DialogPage.razor index 96d52fad33f1..ece313732716 100644 --- a/src/MudBlazor.Docs/Pages/Components/Dialog/DialogPage.razor +++ b/src/MudBlazor.Docs/Pages/Components/Dialog/DialogPage.razor @@ -1,28 +1,29 @@ @page "/components/dialog" - + + + Use dialogs to make sure users act on information. + + - - Note - The Dialog is dependent on IDialogService and MudDialogProvider - Check the Installation page for instructions regarding default setup. - + + The Dialog component relies on IDialogService and MudDialogProvider for its functionality. + See the Installation page for setup instructions. + - Suppose you define a MudDialog in TermsOfServiceDialog.razor. - To show the dialog you simply call: + To use a dialog, define a MudDialog in a Razor component (e.g., TermsOfServiceDialog.razor). + Show it by calling:

DialogService.Show<TermsOfServiceDialog>("Terms");

- The advantage of having the dialog in its own Razor component is obviously the ability to reuse it throughout your code-base. - You can pass parameters to your dialog on show which allow you to load the dialog with custom data. -
+ This approach lets you reuse dialogs throughout your app and pass parameters for customization. - Note: Even though it is technically possible to show any razor component as a dialog it is highly recommended to use a MudDialog as the top-level of your dialogs! + Tip: For best results, use MudDialog as the root element of your dialog component.
@@ -34,14 +35,14 @@ - The dialog's default behavior can be changed in two ways, either globally with parameters in the <MudDialogProvider/> or pass down the DialogOptions class when you open the dialog. + You can change dialog behavior globally by setting parameters on <MudDialogProvider/>, or per dialog by passing a DialogOptions instance when opening a dialog. - In the file where you added <MudDialogProvider/>, we can pass down different options as parameters. See installation page for more information regarding this. + Set options on <MudDialogProvider/> to affect all dialogs. See the installation page for details. @@ -49,7 +50,7 @@ - Below we pass along the DialogOptions class when we open the dialog, this can be done per dialog or you can predefine a bunch of them that you use for specific cases in your system.
+ Pass a DialogOptions object when opening a dialog to override global settings for that instance.
@@ -60,7 +61,7 @@ - The title and the options can also be modified from the dialog component itself by calling SetTitle and SetOptions on the MudDialogInstance object. + You can update the dialog's title and options from within the dialog component using SetTitle and SetOptions on the MudDialogInstance. @@ -73,7 +74,7 @@ - In this section, we will demonstrate how you can build one dialog and reuse it for multiple purposes. + Build a reusable dialog and pass simple data to it for different scenarios. @@ -82,7 +83,7 @@ - Here is a little more advanced use case. We will use the same dialog but feed it with different server data and then mimic a delete operation. + Pass data to a dialog and use it for operations, such as confirming a delete action. @@ -91,7 +92,7 @@ - Quick example on how content that exeeds the available height becomes scrollable. + Dialogs automatically become scrollable if their content exceeds the available height. @@ -100,7 +101,7 @@ - Dialog background can be changed via BackgroundClass dialog option. + Customize the dialog background using the BackgroundClass option. @@ -110,10 +111,7 @@ - You can inline MudDialog directly in another component which, of course, makes most sense for small dialogs that are not re-used somewhere else. - The advantage is that you can easily share code and data between dialog and owning component via bindings. -
- This example also shows how to override the dialog title with a render fragment. + You can place a MudDialog directly in another component. This is useful for small, non-reusable dialogs and allows easy sharing of data and code. You can also override the dialog title with a render fragment.
@@ -124,8 +122,7 @@ - You can inline a MudDialog within another MudDialog, even in another inline dialog. This example shows both ways of nesting - an inline dialog. + Inline dialogs can be nested within each other. This example demonstrates both single and multiple levels of nesting. @@ -136,9 +133,7 @@ - It is possible to open multiple dialogs at the same time. -
- This example also shows how to open a second dialog and cancel all dialogs at once. + You can open multiple dialogs at once. This example also shows how to open a second dialog and close all dialogs simultaneously.
@@ -149,11 +144,9 @@ - Out of the box MudDialog supports the Escape key to close a dialog if you set option @(nameof(DialogOptions.CloseOnEscapeKey)). - Note that @(nameof(DialogOptions.CloseOnEscapeKey)) will close the dialog with DialogResult.Cancel(). -
- If you need more than that or a different behavior you can define your own event handlers with OnKeyDown and OnKeyUp. - In this example we return the selected value on Enter only if a value has been selected. + MudDialog supports closing with the Escape key if @(nameof(DialogOptions.CloseOnEscapeKey)) is enabled. + When closed with Escape, the dialog returns DialogResult.Cancel(). + For custom keyboard handling, use OnKeyDown and OnKeyUp event handlers. The example below returns a value on Enter only if a selection is made.
@@ -164,7 +157,7 @@ - Dialog uses the FocusTrap-Component to keep the keyboard-focus within. By default, the first element is focused With DefaultFocus you can change the behaviour to, for example, focus the last element. + Dialogs use a focus trap to keep keyboard focus inside. By default, the first element is focused, but you can change this with the DefaultFocus option. @@ -175,7 +168,7 @@ - Classes can be applied to the title, content, and action button sections, or the dialog itself, to make customization easier. + You can apply custom classes to the dialog's title, content, actions, or the dialog itself for advanced styling. diff --git a/src/MudBlazor.Docs/Pages/Components/Dialog/Examples/DialogBlurryExample.razor b/src/MudBlazor.Docs/Pages/Components/Dialog/Examples/DialogBlurryExample.razor index c656b76b2b1f..87d46452c3a2 100644 --- a/src/MudBlazor.Docs/Pages/Components/Dialog/Examples/DialogBlurryExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/Dialog/Examples/DialogBlurryExample.razor @@ -4,7 +4,7 @@ - Open Simple Dialog + Open blurry dialog @code { + private const string ExpandAllIcon = """Codestin Search App"""; private const string CollapseAllIcon = """Codestin Search App"""; @@ -171,7 +171,7 @@ private string ThemeIcon => _isDarkMode ? Icons.Material.Filled.LightMode : Icons.Material.Filled.DarkMode; - private string ThemeLabel => _isDarkMode ? "Switch to light theme" : "Switch to dark theme"; + private string ThemeLabel => _isDarkMode ? "Light mode" : "Dark mode"; private void DocsDrawerToggle() => _drawerOpen = !_drawerOpen; @@ -198,6 +198,71 @@ type => type, type => type.Namespace?.Split('.').LastOrDefault() ?? string.Empty ); + + ParseQueryString(); + NavManager.LocationChanged += HandleLocationChanged; + } + + private void HandleLocationChanged(object? sender, LocationChangedEventArgs e) + { + ParseQueryString(); + StateHasChanged(); + } + + public void Dispose() + { + NavManager.LocationChanged -= HandleLocationChanged; + } + + private void ParseQueryString() + { + var uri = new Uri(NavManager.Uri); + var query = uri.Query; + + if (string.IsNullOrEmpty(query)) + return; + + if (query.StartsWith('?')) + query = query.Substring(1); + + var queryParams = query.Split('&'); + + foreach (var param in queryParams) + { + var parts = param.Split('='); + if (parts.Length == 2) + { + var key = parts[0]; + var value = Uri.UnescapeDataString(parts[1]); + + if (string.Equals(key, "component", StringComparison.OrdinalIgnoreCase)) + { + var componentType = _availableComponentTypes.FirstOrDefault(t => + t.Name.Equals(value, StringComparison.OrdinalIgnoreCase)); + + if (componentType != null) + { + _selectedType = componentType; + StateHasChanged(); + break; + } + } + } + } + } + + private void UpdateQueryString(Type componentType) + { + if (componentType == null) return; + + var uri = new Uri(NavManager.Uri); + var baseUrl = uri.GetLeftPart(UriPartial.Path); + + // Create the new query string + var newUrl = $"{baseUrl}?component={componentType.Name}"; + + // Update URL without navigation (false parameter) + NavManager.NavigateTo(newUrl, false); } private RenderFragment TestComponent() => builder => @@ -254,6 +319,7 @@ } _selectedType = entry; + UpdateQueryString(entry); await Task.Yield(); await _autocomplete.ClearAsync(); } diff --git a/src/MudBlazor.UnitTests.Viewer/Shared/MainLayout.razor b/src/MudBlazor.UnitTests.Viewer/Shared/MainLayout.razor index dc0145162ed6..311af89bd931 100644 --- a/src/MudBlazor.UnitTests.Viewer/Shared/MainLayout.razor +++ b/src/MudBlazor.UnitTests.Viewer/Shared/MainLayout.razor @@ -5,10 +5,3 @@ @Body - -@code { - static MainLayout() - { - MudGlobal.TooltipDefaults.Delay = TimeSpan.FromMilliseconds(500); - } -} diff --git a/src/MudBlazor.UnitTests.Viewer/Shared/MainLayoutTwo.razor b/src/MudBlazor.UnitTests.Viewer/Shared/MainLayoutTwo.razor new file mode 100644 index 000000000000..fc486a9b4dc2 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/Shared/MainLayoutTwo.razor @@ -0,0 +1,11 @@ +@inherits LayoutComponentBase + + + + + +@Body + +@code { + private readonly MudTheme _customTheme = new(); +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridColumnGroupingTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridColumnGroupingTest.razor index 4e6ba3996f66..5ee10426a34d 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridColumnGroupingTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridColumnGroupingTest.razor @@ -1,16 +1,25 @@  - - + + -Group By Gender -Group By Age -Group By Profession +Group By Name +Group By Gender +Group By Age +Group By Profession +UnGroup All + + + @code { private readonly Func _groupByProfession = x => string.IsNullOrEmpty(x.Profession) @@ -33,6 +42,14 @@ new("Alice", 32, "Female", "Cook") ]; + private void GroupByName(MouseEventArgs args) + { + IsNameGrouped = true; + IsProfessionGrouped = false; + IsAgeGrouped = false; + IsGenderGrouped = false; + } + private void GroupByGender(MouseEventArgs args) { IsNameGrouped = false; @@ -55,7 +72,15 @@ IsProfessionGrouped = true; IsAgeGrouped = false; IsGenderGrouped = false; - } + } + + private void UnGroupAll(MouseEventArgs args) + { + IsNameGrouped = false; + IsProfessionGrouped = false; + IsAgeGrouped = false; + IsGenderGrouped = false; + } public record Model(string Name, int Age, string Gender, string? Profession); } diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupByNullTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupByNullTest.razor new file mode 100644 index 000000000000..bd8a62fb3ca1 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupByNullTest.razor @@ -0,0 +1,50 @@ + + + Fruits + + + + + + + + Category: @context.Grouping.Key + + + + + +
+ Add Fruit + Add Null Fruit +
+ +@code { + private MudDataGrid _dataGrid = null!; + private readonly List _fruits = + [ + new Fruit("Apple", 2, "Pome"), + new Fruit("Pear", 4, "Pome"), + new Fruit("Orange", 4, "Citrus") + ]; + + public void AddFruit() + { + _fruits.Add(new Fruit("Banana", 5, "Musa")); + } + + public void AddNullFruit() + { + _fruits.Add(new Fruit("NullFruit", 3, null)); + } + + public class Fruit(string name, int count, string? category) + { + public string Name { get; set; } = name; + + public int Count { get; set; } = count; + + public string? Category { get; set; } = category; + } +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/AutoCloseDateRangePickerTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/AutoCloseDateRangePickerTest.razor index 754aaee90923..e263725a6541 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/AutoCloseDateRangePickerTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/AutoCloseDateRangePickerTest.razor @@ -4,7 +4,7 @@ Clear Cancel - Ok + OK diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/AutoCompleteDatePickerTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/AutoCompleteDatePickerTest.razor index 9573115a37a6..ac1fb8d0d8c3 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/AutoCompleteDatePickerTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/AutoCompleteDatePickerTest.razor @@ -4,7 +4,7 @@ Clear Cancel - Ok + OK diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/DatePickerStaticTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/DatePickerStaticTest.razor index da7dac2345ba..d1d9119ce6c8 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/DatePickerStaticTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/DatePickerStaticTest.razor @@ -4,7 +4,7 @@ Clear Cancel - Ok + OK diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/DateRangePickerValidationTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/DateRangePickerValidationTest.razor index fa4521b287b9..42f64eed0c35 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/DateRangePickerValidationTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/DateRangePickerValidationTest.razor @@ -9,7 +9,7 @@ Clear Cancel - Ok + OK diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/SimpleMudDatePickerTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/SimpleMudDatePickerTest.razor index 53038742b4f2..01682d103057 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/SimpleMudDatePickerTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/SimpleMudDatePickerTest.razor @@ -16,7 +16,8 @@ FixYear="@FixYear" MinDate="@MinDate" MaxDate="@MaxDate" - AdditionalDateClassesFunc="@AdditionalDateClassesFunc" /> + AdditionalDateClassesFunc="@AdditionalDateClassesFunc" + Culture="@(new CultureInfo("en-US"))" /> } else { @@ -31,7 +32,8 @@ else FixYear="@FixYear" MinDate="@MinDate" MaxDate="@MaxDate" - AdditionalDateClassesFunc="@AdditionalDateClassesFunc" /> + AdditionalDateClassesFunc="@AdditionalDateClassesFunc" + Culture="@(new CultureInfo("en-US"))" /> } @code { @@ -81,4 +83,4 @@ else public async Task Open() => await InvokeAsync(() => _picker.OpenAsync()); public async Task Close() => await InvokeAsync(() => _picker.CloseAsync()); -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogOkCancel.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogOkCancel.razor index c8438e784696..f92d8e475e57 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogOkCancel.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogOkCancel.razor @@ -4,8 +4,8 @@ Cancel - Ok - Ok Default + OK + OK Default @@ -26,4 +26,4 @@ private void OnKeyDown(KeyboardEventArgs arg) => LastKeyDown = arg; private void OnKeyUp(KeyboardEventArgs arg) => LastKeyUp = arg; -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogOptionMutation.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogOptionMutation.razor index b59b8f1abf50..70e471cb8384 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogOptionMutation.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogOptionMutation.razor @@ -10,7 +10,7 @@ - Ok + OK diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogRender.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogRender.razor index cab1bdfa86fe..f744ff86e7b6 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogRender.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogRender.razor @@ -4,7 +4,7 @@ Cancel - Ok + OK diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogThatUpdatesItsTitle.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogThatUpdatesItsTitle.razor index 254deb2276ef..69c8b41f0866 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogThatUpdatesItsTitle.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogThatUpdatesItsTitle.razor @@ -7,7 +7,7 @@ Cancel - Ok + OK diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogToggleFullscreen.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogToggleFullscreen.razor index 380969964ba6..e8399e2d036d 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogToggleFullscreen.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogToggleFullscreen.razor @@ -1,11 +1,11 @@ - +
Toggle FullScreen
- Ok + OK
@code { diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogWithEventCallback.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogWithEventCallback.razor index 87314f977ed8..34245383c868 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogWithEventCallback.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogWithEventCallback.razor @@ -8,7 +8,7 @@ Cancel - Ok + OK
diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogWithEventCallbackTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogWithEventCallbackTest.razor index 6ab9ed9eb231..74161663a471 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogWithEventCallbackTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogWithEventCallbackTest.razor @@ -1,7 +1,7 @@ @inject IDialogService DialogService - Open Simple Dialog + Open simple dialog

Search Text: @_searchText diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/InlineDialogShowMethod.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/InlineDialogShowMethod.razor index 72c76fd7d7ec..9fea675d3060 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/InlineDialogShowMethod.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/InlineDialogShowMethod.razor @@ -9,7 +9,7 @@

Here be dragons

- Ok + OK diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Drawer/DrawerDialogSelectTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Drawer/DrawerDialogSelectTest.razor index 7d7b07fee521..f3a61ff1028f 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Drawer/DrawerDialogSelectTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Drawer/DrawerDialogSelectTest.razor @@ -1,5 +1,5 @@  - Open Simple Dialog + Open simple dialog diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverTooltipInOverlayTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverTooltipInOverlayTest.razor index 57c1ea9460aa..c1ebfa49897b 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverTooltipInOverlayTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverTooltipInOverlayTest.razor @@ -1,8 +1,15 @@ - +@inject NavigationManager NavManager - - Tooltip Test - + + + Tooltip Test + + + Popover Tooltip In Overlay + Same but with different Popover provider and Page + + @@ -32,4 +39,9 @@ public static string __description__ = "Popover Tooltip inside Overlay"; private bool _isVisible; + + private void NavigateToTest() + { + NavManager.NavigateTo("/popover-switch-test/PopoverTwoLayoutsTest"); + } } diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverTwoLayoutsTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverTwoLayoutsTest.razor new file mode 100644 index 000000000000..aaae15bb420f --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverTwoLayoutsTest.razor @@ -0,0 +1,62 @@ +@layout MainLayoutTwo +@page "/popover-switch-test/PopoverTwoLayoutsTest" +@inject NavigationManager NavManager + + + + + + Tooltip Test + + + Popover Tooltip In Overlay + Same but with different Popover provider + + + + + +
+ + Test +
+ + + + + + +
+
+ + Save + Cancel + +
+
+
+
+
+ +@code { + public static string __description__ = "Tests the popover with a different layout than the default, presumabily with a new observer on the new provider."; + + private bool _isVisible = false; + + protected override void OnAfterRender(bool firstRender) + { + if (firstRender) + { + if (!NavManager.Uri.Contains("popover-switch-test")) + { + NavManager.NavigateTo("/popover-switch-test/PopoverTwoLayoutsTest"); + } + } + } + + private void NavigateToTest() + { + NavManager.NavigateTo($"/?component={nameof(PopoverTooltipInOverlayTest)}"); + } +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Tabs/LabelSortTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Tabs/LabelSortTest.razor new file mode 100644 index 000000000000..7f1182259166 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Tabs/LabelSortTest.razor @@ -0,0 +1,69 @@ +@if (SortKeys is null && SortComparer is null) +{ + @if (SortDirection.HasValue) + { + @* default case - sorting by label text *@ + + + + + + } + else + { + @* default case - natural order when unspecified *@ + + + + + + } +} +else if (SortKeys is not null) +{ + @if (SortDirection.HasValue) + { + @* default case - sorting by label text *@ + + + + + + + } + else + { + @* default case - natural order when unspecified *@ + + + + + + + } +} +else if (SortComparer is not null) +{ + + + + + +} + +@code { + [Parameter] + public SortDirection? SortDirection { get; set; } + + [Parameter] + public string[]? SortKeys { get; set; } + + [Parameter] + public IComparer? SortComparer { get; set; } + + public class TestComparer : IComparer + { + public int Compare(MudTabPanel? x, MudTabPanel? y) + => Comparer.Default.Compare(x?.Tag as string, y?.Tag as string); + } +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/TimePicker/AutoCompleteTimePickerTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/TimePicker/AutoCompleteTimePickerTest.razor index 7c85f13be699..9e0df6f13a40 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/TimePicker/AutoCompleteTimePickerTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/TimePicker/AutoCompleteTimePickerTest.razor @@ -4,7 +4,7 @@ Clear Cancel - Ok + OK diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/ToggleGroup/ToggleGroupInterceptValueTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/ToggleGroup/ToggleGroupInterceptValueTest.razor new file mode 100644 index 000000000000..a370069acec4 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/ToggleGroup/ToggleGroupInterceptValueTest.razor @@ -0,0 +1,52 @@ + + + + + + + @UserAttendanceStatus.ToString() + + + + Simulate Success + + + Simulate Failure + + +@code { + public static string __description__ = "Visual Test for intercepted ValueChanged ToggleGroup"; + public enum AttendanceStatus { Accepted, Declined, Maybe } + public AttendanceStatus UserAttendanceStatus { get; private set; } = AttendanceStatus.Declined; + private bool _updateStatus = true; + private AttendanceStatus prevStatus = default!; + + protected override void OnInitialized() + { + prevStatus = UserAttendanceStatus; + } + + private void OnAttendanceStatusChanged(AttendanceStatus newStatus) + { + Console.WriteLine(prevStatus.ToString() + " -> " + newStatus.ToString()); + // _updateStatus is a simulated success, !_updateStatus is a simulated failure + if (_updateStatus) + { + // success + prevStatus = newStatus; + UserAttendanceStatus = newStatus; + } + else + { + // fail + UserAttendanceStatus = prevStatus; + } + StateHasChanged(); + } + +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipScrollTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipScrollTest.razor new file mode 100644 index 000000000000..fdb4e9c9d9d9 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipScrollTest.razor @@ -0,0 +1,24 @@ +
+ Inside Container + @for (int i = 1; i < 100; i++) + { +
+ + + +
+ } +
+Scrollable Body +@for (int i = 1; i < 100; i++) +{ +
+ + + +
+} + +@code { + public static string __description__ = "Scrolling Tooltips"; +} diff --git a/src/MudBlazor.UnitTests/Analyzers/ValidAttributeTests.cs b/src/MudBlazor.UnitTests/Analyzers/ValidAttributeTests.cs index 76f21c833ad3..4e2713962786 100644 --- a/src/MudBlazor.UnitTests/Analyzers/ValidAttributeTests.cs +++ b/src/MudBlazor.UnitTests/Analyzers/ValidAttributeTests.cs @@ -11,6 +11,7 @@ namespace MudBlazor.UnitTests.Analyzers { #nullable enable [TestFixture] + [Ignore("Until a solution for matching SDK/roslyn package reference is found see https://github.com/dotnet/roslyn/issues/77979")] public class ValidAttributeTests : BunitTest { ProjectCompilation Workspace { get; set; } = default!; diff --git a/src/MudBlazor.UnitTests/Components/CarouselTests.cs b/src/MudBlazor.UnitTests/Components/CarouselTests.cs index 9b97e89a2afd..cf9bbc0356b7 100644 --- a/src/MudBlazor.UnitTests/Components/CarouselTests.cs +++ b/src/MudBlazor.UnitTests/Components/CarouselTests.cs @@ -249,12 +249,12 @@ public async Task CarouselTest_DisableSwipeGesture() #pragma warning disable BL0005 // Component parameter should not be set outside of its component. comp.Instance.EnableSwipeGesture = false; await comp.InvokeAsync(() => mudSwipeArea.OnPointerDown(new PointerEventArgs { ClientX = 200, ClientY = 0 })); - await comp.InvokeAsync(async () => await mudSwipeArea.OnPointerUp(new PointerEventArgs { ClientX = 100, ClientY = 0 })); + await comp.InvokeAsync(async () => await mudSwipeArea.OnPointerUpAsync(new PointerEventArgs { ClientX = 100, ClientY = 0 })); comp.Instance.SelectedIndex.Should().Be(0); comp.Instance.EnableSwipeGesture = true; await comp.InvokeAsync(() => mudSwipeArea.OnPointerDown(new PointerEventArgs { ClientX = 200, ClientY = 0 })); - await comp.InvokeAsync(async () => await mudSwipeArea.OnPointerUp(new PointerEventArgs { ClientX = 100, ClientY = 0 })); + await comp.InvokeAsync(async () => await mudSwipeArea.OnPointerUpAsync(new PointerEventArgs { ClientX = 100, ClientY = 0 })); comp.Instance.SelectedIndex.Should().Be(1); #pragma warning restore BL0005 // Component parameter should not be set outside of its component. } diff --git a/src/MudBlazor.UnitTests/Components/ChartTests.cs b/src/MudBlazor.UnitTests/Components/ChartTests.cs index 4b9b1411367a..34bd73c55129 100644 --- a/src/MudBlazor.UnitTests/Components/ChartTests.cs +++ b/src/MudBlazor.UnitTests/Components/ChartTests.cs @@ -606,5 +606,31 @@ public void HeatMap_Override_Min_Max(double? min, double? max) heatmap.Instance._maxValue.Should().Be(max.HasValue ? max : .98); } + [TestCase(ChartType.Donut)] + [TestCase(ChartType.Line)] + [TestCase(ChartType.Pie)] + [TestCase(ChartType.Bar)] + [TestCase(ChartType.StackedBar)] + [TestCase(ChartType.HeatMap)] + [Test] + public void NoLabel_Chart_IsValid(ChartType chart) + { + var series = new List() + { + new() { Name = "Series 1", Data = [90, 79, 72, 69, 62, 62, 55, 65, 70] }, + new() { Name = "Series 2", Data = [10, 41, 35, 51, 49, 62, 69, 91, 148] }, + }; + + double[] data = { 50, 25, 20, 5, 16, 14, 8, 4, 2, 8, 10, 19, 8, 17, 6, 11, 19, 24, 35, 13, 20, 12 }; + + var isRadial = chart == ChartType.Donut || chart == ChartType.Pie; + + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.ChartType, chart) + .Add(p => p.ChartOptions, new ChartOptions { InterpolationOption = InterpolationOption.Periodic }) + .Add(p => p.ChartSeries, !isRadial ? series : null) + .Add(p => p.InputData, isRadial ? data : null)); + + } } } diff --git a/src/MudBlazor.UnitTests/Components/CheckBoxTests.cs b/src/MudBlazor.UnitTests/Components/CheckBoxTests.cs index 2e6f706bc12b..0b8f8f6551f1 100644 --- a/src/MudBlazor.UnitTests/Components/CheckBoxTests.cs +++ b/src/MudBlazor.UnitTests/Components/CheckBoxTests.cs @@ -355,46 +355,42 @@ public void CheckBoxLabelTest() } /// - /// Optional CheckBox should not have required attribute and aria-required should be false. + /// Optional CheckBox should not have required attribute. /// [Test] - public void OptionalCheckBox_Should_NotHaveRequiredAttributeAndAriaRequiredShouldBeFalse() + public void OptionalCheckBox_Should_NotHaveRequiredAttribute() { var comp = Context.RenderComponent>(); comp.Find("input").HasAttribute("required").Should().BeFalse(); - comp.Find("input").GetAttribute("aria-required").Should().Be("false"); } /// - /// Required CheckBox should have required and aria-required attributes. + /// Required CheckBox should have required attribute. /// [Test] - public void RequiredCheckBox_Should_HaveRequiredAndAriaRequiredAttributes() + public void RequiredCheckBox_Should_HaveRequiredAttribute() { var comp = Context.RenderComponent>(parameters => parameters .Add(p => p.Required, true)); comp.Find("input").HasAttribute("required").Should().BeTrue(); - comp.Find("input").GetAttribute("aria-required").Should().Be("true"); } /// - /// Required and aria-required CheckBox attributes should be dynamic. + /// Required CheckBox attribute should be dynamic. /// [Test] - public void RequiredAndAriaRequiredCheckBoxAttributes_Should_BeDynamic() + public void RequiredCheckBoxAttributes_Should_BeDynamic() { var comp = Context.RenderComponent>(); var input = () => comp.Find("input"); input().HasAttribute("required").Should().BeFalse(); - input().GetAttribute("aria-required").Should().Be("false"); comp.SetParametersAndRender(parameters => parameters .Add(p => p.Required, true)); input().HasAttribute("required").Should().BeTrue(); - input().GetAttribute("aria-required").Should().Be("true"); } [Test] diff --git a/src/MudBlazor.UnitTests/Components/CollapseTests.cs b/src/MudBlazor.UnitTests/Components/CollapseTests.cs index 1611622a0d5e..ac52dd9a91cb 100644 --- a/src/MudBlazor.UnitTests/Components/CollapseTests.cs +++ b/src/MudBlazor.UnitTests/Components/CollapseTests.cs @@ -3,7 +3,9 @@ // See the LICENSE file in the project root for more information. using AngleSharp.Dom; +using AngleSharp.Html.Dom; using Bunit; +using Bunit.Web.AngleSharp; using FluentAssertions; using MudBlazor.UnitTests.TestComponents.Collapse; using NUnit.Framework; @@ -25,23 +27,23 @@ public void Collapse_TwoWayBinding_Test1() IRenderedComponent> MudSwitch() => comp.FindComponent>(); // Initial state is expanded - MudSwitch().Find("input").GetAttribute("aria-checked").Should().Be("true"); + MudSwitch().Find("input").HasAttribute("checked").Should().BeTrue(); // Collapse via button Button().Click(); - MudSwitch().Find("input").GetAttribute("aria-checked").Should().Be("false"); + MudSwitch().Find("input").HasAttribute("checked").Should().BeFalse(); // Expand via button Button().Click(); - MudSwitch().Find("input").GetAttribute("aria-checked").Should().Be("true"); + MudSwitch().Find("input").HasAttribute("checked").Should().BeTrue(); // Collapse via switch MudSwitch().Find("input").Change(false); - MudSwitch().Find("input").GetAttribute("aria-checked").Should().Be("false"); + MudSwitch().Find("input").HasAttribute("checked").Should().BeFalse(); // Expand via switch MudSwitch().Find("input").Change(true); - MudSwitch().Find("input").GetAttribute("aria-checked").Should().Be("true"); + MudSwitch().Find("input").HasAttribute("checked").Should().BeTrue(); } } } diff --git a/src/MudBlazor.UnitTests/Components/DataGridGroupingTests.cs b/src/MudBlazor.UnitTests/Components/DataGridGroupingTests.cs index d9613c4de469..145911632c0d 100644 --- a/src/MudBlazor.UnitTests/Components/DataGridGroupingTests.cs +++ b/src/MudBlazor.UnitTests/Components/DataGridGroupingTests.cs @@ -314,6 +314,13 @@ public void DataGridGroupingTestBoundAndUnboundScenarios() professionCells[1].TextContent.Should().Be("Profession: (None)"); Rows().Count.Should().Be(4, because: "1 header row + 2 data rows + 1 footer row"); + var ungroup = comp.Find(".UnGroupAll"); + ungroup.Click(); + comp.Instance.IsAgeGrouped.Should().Be(false); + comp.Instance.IsGenderGrouped.Should().Be(false); + comp.Instance.IsProfessionGrouped.Should().Be(false); + comp.Instance.IsNameGrouped.Should().Be(false); + //click age grouping in grid var headerOption = comp.Find("th.age .mud-menu button"); headerOption.Click(); @@ -321,10 +328,14 @@ public void DataGridGroupingTestBoundAndUnboundScenarios() listItems.Count.Should().Be(2); var clickablePopover = listItems[1].Find(".mud-menu-item"); clickablePopover.Click(); + comp.Instance.IsAgeGrouped.Should().Be(true); + Rows().Count.Should().Be(5, because: "1 header row + 3 data rows + 1 footer row"); + + ungroup.Click(); comp.Instance.IsAgeGrouped.Should().Be(false); comp.Instance.IsGenderGrouped.Should().Be(false); - comp.Instance.IsProfessionGrouped.Should().Be(true); - Rows().Count.Should().Be(5, because: "1 header row + 3 data rows + 1 footer row"); + comp.Instance.IsProfessionGrouped.Should().Be(false); + comp.Instance.IsNameGrouped.Should().Be(false); //click gender grouping in grid headerOption = comp.Find("th.gender .mud-menu button"); @@ -334,9 +345,13 @@ public void DataGridGroupingTestBoundAndUnboundScenarios() clickablePopover = listItems[1].Find(".mud-menu-item"); clickablePopover.Click(); comp.Instance.IsGenderGrouped.Should().Be(true); + Rows().Count.Should().Be(4, because: "1 header row + 2 data rows + 1 footer row"); + + ungroup.Click(); comp.Instance.IsAgeGrouped.Should().Be(false); - comp.Instance.IsProfessionGrouped.Should().Be(true); - Rows().Count.Should().Be(5, because: "1 header row + 3 data rows + 1 footer row"); + comp.Instance.IsGenderGrouped.Should().Be(false); + comp.Instance.IsProfessionGrouped.Should().Be(false); + comp.Instance.IsNameGrouped.Should().Be(false); //click Name grouping in grid headerOption = comp.Find("th.name .mud-menu button"); @@ -345,11 +360,15 @@ public void DataGridGroupingTestBoundAndUnboundScenarios() listItems.Count.Should().Be(2); clickablePopover = listItems[1].Find(".mud-menu-item"); clickablePopover.Click(); - comp.Instance.IsGenderGrouped.Should().Be(true); - comp.Instance.IsAgeGrouped.Should().Be(false); - comp.Instance.IsProfessionGrouped.Should().Be(true); + comp.Instance.IsNameGrouped.Should().Be(true); Rows().Count.Should().Be(6, because: "1 header row + 4 data rows + 1 footer row"); + ungroup.Click(); + comp.Instance.IsAgeGrouped.Should().Be(false); + comp.Instance.IsGenderGrouped.Should().Be(false); + comp.Instance.IsProfessionGrouped.Should().Be(false); + comp.Instance.IsNameGrouped.Should().Be(false); + //click profession grouping in grid headerOption = comp.Find("th.profession .mud-menu button"); headerOption.Click(); @@ -357,12 +376,72 @@ public void DataGridGroupingTestBoundAndUnboundScenarios() listItems.Count.Should().Be(2); clickablePopover = listItems[1].Find(".mud-menu-item"); clickablePopover.Click(); + comp.Instance.IsProfessionGrouped.Should().Be(true); + Rows().Count.Should().Be(4, because: "1 header row + 2 data rows + 1 footer row"); + } + + [Test] + public void DataGridGrouping_ManualGroupByOrderTest() + { + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + var popoverProvider = comp.FindComponent(); + + var nameCol = dataGrid.Instance.RenderedColumns[0]; // Name col + nameCol.PropertyName.Should().Be("Name"); + var ageCol = dataGrid.Instance.RenderedColumns[1]; // Age col + ageCol.PropertyName.Should().Be("Age"); + var genderCol = dataGrid.Instance.RenderedColumns[2]; // Gender col + genderCol.PropertyName.Should().Be("Gender"); + + // Assert that initially, before any user interaction, groupbyorder on name is 0 + // note that name starts grouped + nameCol._groupByOrderState.Value.Should().Be(0); + + // add age grouping + var headerOption = comp.Find("th.age .mud-menu button"); + headerOption.Click(); + var listItems = popoverProvider.FindComponents(); + listItems.Count.Should().Be(2); + var clickablePopover = listItems[1].Find(".mud-menu-item"); + clickablePopover.Click(); + comp.Instance.IsAgeGrouped.Should().Be(true); + // Assert that after user interaction, groupbyorder on age is 1 (+1) + ageCol._groupByOrderState.Value.Should().Be(1); + + // add gender grouping + headerOption = comp.Find("th.gender .mud-menu button"); + headerOption.Click(); + listItems = popoverProvider.FindComponents(); + listItems.Count.Should().Be(2); + clickablePopover = listItems[1].Find(".mud-menu-item"); + clickablePopover.Click(); comp.Instance.IsGenderGrouped.Should().Be(true); + // Assert that after user interaction, groupbyorder on gender is 2 (+1 of max) + genderCol._groupByOrderState.Value.Should().Be(2); + + // remove age grouping + headerOption = comp.Find("th.age .mud-menu button"); + headerOption.Click(); + listItems = popoverProvider.FindComponents(); + listItems.Count.Should().Be(2); + clickablePopover = listItems[1].Find(".mud-menu-item"); + clickablePopover.Click(); comp.Instance.IsAgeGrouped.Should().Be(false); - comp.Instance.IsProfessionGrouped.Should().Be(false); - Rows().Count.Should().Be(6, because: "1 header row + 4 data rows + 1 footer row"); - } + // Assert that after user interaction, groupbyorder on age is 0 + ageCol._groupByOrderState.Value.Should().Be(0); + // remove gender grouping + headerOption = comp.Find("th.gender .mud-menu button"); + headerOption.Click(); + listItems = popoverProvider.FindComponents(); + listItems.Count.Should().Be(2); + clickablePopover = listItems[1].Find(".mud-menu-item"); + clickablePopover.Click(); + comp.Instance.IsGenderGrouped.Should().Be(false); + // Assert that after user interaction, groupbyorder on gender is 0 + genderCol._groupByOrderState.Value.Should().Be(0); + } [Test] public async Task DataGridGroupedWithServerDataPaginationTest() @@ -463,22 +542,24 @@ public async Task GroupExpandClick_ShouldToggleExpandedState() var row = rows[0]; // Test the method // Act + var col = dataGrid.Instance.RenderedColumns[3]; // primary industry col + col.PropertyName.Should().Be("PrimaryIndustry"); + var defaultExpanded = col._groupExpandedState.Value; void GetCount(bool currExpanded) { - var defaultExpanded = row.Instance.GroupDefinition.Expanded; // Whatever the expanded state is if it differs from the default it should be in the dictionary - dataGrid.Instance._groupExpansionsDict.Count.Should().Be(currExpanded != defaultExpanded ? 1 : 0); + dataGrid.Instance._groupExpansionsDict.Count.Should().BeGreaterThan(currExpanded != defaultExpanded ? 1 : 0); } // Test the UI - var expandButton = () => row.Find(".mud-datagrid-group-button"); + var expandButton = () => row.Find(".mud-table-row-expander"); expandButton.Should().NotBeNull(); - expandButton().Click(); + expandButton().Click(); // collapse the group row.WaitForAssertion(() => row.Instance._expanded.Should().BeFalse()); row.WaitForAssertion(() => GetCount(false)); - expandButton().Click(); + expandButton().Click(); // expand the group row.WaitForAssertion(() => row.Instance._expanded.Should().BeTrue()); row.WaitForAssertion(() => GetCount(true)); } @@ -540,5 +621,29 @@ public async Task DataGrid_Grouping_GroupDefinition() row.Instance.Items.Should().NotBeNull(); row.Instance.Items.Count().Should().Be(2); } + + // https://github.com/MudBlazor/MudBlazor/pull/10213 + // Allow grouping by null valus and toggle grouping keeps initial state on other groups + [Test] + public void DataGrid_Grouping_ByNull() + { + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + // until a change happens this bool tracks whether GroupExpanded is applied. + dataGrid.Instance._groupInitialExpanded = true; + comp.FindAll("tbody .mud-table-row").Count.Should().Be(7); + var btn = comp.Find(".addnull-button"); + btn.Should().NotBeNull(); + btn.Click(); + // group header, group row, and group footer so + 3 + comp.FindAll("tbody .mud-table-row").Count.Should().Be(10); + // ensure toggling single expansion doesn't close all + var expanderButtons = comp.FindAll("button.mud-table-row-expander"); + // nulled expander + var expander = expanderButtons[expanderButtons.Count - 1]; + // clicking should close 2 rows, footer and group row. header should still exist + expander.Click(); + comp.FindAll("tbody .mud-table-row").Count.Should().Be(8); + } } } diff --git a/src/MudBlazor.UnitTests/Components/DatePickerTests.cs b/src/MudBlazor.UnitTests/Components/DatePickerTests.cs index 105a83b123dc..9a6d4457715e 100644 --- a/src/MudBlazor.UnitTests/Components/DatePickerTests.cs +++ b/src/MudBlazor.UnitTests/Components/DatePickerTests.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Time.Testing; -using MudBlazor.UnitTests.TestComponents; using MudBlazor.UnitTests.TestComponents.DatePicker; using NUnit.Framework; using static Bunit.ComponentParameterFactory; @@ -339,7 +338,7 @@ public void Open_CloseBySelectingADate_CheckClosed() { var comp = OpenPicker(); // clicking a day button to select a date and close - comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("23")).Click(); + comp.SelectDate("23"); comp.WaitForAssertion(() => comp.FindAll("div.mud-picker-open").Count.Should().Be(0), TimeSpan.FromSeconds(5)); comp.Instance.Date.Should().NotBeNull(); } @@ -351,10 +350,7 @@ public void Open_CloseBySelectingADate_CheckClosed_Check_DateChangedCount() DateTime? returnDate = null; var comp = OpenPicker(EventCallback(nameof(MudDatePicker.DateChanged), (DateTime? date) => { eventCount++; returnDate = date; })); // clicking a day button to select a date and close - comp.FindAll("button.mud-picker-calendar-day") - .Where(x => !x.ClassList.Contains("mud-hidden") && x.TrimmedText().Equals("23")) - .First() - .Click(); + comp.SelectDate("23"); comp.WaitForAssertion(() => comp.FindAll("div.mud-picker-open").Count.Should().Be(0), TimeSpan.FromSeconds(5)); comp.Instance.Date.Should().NotBeNull(); eventCount.Should().Be(1); @@ -436,7 +432,7 @@ public void OpenToMonth_Select3rdMonth_Select2ndDay_CheckDate() // should show months comp.FindAll("div.mud-picker-month-container").Count.Should().Be(1); comp.FindAll("div.mud-picker-calendar-container > div.mud-picker-month-container > button.mud-picker-month")[2].Click(); - comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("2")).Click(); + comp.SelectDate("2"); comp.Instance.Date?.Date.Should().Be(new DateTime(DateTime.Now.Year, 3, 2)); } @@ -455,7 +451,7 @@ public void Open_ClickCalendarHeader_Click4thMonth_Click23rdDay_CheckDate() // should show months comp.FindAll("div.mud-picker-month-container").Count.Should().Be(1); comp.FindAll("div.mud-picker-calendar-container > div.mud-picker-month-container > button.mud-picker-month")[3].Click(); - comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("23")).Click(); + comp.SelectDate("23"); comp.Instance.Date?.Date.Should().Be(new DateTime(DateTime.Now.Year, 4, 23)); } @@ -467,7 +463,7 @@ public void DatePickerStaticWithPickerActionsDayClick_Test() picker.Markup.Should().Contain("mud-selected"); //confirm selected date is shown - comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("23")).Click(); + comp.SelectDate("23"); var date = DateTime.Today.Subtract(TimeSpan.FromDays(60)); @@ -536,7 +532,7 @@ public void Open_ClickYear_ClickCurrentYear_Click2ndMonth_Click1_CheckDate() comp.FindAll("div.mud-picker-month-container").Count.Should().Be(1); comp.FindAll("div.mud-picker-calendar-container > div.mud-picker-month-container > button.mud-picker-month")[1].Click(); comp.FindAll("div.mud-picker-calendar-container > div.mud-picker-calendar-header").Count.Should().Be(1); - comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("1")).Click(); + comp.SelectDate("1"); comp.Instance.Date?.Date.Should().Be(new DateTime(2022, 2, 1)); } @@ -550,7 +546,7 @@ public void Open_FixYear_Click2ndMonth_Click3_CheckDate() comp.FindAll("div.mud-picker-month-container").Count.Should().Be(1); comp.FindAll("div.mud-picker-calendar-container > div.mud-picker-month-container > button.mud-picker-month")[1].Click(); comp.FindAll("div.mud-picker-calendar-container > div.mud-picker-calendar-header").Count.Should().Be(1); - comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("3")).Click(); + comp.SelectDate("3"); comp.Instance.Date?.Date.Should().Be(new DateTime(2021, 2, 3)); } @@ -576,7 +572,7 @@ public void Open_FixMonth_ClickYear_Click3_CheckDate() comp.FindAll("div.mud-picker-calendar-container > div.mud-picker-year-container > div.mud-picker-year").First(x => x.TrimmedText().Contains("2022")).Click(); comp.FindAll("div.mud-picker-month-container").Count.Should().Be(0); comp.FindAll("div.mud-picker-calendar-container > div.mud-picker-calendar-header").Count.Should().Be(1); - comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("3")).Click(); + comp.SelectDate("3"); comp.Instance.Date?.Date.Should().Be(new DateTime(2022, 1, 3)); } @@ -589,7 +585,7 @@ public void Open_FixYear_FixMonth_Click3_CheckDate() comp.FindAll("div.mud-picker-calendar-container > div.mud-picker-year-container").Count.Should().Be(0); comp.FindAll("div.mud-picker-calendar-container > .mud-picker-calendar-header > .mud-picker-calendar-header-switch > .mud-button-month").Count.Should().Be(0); comp.FindAll("div.mud-picker-calendar-container > div.mud-picker-calendar-header").Count.Should().Be(1); - comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("3")).Click(); + comp.SelectDate("3"); comp.Instance.Date?.Date.Should().Be(new DateTime(2022, 1, 3)); } @@ -984,13 +980,11 @@ public async Task CheckAutoCloseDatePickerTest() // So the test is working when the day is 20 if (now.Day != 20) { - comp - .FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("20")).Click(); + comp.SelectDate("20"); } else { - comp - .FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("19")).Click(); + comp.SelectDate("19"); } // Check that the date should remain the same because autoclose is false @@ -1017,11 +1011,11 @@ public async Task CheckAutoCloseDatePickerTest() // Clicking a day button to select a date if (now.Day != 20) { - comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("20")).Click(); + comp.SelectDate("20"); } else { - comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("19")).Click(); + comp.SelectDate("19"); } // Check that the date should be equal to the new date 19 or 20 @@ -1059,11 +1053,11 @@ public async Task CheckReadOnlyTest() // So the test is working when the day is 20 if (now.Day != 20) { - comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("20")).Click(); + comp.SelectDate("20"); } else { - comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("19")).Click(); + comp.SelectDate("19"); } // Close the datepicker @@ -1094,11 +1088,11 @@ public async Task CheckReadOnlyTest() // Clicking a day button to select a date if (now.Day != 21) { - comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("22")).Click(); + comp.SelectDate("22"); } else { - comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("21")).Click(); + comp.SelectDate("21"); } // Close the datepicker @@ -1120,7 +1114,7 @@ public async Task CheckDateTimeMinValueTest() // An error should be raised if the datepicker could not be not opened and the days could not generated // It means that there would be an exception! - comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("1")).Click(); + comp.SelectDate("1"); } /// @@ -1363,7 +1357,7 @@ public void Display_SelectedDate_WhenWrapped() comp.Find(".mud-input-adornment button").Click(); comp.FindAll("div.mud-picker-open").Count.Should().Be(1); - comp.FindAll("button.mud-picker-calendar-day").First(x => x.TrimmedText().Equals("15")).Click(); + comp.SelectDate("15"); ((IHtmlInputElement)comp.FindAll("input")[0]).Value.Should().Be(comp.Instance.Picker.Text); } @@ -1449,6 +1443,7 @@ public void RequiredAndAriaRequiredDatePickerAttributes_Should_BeDynamic() /// Test to check if the outlined dates class shows up correctly /// [Test] + [SetCulture("en-US")] public void DatePicker_CustomTimerProviderTest() { var timeProvider = new FakeTimeProvider(); diff --git a/src/MudBlazor.UnitTests/Components/DateRangePickerTests.cs b/src/MudBlazor.UnitTests/Components/DateRangePickerTests.cs index fd4eef1567fb..bb4033aa946b 100644 --- a/src/MudBlazor.UnitTests/Components/DateRangePickerTests.cs +++ b/src/MudBlazor.UnitTests/Components/DateRangePickerTests.cs @@ -828,6 +828,7 @@ public void RequiredAndAriaRequiredDateRangePickerAttributes_Should_BeDynamic() } [Test] + [SetCulture("en-US")] public void FormatFirst_Should_RenderCorrectly() { DateRange range = new DateRange(new DateTime(2024, 04, 22), new DateTime(2024, 04, 23)); @@ -845,6 +846,7 @@ public void FormatFirst_Should_RenderCorrectly() } [Test] + [SetCulture("en-US")] public void FormatLast_Should_RenderCorrectly() { DateRange range = new DateRange(new DateTime(2024, 04, 22), new DateTime(2024, 04, 23)); diff --git a/src/MudBlazor.UnitTests/Components/MaskTests.cs b/src/MudBlazor.UnitTests/Components/MaskTests.cs index a0fc65492908..da892d3d4508 100644 --- a/src/MudBlazor.UnitTests/Components/MaskTests.cs +++ b/src/MudBlazor.UnitTests/Components/MaskTests.cs @@ -957,5 +957,116 @@ public async Task ClearableReadOnlyMask_Should_NotHaveClearButton() comp.FindAll(".mud-input-clear-button").Count.Should().Be(0); } + + [Test] + public async Task MetaKeyShortcuts_Should_NotIntroduceExtraCharacters() + { + var comp = Context.RenderComponent>(); + comp.SetParam(x => x.Mask, RegexMask.Email()); + var tf = comp.Instance; + var maskField = comp.FindComponent().Instance; + + // prep field + await comp.InvokeAsync(() => maskField.OnFocused(new FocusEventArgs())); + await comp.InvokeAsync(() => maskField.HandleKeyDown(new KeyboardEventArgs() + { + Key = "a" + })); + comp.WaitForAssertion(() => maskField.Text.Should().Be("a")); + comp.WaitForAssertion(() => tf.Value.Should().Be("a")); + await comp.InvokeAsync(() => maskField.HandleKeyDown(new KeyboardEventArgs() + { + Key = "b" + })); + comp.WaitForAssertion(() => maskField.Text.Should().Be("ab")); + comp.WaitForAssertion(() => tf.Value.Should().Be("ab")); + await comp.InvokeAsync(() => maskField.HandleKeyDown(new KeyboardEventArgs() + { + Key = "c" + })); + comp.WaitForAssertion(() => maskField.Text.Should().Be("abc")); + comp.WaitForAssertion(() => tf.Value.Should().Be("abc")); + + // test common shortcuts + await comp.InvokeAsync(() => maskField.HandleKeyDown(new KeyboardEventArgs() + { + Key = "c", + MetaKey = true + })); + comp.WaitForAssertion(() => maskField.Text.Should().Be("abc")); + comp.WaitForAssertion(() => tf.Value.Should().Be("abc")); + await comp.InvokeAsync(() => maskField.HandleKeyDown(new KeyboardEventArgs() + { + Key = "v", + MetaKey = true + })); + comp.WaitForAssertion(() => maskField.Text.Should().Be("abc")); + comp.WaitForAssertion(() => tf.Value.Should().Be("abc")); + await comp.InvokeAsync(() => maskField.HandleKeyDown(new KeyboardEventArgs() + { + Key = "x", + MetaKey = true + })); + comp.WaitForAssertion(() => maskField.Text.Should().Be("abc")); + comp.WaitForAssertion(() => tf.Value.Should().Be("abc")); + } + + [Test] + public async Task CutShortcut_Should_ClearSelectionAndCopyItToClipboard() + { + var comp = Context.RenderComponent>(); + comp.SetParam(x => x.Mask, RegexMask.Email()); + var tf = comp.Instance; + var maskField = comp.FindComponent().Instance; + + // prep field + await comp.InvokeAsync(() => maskField.OnFocused(new FocusEventArgs())); + await comp.InvokeAsync(() => maskField.HandleKeyDown(new KeyboardEventArgs() + { + Key = "a" + })); + comp.WaitForAssertion(() => maskField.Text.Should().Be("a")); + comp.WaitForAssertion(() => tf.Value.Should().Be("a")); + await comp.InvokeAsync(() => maskField.HandleKeyDown(new KeyboardEventArgs() + { + Key = "b" + })); + comp.WaitForAssertion(() => maskField.Text.Should().Be("ab")); + comp.WaitForAssertion(() => tf.Value.Should().Be("ab")); + await comp.InvokeAsync(() => maskField.HandleKeyDown(new KeyboardEventArgs() + { + Key = "c" + })); + comp.WaitForAssertion(() => maskField.Text.Should().Be("abc")); + comp.WaitForAssertion(() => tf.Value.Should().Be("abc")); + + // select middle character ('b') and cut it + await comp.InvokeAsync(() => + { + maskField.OnSelect(1, 2); + comp.Find("input").CutAsync(new ClipboardEventArgs + { + Type = "cut" + }); + }); + comp.WaitForAssertion(() => maskField.Text.Should().Be("ac")); + comp.WaitForAssertion(() => tf.Value.Should().Be("ac")); + Context.JSInterop.VerifyInvoke("mudWindow.copyToClipboard", 1); + Context.JSInterop.Invocations["mudWindow.copyToClipboard"].Single().Arguments.Should().BeEquivalentTo(["b"]); + + // select last character ('c') and cut it + await comp.InvokeAsync(() => + { + maskField.OnSelect(1, 2); + comp.Find("input").CutAsync(new ClipboardEventArgs + { + Type = "cut" + }); + }); + comp.WaitForAssertion(() => maskField.Text.Should().Be("a")); + comp.WaitForAssertion(() => tf.Value.Should().Be("a")); + Context.JSInterop.VerifyInvoke("mudWindow.copyToClipboard", 2); + Context.JSInterop.Invocations["mudWindow.copyToClipboard"][1].Arguments.Should().BeEquivalentTo(["c"]); + } } } diff --git a/src/MudBlazor.UnitTests/Components/OverlayTests.cs b/src/MudBlazor.UnitTests/Components/OverlayTests.cs index f94dfa7ff483..3f48c8fdf05e 100644 --- a/src/MudBlazor.UnitTests/Components/OverlayTests.cs +++ b/src/MudBlazor.UnitTests/Components/OverlayTests.cs @@ -311,7 +311,6 @@ public async Task Overlay_HandleLockScrollChanges(bool absolute, bool lockscroll { var scrollManagerMock = new Mock(); Context.Services.AddSingleton(scrollManagerMock.Object); - var providerComp = Context.RenderComponent(); var visible = true; @@ -324,7 +323,7 @@ public async Task Overlay_HandleLockScrollChanges(bool absolute, bool lockscroll var mudOverlay = comp.Instance; - // Initial unlock state + // Initial unlock state without JSRuntime scrollManagerMock.Verify(s => s.UnlockScrollAsync(It.IsAny(), It.IsAny()), Times.Never()); if (!absolute && lockscroll) diff --git a/src/MudBlazor.UnitTests/Components/ProgressCircularTests.cs b/src/MudBlazor.UnitTests/Components/ProgressCircularTests.cs index 67a3d710aa72..e568bd8e6ead 100644 --- a/src/MudBlazor.UnitTests/Components/ProgressCircularTests.cs +++ b/src/MudBlazor.UnitTests/Components/ProgressCircularTests.cs @@ -53,7 +53,7 @@ public void DefaultStructure(bool indeterminate) container.GetAttribute("aria-live").Should().Be( indeterminate ? null : "polite"); - container.ChildElementCount.Should().Be(1); + container.ChildElementCount.Should().Be(2); var circleContainer = container.Children[0]; circleContainer.ClassList.Should().Contain("mud-progress-circular-svg"); diff --git a/src/MudBlazor.UnitTests/Components/RatingTests.cs b/src/MudBlazor.UnitTests/Components/RatingTests.cs index b84d7250c984..421382251cb2 100644 --- a/src/MudBlazor.UnitTests/Components/RatingTests.cs +++ b/src/MudBlazor.UnitTests/Components/RatingTests.cs @@ -223,14 +223,6 @@ public void RatingTestIconColors() RatingItemsSpans()[1].PointerOut(); } - [Test] - public void ReadOnlyRating_ShouldNotRenderInputs() - { - var comp = Context.RenderComponent(parameters => parameters - .Add(p => p.ReadOnly, true)); - comp.FindAll("input").Should().BeEmpty(); - } - [Test] public async Task RatingTest_KeyboardNavigation() { diff --git a/src/MudBlazor.UnitTests/Components/SwipeTests.cs b/src/MudBlazor.UnitTests/Components/SwipeTests.cs index 3e88136a27d6..5f1f5578da6f 100644 --- a/src/MudBlazor.UnitTests/Components/SwipeTests.cs +++ b/src/MudBlazor.UnitTests/Components/SwipeTests.cs @@ -18,12 +18,12 @@ public async Task SwipeTest_1() var swipe = comp.FindComponent(); await comp.InvokeAsync(() => swipe.Instance._yDown = 50); - await comp.InvokeAsync(() => swipe.Instance.OnPointerUp(new PointerEventArgs())); + await comp.InvokeAsync(() => swipe.Instance.OnPointerUpAsync(new PointerEventArgs())); - await comp.InvokeAsync(() => swipe.Instance.OnPointerCancel(new PointerEventArgs())); + await comp.InvokeAsync(() => swipe.Instance.OnPointerCancelAsync(new PointerEventArgs())); comp.WaitForAssertion(() => swipe.Instance._xDown.Should().Be(null)); - await comp.InvokeAsync(() => swipe.Instance.OnPointerUp(new PointerEventArgs())); + await comp.InvokeAsync(() => swipe.Instance.OnPointerUpAsync(new PointerEventArgs())); comp.WaitForAssertion(() => swipe.Instance._xDown.Should().Be(null)); } @@ -36,14 +36,14 @@ public async Task SwipeTest_2() // Swipe below the sensitivity should not make change. await comp.InvokeAsync(() => swipe.Instance.OnPointerDown(new PointerEventArgs { ClientX = 0, ClientY = 0 })); - await comp.InvokeAsync(() => swipe.Instance.OnPointerUp(new PointerEventArgs { ClientX = 20, ClientY = 20 })); + await comp.InvokeAsync(() => swipe.Instance.OnPointerUpAsync(new PointerEventArgs { ClientX = 20, ClientY = 20 })); comp.WaitForAssertion(() => comp.Instance.SwipeDirection.Should().Be(SwipeDirection.None)); comp.WaitForAssertion(() => comp.Instance.SwipeDelta.Should().Be(null)); await comp.InvokeAsync(() => swipe.Instance.OnPointerDown(new PointerEventArgs { ClientX = 0, ClientY = 0 })); - await comp.InvokeAsync(() => swipe.Instance.OnPointerUp(new PointerEventArgs { ClientX = 150, ClientY = 200 })); - await comp.InvokeAsync(() => swipe.Instance.OnPointerUp(new PointerEventArgs { ClientX = 100, ClientY = 50 })); + await comp.InvokeAsync(() => swipe.Instance.OnPointerUpAsync(new PointerEventArgs { ClientX = 150, ClientY = 200 })); + await comp.InvokeAsync(() => swipe.Instance.OnPointerUpAsync(new PointerEventArgs { ClientX = 100, ClientY = 50 })); comp.WaitForAssertion(() => comp.Instance.SwipeDirection.Should().Be(SwipeDirection.TopToBottom)); comp.WaitForAssertion(() => comp.Instance.SwipeDelta.Should().Be(-200)); @@ -52,7 +52,7 @@ public async Task SwipeTest_2() [Test] public void SwipeTest_PreventDefault_SetTrue() { - var listenerIds = new int[] { 1, 2, 3 }; + var listenerIds = new int[] { 1, 2, 3, 4, 5 }; var handler = Context.JSInterop.Setup(invocation => invocation.Identifier == "mudElementRef.addDefaultPreventingHandlers") .SetResult(listenerIds); @@ -64,13 +64,13 @@ public void SwipeTest_PreventDefault_SetTrue() var invocation = handler.VerifyInvoke("mudElementRef.addDefaultPreventingHandlers"); invocation.Arguments.Count.Should().Be(2); - invocation.Arguments[1].Should().BeEquivalentTo(new[] { "onpointerdown", "onpointerup", "onpointercancel" }); + invocation.Arguments[1].Should().BeEquivalentTo(new[] { "onpointerdown", "onpointerup", "onpointercancel", "onpointermove", "onpointerleave" }); } [Test] public void SwipeTest_PreventDefault_SetFalse() { - var listenerIds = new int[] { 1, 2, 3 }; + var listenerIds = new int[] { 1, 2, 3, 4, 5 }; Context.JSInterop.Setup(invocation => invocation.Identifier == "mudElementRef.addDefaultPreventingHandlers") .SetResult(listenerIds); @@ -87,7 +87,7 @@ public void SwipeTest_PreventDefault_SetFalse() var invocation = handler.VerifyInvoke("mudElementRef.removeDefaultPreventingHandlers"); invocation.Arguments.Count.Should().Be(3); - invocation.Arguments[1].Should().BeEquivalentTo(new[] { "onpointerdown", "onpointerup", "onpointercancel" }); + invocation.Arguments[1].Should().BeEquivalentTo(new[] { "onpointerdown", "onpointerup", "onpointercancel", "onpointermove", "onpointerleave" }); invocation.Arguments[2].Should().BeEquivalentTo(listenerIds); } } diff --git a/src/MudBlazor.UnitTests/Components/SwitchTests.cs b/src/MudBlazor.UnitTests/Components/SwitchTests.cs index cfa4b2a5a5b6..2c0bd7a370a9 100644 --- a/src/MudBlazor.UnitTests/Components/SwitchTests.cs +++ b/src/MudBlazor.UnitTests/Components/SwitchTests.cs @@ -124,46 +124,42 @@ public void SwitchLabelTextSizeTest() } /// - /// Optional Switch should not have required attribute and aria-required should be false. + /// Optional Switch should not have required attribute should be false. /// [Test] - public void OptionalSwitch_Should_NotHaveRequiredAttributeAndAriaRequiredShouldBeFalse() + public void OptionalSwitch_Should_NotHaveRequiredAttribute() { var comp = Context.RenderComponent>(); comp.Find("input").HasAttribute("required").Should().BeFalse(); - comp.Find("input").GetAttribute("aria-required").Should().Be("false"); } /// - /// Required Switch should have required and aria-required attributes. + /// Required Switch should have the required attribute. /// [Test] - public void RequiredSwitch_Should_HaveRequiredAndAriaRequiredAttributes() + public void RequiredSwitch_Should_HaveRequiredAttribute() { var comp = Context.RenderComponent>(parameters => parameters .Add(p => p.Required, true)); comp.Find("input").HasAttribute("required").Should().BeTrue(); - comp.Find("input").GetAttribute("aria-required").Should().Be("true"); } /// - /// Required and aria-required Switch attributes should be dynamic. + /// Required Switch attribute should be dynamic. /// [Test] - public void RequiredAndAriaRequiredSwitchAttributes_Should_BeDynamic() + public void RequiredSwitchAttribute_Should_BeDynamic() { var comp = Context.RenderComponent>(); comp.Find("input").HasAttribute("required").Should().BeFalse(); - comp.Find("input").GetAttribute("aria-required").Should().Be("false"); comp.SetParametersAndRender(parameters => parameters .Add(p => p.Required, true)); comp.Find("input").HasAttribute("required").Should().BeTrue(); - comp.Find("input").GetAttribute("aria-required").Should().Be("true"); } diff --git a/src/MudBlazor.UnitTests/Components/TableTests.cs b/src/MudBlazor.UnitTests/Components/TableTests.cs index 2b0cbcc8bd28..bc3fc0989982 100644 --- a/src/MudBlazor.UnitTests/Components/TableTests.cs +++ b/src/MudBlazor.UnitTests/Components/TableTests.cs @@ -13,6 +13,15 @@ namespace MudBlazor.UnitTests.Components [TestFixture] public class TableTests : BunitTest { + [Test] + public void CustomTableClass() + { + var comp = Context.RenderComponent(); + var table = comp.FindComponent>(); + table.SetParametersAndRender(parameters => parameters.Add(x => x.TableClass, "table-custom-class")); + table.Markup.Should().Contain("class=\"mud-table-root table-custom-class\""); + } + /// /// OnRowClick event callback should be fired regardless of the selection state /// diff --git a/src/MudBlazor.UnitTests/Components/TabsTests.cs b/src/MudBlazor.UnitTests/Components/TabsTests.cs index 93c255908181..b2f6cc6a31fa 100644 --- a/src/MudBlazor.UnitTests/Components/TabsTests.cs +++ b/src/MudBlazor.UnitTests/Components/TabsTests.cs @@ -195,7 +195,7 @@ public void ScrollToItem_NoScrollingNeeded() styleAttr.Should().Be("transform:translateX(-0px);"); - GetSliderValue(comp).Should().Be(i * 250.0); + GetSliderValue(comp).Should().BeApproximately(i * (1.0 / 6.0) * 100, 0.00001); } } @@ -256,7 +256,7 @@ public void ScrollToItem_CentralizeViewAroundActiveItem(double totalSize, double var styleAttr = toolbarWrapper.GetAttribute("style"); styleAttr.Should().Be($"transform:translateX(-{expectedTranslation.ToString(CultureInfo.InvariantCulture)}px);"); - GetSliderValue(comp).Should().Be(2 * 100.0); + GetSliderValue(comp).Should().BeApproximately((2.0 / 6.0) * 100.0, 0.00001); } [Test] @@ -290,7 +290,7 @@ public void ScrollToItem_CentralizeViewAroundActiveItem_ScrollVertically(double var styleAttr = toolbarWrapper.GetAttribute("style"); styleAttr.Should().Be($"transform:translateY(-{expectedTranslation.ToString(CultureInfo.InvariantCulture)}px);"); - GetSliderValue(comp, "top").Should().Be(2 * 100.0); + GetSliderValue(comp, "top").Should().BeApproximately((2.0 / 6.0) * 100.0, 0.00001); } [Test] @@ -330,7 +330,7 @@ public void ScrollToItem_CentralizeView_ActivateAllItems() var styleAttr = toolbarWrapper.GetAttribute("style"); styleAttr.Should().Be($"transform:translateX(-{expectedTranslations[i].ToString(CultureInfo.InvariantCulture)}px);"); - GetSliderValue(comp).Should().Be(i * 100.0); + GetSliderValue(comp).Should().BeApproximately((i / 6.0) * 100.0, 0.00001); } } @@ -470,7 +470,7 @@ public void ScrollPrev() var styleAttr = toolbarWrapper.GetAttribute("style"); styleAttr.Should().Be($"transform:translateX(-{expectedTranslation.ToString(CultureInfo.InvariantCulture)}px);"); - GetSliderValue(comp).Should().Be(5 * 100.0); + GetSliderValue(comp).Should().BeApproximately((5.0 / 6.0) * 100.0, 0.00001); } } @@ -493,12 +493,12 @@ public void Handle_ResizeOfPanel() var scrollButtons = comp.FindComponents(); scrollButtons.First().Instance.Disabled.Should().BeTrue(); - GetSliderValue(comp).Should().Be(1 * 100.0); + GetSliderValue(comp).Should().BeApproximately((1.0 / 6.0) * 100.0, 0.00001); observer.UpdateTotalPanelSize(200.0); scrollButtons.First().Instance.Disabled.Should().BeTrue(); - GetSliderValue(comp).Should().Be(1 * 100.0); + GetSliderValue(comp).Should().BeApproximately((1.0 / 6.0) * 100.0, 0.00001); } [Test] @@ -677,12 +677,12 @@ public void Handle_ResizeOfElement() var scrollButtons = comp.FindComponents(); scrollButtons.First().Instance.Disabled.Should().BeTrue(); - GetSliderValue(comp).Should().Be(1 * 100.0); + GetSliderValue(comp).Should().BeApproximately((1.0 / 6.0) * 100.0, 0.00001); observer.UpdatePanelSize(0, 200.0); scrollButtons.First().Instance.Disabled.Should().BeTrue(); - GetSliderValue(comp).Should().Be(200.0); + GetSliderValue(comp).Should().BeApproximately((2.0 / 7.0) * 100.0, 0.00001); } [Test] @@ -701,11 +701,11 @@ public async Task Handle_Add() comp.Instance.SetPanelActive(4); - GetSliderValue(comp).Should().Be(4 * 100.0); + GetSliderValue(comp).Should().BeApproximately((4.0 / 6.0) * 100.0, 0.00001); await comp.Instance.AddPanel(); - GetSliderValue(comp).Should().Be(4 * 100.0); + GetSliderValue(comp).Should().BeApproximately((4.0 / 7.0) * 100.0, 0.00001); var scrollButtons = comp.FindComponents(); scrollButtons.Should().HaveCount(2); @@ -739,7 +739,7 @@ public async Task Handle_Remove_BeforeSelection() comp.Instance.SetPanelActive(2); - GetSliderValue(comp).Should().Be(2 * 100.0); + GetSliderValue(comp).Should().BeApproximately((2.0 / 6.0) * 100.0, 0.00001); var scrollButtons = comp.FindComponents(); @@ -756,7 +756,7 @@ public async Task Handle_Remove_BeforeSelection() styleAttr.Should().Be($"transform:translateX(-100px);"); var sliderValue = GetSliderValue(comp); - GetSliderValue(comp).Should().Be(1 * 100.0); + GetSliderValue(comp).Should().BeApproximately((1.0 / 5.0) * 100.0, 0.00001); } [Test] @@ -784,7 +784,7 @@ public async Task Handle_Remove_AfterSelection() toolbarWrapper.HasAttribute("style").Should().Be(true); var styleAttr = toolbarWrapper.GetAttribute("style"); styleAttr.Should().Be($"transform:translateX(-100px);"); - GetSliderValue(comp).Should().Be(2 * 100.0); + GetSliderValue(comp).Should().BeApproximately((2.0 / 6.0) * 100.0, 0.00001); } await comp.Instance.RemovePanel(5); @@ -797,7 +797,7 @@ public async Task Handle_Remove_AfterSelection() toolbarWrapper.HasAttribute("style").Should().Be(true); var styleAttr = toolbarWrapper.GetAttribute("style"); styleAttr.Should().Be($"transform:translateX(-100px);"); - GetSliderValue(comp).Should().Be(2 * 100.0); + GetSliderValue(comp).Should().BeApproximately((2.0 / 5.0) * 100.0, 0.00001); } } @@ -1249,7 +1249,7 @@ private static double GetSliderValue(IRenderedComponent comp var styleAttribute = slider.GetAttribute("style"); var indexToSplit = styleAttribute.IndexOf($"{attribute}:"); var substring = styleAttribute.Substring(indexToSplit + attribute.Length + 1).Split(';')[0]; - substring = substring.Remove(substring.Length - 2); + substring = substring.Remove(substring.Length - 1); var value = double.Parse(substring, CultureInfo.InvariantCulture); return value; @@ -1335,5 +1335,122 @@ public void TabPanel_Hidden_Class(bool visible) panel.ClassList.Should().Contain("mud-tab-panel-hidden"); } } + + [Test] + public void LabelSorting_NaturalOrderIfSortingUnspecified() + { + // all parameters unspecified + var comp = Context.RenderComponent(); + + // all labels should be present and in natural order + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab").Count.Should().Be(3); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[0].InnerHtml.Should().Be("2"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[1].InnerHtml.Should().Be("1"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[2].InnerHtml.Should().Be("3"); + } + + [Test] + public void LabelSorting_SpecifiedDirectionWithoutKeysOrComparer() + { + /* *** + * all labels should be present and in natural order + */ + var comp = Context.RenderComponent( + ComponentParameter.CreateParameter("SortDirection", SortDirection.None) + ); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab").Count.Should().Be(3); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[0].InnerHtml.Should().Be("2"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[1].InnerHtml.Should().Be("1"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[2].InnerHtml.Should().Be("3"); + + /* *** + * all labels should be present and in lexicographically ascending order + */ + comp = Context.RenderComponent( + ComponentParameter.CreateParameter("SortDirection", SortDirection.Ascending) + ); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab").Count.Should().Be(3); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[0].InnerHtml.Should().Be("1"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[1].InnerHtml.Should().Be("2"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[2].InnerHtml.Should().Be("3"); + + /* *** + * all labels should be present and in lexicographically descending order + */ + comp = Context.RenderComponent( + ComponentParameter.CreateParameter("SortDirection", SortDirection.Descending) + ); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab").Count.Should().Be(3); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[0].InnerHtml.Should().Be("3"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[1].InnerHtml.Should().Be("2"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[2].InnerHtml.Should().Be("1"); + } + + [Test] + public void LabelSorting_SpecifiedDirectionWithKeysAndDefaultComparer() + { + // Caution: intentionally descending order to ensure this behaviour overrides Text ordering + string[] sortKeys = ["c", "b", "a"]; + + /* *** + * all labels should be present and in natural order + */ + var comp = Context.RenderComponent( + ComponentParameter.CreateParameter("SortDirection", SortDirection.None), + ComponentParameter.CreateParameter("SortKeys", sortKeys) + ); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab").Count.Should().Be(4); + // sort order is per markup: 2, 1, 3, 4. Keys are ignored as list is unsorted. + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[0].InnerHtml.Should().Be("2"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[1].InnerHtml.Should().Be("1"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[2].InnerHtml.Should().Be("3"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[3].InnerHtml.Should().Be("4"); + + /* *** + * all labels should be present and in lexicographically ascending order + */ + comp = Context.RenderComponent( + ComponentParameter.CreateParameter("SortDirection", SortDirection.Ascending), + ComponentParameter.CreateParameter("SortKeys", sortKeys) + ); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab").Count.Should().Be(4); + // sort order is: 4, a=3, b=1, c=2 + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[0].InnerHtml.Should().Be("4"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[1].InnerHtml.Should().Be("3"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[2].InnerHtml.Should().Be("1"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[3].InnerHtml.Should().Be("2"); + + /* *** + * all labels should be present and in lexicographically descending order + */ + comp = Context.RenderComponent( + ComponentParameter.CreateParameter("SortDirection", SortDirection.Descending), + ComponentParameter.CreateParameter("SortKeys", sortKeys) + ); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab").Count.Should().Be(4); + // sort order is: c=2, b=1, a=3, 4 + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[0].InnerHtml.Should().Be("2"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[1].InnerHtml.Should().Be("1"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[2].InnerHtml.Should().Be("3"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[3].InnerHtml.Should().Be("4"); + } + + [Test] + public void LabelSorting_CustomSortComparer() + { + /* *** + * All labels should be present and in Tag order, ignoring SortDirection and Keys. + * For this test the Tabs.SortDirection is set to Descending in markup, and the SortKeys + * are set to Apple=3, Banana=2, Cherry=1, so there is no combination of SortKey, Label + * or SortDirection that could ellicit the same sort order as we get from TestComparer. + */ + var comp = Context.RenderComponent( + ComponentParameter.CreateParameter("SortComparer", new LabelSortTest.TestComparer()) + ); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab").Count.Should().Be(3); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[0].InnerHtml.Should().Be("Cherry"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[1].InnerHtml.Should().Be("Apple"); + comp.FindAll("div.mud-tabs-tabbar-wrapper div.mud-tab")[2].InnerHtml.Should().Be("Banana"); + } } } diff --git a/src/MudBlazor.UnitTests/Components/ThemeProviderTests.cs b/src/MudBlazor.UnitTests/Components/ThemeProviderTests.cs index 19495f9af4cc..aa2534f26e6e 100644 --- a/src/MudBlazor.UnitTests/Components/ThemeProviderTests.cs +++ b/src/MudBlazor.UnitTests/Components/ThemeProviderTests.cs @@ -118,6 +118,7 @@ public void DifferentCultures(string cultureString) "--mud-palette-gray-darker: #616161;", "--mud-palette-overlay-dark: rgba(33,33,33,0.4980392156862745);", "--mud-palette-overlay-light: rgba(255,255,255,0.4980392156862745);", + "--mud-palette-border-opacity: 1;", "--mud-ripple-color: var(--mud-palette-text-primary);", "--mud-ripple-opacity: 0.1;", "--mud-ripple-opacity-secondary: 0.2;", @@ -253,7 +254,7 @@ public void DifferentCultures(string cultureString) } [Test] - public void DarkMode_Test() + public void IsDarkModeTest() { var comp = Context.RenderComponent(parameters => parameters .Add(p => p.IsDarkMode, true)); @@ -315,7 +316,7 @@ public void CustomThemeDefaultTest() } [Test] - public async Task WatchSystemTest() + public async Task WatchSystemDarkModeTest() { var systemMockValue = false; Task SystemChangedResult(bool newValue) @@ -324,8 +325,8 @@ Task SystemChangedResult(bool newValue) return Task.CompletedTask; } var comp = Context.RenderComponent(); - await comp.Instance.WatchSystemPreference(SystemChangedResult); - await comp.Instance.SystemPreferenceChanged(true); + await comp.Instance.WatchSystemDarkModeAsync(SystemChangedResult); + await comp.Instance.SystemDarkModeChangedAsync(true); systemMockValue.Should().BeTrue(); } diff --git a/src/MudBlazor.UnitTests/Components/ToggleGroupTests.cs b/src/MudBlazor.UnitTests/Components/ToggleGroupTests.cs index d97bf79ca7c5..a9472da8aa7b 100644 --- a/src/MudBlazor.UnitTests/Components/ToggleGroupTests.cs +++ b/src/MudBlazor.UnitTests/Components/ToggleGroupTests.cs @@ -6,6 +6,7 @@ using AngleSharp.Dom; using Bunit; using FluentAssertions; +using Microsoft.AspNetCore.Components.Web.Virtualization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using MudBlazor.UnitTests.Mocks; @@ -289,6 +290,92 @@ public void ToggleGroup_SetSelectedFromValuesTest(SelectionMode selMode, string } } + /// + /// This test will fail if selection isn't working without someone being subscribed to the ValueChanged event + /// + [Test] + [TestCase(SelectionMode.SingleSelection)] + [TestCase(SelectionMode.ToggleSelection)] + public void ToggleGroup_UnselectPreviousValue_OnToggle_Test(SelectionMode selMode) + { + // Arrange + var comp = Context.RenderComponent>(parameters => parameters + .Add(p => p.SelectionMode, selMode) + .AddChildContent>(item => item.Add(x => x.Value, "a")) + .AddChildContent>(item => item.Add(x => x.Value, "b")) + .AddChildContent>(item => item.Add(x => x.Value, "c")) + ); + + var toggleGroup = comp.Instance; + var items = toggleGroup.GetItems().ToList(); + + for (var i = 0; i < items.Count; i++) + { + // Act + comp.FindAll(".mud-toggle-item").GetItemByIndex(i).Click(); + // Assert + var currentItem = items[i]; + currentItem.Selected.Should().BeTrue(); + items.Except([currentItem]).All(x => !x.Selected).Should().BeTrue(); + if (selMode == SelectionMode.ToggleSelection) + { + comp.FindAll(".mud-toggle-item").GetItemByIndex(i).Click(); + currentItem.Selected.Should().BeFalse(); + } + } + } + + [Test] + [TestCase(SelectionMode.SingleSelection, "b")] + [TestCase(SelectionMode.MultiSelection, "b")] + [TestCase(SelectionMode.ToggleSelection, "b")] + public void ToggleGroup_SetSelectedFromValuesTest_WithAsyncItems(SelectionMode selMode, string selectedValues) + { + // Arrange + var comp = Context.RenderComponent>(parameters => parameters + .Add(p => p.SelectionMode, selMode) + .AddChildContent>(v => + v.Add(x => x.Items, ["a", "b", "c"]) + .Add, string>(x => x.ItemContent, + value => item => item.Add(x => x.Value, value)) + ) + ); + + var toggleGroup = comp.Instance; + var items = toggleGroup.GetItems().ToList(); + + // Act + if (selMode == SelectionMode.MultiSelection) + { + comp.SetParametersAndRender(parameters => parameters.Add(p => p.Values, [selectedValues])); + } + else + { + comp.SetParametersAndRender(parameters => parameters.Add(p => p.Value, selectedValues)); + } + + // Assert + // Verify only the selected item has the selected state + items.Single(x => x.Value == selectedValues).Selected.Should().BeTrue(); + items.Where(x => x.Value != selectedValues).All(x => !x.Selected).Should().BeTrue(); + + // Verify the UI reflects the selection + comp.FindAll("button.mud-toggle-item-selected").Count.Should().Be(1); + comp.Find("button.mud-toggle-item-selected").TextContent.Should().Contain(selectedValues); + + // Verify the internal state matches + if (selMode == SelectionMode.MultiSelection) + { + toggleGroup.Values.Should().BeEquivalentTo([selectedValues]); + toggleGroup.Value.Should().BeNull(); + } + else + { + toggleGroup.Value.Should().Be(selectedValues); + toggleGroup.Values.Should().BeNull(); + } + } + [TestCase(true)] [TestCase(false)] [Test] @@ -407,5 +494,65 @@ public void ToggleGroup_MultipleSelectionTest() toggleGroup.Values.Should().BeEquivalentTo(["a"]); toggleGroup.Value.Should().BeNull(); } + + /// + /// This test is based on https://github.com/MudBlazor/MudBlazor/issues/11384 + /// When a ToggleGroupItem is clicked, the value should be set or intercepted via the ValueChanged event + /// This test verifies that both scenarios update the ToggleGroupItem Selected state + /// + [Test] + public void ToggleGroup_ToggleSelectionTest() + { + var comp = Context.RenderComponent(); + IElement GetYesButton() => comp.FindAll(".mud-toggle-group .mud-toggle-item")[0]; + IElement GetNoButton() => comp.FindAll(".mud-toggle-group .mud-toggle-item")[1]; + IElement GetMaybeButton() => comp.FindAll(".mud-toggle-group .mud-toggle-item")[2]; + + var toggleGroup = comp.FindComponent>(); + // verify 3 ToggleGroupItems + var nodes = comp.FindAll(".mud-toggle-group .mud-toggle-item"); + nodes.Count.Should().Be(3); + GetYesButton().ClassList.Should().NotContain("mud-toggle-item-selected"); + GetNoButton().ClassList.Should().Contain("mud-toggle-item-selected"); + GetMaybeButton().ClassList.Should().NotContain("mud-toggle-item-selected"); + + // verify initial state + comp.Instance.UserAttendanceStatus.Should().Be(ToggleGroupInterceptValueTest.AttendanceStatus.Declined); + + var success = comp.Find(".simulate-success input"); // radio button + var failure = comp.Find(".simulate-failure input"); // radio button + + // start in success mode + var successStatus = comp.Find(".simulate-success .mud-button-root"); + successStatus.ClassList.Should().Contain("mud-checked"); + var failureStatus = comp.Find(".simulate-failure .mud-button-root"); + failureStatus.ClassList.Should().NotContain("mud-checked"); + + // change to yes + GetYesButton().Click(); + comp.WaitForAssertion(() => GetYesButton().ClassList.Should().Contain("mud-toggle-item-selected")); + GetNoButton().ClassList.Should().NotContain("mud-toggle-item-selected"); + GetMaybeButton().ClassList.Should().NotContain("mud-toggle-item-selected"); + comp.Instance.UserAttendanceStatus.Should().Be(ToggleGroupInterceptValueTest.AttendanceStatus.Accepted); + + // change to maybe + GetMaybeButton().Click(); + comp.WaitForAssertion(() => GetYesButton().ClassList.Should().NotContain("mud-toggle-item-selected")); + GetNoButton().ClassList.Should().NotContain("mud-toggle-item-selected"); + GetMaybeButton().ClassList.Should().Contain("mud-toggle-item-selected"); + comp.Instance.UserAttendanceStatus.Should().Be(ToggleGroupInterceptValueTest.AttendanceStatus.Maybe); + + // simulate failure where it saves last success + failure.Click(); + + // click yes with failure enabled to simulate no change + GetYesButton().Click(); + // check value has not changed, should still be maybe + comp.WaitForAssertion(() => comp.Instance.UserAttendanceStatus.Should().Be(ToggleGroupInterceptValueTest.AttendanceStatus.Maybe, "Value should not have changed form Maybe")); + // check selected has not changed, should still be maybe + GetYesButton().ClassList.Should().NotContain("mud-toggle-item-selected", "Selection should not have changed from maybe."); + GetNoButton().ClassList.Should().NotContain("mud-toggle-item-selected", "Selection should not have changed from maybe."); + GetMaybeButton().ClassList.Should().Contain("mud-toggle-item-selected", "Selection should still be maybe."); + } } } diff --git a/src/MudBlazor.UnitTests/Extensions/DataGridExtensionsTests.cs b/src/MudBlazor.UnitTests/Extensions/DataGridExtensionsTests.cs new file mode 100644 index 000000000000..3a8e3af4d507 --- /dev/null +++ b/src/MudBlazor.UnitTests/Extensions/DataGridExtensionsTests.cs @@ -0,0 +1,131 @@ +// Copyright (c) MudBlazor 2025 +// MudBlazor licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using FluentAssertions; +using NUnit.Framework; + +namespace MudBlazor.UnitTests.Extensions +{ + [TestFixture] + public class DataGridExtensionsTests + { + private class Person + { + public string Name { get; set; } = null!; + public int Age { get; set; } + } + + private static SortDefinition ByAge(bool descending = false) + { + return new SortDefinition("Age", descending, 0, p => p.Age); + } + + private static SortDefinition ByName(bool descending = false) + { + return new SortDefinition("Name", descending, 0, p => p.Name); + } + + [Test] + public void OrderBy_ICollection_EmptySource_ReturnsEmpty() + { + var sortDefs = new List> { ByAge() } as ICollection>; + var result = Array.Empty().OrderBySortDefinitions(sortDefs); + result.Should().BeEmpty(); + } + + [Test] + public void OrderBy_ICollection_NoDefinitions_ReturnsOriginalOrder() + { + var p1 = new Person { Name = "A", Age = 2 }; + var p2 = new Person { Name = "B", Age = 1 }; + var source = new[] { p1, p2 }; + ICollection> sortDefs = new List>(); + var result = source.OrderBySortDefinitions(sortDefs); + result.Should().Equal(source); + } + + [Test] + public void OrderBy_ICollection_SingleAscending_SortsByAge() + { + var p1 = new Person { Name = "X", Age = 40 }; + var p2 = new Person { Name = "Y", Age = 20 }; + var p3 = new Person { Name = "Z", Age = 30 }; + var source = new[] { p1, p2, p3 }; + ICollection> sortDefs = new List> { ByAge() }; + var result = source.OrderBySortDefinitions(sortDefs).ToList(); + result.Select(x => x.Age).Should().ContainInOrder(20, 30, 40); + } + + [Test] + public void OrderBy_IReadOnlyCollection_SingleDescending_SortsByAgeDesc() + { + var p1 = new Person { Name = "X", Age = 40 }; + var p2 = new Person { Name = "Y", Age = 20 }; + var p3 = new Person { Name = "Z", Age = 30 }; + var source = new[] { p1, p2, p3 }; + IReadOnlyCollection> sortDefs = new List> { ByAge(true) }; + var result = source.OrderBySortDefinitions(sortDefs).ToList(); + result.Select(x => x.Age).Should().ContainInOrder(40, 30, 20); + } + + [Test] + public void OrderBy_ICollection_MultipleColumns_AgeThenName() + { + var p1 = new Person { Name = "B", Age = 20 }; + var p2 = new Person { Name = "A", Age = 20 }; + var p3 = new Person { Name = "C", Age = 10 }; + var source = new[] { p1, p2, p3 }; + ICollection> sortDefs = new List> + { + ByAge(), + ByName() + }; + var result = source.OrderBySortDefinitions(sortDefs).ToList(); + // Age 10 first, then age 20 sorted by Name A,B + result.Select(x => (x.Age, x.Name)) + .Should().ContainInOrder((10, "C"), (20, "A"), (20, "B")); + } + + [Test] + public void OrderBy_ICollection_MultipleColumns_ThenByDescendingOnSecondDefinition() + { + // This forces the 'else' branch + ThenByDescending + var p1 = new Person { Name = "A", Age = 10 }; + var p2 = new Person { Name = "B", Age = 10 }; + var p3 = new Person { Name = "C", Age = 5 }; + var source = new[] { p1, p2, p3 }; + ICollection> sortDefs = new List> + { + ByAge(), // first => OrderBy + ByName(true) // second => ThenByDescending + }; + var result = source.OrderBySortDefinitions(sortDefs).ToList(); + // Age 5 first, then among Age 10 names in descending order B, A + result.Select(x => x.Name) + .Should().ContainInOrder("C", "B", "A"); + } + + [Test] + public void OrderBy_GridState_UsesItsSortDefinitions() + { + var p1 = new Person { Name = "Alpha", Age = 5 }; + var p2 = new Person { Name = "Beta", Age = 1 }; + var source = new[] { p1, p2 }; + var state = new GridState { SortDefinitions = new List> { ByName(true) } }; + var result = source.OrderBySortDefinitions(state).ToList(); + result.Select(x => x.Name).Should().ContainInOrder("Beta", "Alpha"); + } + + [Test] + public void OrderBy_GridStateVirtualize_UsesItsSortDefinitions() + { + var p1 = new Person { Name = "Alpha", Age = 5 }; + var p2 = new Person { Name = "Beta", Age = 1 }; + var source = new[] { p1, p2 }; + var vstate = new GridStateVirtualize { SortDefinitions = new List> { ByName() } }; + var result = source.OrderBySortDefinitions(vstate).ToList(); + result.Select(x => x.Name).Should().ContainInOrder("Alpha", "Beta"); + } + } +} diff --git a/src/MudBlazor.UnitTests/Utilities/NaturalComparerTest.cs b/src/MudBlazor.UnitTests/Utilities/NaturalComparerTest.cs index bb46796ccbd4..5935dd328e50 100644 --- a/src/MudBlazor.UnitTests/Utilities/NaturalComparerTest.cs +++ b/src/MudBlazor.UnitTests/Utilities/NaturalComparerTest.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 System.Text; using FluentAssertions; using MudBlazor.Utilities; @@ -337,6 +334,7 @@ public class NaturalComparerTest /// Test if comparer works as intended /// [Test] + [SetCulture("en-US")] public void SortFiles() { var fileNames = Encoding.UTF8.GetString(Convert.FromBase64String(s_encodedFileNames)) diff --git a/src/MudBlazor/Components/Autocomplete/MudAutocomplete.razor b/src/MudBlazor/Components/Autocomplete/MudAutocomplete.razor index 7330195c3ab3..2b0ca6e2aa3f 100644 --- a/src/MudBlazor/Components/Autocomplete/MudAutocomplete.razor +++ b/src/MudBlazor/Components/Autocomplete/MudAutocomplete.razor @@ -146,5 +146,5 @@ diff --git a/src/MudBlazor/Components/Autocomplete/MudAutocomplete.razor.cs b/src/MudBlazor/Components/Autocomplete/MudAutocomplete.razor.cs index 82b2da1ccb1f..5ebd0ad1a47c 100644 --- a/src/MudBlazor/Components/Autocomplete/MudAutocomplete.razor.cs +++ b/src/MudBlazor/Components/Autocomplete/MudAutocomplete.razor.cs @@ -491,6 +491,16 @@ protected string GetListItemClassname(bool isSelected) => [Parameter] public EventCallback ReturnedItemsCountChanged { get; set; } + /// + /// Prevents scrolling while the dropdown is open. + /// + /// + /// Defaults to false. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.ListBehavior)] + public bool LockScroll { get; set; } + /// /// Displays the search result drop-down. /// @@ -1130,7 +1140,6 @@ protected override async ValueTask DisposeAsyncCore() /// public override ValueTask FocusAsync() { - _handleNextFocus = true; // Let the event handler know it was not triggered by the user. return _elementReference.FocusAsync(); } @@ -1174,6 +1183,7 @@ private async Task OnTextChangedAsync(string? text) private async Task ListItemOnClickAsync(T item) { await SelectOptionAsync(item); + _handleNextFocus = true; // Let the event handler know it doesn't need to do anything. await FocusAsync(); } } diff --git a/src/MudBlazor/Components/Carousel/MudCarousel.razor.cs b/src/MudBlazor/Components/Carousel/MudCarousel.razor.cs index c492e0462ce3..9cbf403378c8 100644 --- a/src/MudBlazor/Components/Carousel/MudCarousel.razor.cs +++ b/src/MudBlazor/Components/Carousel/MudCarousel.razor.cs @@ -1,7 +1,5 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components; +using MudBlazor.State; using MudBlazor.Utilities; namespace MudBlazor @@ -16,9 +14,9 @@ public partial class MudCarousel : MudBaseBindableItemsControl _autoCycleState; + private readonly ParameterState _cycleTimeoutState; protected string Classname => new CssBuilder("mud-carousel") .AddClass($"mud-carousel-{(BulletsColor ?? _currentColor).ToDescriptionString()}") @@ -96,23 +94,7 @@ public partial class MudCarousel : MudBaseBindableItemsControl [Parameter] [Category(CategoryTypes.Carousel.Behavior)] - public bool AutoCycle - { - get => _autoCycle; - set - { - _autoCycle = value; - - if (_autoCycle) - { - InvokeAsync(async () => await ResetTimerAsync()); - } - else - { - InvokeAsync(async () => await StopTimerAsync()); - } - } - } + public bool AutoCycle { get; set; } = true; /// /// The delay before displaying the next when is true. @@ -122,23 +104,7 @@ public bool AutoCycle /// [Parameter] [Category(CategoryTypes.Carousel.Behavior)] - public TimeSpan AutoCycleTime - { - get => _cycleTimeout; - set - { - _cycleTimeout = value; - - if (_autoCycle) - { - InvokeAsync(async () => await ResetTimerAsync()); - } - else - { - InvokeAsync(async () => await StopTimerAsync()); - } - } - } + public TimeSpan AutoCycleTime { get; set; } = TimeSpan.FromSeconds(5); /// /// The custom CSS classes for the "Next" and "Previous" icons when is true. @@ -228,6 +194,35 @@ public TimeSpan AutoCycleTime [Parameter] public bool EnableSwipeGesture { get; set; } = true; + public MudCarousel() + { + using var registerScope = CreateRegisterScope(); + _autoCycleState = registerScope.RegisterParameter(nameof(AutoCycle)) + .WithParameter(() => AutoCycle) + .WithChangeHandler(OnAutoCycleChangedAsync); + + _cycleTimeoutState = registerScope.RegisterParameter(nameof(AutoCycleTime)) + .WithParameter(() => AutoCycleTime) + .WithChangeHandler(OnAutoCycleTimeChangedAsync); + + } + + private async Task OnAutoCycleChangedAsync(ParameterChangedEventArgs args) + { + if (args.Value) + await ResetTimerAsync(); + else + await StopTimerAsync(); + } + + private async Task OnAutoCycleTimeChangedAsync(ParameterChangedEventArgs args) + { + if (_autoCycleState.Value) + await ResetTimerAsync(); + else + await StopTimerAsync(); + } + /// /// Occurs when the SelectedIndex has changed. /// @@ -293,9 +288,9 @@ private void OnSwipeEnd(SwipeEventArgs e) /// private ValueTask StartTimerAsync() { - if (AutoCycle && !_disposing) + if (_autoCycleState.Value && !_disposing) { - _timer?.Change(AutoCycleTime, TimeSpan.Zero); + _timer?.Change(_cycleTimeoutState.Value, TimeSpan.Zero); } return ValueTask.CompletedTask; @@ -337,7 +332,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { // Prevent timer creation after or while disposal, which would result in a memory leak. if (_disposing) return; - _timer = new Timer(TimerElapsed, null, AutoCycle ? AutoCycleTime : Timeout.InfiniteTimeSpan, AutoCycleTime); + _timer = new Timer(TimerElapsed, null, _autoCycleState.Value ? _cycleTimeoutState.Value : Timeout.InfiniteTimeSpan, _cycleTimeoutState.Value); } } diff --git a/src/MudBlazor/Components/Chart/Charts/Line.razor.cs b/src/MudBlazor/Components/Chart/Charts/Line.razor.cs index 8e9a1efef6b4..8b46ff9b3214 100644 --- a/src/MudBlazor/Components/Chart/Charts/Line.razor.cs +++ b/src/MudBlazor/Components/Chart/Charts/Line.razor.cs @@ -139,7 +139,7 @@ private void GenerateVerticalGridLines(int numVerticalLines, double gridXUnits, }; _verticalLines.Add(line); - var xLabels = i < XAxisLabels.Length ? XAxisLabels[i] : ""; + var xLabels = i < XAxisLabels.Length ? XAxisLabels[i] : string.Empty; var lineValue = new SvgText() { X = x, @@ -204,13 +204,16 @@ double GetYForZeroPoint() continue; } + var index = j / interpolationResolution; + var xLabels = index < XAxisLabels.Length ? XAxisLabels[index] : string.Empty; + chartDataCircles.Add(new() { Index = j, CX = x, CY = y, LabelX = x, - LabelXValue = XAxisLabels[j / interpolationResolution], + LabelXValue = xLabels, LabelY = y, LabelYValue = dataValue.ToString(series.DataMarkerTooltipYValueFormat), }); diff --git a/src/MudBlazor/Components/CheckBox/MudCheckBox.razor b/src/MudBlazor/Components/CheckBox/MudCheckBox.razor index 32f9b401fd6b..76df0077ea69 100644 --- a/src/MudBlazor/Components/CheckBox/MudCheckBox.razor +++ b/src/MudBlazor/Components/CheckBox/MudCheckBox.razor @@ -5,9 +5,9 @@ - \ No newline at end of file + diff --git a/src/MudBlazor/Components/ColorPicker/MudColorPicker.razor b/src/MudBlazor/Components/ColorPicker/MudColorPicker.razor index 242676b70b54..16e2cb08f2de 100644 --- a/src/MudBlazor/Components/ColorPicker/MudColorPicker.razor +++ b/src/MudBlazor/Components/ColorPicker/MudColorPicker.razor @@ -85,7 +85,7 @@
+ aria-label="@item.ToString(MudColorOutputFormats.HexA)"> } } @@ -103,7 +103,7 @@ {
+ aria-label="@Localizer[LanguageResource.MudColorPicker_ToggleCollection]">
} @@ -144,7 +144,7 @@
+ aria-label="@item.ToString(MudColorOutputFormats.HexA)"> } } @@ -272,7 +272,7 @@
+ aria-label="@item.ToString(MudColorOutputFormats.HexA)"> } diff --git a/src/MudBlazor/Components/ColorPicker/MudColorPicker.razor.cs b/src/MudBlazor/Components/ColorPicker/MudColorPicker.razor.cs index eec25e6c78d6..f205f64e3c04 100644 --- a/src/MudBlazor/Components/ColorPicker/MudColorPicker.razor.cs +++ b/src/MudBlazor/Components/ColorPicker/MudColorPicker.razor.cs @@ -296,11 +296,11 @@ public MudColor? Value /// The delay, in milliseconds, between updates to the selected color when is true. ///
/// - /// Defaults to 300 milliseconds between updates. + /// Defaults to 50 milliseconds between updates. /// [Parameter] [Category(CategoryTypes.FormComponent.PickerBehavior)] - public int ThrottleInterval { get; set; } = 300; + public int ThrottleInterval { get; set; } = 50; /// protected override void OnInitialized() diff --git a/src/MudBlazor/Components/DataGrid/Column.razor.cs b/src/MudBlazor/Components/DataGrid/Column.razor.cs index a66d257d6740..352579d0153c 100644 --- a/src/MudBlazor/Components/DataGrid/Column.razor.cs +++ b/src/MudBlazor/Components/DataGrid/Column.razor.cs @@ -593,32 +593,11 @@ protected Column() .WithChangeHandler(OnGroupByOrderChangedAsync); } - private async Task OnGroupingParameterChangedAsync() - { - // Regroup DataGrid - if (DataGrid is not null) - { - await DataGrid.ChangedGrouping(this); - } - } + private void OnGroupingParameterChangedAsync() => DataGrid?.GroupItems(); - private async Task OnGroupExpandedChangedAsync() - { - // Regroup DataGrid - if (DataGrid is not null) - { - await DataGrid.ChangedGrouping(); - } - } + private void OnGroupByOrderChangedAsync() => DataGrid?.GroupItems(); - private async Task OnGroupByOrderChangedAsync() - { - // Regroup DataGrid - if (DataGrid is not null) - { - await DataGrid.ChangedGrouping(); - } - } + private void OnGroupExpandedChangedAsync() => DataGrid?.GroupItems(); protected override void OnInitialized() { diff --git a/src/MudBlazor/Components/DataGrid/DataGridGroupRow.razor b/src/MudBlazor/Components/DataGrid/DataGridGroupRow.razor index aa6a53ed0b05..8d91dabe515f 100644 --- a/src/MudBlazor/Components/DataGrid/DataGridGroupRow.razor +++ b/src/MudBlazor/Components/DataGrid/DataGridGroupRow.razor @@ -5,7 +5,7 @@
- @if (GroupDefinition?.GroupTemplate == null) { diff --git a/src/MudBlazor/Components/DataGrid/DataGridGroupRow.razor.cs b/src/MudBlazor/Components/DataGrid/DataGridGroupRow.razor.cs index f33d2bc2904a..de16fa03c0cd 100644 --- a/src/MudBlazor/Components/DataGrid/DataGridGroupRow.razor.cs +++ b/src/MudBlazor/Components/DataGrid/DataGridGroupRow.razor.cs @@ -82,26 +82,8 @@ protected override void OnParametersSet() internal void GroupExpandClick() { _expanded = !_expanded; - // update the expansion state for _groupExpansionsDict - // if it has a key we see if it differs from the definition Expanded State and update accordingly - // if it doesn't we add it if the new state doesn't match the definition if (Items != null) - { - var key = new { GroupDefinition.Title, Items.Key }; - if (DataGrid._groupExpansionsDict.ContainsKey(key)) - { - if (_expanded == GroupDefinition.Expanded) - DataGrid._groupExpansionsDict.Remove(key); - else - DataGrid._groupExpansionsDict[key] = _expanded; - } - else - { - if (_expanded != GroupDefinition.Expanded) - DataGrid._groupExpansionsDict.TryAdd(key, _expanded); - } - } - DataGrid._groupInitialExpanded = false; + DataGrid.ToggleGroupExpandAsync(GroupDefinition.Title, Items.Key, GroupDefinition, _expanded); } } } diff --git a/src/MudBlazor/Components/DataGrid/HeaderCell.razor.cs b/src/MudBlazor/Components/DataGrid/HeaderCell.razor.cs index add581867239..c54a36e82e1f 100644 --- a/src/MudBlazor/Components/DataGrid/HeaderCell.razor.cs +++ b/src/MudBlazor/Components/DataGrid/HeaderCell.razor.cs @@ -538,12 +538,9 @@ internal async Task GroupColumnAsync() if (Column is not null) { await Column.SetGroupingAsync(true); - await DataGrid.ChangedGrouping(Column); - } - else - { - await DataGrid.ChangedGrouping(); + await DataGrid.UpdateGroupingOrder(Column, true); } + DataGrid.GroupItems(); DataGrid.DropContainerHasChanged(); } @@ -552,8 +549,9 @@ internal async Task UngroupColumnAsync() if (Column is not null) { await Column.SetGroupingAsync(false); + await DataGrid.UpdateGroupingOrder(Column, false); } - await DataGrid.ChangedGrouping(); + DataGrid.GroupItems(); DataGrid.DropContainerHasChanged(); } diff --git a/src/MudBlazor/Components/DataGrid/MudDataGrid.razor b/src/MudBlazor/Components/DataGrid/MudDataGrid.razor index b2ccf37d5a43..67c191054fab 100644 --- a/src/MudBlazor/Components/DataGrid/MudDataGrid.razor +++ b/src/MudBlazor/Components/DataGrid/MudDataGrid.razor @@ -278,7 +278,7 @@ }
- + @foreach (var column in RenderedColumns) diff --git a/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs b/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs index 05c0d08465ec..68524a64b5a9 100644 --- a/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs +++ b/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs @@ -39,7 +39,7 @@ public partial class MudDataGrid<[DynamicallyAccessedMembers(DynamicallyAccessed private CancellationTokenSource _serverDataCancellationTokenSource; private IEnumerable _currentRenderFilteredItemsCache = null; internal GroupDefinition _groupDefinition; - internal Dictionary, bool> _groupExpansionsDict = []; + internal Dictionary _groupExpansionsDict = []; private GridData _serverData = new() { TotalItems = 0, Items = Array.Empty() }; private Func> _defaultFilterDefinitionFactory = () => new FilterDefinition(); @@ -2109,12 +2109,12 @@ internal IEnumerable> GetGroupDefinitions(GroupDefinition foreach (var group in groups) { var expanded = false; - if (group is { Key: not null }) + if (group is not null) { - var key = new { groupDef.Title, group.Key }; - expanded = _groupExpansionsDict.ContainsKey(key) ? - _groupExpansionsDict[key] : + var key = new GroupKey(groupDef.Title, group.Key); + expanded = _groupExpansionsDict.TryGetValue(key, out var value) ? value : groupDef.Expanded; + _groupExpansionsDict.TryAdd(key, expanded); } result.Add(new GroupDefinition { @@ -2125,23 +2125,57 @@ internal IEnumerable> GetGroupDefinitions(GroupDefinition Title = groupDef.Title, Parent = groupDef.Parent, InnerGroup = groupDef.InnerGroup, - Grouping = group, + Grouping = group ?? new EmptyGrouping(null) }); } return result; } - internal async Task ChangedGrouping(Column? col = null) + internal async Task UpdateGroupingOrder(Column column, bool added) { - // If col is not null add GroupByOrder is not set set it to the end - if (col is { _groupByOrderState.Value: 0 }) + // if added then add to the end if no _groupByOrderState.Value + if (added) { - var maxOrder = RenderedColumns.Max(x => x._groupByOrderState.Value); - await col._groupByOrderState.SetValueAsync(maxOrder + 1); + var groupedColumns = RenderedColumns.Where(x => x.GroupingState.Value && x != column); + var newOrder = groupedColumns.Any() ? groupedColumns.Max(x => x._groupByOrderState.Value) + 1 : 0; + await column._groupByOrderState.SetValueAsync(newOrder); + } + // if removed then reset _groupByOrderState.Value + else + { + await column._groupByOrderState.SetValueAsync(default); + } + // expand all but last grouped column when changed + await GroupExpansion(); + } + + private async Task GroupExpansion() + { + var groupedColumns = RenderedColumns.Where(x => x.GroupingState.Value).OrderBy(x => x._groupByOrderState.Value).SkipLast(1); + foreach (var col in groupedColumns.OrderBy(x => x._groupByOrderState.Value)) + { + await col._groupExpandedState.SetValueAsync(true); } - GroupItems(); + } + + internal void ToggleGroupExpandAsync(string title, object? key, GroupDefinition groupDef, bool expanded) + { + var groupKey = new GroupKey(title, key); + + // update the expansion state for _groupExpansionsDict + // if it has a key we see if it differs from the definition Expanded State and update accordingly + // if it doesn't we add it if the new state doesn't match the definition + var col = RenderedColumns.FirstOrDefault(x => x.GroupBy == groupDef.Selector); + if (expanded == col?._groupExpandedState.Value) + _groupExpansionsDict.Remove(groupKey); + else + _groupExpansionsDict[groupKey] = expanded; + + _groupInitialExpanded = false; + StateHasChanged(); } #nullable disable + /// /// Expands all groups async. /// @@ -2305,5 +2339,7 @@ public EmptyGrouping(TKey key) System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); } + + internal record GroupKey(string Title, object ItemsKey); } } diff --git a/src/MudBlazor/Components/DatePicker/MudDateRangePicker.razor b/src/MudBlazor/Components/DatePicker/MudDateRangePicker.razor index 15f73b4b82ba..2eb6ccadc497 100644 --- a/src/MudBlazor/Components/DatePicker/MudDateRangePicker.razor +++ b/src/MudBlazor/Components/DatePicker/MudDateRangePicker.razor @@ -35,8 +35,9 @@ PlaceholderEnd="@PlaceholderEnd" SeparatorIcon="@SeparatorIcon" Clearable="@(Clearable && !GetReadOnlyState())" - Underline="@Underline" /> + Underline="@Underline" + ShrinkLabel="@ShrinkLabel" /> ; -} \ No newline at end of file +} diff --git a/src/MudBlazor/Components/ExpansionPanel/MudExpansionPanel.razor.cs b/src/MudBlazor/Components/ExpansionPanel/MudExpansionPanel.razor.cs index 7f3daf6b01e7..47ad7f5e69df 100644 --- a/src/MudBlazor/Components/ExpansionPanel/MudExpansionPanel.razor.cs +++ b/src/MudBlazor/Components/ExpansionPanel/MudExpansionPanel.razor.cs @@ -40,7 +40,7 @@ public partial class MudExpansionPanel : MudComponentBase, IDisposable protected string PanelContentClassname => new CssBuilder("mud-expand-panel-content") - .AddClass("mud-expand-panel-gutters", Gutters || Parent?.Gutters == true) + .AddClass("mud-expand-panel-gutters", Gutters && Parent?.Gutters != false) .AddClass("mud-expand-panel-dense", Dense || Parent?.Dense == true) .Build(); diff --git a/src/MudBlazor/Components/Input/MudInput.razor.cs b/src/MudBlazor/Components/Input/MudInput.razor.cs index 4f8a08ff5cd0..6ef71108ac73 100644 --- a/src/MudBlazor/Components/Input/MudInput.razor.cs +++ b/src/MudBlazor/Components/Input/MudInput.razor.cs @@ -376,7 +376,7 @@ protected override async ValueTask DisposeAsyncCore() { if (IsJSRuntimeAvailable) { - await JsRuntime.InvokeVoidAsyncWithErrorHandling("mudElementRef.removeOnBlurEvent", ElementReference, _dotNetReferenceLazy); + await ElementReference.MudDetachBlurEventWithJS(_dotNetReferenceLazy.Value); if (AutoGrow) { await JsRuntime.InvokeVoidAsyncWithErrorHandling("mudInputAutoGrow.destroy", ElementReference); diff --git a/src/MudBlazor/Components/Input/MudRangeInput.razor.cs b/src/MudBlazor/Components/Input/MudRangeInput.razor.cs index e698897722e2..5be624495a00 100644 --- a/src/MudBlazor/Components/Input/MudRangeInput.razor.cs +++ b/src/MudBlazor/Components/Input/MudRangeInput.razor.cs @@ -26,7 +26,11 @@ public MudRangeInput() } protected string Classname => MudInputCssHelper.GetClassname(this, - () => !string.IsNullOrEmpty(Text) || Adornment == Adornment.Start || !string.IsNullOrWhiteSpace(PlaceholderStart) || !string.IsNullOrWhiteSpace(PlaceholderEnd)); + () => !string.IsNullOrEmpty(Text) + || Adornment == Adornment.Start + || !string.IsNullOrWhiteSpace(PlaceholderStart) + || !string.IsNullOrWhiteSpace(PlaceholderEnd) + || ShrinkLabel); internal override InputType GetInputType() => InputType; diff --git a/src/MudBlazor/Components/Mask/MudMask.razor b/src/MudBlazor/Components/Mask/MudMask.razor index 2e45665d0c16..3faefd64710b 100644 --- a/src/MudBlazor/Components/Mask/MudMask.razor +++ b/src/MudBlazor/Components/Mask/MudMask.razor @@ -119,4 +119,4 @@ } - \ No newline at end of file + diff --git a/src/MudBlazor/Components/Mask/MudMask.razor.cs b/src/MudBlazor/Components/Mask/MudMask.razor.cs index 1cfb84911e04..447849393f82 100644 --- a/src/MudBlazor/Components/Mask/MudMask.razor.cs +++ b/src/MudBlazor/Components/Mask/MudMask.razor.cs @@ -199,7 +199,12 @@ protected internal async Task HandleKeyDown(KeyboardEventArgs e) { try { - if ((e.CtrlKey && e.Key != "Backspace") || e.AltKey || GetReadOnlyState()) + if (e.CtrlKey && e.Key != "Backspace" + // on macOS, the copy-paste command is Cmd + V + // cmd is identified using the MetaKey property + || e.MetaKey && e.Key != "Backspace" + || e.AltKey + || GetReadOnlyState()) return; switch (e.Key) { @@ -364,15 +369,7 @@ public override ValueTask SelectRangeAsync(int pos1, int pos2) } internal void OnCopy() - { - var text = Text; - if (Mask.Selection != null) - { - (_, text, _) = BaseMask.SplitSelection(text, Mask.Selection.Value); - } - - JsApiService.CopyToClipboardAsync(text); - } + => CopySelectionToClipboard(); internal async void OnPaste(string? text) { @@ -467,6 +464,7 @@ private async void OnCut(ClipboardEventArgs obj) if (GetReadOnlyState()) return; + CopySelectionToClipboard(); if (_selection != null) Mask.Delete(); await UpdateAsync(); @@ -490,6 +488,20 @@ protected override async ValueTask DisposeAsyncCore() } } + /// + /// Copies the currently selected text (or the entire text if nothing is selected) to the clipboard. + /// + private void CopySelectionToClipboard() + { + var text = Text; + if (Mask.Selection != null) + { + (_, text, _) = BaseMask.SplitSelection(text, Mask.Selection.Value); + } + + JsApiService.CopyToClipboardAsync(text); + } + [GeneratedRegex(@"^.$")] private static partial Regex ValidCharacterRegularExpression(); } diff --git a/src/MudBlazor/Components/MessageBox/MudMessageBox.razor b/src/MudBlazor/Components/MessageBox/MudMessageBox.razor index 82d3f863835a..98079b05e27c 100644 --- a/src/MudBlazor/Components/MessageBox/MudMessageBox.razor +++ b/src/MudBlazor/Components/MessageBox/MudMessageBox.razor @@ -28,7 +28,7 @@ } -
+
@if (CancelButton is not null) { diff --git a/src/MudBlazor/Components/Overlay/MudOverlay.razor.cs b/src/MudBlazor/Components/Overlay/MudOverlay.razor.cs index 25fe5b3eb153..9e63db1c21bf 100644 --- a/src/MudBlazor/Components/Overlay/MudOverlay.razor.cs +++ b/src/MudBlazor/Components/Overlay/MudOverlay.razor.cs @@ -213,17 +213,25 @@ public MudOverlay() protected override async Task OnAfterRenderAsync(bool firstTime) { - // If the overlay is initially visible and modeless auto-close is enabled, - // then start tracking pointer down events. - if (firstTime && Visible && !Modal && AutoClose) + // set initial handlelockscrollchanges + if (firstTime) { - await StartModelessAutoCloseTrackingAsync(); + _previousLockScroll = LockScroll; + _previousAbsolute = Absolute; + await HandleLockScrollChange(); + + // If the overlay is initially visible and modeless auto-close is enabled, + // then start tracking pointer down events. + if (Visible && !Modal && AutoClose) + { + await StartModelessAutoCloseTrackingAsync(); + } } } protected override async Task OnParametersSetAsync() { - if (_previousLockScroll != LockScroll || _previousAbsolute != Absolute) + if (IsJSRuntimeAvailable && (_previousLockScroll != LockScroll || _previousAbsolute != Absolute)) { // handle lock scroll change when user changes LockScroll parameter _previousLockScroll = LockScroll; @@ -286,6 +294,11 @@ internal async Task CloseOverlayAsync() /// private ValueTask BlockScrollAsync() { + if (!IsJSRuntimeAvailable) + { + return ValueTask.CompletedTask; + } + // we only want to lock scroll once if (_lockCount > 0) { @@ -301,6 +314,11 @@ private ValueTask BlockScrollAsync() /// private ValueTask UnblockScrollAsync() { + if (!IsJSRuntimeAvailable) + { + return ValueTask.CompletedTask; + } + _lockCount = Math.Max(0, _lockCount - 1); return ScrollManager.UnlockScrollAsync("body", LockScrollClass); } diff --git a/src/MudBlazor/Components/Picker/MudPicker.razor b/src/MudBlazor/Components/Picker/MudPicker.razor index 29a027fdd673..61e26135feb7 100644 --- a/src/MudBlazor/Components/Picker/MudPicker.razor +++ b/src/MudBlazor/Components/Picker/MudPicker.razor @@ -36,6 +36,7 @@ Clearable="@(Clearable && !GetReadOnlyState())" OnClearButtonClick="@(async () => await ClearAsync())" TextUpdateSuppression="@(Editable && !GetReadOnlyState())" + ShrinkLabel="@ShrinkLabel" @onclick="OnClickAsync" />; #nullable enable diff --git a/src/MudBlazor/Components/Picker/MudPicker.razor.cs b/src/MudBlazor/Components/Picker/MudPicker.razor.cs index 833f5577131a..56e018606624 100644 --- a/src/MudBlazor/Components/Picker/MudPicker.razor.cs +++ b/src/MudBlazor/Components/Picker/MudPicker.razor.cs @@ -295,11 +295,11 @@ protected MudPicker(Converter converter) : base(converter) { } /// The display variant of the text input. /// /// - /// Defaults to . + /// Defaults to in . /// [Parameter] [Category(CategoryTypes.FormComponent.Appearance)] - public Variant Variant { get; set; } = Variant.Text; + public Variant Variant { get; set; } = MudGlobal.InputDefaults.Variant; /// /// The location of the for the input. @@ -391,11 +391,25 @@ public string? Text public RenderFragment>? PickerActions { get; set; } /// - /// Applies vertical spacing. + /// The amount of vertical spacing for the text input. /// + /// + /// Defaults to in . + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Appearance)] + public Margin Margin { get; set; } = MudGlobal.InputDefaults.Margin; + + /// + /// Shows the label inside the text input if no is specified. + /// + /// + /// Defaults to false in . + /// When true, the label will not move into the input when the input is empty. + /// [Parameter] [Category(CategoryTypes.FormComponent.Appearance)] - public Margin Margin { get; set; } = Margin.None; + public bool ShrinkLabel { get; set; } = MudGlobal.InputDefaults.ShrinkLabel; /// /// The mask to apply to input values when is true. diff --git a/src/MudBlazor/Components/Progress/MudProgressCircular.razor b/src/MudBlazor/Components/Progress/MudProgressCircular.razor index de450a559c14..82bccd5d499b 100644 --- a/src/MudBlazor/Components/Progress/MudProgressCircular.razor +++ b/src/MudBlazor/Components/Progress/MudProgressCircular.razor @@ -10,14 +10,24 @@ class="@Classname" style="@Style" @attributes="UserAttributes"> - - @if (Indeterminate) - { + @if (Indeterminate) + { + - } - else - { + + +
+ @ChildContent +
+ } + else + { + - } - + + +
+ @ChildContent +
+ }
diff --git a/src/MudBlazor/Components/Progress/MudProgressCircular.razor.cs b/src/MudBlazor/Components/Progress/MudProgressCircular.razor.cs index 471441f2ed32..d14eecf5ff04 100644 --- a/src/MudBlazor/Components/Progress/MudProgressCircular.razor.cs +++ b/src/MudBlazor/Components/Progress/MudProgressCircular.razor.cs @@ -118,6 +118,13 @@ public partial class MudProgressCircular : MudComponentBase [Category(CategoryTypes.ProgressCircular.Appearance)] public int StrokeWidth { get; set; } = 3; + /// + /// RenderFragment for rendering custom content + /// + [Parameter] + [Category(CategoryTypes.ProgressCircular.Appearance)] + public RenderFragment? ChildContent { get; set; } + public MudProgressCircular() { using var registerScope = CreateRegisterScope(); diff --git a/src/MudBlazor/Components/Radio/MudRadio.razor b/src/MudBlazor/Components/Radio/MudRadio.razor index 60a8f9e7ec41..648a83d27a8a 100644 --- a/src/MudBlazor/Components/Radio/MudRadio.razor +++ b/src/MudBlazor/Components/Radio/MudRadio.razor @@ -5,8 +5,8 @@
diff --git a/src/MudBlazor/Components/SwipeArea/MudSwipeArea.razor.cs b/src/MudBlazor/Components/SwipeArea/MudSwipeArea.razor.cs index a9889bf174c5..965f3b7d1b97 100644 --- a/src/MudBlazor/Components/SwipeArea/MudSwipeArea.razor.cs +++ b/src/MudBlazor/Components/SwipeArea/MudSwipeArea.razor.cs @@ -7,15 +7,17 @@ namespace MudBlazor #nullable enable /// - /// An area which receives swipe events for devices where touch events are supported. + /// An area which receives swipe events. /// public partial class MudSwipeArea : MudComponentBase { - private static readonly string[] _preventDefaultEventNames = ["onpointerdown", "onpointerup", "onpointercancel"]; + private static readonly string[] _preventDefaultEventNames = ["onpointerdown", "onpointerup", "onpointercancel", "onpointermove", "onpointerleave"]; private double? _swipeDelta; internal int[]? _listenerIds; internal double? _xDown, _yDown; + private double? _xDownway, _yDownway; + private bool _isSwipeOnProgress; private bool _preventDefaultChanged; private ElementReference _componentRef; @@ -26,6 +28,13 @@ public partial class MudSwipeArea : MudComponentBase [Category(CategoryTypes.SwipeArea.Behavior)] public RenderFragment? ChildContent { get; set; } + /// + /// Occurs when a swipe has on progress. Ignores sensitivity. + /// + [Parameter] + [Category(CategoryTypes.SwipeArea.Behavior)] + public EventCallback OnSwipeMove { get; set; } + /// /// Occurs when a swipe has ended. /// @@ -33,6 +42,20 @@ public partial class MudSwipeArea : MudComponentBase [Category(CategoryTypes.SwipeArea.Behavior)] public EventCallback OnSwipeEnd { get; set; } + /// + /// Occurs when a swipe leaves the area. + /// + [Parameter] + [Category(CategoryTypes.SwipeArea.Behavior)] + public EventCallback OnSwipeLeave { get; set; } + + /// + /// Occurs when a swipe cancelled. + /// + [Parameter] + [Category(CategoryTypes.SwipeArea.Behavior)] + public EventCallback OnSwipeCancel { get; set; } + /// /// The amount of pixels which must be swiped to raise the event. /// @@ -98,23 +121,53 @@ protected override async Task OnAfterRenderAsync(bool firstRender) internal void OnPointerDown(PointerEventArgs arg) { + _isSwipeOnProgress = true; _xDown = arg.ClientX; _yDown = arg.ClientY; + _xDownway = arg.ClientX; + _yDownway = arg.ClientY; } - internal async Task OnPointerUp(PointerEventArgs arg) + private async Task OnPointerMoveAsync(PointerEventArgs arg) + { + if (!_isSwipeOnProgress) + { + return; + } + + var xDiff = (_xDownway - arg.ClientX) ?? 0; + var yDiff = (_yDownway - arg.ClientY) ?? 0; + + if (Math.Abs(xDiff) > Math.Abs(yDiff)) + { + _swipeDelta = xDiff; + } + else + { + _swipeDelta = yDiff; + } + + var swipeDirection = GetSwipeDirections(xDiff, yDiff); + await OnSwipeMove.InvokeAsync(new MultiDimensionSwipeEventArgs(arg, swipeDirection, [xDiff, yDiff], this)); + + _xDownway = arg.ClientX; + _yDownway = arg.ClientY; + } + + internal async Task OnPointerUpAsync(PointerEventArgs arg) { if (_xDown is null || _yDown is null) { + _isSwipeOnProgress = false; return; } var xDiff = _xDown.Value - arg.ClientX; var yDiff = _yDown.Value - arg.ClientY; - if (Math.Abs(xDiff) < Sensitivity && Math.Abs(yDiff) < Sensitivity) + if (!OnSwipeMove.HasDelegate && Math.Abs(xDiff) < Sensitivity && Math.Abs(yDiff) < Sensitivity) { - _xDown = _yDown = null; + Cancel(); return; } @@ -132,12 +185,42 @@ internal async Task OnPointerUp(PointerEventArgs arg) } await OnSwipeEnd.InvokeAsync(new SwipeEventArgs(arg, swipeDirection, _swipeDelta, this)); - _xDown = _yDown = null; + _xDown = _yDown = _xDownway = _yDownway = null; + _isSwipeOnProgress = false; + } + + internal Task OnPointerCancelAsync(PointerEventArgs arg) + { + Cancel(); + return OnSwipeCancel.InvokeAsync(arg); } - internal void OnPointerCancel(PointerEventArgs arg) + public void Cancel() { - _xDown = _yDown = null; + _xDown = _yDown = _xDownway = _yDownway = null; + _isSwipeOnProgress = false; + } + + private static IReadOnlyList GetSwipeDirections(double xDiff, double yDiff) + { + var horizontalDirection = GetDirection(xDiff, SwipeDirection.RightToLeft, SwipeDirection.LeftToRight); + var verticalDirection = GetDirection(yDiff, SwipeDirection.BottomToTop, SwipeDirection.TopToBottom); + + return [horizontalDirection, verticalDirection]; + + SwipeDirection GetDirection(double diff, SwipeDirection positiveDirection, SwipeDirection negativeDirection) + { + const double Epsilon = 1e-6; + + if (Math.Abs(diff) < Epsilon) + { + return SwipeDirection.None; + } + + return diff > Epsilon + ? positiveDirection + : negativeDirection; + } } } } diff --git a/src/MudBlazor/Components/SwipeArea/MultiDimensionSwipeEventArgs.cs b/src/MudBlazor/Components/SwipeArea/MultiDimensionSwipeEventArgs.cs new file mode 100644 index 000000000000..e9c4ca75abe5 --- /dev/null +++ b/src/MudBlazor/Components/SwipeArea/MultiDimensionSwipeEventArgs.cs @@ -0,0 +1,46 @@ +// Copyright (c) MudBlazor 2021 +// MudBlazor licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.AspNetCore.Components.Web; + +namespace MudBlazor; + +#nullable enable +public class MultiDimensionSwipeEventArgs +{ + /// + /// The information about the pointer. + /// + public PointerEventArgs TouchEventArgs { get; } + + /// + /// The distance of the swipe gestures in pixels. Has two values, one for the x-axis and one for the y-axis. + /// + public IReadOnlyList SwipeDeltas { get; } + + /// + /// The which raised the swipe event. + /// + public MudSwipeArea Sender { get; } + + /// + /// The direction list of the swipe. Has two values, one for the x-axis and one for the y-axis. + /// + public IReadOnlyList SwipeDirections { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The size, pressure, and tilt of the pointer. + /// The direction of the swipe. + /// The distance of the swipe movement, in pixels. + /// The which originated the swipe event. + public MultiDimensionSwipeEventArgs(PointerEventArgs touchEventArgs, IReadOnlyList swipeDirections, IReadOnlyList swipeDeltas, MudSwipeArea sender) + { + TouchEventArgs = touchEventArgs; + SwipeDirections = swipeDirections; + SwipeDeltas = swipeDeltas; + Sender = sender; + } +} diff --git a/src/MudBlazor/Components/Switch/MudSwitch.razor b/src/MudBlazor/Components/Switch/MudSwitch.razor index 168ed7df1403..2ba2e70a5bb6 100644 --- a/src/MudBlazor/Components/Switch/MudSwitch.razor +++ b/src/MudBlazor/Components/Switch/MudSwitch.razor @@ -6,9 +6,9 @@