diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 21a623a9ef38..056721081dbf 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -119,14 +119,17 @@ body: - Other validations: required: true - - - type: checkboxes - id: pr + - type: dropdown + id: rendering-mode attributes: - label: Pull Request - description: If you need this fixed soon, consider submitting a Pull Request! Our team has limited resources so we rely on community contributions. We greatly appreciate all contributions and will review them as soon as possible for inclusion in the next version! + label: Blazor rendering mode + description: Specify the Blazor rendering mode you are using. + multiple: false options: - - label: I would like to do a Pull Request + - WASM + - Server + - Hybrid + default: 0 - type: checkboxes id: terms attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index b809ef3dfa49..ac7f2179322d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -49,13 +49,6 @@ body: label: Describe alternatives you've considered description: Please provide a clear and concise description of any alternative solutions or features you've considered. placeholder: Describe alternatives! - - type: checkboxes - id: pr - attributes: - label: Pull Request - description: If you want to see this feature soon, consider submitting a Pull Request! Our team has limited resources so we rely on community contributions. We greatly appreciate all contributions and will review them as soon as possible for inclusion in the next version! - options: - - label: I would like to do a Pull Request - type: checkboxes id: terms attributes: diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 7a8f6f9fedfe..315707cf9e99 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -11,24 +11,6 @@ jobs: script: | const body = context.payload.issue.body; let labels = []; - if (body.includes("- [X] I would like to do a Pull Request")) { - labels.push("wants to do a PR"); - const author = context.payload.issue.user.login; - const comment = 'Thanks for wanting to do a PR, @' + author + ' !\n\nWe try to merge all non-breaking bugfixes and will deliberate the value of new features for the community. Please note there is no guarantee your pull request will be merged, so if you want to be sure before investing the work, feel free to contact the team first.' - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: comment - }) - github.rest.issues.addAssignees({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - assignees: [author] - }) - console.log("Author wants to do a PR."); - } if (body.includes("### Feature request type\n\nNew component")) { labels.push("new component"); diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3e2ae3659a8..76ca3bf4468f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,12 @@ -# ![MudBlazor](content/MudBlazor-GitHub-NoBg.png) +

+ + + + MudBlazor + +

- -- [](#) - [Information and Guidelines for Contributors](#information-and-guidelines-for-contributors) - [Code of Conduct](#code-of-conduct) - [Minimal Prerequisites to Compile from Source](#minimal-prerequisites-to-compile-from-source) diff --git a/README.md b/README.md index efd05175c38c..d3a7b45b92d3 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,10 @@ For larger features or changes, feel free to chat with us [on Discord](https://d 📚 Check out our [contribution guidelines](/CONTRIBUTING.md) to get started and learn more about how the project works. +### Testing a PR Locally + +✅ If a PR fixes something you reported, [locally test a preview version](/TESTING.md) to ensure your app works as expected before the update is released. + ## 🚀 Getting Started We have ready-to-go templates at the [MudBlazor.Templates](https://github.com/mudblazor/Templates) repository, or follow the quick install guide to set things up manually: diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 000000000000..e49cb1bd7d5b --- /dev/null +++ b/TESTING.md @@ -0,0 +1,58 @@ +# Testing a PR Locally + +1. **Clone and check out the PR (replace `PR_NUMBER`):** + +```bash +git clone https://github.com/MudBlazor/MudBlazor.git +cd MudBlazor +git fetch origin pull/PR_NUMBER/head:pr-branch +git checkout pr-branch +``` + +2. **Pack MudBlazor with a custom version:** + +```bash +dotnet pack src/MudBlazor/MudBlazor.csproj -c Release -o ./LocalNuGet -p:Version=8.0.0-custom +``` + +3. **Add a `nuget.config` in your app folder:** + +```xml + + + + + + + +``` + +4. **Update your app's `.csproj`:** + +```xml + +``` + +5. **Restore and build:** + +```bash +dotnet restore +dotnet build +``` + +## 🔄 Undo the changes after testing + +Once you're done testing, switch back to the official release by: + +- Removing the local source from your `nuget.config` (or deleting the file entirely if not otherwise needed). +- Restoring the original version in your `.csproj`: + +```xml + +``` + +Then run: + +```bash +dotnet restore +``` diff --git a/src/MudBlazor.Docs/Components/MudTeamCard.razor b/src/MudBlazor.Docs/Components/MudTeamCard.razor index 6af2f3c928e0..0a12287ed357 100644 --- a/src/MudBlazor.Docs/Components/MudTeamCard.razor +++ b/src/MudBlazor.Docs/Components/MudTeamCard.razor @@ -1,37 +1,60 @@ - -
- - - -
- @Member.Name - @Member.GitHub -
-
- @if(Member.LinkedIn != null) - { - - - - } - @if(Member.GitHubSponsor) + + + + + + + + + +
+
+ @Member.Name + + @Member.GitHub +
+ + @if (!string.IsNullOrWhiteSpace(Member.From)) + { +
+ + + + @Member.From + +
+ } +
+
+
+ + + @if (!string.IsNullOrWhiteSpace(Member.Bio)) { - - - + @Member.Bio } - - - -
-
-
-
- - @Member.From -
-
-
+ + + + + GitHub + + + @if (Member.GitHubSponsor) + { + + Sponsor + + } + + @if (!string.IsNullOrWhiteSpace(Member.LinkedIn)) + { + + LinkedIn + + } + + @code { [Parameter] public TeamMember Member { get; set; } diff --git a/src/MudBlazor.Docs/Models/TeamMember.cs b/src/MudBlazor.Docs/Models/TeamMember.cs index 303692b855d3..24dceac2c29c 100644 --- a/src/MudBlazor.Docs/Models/TeamMember.cs +++ b/src/MudBlazor.Docs/Models/TeamMember.cs @@ -12,5 +12,6 @@ public class TeamMember public bool GitHubSponsor { get; set; } public string Avatar { get; set; } public string LinkedIn { get; set; } + public string Bio { get; set; } } } diff --git a/src/MudBlazor.Docs/Models/TeamMemberData.cs b/src/MudBlazor.Docs/Models/TeamMemberData.cs new file mode 100644 index 000000000000..2e99aab05b47 --- /dev/null +++ b/src/MudBlazor.Docs/Models/TeamMemberData.cs @@ -0,0 +1,215 @@ +// 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. + +namespace MudBlazor.Docs.Models +{ + /// + /// The content for the team page + /// + /// + /// Data is taken from GitHub and updated manually. In the future this could be automated. + /// + public class TeamMemberData + { + public static TeamMember[] GetCoreTeam() => + [ + new TeamMember + { + Name = "Jonny Larsson (Creator)", + From = "Örebro, Sweden", + GitHub = "Garderoben", + Avatar = "https://avatars.githubusercontent.com/u/10367109?v=4", + LinkedIn = "https://www.linkedin.com/in/jonny-larsson-b72480161/", + Bio = "DevOps Engineer.\r\nRecently been doing more developing in free time and for in house applications for my company to automate processes.", + }, + new TeamMember + { + Name = "Meinrad Recheis (Co-Creator)", + From = "Vienna, Austria", + GitHub = "henon", + Avatar = "https://avatars.githubusercontent.com/u/44090?v=4", + LinkedIn = "https://www.linkedin.com/in/meinrad-recheis-6a9885171/", + Bio = "Entrepreneur and Open Source contributor. Co-founder of MudBlazor. Creator of Numpy.NET and Python.Included. ", + }, + new TeamMember + { + Name = "András Tangl", + From = "Szombathely, Hungary", + GitHub = "tungi52", + Avatar = "https://avatars.githubusercontent.com/u/22996720?v=4", + LinkedIn = "https://www.linkedin.com/in/andr%C3%A1s-tangl-683a20215/", + Bio = null, + }, + new TeamMember + { + Name = "Mike Surcouf", + From = "Jersey, Channel Islands", + GitHub = "mikes-gh", + Avatar = "https://avatars.githubusercontent.com/u/16208742?v=4", + LinkedIn = null, + Bio = null, + }, + new TeamMember + { + Name = "Benjamin Kappel", + From = "Malaysia", + GitHub = "just-the-benno", + Avatar = "https://avatars.githubusercontent.com/u/51370361?v=4", + LinkedIn = "https://www.linkedin.com/in/benjamin-kappel-558428168/", + Bio = "A long time ago, there was an enormous book with an even more significant discount on it, titled C#. That is the starting point of still ongoing love.", + }, + new TeamMember + { + Name = "Jonas B.", + From = "Germany", + GitHub = "JonBunator", + Avatar = "https://avatars.githubusercontent.com/u/62108893?v=4", + LinkedIn = null, + Bio = null, + }, + new TeamMember + { + Name = "Riley Nielsen", + From = "Minnesota, United States", + GitHub = "Mr-Technician", + Avatar = "https://avatars.githubusercontent.com/u/26885142?v=4", + LinkedIn = "https://www.linkedin.com/in/riley-nielsen-a57399223/", + Bio = null, + }, + new TeamMember + { + Name = "Artyom M.", + From = "Estonia, Tallinn", + GitHub = "ScarletKuro", + GitHubSponsor = true, + Avatar = "https://avatars.githubusercontent.com/u/19953225?v=4", + LinkedIn = "https://www.linkedin.com/in/artyommelnikov/", + Bio = ".NET/Java developer", + }, + new TeamMember + { + Name = "Daniel Chalmers", + From = "Texas, United States", + GitHub = "danielchalmers", + Avatar = "https://avatars.githubusercontent.com/u/7112040?v=4", + LinkedIn = "https://www.linkedin.com/in/daniel-c-5799252b1", + Bio = "Full stack engineer specializing in .NET, Blazor, CSS, WPF. Building accessible apps with a focus on modern UI/UX", + }, + ]; + + public static TeamMember[] GetContributionTeam() => + [ + new TeamMember + { + Name = "Henrique Clausing", + From = "Brazil", + GitHub = "HClausing", + Avatar = "https://avatars.githubusercontent.com/u/15158923?v=4", + LinkedIn = "https://www.linkedin.com/in/henrique-clausing-cunha-45085944/", + Bio = "Accountant and Full-Stack .NET developer. Founder of Eficaz Sistemas.", + }, + new TeamMember + { + Name = "Porkopek", + From = "Fundão, Portugal", + GitHub = "porkopek", + Avatar = "https://avatars.githubusercontent.com/u/13745954?v=4", + LinkedIn = null, + Bio = null, + }, + new TeamMember + { + Name = "Mehmet Can Karagöz", + From = "Alanya, Turkey", + GitHub = "mckaragoz", + Avatar = "https://avatars.githubusercontent.com/u/78308169?v=4", + LinkedIn = null, + Bio = null, + }, + new TeamMember + { + Name = "Jon Person", + From = "Denver, CO", + GitHub = "jperson2000", + Avatar = "https://avatars.githubusercontent.com/u/18043079?v=4", + LinkedIn = null, + Bio = "I'm a full-stack C# developer for the U.S. Department of Defense who specializes in Blazor, durable back-end systems, and reusable frameworks.", + }, + new TeamMember + { + Name = "Lukas Klinger", + From = "Germany", + GitHub = "Flaflo", + Avatar = "https://avatars.githubusercontent.com/u/12973684?v=4", + LinkedIn = null, + Bio = ".NET & Java Full-Stack Developer", + }, + new TeamMember + { + Name = "Jason Rebelo", + From = "Luxembourg", + GitHub = "igotinfected", + GitHubSponsor = true, + Avatar = "https://avatars.githubusercontent.com/u/15004223?v=4", + LinkedIn = null, + Bio = "Web & Mobile Analyst Developer with a faible for Open Source and Cybersecurity.", + }, + new TeamMember + { + Name = "Samuel Meenzen", + From = "Germany", + GitHub = "meenzen", + Avatar = "https://avatars.githubusercontent.com/u/22305878?v=4", + LinkedIn = null, + Bio = null, + }, + new TeamMember + { + Name = "Justin Lampe", + From = null, + GitHub = "xC0dex", + Avatar = "https://avatars.githubusercontent.com/u/22918366?v=4", + LinkedIn = null, + Bio = null, + }, + new TeamMember + { + Name = "Roman Alvarez", + From = "Uruguay", + GitHub = "ralvarezing", + Avatar = "https://avatars.githubusercontent.com/u/40799354?v=4", + LinkedIn = null, + Bio = "DevOps at SONDA & Computing Engineering student.", + }, + new TeamMember + { + Name = "Versile Johnson II", + From = "Texas, United States", + GitHub = "versile2", + GitHubSponsor = true, + Avatar = "https://avatars.githubusercontent.com/u/148913404?v=4", + LinkedIn = null, + Bio = null, + }, + new TeamMember + { + Name = "Anu6is", + From = "Trinidad & Tobago", + GitHub = "anu6is", + Avatar = "https://avatars.githubusercontent.com/u/4596077?v=4", + LinkedIn = null, + Bio = null, + }, + new TeamMember + { + Name = "digitaldirk", + From = "the woods", + GitHub = "digitaldirk", + Avatar = "https://avatars.githubusercontent.com/u/22691956?v=4", + LinkedIn = null, + Bio = "C# and game things", + }, + ]; + } +} diff --git a/src/MudBlazor.Docs/Pages/Components/ScrollToTop/Examples/CustomScrollToTopExample.razor b/src/MudBlazor.Docs/Pages/Components/ScrollToTop/Examples/CustomScrollToTopExample.razor index 4a6ad8e86543..7f3294665e6d 100644 --- a/src/MudBlazor.Docs/Pages/Components/ScrollToTop/Examples/CustomScrollToTopExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/ScrollToTop/Examples/CustomScrollToTopExample.razor @@ -6,8 +6,11 @@ Some initial long text Middle text Bottom text - -
Scroll to top custom button
+ +
Scroll to top custom button
- \ No newline at end of file + diff --git a/src/MudBlazor.Docs/Pages/Components/ScrollToTop/Examples/ScrollToTopExample.razor b/src/MudBlazor.Docs/Pages/Components/ScrollToTop/Examples/ScrollToTopExample.razor index 939c43306058..a5ffdc2b29b9 100644 --- a/src/MudBlazor.Docs/Pages/Components/ScrollToTop/Examples/ScrollToTopExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/ScrollToTop/Examples/ScrollToTopExample.razor @@ -28,4 +28,4 @@ _=>Color.Error }; } -} \ No newline at end of file +} diff --git a/src/MudBlazor.Docs/Pages/Getting Started/Installation/ManualMarkdown/InstallScriptManualNet8Code.html b/src/MudBlazor.Docs/Pages/Getting Started/Installation/ManualMarkdown/InstallScriptManualNet8Code.html index 7ee7eed40368..dc7da2d980e1 100644 --- a/src/MudBlazor.Docs/Pages/Getting Started/Installation/ManualMarkdown/InstallScriptManualNet8Code.html +++ b/src/MudBlazor.Docs/Pages/Getting Started/Installation/ManualMarkdown/InstallScriptManualNet8Code.html @@ -1,7 +1,7 @@ 
-<script src="_content/MudBlazor/MudBlazor.min.css?v=@(MudBlazor.Metadata.Version)"></script>
+<script src="_content/MudBlazor/MudBlazor.min.js?v=@(MudBlazor.Metadata.Version)"></script>
 
\ No newline at end of file diff --git a/src/MudBlazor.Docs/Pages/Getting Started/Installation/ManualMarkdown/InstallScriptManualWasmStandaloneCode.html b/src/MudBlazor.Docs/Pages/Getting Started/Installation/ManualMarkdown/InstallScriptManualWasmStandaloneCode.html index a6ca822456bd..bb67cec5f794 100644 --- a/src/MudBlazor.Docs/Pages/Getting Started/Installation/ManualMarkdown/InstallScriptManualWasmStandaloneCode.html +++ b/src/MudBlazor.Docs/Pages/Getting Started/Installation/ManualMarkdown/InstallScriptManualWasmStandaloneCode.html @@ -2,7 +2,7 @@
 <!-- Important: Increment the version parameter whenever you update MudBlazor to prevent caching issues -->
-<script src="_content/MudBlazor/MudBlazor.min.css?v=1"></script>
+<script src="_content/MudBlazor/MudBlazor.min.js?v=1"></script>
 
\ No newline at end of file diff --git a/src/MudBlazor.Docs/Pages/Mud/Project/Team.razor b/src/MudBlazor.Docs/Pages/Mud/Project/Team.razor index b2fd6374dfea..3a00138b3047 100644 --- a/src/MudBlazor.Docs/Pages/Mud/Project/Team.razor +++ b/src/MudBlazor.Docs/Pages/Mud/Project/Team.razor @@ -1,85 +1,26 @@ @page "/mud/project/team" @inject GitHubApiClient _gitHubApiClient; -Meet the team - MudBlazor +Meet the Team - MudBlazor + - Meet the team + @* Preamble *@ + Meet the Team - MudBlazor is not a one-person show. A nice little community has emerged and a couple of highly motivated people are working on improvements and are regularly adding more components. - Everyone is welcome to join in and contribute to make this library even more awesome than it already is. + MudBlazor is an open-source project built by a growing community of contributors from around the world. + What started as a small initiative has evolved into a mature UI framework, maintained by volunteers who review code, guide development, and keep the project moving forward. + Contributions are always welcome, from a single PR to ongoing involvement, and the community continues to grow with each release. + Read more + @* Core Team *@ Core Team - The Core Team who guide, develop and direct the development of MudBlazor. + + Project leads who oversee development and review contributions in their free time. + - - - - - - - - - - Jonny Larsson - Creator - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Meinrad Recheis - Co-Creator - - - - - - - - - - - - - - - - - - - - - - - @foreach (var member in _coreTeam) + @foreach (var member in TeamMemberData.GetCoreTeam()) { @@ -87,11 +28,13 @@ } + @* Contribution Team *@ Contribution Team - Core Contributors who work closely with the Core Team and are actively working on MudBlazor. - + + Community members in close collaboration, recognized for their high-quality contributions. + - @foreach (var contributor in _contributors) + @foreach (var contributor in TeamMemberData.GetContributionTeam()) { @@ -99,8 +42,11 @@ } + @* Awesome Coders *@ Awesome coders - Who allowed us to use some of their code in MudBlazor. + + Developers who generously allowed us to reuse parts of their code. + @@ -112,13 +58,18 @@ Donated the Blazored Modal source code - + Donated the XmlDoc loading code for the docs + + @* All Contributors *@ All contributors + + A growing list of people who’ve helped shape the project. + @if (_githubContributors == null) { @@ -135,9 +86,11 @@ } +
+
@code @@ -148,30 +101,4 @@ _githubContributors = await _gitHubApiClient.GetContributorsAsync(); StateHasChanged(); } - - private readonly TeamMember[] _coreTeam = new TeamMember[] - { - new TeamMember { Name = "András Tangl", From = "Szombathely, Hungary", GitHub = "tungi52", Avatar = "https://avatars.githubusercontent.com/u/22996720?v=4", LinkedIn = "https://www.linkedin.com/in/andr%C3%A1s-tangl-683a20215/"}, - new TeamMember { Name = "Mike Surcouf", From = "Jersey, Channel Islands", GitHub = "mikes-gh", Avatar = "https://avatars.githubusercontent.com/u/16208742?v=4", LinkedIn = null}, - new TeamMember { Name = "Benjamin Kappel", From = "Mexico", GitHub = "just-the-benno", Avatar = "https://avatars.githubusercontent.com/u/51370361?v=4", LinkedIn = "https://www.linkedin.com/in/benjamin-kappel-558428168/"}, - new TeamMember { Name = "Jonas B", From = "Germany", GitHub = "JonBunator", Avatar = "https://avatars.githubusercontent.com/u/62108893?v=4", LinkedIn = null}, - new TeamMember { Name = "Riley Nielsen", From = "Minnesota, United States", GitHub = "Mr-Technician", Avatar = "https://avatars.githubusercontent.com/u/26885142?v=4", LinkedIn = "https://www.linkedin.com/in/riley-nielsen-a57399223/"}, - new TeamMember { Name = "Artyom Melnikov", From = "Tallinn, Harjumaa, Estonia", GitHub = "ScarletKuro", GitHubSponsor = true, Avatar = "https://avatars.githubusercontent.com/u/19953225?v=4", LinkedIn = "https://www.linkedin.com/in/artyommelnikov/"}, - new TeamMember { Name = "Daniel Chalmers", From = "United States", GitHub = "danielchalmers", Avatar = "https://avatars.githubusercontent.com/u/7112040?v=4", LinkedIn = "https://www.linkedin.com/in/daniel-c-5799252b1"}, - }; - - private readonly TeamMember[] _contributors = new TeamMember[] - { - new TeamMember { Name = "Henrique Clausing", From = "Minas Gerais, Brazil", GitHub = "HClausing", Avatar = "https://avatars.githubusercontent.com/u/15158923?v=4", LinkedIn = "https://www.linkedin.com/in/henrique-clausing-cunha-45085944/"}, - new TeamMember { Name = "Porkopek", From = "Fundão, Portugal", GitHub = "porkopek", Avatar = "https://avatars.githubusercontent.com/u/13745954?v=4", LinkedIn = null}, - new TeamMember { Name = "Mehmet Can Karagöz", From = "Alanya, Turkey", GitHub = "mckaragoz", Avatar = "https://avatars.githubusercontent.com/u/78308169?v=4", LinkedIn = null}, - new TeamMember { Name = "Jon Person", From = "Colorado, United States", GitHub = "jperson2000", Avatar = "https://avatars.githubusercontent.com/u/18043079?v=4", LinkedIn = null}, - new TeamMember { Name = "Lukas Klinger", From = "Germany", GitHub = "Flaflo", Avatar = "https://avatars.githubusercontent.com/u/12973684?v=4", LinkedIn = null}, - new TeamMember { Name = "Jason Rebelo", From = "Luxembourg", GitHub = "igotinfected", GitHubSponsor = true, Avatar = "https://avatars.githubusercontent.com/u/15004223?v=4", LinkedIn = null}, - new TeamMember { Name = "Samuel Meenzen", From = "Germany", GitHub = "meenzen", Avatar = "https://avatars.githubusercontent.com/u/22305878?v=4", LinkedIn = null}, - new TeamMember { Name = "Justin Lampe", From = "Germany", GitHub = "xC0dex", Avatar = "https://avatars.githubusercontent.com/u/22918366?v=4", LinkedIn = null}, - new TeamMember { Name = "Roman Alvarez", From = "Uruguay", GitHub = "ralvarezing", Avatar = "https://avatars.githubusercontent.com/u/40799354?v=4", LinkedIn = null}, - new TeamMember { Name = "Versile Johnson", From = "Texas, United States", GitHub = "versile2", GitHubSponsor = true, Avatar = "https://avatars.githubusercontent.com/u/148913404?v=4", LinkedIn = null}, - new TeamMember { Name = "Curtis Mayers", From = "Trinidad & Tobago", GitHub = "anu6is", Avatar = "https://avatars.githubusercontent.com/u/4596077?v=4", LinkedIn = null}, - }; } diff --git a/src/MudBlazor.Docs/Shared/Appbar.razor b/src/MudBlazor.Docs/Shared/Appbar.razor index a55782768699..2ef0ccaae3ca 100644 --- a/src/MudBlazor.Docs/Shared/Appbar.razor +++ b/src/MudBlazor.Docs/Shared/Appbar.razor @@ -17,17 +17,17 @@ Get Started Docs - Learn More + Learn More Official - +
MudBlazor
- Blazor component library + What is MudBlazor?
diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridAllowUnsortedTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridAllowUnsortedTest.razor new file mode 100644 index 000000000000..0efe1414ddd3 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridAllowUnsortedTest.razor @@ -0,0 +1,26 @@ + + + + + + + + +@code { + [Parameter] + public bool AllowUnsorted { get; set; } = true; + + private readonly List _items = new List + { + new("C", 42, "555"), + new("A", 73, "7"), + new("B", 11, "4444"), + }; + + public class Item(string name, int value, string misc) + { + public string Name { get; set; } = name; + public int Value { get; set; } = value; + public string Misc { get; set; } = misc; + } +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridDateOnlyFilterTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridDateOnlyFilterTest.razor new file mode 100644 index 000000000000..8b0b73eca422 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridDateOnlyFilterTest.razor @@ -0,0 +1,20 @@ + + + + + + + + +@code { + public static string __description__ = @"Test DateOnly filtering in DataGrid"; + public record Model(string Name, int? Age, DateOnly? HiredOn); + + private readonly IEnumerable _people = + [ + new("Sam", 56, new DateOnly(2020, 3, 10)), + new("Alicia", 54, null), + new("Ira", 27, new DateOnly(2011, 1, 2)), + new("John", 32, new DateOnly(2022, 6, 15)) + ]; +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogCloseOnEscapeTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogCloseOnEscapeTest.razor new file mode 100644 index 000000000000..e2234160cf6b --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/DialogCloseOnEscapeTest.razor @@ -0,0 +1,45 @@ +@inject IDialogService DialogService +@inject ISnackbar Snackbar + +Open Dialog (Test Esc Key) + +@code { + public static string __description__ = "Open Dialog -> Click overlay -> Press Esc"; + + private async Task OpenDialog() + { + var options = new DialogOptions + { + CloseOnEscapeKey = true, + BackdropClick = true, + }; + + // Define a RenderFragment for the dialog content directly + RenderFragment dialogContent = builder => + { + builder.OpenComponent(0); + builder.AddAttribute(1, "ChildContent", (RenderFragment)(b => b.AddContent(2, "Click the overlay (I shouldn't close), then press Esc. Or click inside me, then press Esc."))); + builder.CloseComponent(); + // Add a button to ensure focus can shift to an interactive element within the dialog + builder.OpenComponent(3); + builder.AddAttribute(4, "Variant", Variant.Filled); + builder.AddAttribute(5, "Class", "ma-4"); + builder.AddAttribute(6, "OnClick", EventCallback.Factory.Create(this, () => Snackbar.Add("Button clicked", Severity.Info))); + builder.AddAttribute(7, "ChildContent", (RenderFragment)(b => b.AddContent(8, "Test Button"))); + builder.CloseComponent(); + }; + + var parameters = new DialogParameters + { + [nameof(MudDialog.DialogContent)] = dialogContent, // Pass the RenderFragment + [nameof(MudDialog.OnBackdropClick)] = new EventCallback(null, (MouseEventArgs args) => + { + Snackbar.Add("Overlay clicked! Dialog should still be open. Now try pressing Escape.", Severity.Info); + // This handler intentionally does not close the dialog, to test refocus. + }) + }; + + // Use the built-in MudDialog and pass content and handlers through parameters + var dialog = await DialogService.ShowAsync("Test Escape Key", parameters, options); + } +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/InlineDialogTitleRefreshTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/InlineDialogTitleRefreshTest.razor new file mode 100644 index 000000000000..971330d7b547 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Dialog/InlineDialogTitleRefreshTest.razor @@ -0,0 +1,45 @@ +
+ + Show Dialog + +
+ + + + + @DialogTitle + + + + + + + +@code { + public static string __description__ = "Dialog title should update along with Progress Bar content"; + + private readonly DialogOptions _dialogOptions = new() { FullWidth = true }; + private int DialogProgress { get; set; } = 0; + private bool DialogVisible { get; set; } = false; + private string DialogTitle { get; set; } = "Initial state"; + + private async Task OpenDialog() + { + DialogVisible = true; + + for (int i = 0; i <= 10; i++) + { + await Task.Delay(150); // Simulate some work + DialogProgress = i * 10; // Simulate progress update + DialogTitle = $"Progress: {DialogProgress}%"; + StateHasChanged(); + } + + // Keep dialog open for a bit to observe final title + await Task.Delay(2000); + + DialogVisible = false; + DialogProgress = 0; + DialogTitle = "Initial state"; + } +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/ExpansionPanel/ExpansionPanelKeyboardTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/ExpansionPanel/ExpansionPanelKeyboardTest.razor new file mode 100644 index 000000000000..c9cf484dca70 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/ExpansionPanel/ExpansionPanelKeyboardTest.razor @@ -0,0 +1,8 @@ + + + Button 1 + + + Button 2 + + diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Highlighter/HtmlEncodeTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Highlighter/HtmlEncodeTest.razor new file mode 100644 index 000000000000..4147855063b2 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Highlighter/HtmlEncodeTest.razor @@ -0,0 +1,34 @@ + + + @foreach (var paragraph in paragraphs) + { + + + + } + + + + + + +@code { + public static string __description__ = "Style (color and formatting) should be present"; + + string highlightedText = "'"; + bool untilNextBoundary; + bool caseSensitive; + bool markup = true; + IEnumerable paragraphs = new List + { + $"MudBlazor is an Material Design component framework for Blazor with an emphasis on ease of use and clear structure.", + $"MudLists are easily customizable and scrollable lists. Make them suit your needs with avatars, icons, or something like checkboxes.", + $"Use mud-* classes to customize your MudBlazor components.", + $"'text in single quote'" + }; +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Mask/AutoFillMaskTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Mask/AutoFillMaskTest.razor new file mode 100644 index 000000000000..f5c0892fc8a6 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Mask/AutoFillMaskTest.razor @@ -0,0 +1,23 @@ + + + + + + +@_phone + + +@code { + private string? _phone; +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Progress/MudProgressCircularWidthTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Progress/MudProgressCircularWidthTest.razor new file mode 100644 index 000000000000..6f0bad5d6e86 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Progress/MudProgressCircularWidthTest.razor @@ -0,0 +1,12 @@ + + + @_width + + +@($"Stroke-Width: {_width}") + +@code { + public static string __description__ = "Circle should render correctly with increasing stroke width"; + + int _width = 2; +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectMultiSelectFieldChangedTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectMultiSelectFieldChangedTest.razor new file mode 100644 index 000000000000..094fe1fffbc1 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/SelectMultiSelectFieldChangedTest.razor @@ -0,0 +1,51 @@ +@using MudBlazor.Utilities + + + + + @foreach (var state in States) + { + @state + } + + + +@code { + public static string __description__ = "FieldChanged should trigger when T != string and MultiSelect=true"; + private IEnumerable _options = new HashSet { new("Alaska") }; + + public State[] States { get; init; } = + [ + new("Alabama"), new("Alaska"), new("Arizona"), new("Arkansas"), new("California"), + new("Colorado"), new("Connecticut"), new("Delaware"), new("Florida"), new("Georgia"), + new("Hawaii"), new("Idaho"), new("Illinois"), new("Indiana"), new("Iowa"), new("Kansas"), + new("Kentucky"), new("Louisiana"), new("Maine"), new("Maryland"), new("Massachusetts"), + new("Michigan"), new("Minnesota"), new("Mississippi"), new("Missouri"), new("Montana"), + new("Nebraska"), new("Nevada"), new("New Hampshire"), new("New Jersey"), new("New Mexico"), + new("New York"), new("North Carolina"), new("North Dakota"), new("Ohio"), new("Oklahoma"), + new("Oregon"), new("Pennsylvania"), new("Rhode Island"), new("South Carolina"), new("South Dakota"), + new("Tennessee"), new("Texas"), new("Utah"), new("Vermont"), new("Virginia"), + new("Washington"), new("West Virginia"), new("Wisconsin"), new("Wyoming") + ]; + + public class State(string name) : IEquatable + { + + public string Name { get; } = name; + + public bool Equals(State? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Name == other.Name; + } + + public override bool Equals(object? obj) => obj is State state && Equals(state); + + public override int GetHashCode() => Name.GetHashCode(); + + public override string ToString() => Name; + } + + public FormFieldChangedEventArgs? FormFieldChangedEventArgs { get; private set; } +} diff --git a/src/MudBlazor.UnitTests/Components/AutocompleteTests.cs b/src/MudBlazor.UnitTests/Components/AutocompleteTests.cs index 926cbfaac0a0..13c65fdf6496 100644 --- a/src/MudBlazor.UnitTests/Components/AutocompleteTests.cs +++ b/src/MudBlazor.UnitTests/Components/AutocompleteTests.cs @@ -1491,7 +1491,7 @@ public async Task AutocompleteStrictFalseTest(int index) var items = comp.FindComponents>().ToArray(); items.Length.Should().Be(10); var item = items.SingleOrDefault(x => x.Markup.Contains(californiaString)); - items.ToList().IndexOf(item).Should().Be(4); + items.ToList().IndexOf(item).Should().Be(5); comp.WaitForAssertion(() => items.Single(s => s.Markup.Contains(californiaString)).Find(listItemQuerySelector).ClassList.Should().Contain(selectedItemClassName)); await comp.InvokeAsync(async () => await autocompleteComponent.Find("input").KeyUpAsync(new KeyboardEventArgs() { Key = "Escape" })); // Close autocomplete. diff --git a/src/MudBlazor.UnitTests/Components/DataGridTests.cs b/src/MudBlazor.UnitTests/Components/DataGridTests.cs index bb8c2b1a3fb0..bf05dd6ba7e3 100644 --- a/src/MudBlazor.UnitTests/Components/DataGridTests.cs +++ b/src/MudBlazor.UnitTests/Components/DataGridTests.cs @@ -2235,6 +2235,285 @@ public void FilterDefinitionDateTimeTest() func.Invoke(new("Joe", 45, null, null, null, null)).Should().BeTrue(); } + [Test] + public void FilterDefinitionDateOnlyTest() + { + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + var dateColumn = dataGrid.Instance.GetColumnByPropertyName("HiredOn"); + var testDate = new DateOnly(2020, 3, 10); + + #region FilterOperator.DateOnly.Is + + var filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = FilterOperator.DateOnly.Is, + Value = testDate + }; + var func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeTrue(); + func.Invoke(new("Joe", 32, null)).Should().BeFalse(); + func.Invoke(new("Ira", 27, new DateOnly(2011, 1, 2))).Should().BeFalse(); + + // null value + filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = FilterOperator.DateOnly.Is, + Value = null + }; + func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeTrue(); + func.Invoke(new("Joe", 32, null)).Should().BeTrue(); + + #endregion + + #region FilterOperator.DateOnly.IsNot + + filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = FilterOperator.DateOnly.IsNot, + Value = testDate + }; + func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeFalse(); + func.Invoke(new("Joe", 32, null)).Should().BeTrue(); + func.Invoke(new("Ira", 27, new DateOnly(2011, 1, 2))).Should().BeTrue(); + + // null value + filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = FilterOperator.DateOnly.IsNot, + Value = null + }; + func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeTrue(); + func.Invoke(new("Joe", 32, null)).Should().BeTrue(); + + #endregion + + #region FilterOperator.DateOnly.After + + filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = FilterOperator.DateOnly.After, + Value = testDate + }; + func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeFalse(); + func.Invoke(new("Joe", 32, null)).Should().BeFalse(); + func.Invoke(new("John", 32, new DateOnly(2022, 6, 15))).Should().BeTrue(); + + // null value + filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = FilterOperator.DateOnly.After, + Value = null + }; + func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeTrue(); + func.Invoke(new("Joe", 32, null)).Should().BeTrue(); + + #endregion + + #region FilterOperator.DateOnly.OnOrAfter + + filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = FilterOperator.DateOnly.OnOrAfter, + Value = testDate + }; + func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeTrue(); + func.Invoke(new("Joe", 32, null)).Should().BeFalse(); + func.Invoke(new("John", 32, new DateOnly(2022, 6, 15))).Should().BeTrue(); + + // null value + filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = FilterOperator.DateOnly.OnOrAfter, + Value = null + }; + func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeTrue(); + func.Invoke(new("Joe", 32, null)).Should().BeTrue(); + + #endregion + + #region FilterOperator.DateOnly.Before + + filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = FilterOperator.DateOnly.Before, + Value = testDate + }; + func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeFalse(); + func.Invoke(new("Joe", 32, null)).Should().BeFalse(); + func.Invoke(new("Ira", 27, new DateOnly(2011, 1, 2))).Should().BeTrue(); + + // null value + filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = FilterOperator.DateOnly.Before, + Value = null + }; + func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeTrue(); + func.Invoke(new("Joe", 32, null)).Should().BeTrue(); + + #endregion + + #region FilterOperator.DateOnly.OnOrBefore + + filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = FilterOperator.DateOnly.OnOrBefore, + Value = testDate + }; + func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeTrue(); + func.Invoke(new("Joe", 32, null)).Should().BeFalse(); + func.Invoke(new("Ira", 27, new DateOnly(2011, 1, 2))).Should().BeTrue(); + + // null value + filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = FilterOperator.DateOnly.OnOrBefore, + Value = null + }; + func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeTrue(); + func.Invoke(new("Joe", 32, null)).Should().BeTrue(); + + #endregion + + #region FilterOperator.DateOnly.Empty + + filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = FilterOperator.DateOnly.Empty, + Value = testDate + }; + func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeFalse(); + func.Invoke(new("Joe", 32, null)).Should().BeTrue(); + func.Invoke(new("Ira", 27, new DateOnly(2011, 1, 2))).Should().BeFalse(); + + // null value + filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = FilterOperator.DateOnly.Empty, + Value = null + }; + func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeFalse(); + func.Invoke(new("Joe", 32, null)).Should().BeTrue(); + + #endregion + + #region FilterOperator.DateOnly.NotEmpty + + filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = FilterOperator.DateOnly.NotEmpty, + Value = testDate + }; + func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeTrue(); + func.Invoke(new("Joe", 32, null)).Should().BeFalse(); + func.Invoke(new("Ira", 27, new DateOnly(2011, 1, 2))).Should().BeTrue(); + + // null value + filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = FilterOperator.DateOnly.NotEmpty, + Value = null + }; + func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeTrue(); + func.Invoke(new("Joe", 32, null)).Should().BeFalse(); + + #endregion + + // null operator + filterDefinition = new FilterDefinition + { + Column = dateColumn, + Operator = null, + Value = testDate + }; + func = filterDefinition.GenerateFilterFunction(new FilterOptions + { + FilterCaseSensitivity = dataGrid.Instance.FilterCaseSensitivity + }); + func.Invoke(new("Sam", 56, testDate)).Should().BeTrue(); + func.Invoke(new("Joe", 32, null)).Should().BeTrue(); + } + [Test] public void FilterDefinitionNumberTest() { @@ -2511,6 +2790,9 @@ public async Task DataGridCloseFiltersTest() // check the number of filters displayed in the filters panel is 1 comp.FindAll(".filters-panel .mud-grid-item.d-flex").Count.Should().Be(1); + // Wait for the filter panel to render properly + comp.WaitForState(() => comp.FindAll(".filter-operator").Count > 0, timeout: TimeSpan.FromSeconds(5)); + await comp.Find(".filter-operator").MouseDownAsync(new MouseEventArgs()); //set operator to CONTAINS @@ -2524,6 +2806,9 @@ public async Task DataGridCloseFiltersTest() //set operator to NOT CONTAINS FilterButton().Click(); + // Wait for the filter panel to render properly + comp.WaitForState(() => comp.FindAll(".filter-operator").Count > 0, timeout: TimeSpan.FromSeconds(5)); + await comp.Find(".filter-operator").MouseDownAsync(new MouseEventArgs()); comp.FindAll(".mud-list .mud-list-item")[1].Click(); @@ -4936,5 +5221,61 @@ public void DataGrid_HierarchyExpandSingleRowTest() dataGrid.Instance._openHierarchies.First().Should().Be(item); } + + [Test] + public async Task DataGridShouldAllowUnsortedAscDescOnly() + { + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.AllowUnsorted, false)); + var dataGrid = comp.FindComponent>(); + var headerCell = dataGrid.FindComponents>()[0]; + + await comp.InvokeAsync(() => headerCell.Instance.SortChangedAsync(new MouseEventArgs() { Button = 0 })); + headerCell.Instance.SortDirection.Should().Be(SortDirection.Ascending); + var cells = dataGrid.FindAll("td"); + cells[0].TextContent.Should().Be("A"); + cells[3].TextContent.Should().Be("B"); + cells[6].TextContent.Should().Be("C"); + + await comp.InvokeAsync(() => headerCell.Instance.SortChangedAsync(new MouseEventArgs() { Button = 0 })); + headerCell.Instance.SortDirection.Should().Be(SortDirection.Descending); + cells = dataGrid.FindAll("td"); + cells[0].TextContent.Should().Be("C"); + cells[3].TextContent.Should().Be("B"); + cells[6].TextContent.Should().Be("A"); + + await comp.InvokeAsync(() => headerCell.Instance.SortChangedAsync(new MouseEventArgs() { Button = 0 })); + headerCell.Instance.SortDirection.Should().Be(SortDirection.Ascending); + cells = dataGrid.FindAll("td"); + cells[0].TextContent.Should().Be("A"); + cells[3].TextContent.Should().Be("B"); + cells[6].TextContent.Should().Be("C"); + + comp = Context.RenderComponent(parameters => parameters + .Add(p => p.AllowUnsorted, true)); + dataGrid = comp.FindComponent>(); + headerCell = dataGrid.FindComponents>()[0]; + + await comp.InvokeAsync(() => headerCell.Instance.SortChangedAsync(new MouseEventArgs() { Button = 0 })); + headerCell.Instance.SortDirection.Should().Be(SortDirection.Ascending); + cells = dataGrid.FindAll("td"); + cells[0].TextContent.Should().Be("A"); + cells[3].TextContent.Should().Be("B"); + cells[6].TextContent.Should().Be("C"); + + await comp.InvokeAsync(() => headerCell.Instance.SortChangedAsync(new MouseEventArgs() { Button = 0 })); + headerCell.Instance.SortDirection.Should().Be(SortDirection.Descending); + cells = dataGrid.FindAll("td"); + cells[0].TextContent.Should().Be("C"); + cells[3].TextContent.Should().Be("B"); + cells[6].TextContent.Should().Be("A"); + + await comp.InvokeAsync(() => headerCell.Instance.SortChangedAsync(new MouseEventArgs() { Button = 0 })); + headerCell.Instance.SortDirection.Should().Be(SortDirection.None); + cells = dataGrid.FindAll("td"); + cells[0].TextContent.Should().Be("C"); + cells[3].TextContent.Should().Be("A"); + cells[6].TextContent.Should().Be("B"); + } } } diff --git a/src/MudBlazor.UnitTests/Components/DialogTests.cs b/src/MudBlazor.UnitTests/Components/DialogTests.cs index d76adfcd0a9b..545083f7c278 100644 --- a/src/MudBlazor.UnitTests/Components/DialogTests.cs +++ b/src/MudBlazor.UnitTests/Components/DialogTests.cs @@ -1,8 +1,11 @@ using Bunit; using FluentAssertions; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; -using MudBlazor.UnitTests.TestComponents; +using Microsoft.JSInterop; +using Microsoft.JSInterop.Infrastructure; +using Moq; using MudBlazor.UnitTests.TestComponents.Dialog; using NUnit.Framework; @@ -148,7 +151,7 @@ public void InlineDialogShowMethodTest() // close by click on ok button comp.Find(".mud-message-box button").Click(); - comp.Markup.Trim().Should().BeEmpty(); + comp.WaitForAssertion(() => comp.Markup.Trim().Should().BeEmpty(), timeout: TimeSpan.FromSeconds(5)); } /// @@ -1341,8 +1344,124 @@ public async Task DuplicateDialogTest() await Task.Delay(1000); comp.FindComponents().Count.Should().Be(1); } - } + [Test] + public async Task Dialog_TitleContent_And_ProgressBar_ShouldUpdate() + { + var comp = Context.RenderComponent(); + comp.Markup.Trim().Should().BeEmpty(); + + var service = Context.Services.GetRequiredService(); + + var comp1 = Context.RenderComponent(); + + comp1.Find("button").Click(); + + var dialog = comp.Find("div.mud-dialog"); + + dialog.Should().NotBeNull(); + dialog.TextContent.Trim().Should().Be("Initial state"); // Initial title + comp.FindComponent().Instance.Value.Should().Be(0); // Initial progress value - 0 + + // Wait for mid-progress (simulate that loop is progressing) + await Task.Delay(1000); + //comp.Render(); + + dialog = comp.Find("div.mud-dialog"); + dialog.TextContent.Should().Contain("Progress"); //change from "Initial state" to "Progress" + + var progressComponent = comp.FindComponent(); + var progressValue = progressComponent.Instance.Value; + progressValue.Should().BeGreaterThan(0).And.BeLessThan(100); + + // Wait for dialog to close and state reset + await Task.Delay(3000); // Ensure loop finishes + comp.Render(); + + // Assert the dialog is now hidden + comp.Markup.Trim().Should().NotContain("mud-dialog"); + } + + [Test] + public async Task HandleBackgroundClickAsync_Should_Call_RefocusDialogAsync_When_BackdropClick_Disabled() + { + // Arrange + IDialogReference dialogReference = null; + + var jsRuntimeMock = new Mock(); + + jsRuntimeMock.Setup(x => x.InvokeAsync("Blazor._internal.domWrapper.focus", It.IsAny())); + + Context.Services.AddSingleton(jsRuntimeMock.Object); + + var comp = Context.RenderComponent(); + var service = Context.Services.GetRequiredService(); + var dialogOptions = new DialogOptions { BackdropClick = false, CloseOnEscapeKey = true }; + + //Act + await comp.InvokeAsync(async () => dialogReference = await service.ShowAsync("Test Dialog True", dialogOptions)); + + jsRuntimeMock.Verify(x => x.InvokeAsync("Blazor._internal.domWrapper.focus", + It.Is(args => + args.Length == 2 && + args[0] is ElementReference && + args[1] is bool)), + Times.AtMost(1)); // Focus should be called once when dialog is opened (FocusTrap) + + comp.Find("div.mud-dialog-container").Should().NotBeNull(); // Dialog is open + comp.Find("div.mud-overlay").Click(); // Simulate a click on the backdrop + + //Assert + comp.Find("div.mud-dialog-container").Should().NotBeNull(); // Dialog should still be open + + jsRuntimeMock.Verify(x => x.InvokeAsync("Blazor._internal.domWrapper.focus", + It.Is(args => + args.Length == 2 && + args[0] is ElementReference && + args[1] is bool)), + Times.AtMost(2)); // Focus should be called once when dialog is opened and once when backdrop is clicked (refouus) + } + + [Test] + public async Task HandleBackgroundClickAsync_Should_Call_OnBackdropClick_And_RefocusDialog_When_Delegate_Defined() + { + // Arrange + var jsRuntimeMock = new Mock(); + + jsRuntimeMock.Setup(x => x.InvokeAsync("Blazor._internal.domWrapper.focus", It.IsAny())); + + Context.Services.AddSingleton(jsRuntimeMock.Object); + + var comp = Context.RenderComponent(); + + // Create a dialog component with OnBackdropClick delegate + var dialogComponent = Context.RenderComponent(); + + // Act + await dialogComponent.FindComponent().Find("button").ClickAsync(new MouseEventArgs()); // Open the dialog + + jsRuntimeMock.Verify(x => x.InvokeAsync("Blazor._internal.domWrapper.focus", + It.Is(args => + args.Length == 2 && + args[0] is ElementReference && + args[1] is bool)), + Times.AtMost(1)); // Focus should be called once when dialog is opened + + comp.Find("div.mud-dialog-container").Should().NotBeNull(); // Dialog is open + + await comp.Find("div.mud-overlay").ClickAsync(new MouseEventArgs()); // Simulate a click on the backdrop + + // Assert + comp.Find("div.mud-dialog-container").Should().NotBeNull(); // Dialog should still be open (not closed) + + jsRuntimeMock.Verify(x => x.InvokeAsync("Blazor._internal.domWrapper.focus", + It.Is(args => + args.Length == 2 && + args[0] is ElementReference && + args[1] is bool)), + Times.AtMost(2)); // Focus should be called once when dialog opens and once for refocus after backdrop click + } + } internal class CustomDialogService : DialogService { public override IDialogReference CreateReference() => new CustomDialogReference(Guid.NewGuid(), this); diff --git a/src/MudBlazor.UnitTests/Components/ExpansionPanelTests.cs b/src/MudBlazor.UnitTests/Components/ExpansionPanelTests.cs index 1d9682ebff12..f4c7ec2860fd 100644 --- a/src/MudBlazor.UnitTests/Components/ExpansionPanelTests.cs +++ b/src/MudBlazor.UnitTests/Components/ExpansionPanelTests.cs @@ -1,9 +1,7 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using AngleSharp.Dom; using Bunit; using FluentAssertions; -using MudBlazor.UnitTests.TestComponents; +using Microsoft.AspNetCore.Components.Web; using MudBlazor.UnitTests.TestComponents.ExpansionPanel; using NUnit.Framework; @@ -299,5 +297,94 @@ public async Task MudExpansionPanel_TwoWayBinding() comp.WaitForAssertion(() => comp.Instance.Expansion[2].Should().BeFalse()); comp.WaitForAssertion(() => comp.Instance.Expansion[3].Should().BeFalse()); } + + /// + /// Tests that the panel toggles expansion state when the Enter and Space keys are pressed. + /// + [Test] + public async Task MudExpansionPanel_Handles_KeyDown_For_Toggle() + { + var comp = Context.RenderComponent(); + var header = comp.Find(".mud-expand-panel-header"); + + comp.Markup.Should().NotContain("mud-panel-expanded"); + await header.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = "Enter" }); + comp.Markup.Should().Contain("mud-panel-expanded"); + await header.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = " " }); + comp.Markup.Should().NotContain("mud-panel-expanded"); + } + + /// + /// Tests that buttons and other interactive content within a collapsed expansion panel + /// are properly hidden from accessibility tools and keyboard navigation by being placed + /// in a container with the hidden attribute. + /// + [Test] + public void MudExpansionPanel_Button_IsInHiddenContainer_When_Panel_Collapsed() + { + var comp = Context.RenderComponent(); + var button = comp.Find("button"); + var hiddenParent = button + .GetAncestors() + .OfType() + .FirstOrDefault(e => e.HasAttribute("hidden")); + + hiddenParent.Should().NotBeNull("button should not be accessible when the panel is collapsed"); + } + + /// + /// Tests that HandleKeyDownAsync ignores key presses when the panel is disabled. + /// + [Test] + public async Task MudExpansionPanel_HandleKeyDownAsync_IgnoresKeys_When_Disabled() + { + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Disabled, true) + .Add(p => p.Text, "Disabled Panel")); + + var header = comp.Find(".mud-expand-panel-header"); + comp.Markup.Should().NotContain("mud-panel-expanded"); + + // Try to expand with Enter key - should be ignored + await header.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = "Enter" }); + comp.Markup.Should().NotContain("mud-panel-expanded"); + + // Try to expand with Space key - should be ignored + await header.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = " " }); + comp.Markup.Should().NotContain("mud-panel-expanded"); + } + + /// + /// Tests that content is rendered even when collapsed when KeepContentAlive is true. + /// + [Test] + public void MudExpansionPanel_KeepContentAlive_True_RendersContentWhenCollapsed() + { + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "Test Panel") + .Add(p => p.KeepContentAlive, true) + .Add(p => p.ChildContent, builder => builder.AddMarkupContent(0, ""))); + + comp.Markup.Should().NotContain("mud-panel-expanded"); + comp.Find("button").Should().NotBeNull(); + + var contentDiv = comp.Find(".mud-expand-panel-content"); + contentDiv.HasAttribute("hidden").Should().BeTrue(); + } + + /// + /// Tests that content is not rendered when collapsed when KeepContentAlive is false. + /// + [Test] + public void MudExpansionPanel_KeepContentAlive_False_DoesNotRenderContentWhenCollapsed() + { + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "Test Panel") + .Add(p => p.KeepContentAlive, false) + .Add(p => p.ChildContent, builder => builder.AddMarkupContent(0, ""))); + + comp.Markup.Should().NotContain("mud-panel-expanded"); + comp.FindAll("button").Should().BeEmpty(); + } } } diff --git a/src/MudBlazor.UnitTests/Components/HighlighterTests.cs b/src/MudBlazor.UnitTests/Components/HighlighterTests.cs index a524852a8f72..f7b27dcb4e54 100644 --- a/src/MudBlazor.UnitTests/Components/HighlighterTests.cs +++ b/src/MudBlazor.UnitTests/Components/HighlighterTests.cs @@ -1,7 +1,6 @@ - -using System.Collections.Generic; -using Bunit; +using Bunit; using FluentAssertions; +using MudBlazor.Components.Highlighter; using NUnit.Framework; using static Bunit.ComponentParameterFactory; using static MudBlazor.Components.Highlighter.Splitter; @@ -113,6 +112,161 @@ public void DontMessWithDuplicatedHighlightPatternsInHighlightedTextParameterAnd result.Should().BeEquivalentTo(new List { "This is the first ", "item" }); regex.Should().Be("((?:item)|(?:item))"); } + + [Test] + public void GetFragments_NoHighlightTerms_ReturnsOriginalTextAsSingleFragment() + { + var inputText = "This is some sample text."; + string highlightedText = null; + string[] highlightedTexts = null; + + var result = GetFragments(inputText, highlightedText, highlightedTexts, out var outRegex).ToArray(); + + result.Should().HaveCount(1); + result[0].Should().Be(inputText); + outRegex.Should().Be(string.Empty); + } + + [Test] + public void GetHtmlAwareFragments_NullOrEmptyText_ReturnsEmptyList() + { + var resultNull = GetHtmlAwareFragments(null, "any", null, out var outRegex, false, false); + resultNull.Should().BeEmpty(); + outRegex.Should().Be(string.Empty); + + + // Test with empty text + var resultEmpty = GetHtmlAwareFragments(string.Empty, "any", null, out outRegex, false, false); + resultEmpty.Should().BeEmpty(); + // As per L59, regex is set to string.Empty at the start of the method before the null check. + outRegex.Should().Be(string.Empty); + } + + [Test] + public void GetHtmlAwareFragments_InvalidTag_IsEncodedAsText() + { + var text = "This is text."; + var highlightedText = "text"; + + var fragments = GetHtmlAwareFragments(text, highlightedText, null, out var outRegex, false, false); + + fragments.Should().Contain(new FragmentInfo("", FragmentType.Text)); + + // Verify surrounding text and highlight + var expectedFullSequence = new[] { + new FragmentInfo("This is ", FragmentType.Text), + new FragmentInfo("", FragmentType.Text), + new FragmentInfo(" ", FragmentType.Text), + new FragmentInfo("text", FragmentType.HighlightedText), + new FragmentInfo(".", FragmentType.Text) + }; + fragments.Should().BeEquivalentTo(expectedFullSequence, options => options.WithStrictOrdering()); + } + + [Test] + public void GetHtmlAwareFragments_SelfClosingTag_IsPreservedAsMarkup() + { + var text = "Text with
a line break."; + var highlightedText = "Text"; + + var fragments = GetHtmlAwareFragments(text, highlightedText, null, out var outRegex, false, false); + + var expectedFullSequence = new[] { + new FragmentInfo("Text", FragmentType.HighlightedText), + new FragmentInfo(" with ", FragmentType.Text), + new FragmentInfo("
", FragmentType.Markup), + new FragmentInfo(" a line break.", FragmentType.Text) + }; + fragments.Should().BeEquivalentTo(expectedFullSequence, options => options.WithStrictOrdering()); + } + + [Test] + public void GetHtmlAwareFragments_MismatchedClosingTag_IsEncodedAsText() + { + var text = "
Content
"; + var highlightedText = "Content"; + var fragments = GetHtmlAwareFragments(text, highlightedText, null, out var outRegex, false, false); + + var expectedFullSequence = new[] { + new FragmentInfo("
", FragmentType.Text), + new FragmentInfo("", FragmentType.Markup), + new FragmentInfo("Content", FragmentType.HighlightedText), + new FragmentInfo(System.Net.WebUtility.HtmlEncode("
"), FragmentType.Text), + new FragmentInfo("", FragmentType.Markup) + }; + fragments.Should().BeEquivalentTo(expectedFullSequence, options => options.WithStrictOrdering()); + } + + [Test] + public void GetHtmlAwareFragments_UnmatchedTagWithHighlightAndTrailingText_CorrectlyFragments() + { + var text = "unclosed highlight then_text"; // Simplified input + var highlightedText = "highlight"; + + var fragments = GetHtmlAwareFragments(text, highlightedText, null, out var outRegex, caseSensitive: false, untilNextBoundary: false); + + var expectedSequence = new[] { + new FragmentInfo("", FragmentType.Text), // The unmatched opening tag becomes text + new FragmentInfo("unclosed ", FragmentType.Text), + new FragmentInfo("highlight", FragmentType.HighlightedText), + new FragmentInfo(" then_text", FragmentType.Text) + }; + + fragments.Should().BeEquivalentTo(expectedSequence, options => options.WithStrictOrdering()); + } + + [Test] + public void GetFragments_WithManyTerms_ExercisesStringBuilderCaching() + { + var text = "The quick brown fox jumps over the lazy dog and repeats."; + var highlightedTexts1 = new[] { "quick", "brown", "fox", "jumps", "lazy", "dog", "repeats" }; + + var fragments1 = GetFragments(text, null, highlightedTexts1, out var outRegex1, caseSensitive: false, untilNextBoundary: false).ToArray(); + + fragments1.Should().NotBeEmpty(); + outRegex1.Should().NotBeEmpty(); + outRegex1.Should().ContainAll(highlightedTexts1); + fragments1.Length.Should().Be(highlightedTexts1.Length * 2 - 1 + 2); + + var text2 = "Another test sentence with new words like example and cache."; + var highlightedTexts2 = new[] { "sentence", "words", "example", "cache" }; + var fragments2 = GetFragments(text2, null, highlightedTexts2, out var outRegex2, caseSensitive: false, untilNextBoundary: false).ToArray(); + + fragments2.Should().NotBeEmpty(); + outRegex2.Should().NotBeEmpty(); + outRegex2.Should().ContainAll(highlightedTexts2); + fragments2.Length.Should().Be(highlightedTexts2.Length * 2 - 1 + 2); + + var text3 = "No highlights here."; + var fragments3 = GetFragments(text3, null, [], out var outRegex3, caseSensitive: false, untilNextBoundary: false).ToArray(); + fragments3.Should().HaveCount(1); + fragments3[0].Should().Be(text3); + outRegex3.Should().BeEmpty(); + } + + [Test] + public void GetHtmlAwareFragments_CaseSensitivity_ProducesCorrectFragments() + { + var text = "Highlight highlight"; + var highlightTerm = "Highlight"; + + // Case Sensitive + var fragmentsSensitive = GetHtmlAwareFragments(text, highlightTerm, null, out var outRegex, caseSensitive: true, untilNextBoundary: false); + var expectedSensitive = new[] { + new FragmentInfo("Highlight", FragmentType.HighlightedText), + new FragmentInfo(" highlight", FragmentType.Text) + }; + fragmentsSensitive.Should().BeEquivalentTo(expectedSensitive, options => options.WithStrictOrdering()); + + // Case Insensitive + var fragmentsInsensitive = GetHtmlAwareFragments(text, highlightTerm, null, out outRegex, caseSensitive: false, untilNextBoundary: false); + var expectedInsensitive = new[] { + new FragmentInfo("Highlight", FragmentType.HighlightedText), + new FragmentInfo(" ", FragmentType.Text), + new FragmentInfo("highlight", FragmentType.HighlightedText) + }; + fragmentsInsensitive.Should().BeEquivalentTo(expectedInsensitive, options => options.WithStrictOrdering()); + } } [TestFixture] @@ -261,5 +415,188 @@ public void MudHighlighterMarkupRenderFragmentTest() comp = Context.RenderComponent(text, highlightedText, textAsMarkupTrue); comp.MarkupMatches(formattedOutput); } + + [Test] + public void MudHighlighter_MarkupTrue_HtmlInText_ShouldHighlightCorrectly() + { + var textParam = Parameter(nameof(MudHighlighter.Text), "Hello World"); + var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "Hello"); + var markupParam = Parameter(nameof(MudHighlighter.Markup), true); + var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); + comp.MarkupMatches("Hello World"); + } + + [Test] + public void MudHighlighter_MarkupTrue_HtmlSensitiveCharInHighlightedText_ShouldEncodeAndHighlight() + { + var textParam = Parameter(nameof(MudHighlighter.Text), "Hello "); + var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), ""); + var markupParam = Parameter(nameof(MudHighlighter.Markup), true); + var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); + comp.MarkupMatches("Hello <World>"); + } + + [Test] + public void MudHighlighter_MarkupTrue_HtmlInText_ShouldNotHighlightInTags() + { + var textParam = Parameter(nameof(MudHighlighter.Text), "
div content div
"); + var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "div"); + var markupParam = Parameter(nameof(MudHighlighter.Markup), true); + var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); + comp.MarkupMatches("
div content div
"); + } + + [Test] + public void MudHighlighter_MarkupTrue_HtmlTag_ShouldNotHighlight() + { + var textParam = Parameter(nameof(MudHighlighter.Text), "Hello Mud World"); + var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), ""); + var markupParam = Parameter(nameof(MudHighlighter.Markup), true); + var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); + comp.MarkupMatches("Hello Mud World"); + } + + [Test] + public void MudHighlighter_MarkupTrue_TextWithHtmlEntities_HighlightedTextIsEntityText() + { + var textParam = Parameter(nameof(MudHighlighter.Text), "Hello & World"); + var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "&"); + var markupParam = Parameter(nameof(MudHighlighter.Markup), true); + var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); + // Adjusted to match BUnit's actual output. This implies that when FragmentInfo.Content = "&", + // the rendered @FragmentInfo.Content is captured by BUnit as &amp;. + comp.MarkupMatches("Hello &amp; World"); + } + + [Test] + public void MudHighlighter_MarkupTrue_HighlightedTextWithSingleQuotes_ShouldHighlight() + { + var textParam = Parameter(nameof(MudHighlighter.Text), "This is a 'quoted' text and a \"double quoted\" text."); + var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "'quoted'"); + var markupParam = Parameter(nameof(MudHighlighter.Markup), true); + var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); + comp.MarkupMatches("This is a 'quoted' text and a \"double quoted\" text."); + } + + [Test] + public void MudHighlighter_MarkupTrue_HighlightedTextWithDoubleQuotes_ShouldHighlight() + { + var textParam = Parameter(nameof(MudHighlighter.Text), "This is a 'quoted' text and a \"double quoted\" text."); + var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "\"double quoted\""); + var markupParam = Parameter(nameof(MudHighlighter.Markup), true); + var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); + comp.MarkupMatches("This is a 'quoted' text and a "double quoted" text."); + } + + [Test] + public void MudHighlighter_MarkupTrue_HighlightedTextAsAttributeValue_ShouldNotHighlightInAttribute() + { + var textParam = Parameter(nameof(MudHighlighter.Text), "nothing"); + var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "nothing"); + var markupParam = Parameter(nameof(MudHighlighter.Markup), true); + var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); + comp.MarkupMatches("nothing"); + } + + [Test] + public void MudHighlighter_MarkupTrue_FormattingPreservation_ItalicsAndColor() + { + var textParam = Parameter(nameof(MudHighlighter.Text), "MudBlazor is important"); + var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "MudBlazor"); + var markupParam = Parameter(nameof(MudHighlighter.Markup), true); + var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); + comp.MarkupMatches("MudBlazor is important"); + } + + [Test] + public void MudHighlighter_MarkupTrue_FormattingPreservation_Bold() + { + var textParam = Parameter(nameof(MudHighlighter.Text), "Normal bold normal"); + var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "bold"); + var markupParam = Parameter(nameof(MudHighlighter.Markup), true); + var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); + comp.MarkupMatches("Normal bold normal"); + } + + [Test] + public void MudHighlighter_MarkupTrue_NonStandardTag_NoHighlight() + { + var textParam = Parameter(nameof(MudHighlighter.Text), "Hello world"); + var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), ""); // Or null, effectively no highlight + var markupParam = Parameter(nameof(MudHighlighter.Markup), true); + var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); + comp.MarkupMatches("Hello <ambitious> world"); + } + + [Test] + public void MudHighlighter_MarkupTrue_NonStandardTag_WithHighlightAfterTag() + { + var textParam = Parameter(nameof(MudHighlighter.Text), "Hello world"); + var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "world"); + var markupParam = Parameter(nameof(MudHighlighter.Markup), true); + var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); + comp.MarkupMatches("Hello <ambitious> world"); + } + + [Test] + public void MudHighlighter_MarkupTrue_NonStandardTag_WithHighlightInsideTag() + { + var textParam = Parameter(nameof(MudHighlighter.Text), "Hello world"); + var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "bit"); + var markupParam = Parameter(nameof(MudHighlighter.Markup), true); + var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); + comp.MarkupMatches("Hello <ambitious> world"); + } + + [Test] + public void MudHighlighter_MarkupTrue_NoFragments_RendersTextAsMarkupString() + { + var textParam = Parameter(nameof(MudHighlighter.Text), "Some text"); + var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "zip"); + var markupParam = Parameter(nameof(MudHighlighter.Markup), true); + + var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); + + comp.MarkupMatches("Some text"); + } + + [Test] + public void MudHighlighter_MarkupTrue_WithClass_RendersMarkWithClass() + { + var textParam = Parameter(nameof(MudHighlighter.Text), "Highlight this"); + var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "Highlight"); + var markupParam = Parameter(nameof(MudHighlighter.Markup), true); + var classParam = Parameter(nameof(MudHighlighter.Class), "my-custom-class"); + + var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam, classParam); + + comp.MarkupMatches("Highlight this"); + } + + [Test] + public void MudHighlighter_MarkupFalse_AfterMarkupTrue_ClearsHtmlAwareFragmentsAndRendersCorrectly() + { + var initialText = "Test with HTML and highlight"; + var initialHighlightedText = "highlight"; + + // 1. Render initially with Markup = true + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, initialText) + .Add(p => p.HighlightedText, initialHighlightedText) + .Add(p => p.Markup, true) + ); + + comp.MarkupMatches("Test with HTML and highlight"); + + // 2. Re-render with Markup = false + comp.SetParametersAndRender(parameters => parameters + .Add(p => p.Text, initialText) // Keep text and highlight the same + .Add(p => p.HighlightedText, initialHighlightedText) + .Add(p => p.Markup, false) + ); + + var expectedMarkup = "Test with <b>HTML</b> and highlight"; + comp.MarkupMatches(expectedMarkup); + } } } diff --git a/src/MudBlazor.UnitTests/Components/MaskTests.cs b/src/MudBlazor.UnitTests/Components/MaskTests.cs index da892d3d4508..182b59a81b75 100644 --- a/src/MudBlazor.UnitTests/Components/MaskTests.cs +++ b/src/MudBlazor.UnitTests/Components/MaskTests.cs @@ -4,6 +4,7 @@ using Bunit; using FluentAssertions; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using MudBlazor.UnitTests.TestComponents.Mask; using NUnit.Framework; @@ -1068,5 +1069,28 @@ await comp.InvokeAsync(() => Context.JSInterop.VerifyInvoke("mudWindow.copyToClipboard", 2); Context.JSInterop.Invocations["mudWindow.copyToClipboard"][1].Arguments.Should().BeEquivalentTo(["c"]); } + + [Test] + public async Task Mask_Autofill_ShouldUpdateValueAndText_WhenAutofilled() + { + // Arrange + var mask = new PatternMask("(000) 000-0000"); + var autofillValue = "(123) 456-7890"; + + var comp = Context.RenderComponent>(parameters => parameters + .Add(p => p.Mask, mask) + .Add(p => p.DebounceInterval, 0) + ); + + var textField = comp.Instance; + var inputElement = comp.Find("input"); + + // Act + // Simulate the 'oninput' event that occurs during browser autofill + await inputElement.InputAsync(new ChangeEventArgs() { Value = autofillValue }); + + // Assert + textField.Text.Should().Be(autofillValue); + } } } diff --git a/src/MudBlazor.UnitTests/Components/ProgressCircularTests.cs b/src/MudBlazor.UnitTests/Components/ProgressCircularTests.cs index e568bd8e6ead..fa8ce199ef70 100644 --- a/src/MudBlazor.UnitTests/Components/ProgressCircularTests.cs +++ b/src/MudBlazor.UnitTests/Components/ProgressCircularTests.cs @@ -1,7 +1,4 @@ - -using System; -using System.Globalization; -using Bunit; +using Bunit; using FluentAssertions; using NUnit.Framework; @@ -76,7 +73,7 @@ public void DefaultStructure(bool indeterminate) [Test] [TestCase(true)] - [TestCase(true)] + [TestCase(false)] public void TestClassesForRounded(bool rounded) { var comp = Context.RenderComponent(x => x.Add(y => y.Rounded, rounded)); @@ -95,7 +92,7 @@ public void TestClassesForRounded(bool rounded) [Test] [TestCase(true)] - [TestCase(true)] + [TestCase(false)] public void TestClassesForIntermediate(bool indeterminate) { var comp = Context.RenderComponent(x => x.Add(y => y.Indeterminate, indeterminate)); @@ -105,10 +102,12 @@ public void TestClassesForIntermediate(bool indeterminate) if (indeterminate) { container.ClassList.Should().Contain("mud-progress-indeterminate"); + container.ClassList.Should().NotContain("mud-progress-static"); } else { - container.ClassList.Should().NotContain("mud-progress-static"); + container.ClassList.Should().Contain("mud-progress-static"); + container.ClassList.Should().NotContain("mud-progress-indeterminate"); } var circleContainer = comp.Find(".mud-progress-circular-circle"); @@ -116,10 +115,12 @@ public void TestClassesForIntermediate(bool indeterminate) if (indeterminate) { circleContainer.ClassList.Should().Contain("mud-progress-indeterminate"); + circleContainer.ClassList.Should().NotContain("mud-progress-static"); } else { - circleContainer.ClassList.Should().NotContain("mud-progress-static"); + circleContainer.ClassList.Should().Contain("mud-progress-static"); + circleContainer.ClassList.Should().NotContain("mud-progress-indeterminate"); } } @@ -148,5 +149,38 @@ public void TestClassesForColor(Color color, string expectedString) container.ClassList.Should().Contain($"mud-{expectedString}-text"); } + + [Test] + [TestCase(3, "22.5 22.5 43 43")] + [TestCase(5, "21.5 21.5 45 45")] + [TestCase(10, "19 19 50 50")] + [TestCase(1, "23.5 23.5 41 41")] + [TestCase(0, "24 24 40 40")] + public void MudProgressCircular_ShouldHaveCorrectViewBox_BasedOnStrokeWidth(int strokeWidth, string expectedViewBox) + { + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.StrokeWidth, strokeWidth) + ); + + // Find the SVG element + var svgElement = comp.Find("svg"); + svgElement.Should().NotBeNull(); + + // Check the viewBox attribute + var viewBoxAttribute = svgElement.GetAttribute("viewBox"); + viewBoxAttribute.Should().Be(expectedViewBox); + + // Test with Indeterminate = true as well + var compIndeterminate = Context.RenderComponent(parameters => parameters + .Add(p => p.StrokeWidth, strokeWidth) + .Add(p => p.Indeterminate, true) + ); + + var svgElementIndeterminate = compIndeterminate.Find("svg"); + svgElementIndeterminate.Should().NotBeNull(); + + var viewBoxAttributeIndeterminate = svgElementIndeterminate.GetAttribute("viewBox"); + viewBoxAttributeIndeterminate.Should().Be(expectedViewBox); + } } } diff --git a/src/MudBlazor.UnitTests/Components/SelectTests.cs b/src/MudBlazor.UnitTests/Components/SelectTests.cs index a3f99c3c9d05..5c08a3884050 100644 --- a/src/MudBlazor.UnitTests/Components/SelectTests.cs +++ b/src/MudBlazor.UnitTests/Components/SelectTests.cs @@ -1609,6 +1609,25 @@ public async Task Select_HandleMouseDown(MouseEventArgs args) break; } } + + [Test] + public void SelectMultiSelectFieldChangedTest() + { + var comp = Context.RenderComponent(); + + //default values + comp.Instance.FormFieldChangedEventArgs.Should().BeNull(); + + //open the popover + var input = comp.Find("div.mud-input-control"); + input.MouseDown(); + + //click an item and see the value change + comp.WaitForAssertion(() => comp.FindAll("div.mud-list-item").Count.Should().BeGreaterThan(0)); + comp.Find(".mud-list-item").Click(); + + comp.Instance.FormFieldChangedEventArgs.NewValue.Should().BeEquivalentTo(comp.Instance.States.Take(2).Reverse()); + } #nullable disable } } diff --git a/src/MudBlazor.UnitTests/Other/TypeIdentifierTests.cs b/src/MudBlazor.UnitTests/Other/TypeIdentifierTests.cs index b5cbfd82cab1..56fc0f290a5b 100644 --- a/src/MudBlazor.UnitTests/Other/TypeIdentifierTests.cs +++ b/src/MudBlazor.UnitTests/Other/TypeIdentifierTests.cs @@ -93,6 +93,20 @@ public void IsBoolean_Test(Type type, bool expected) isBoolean.Should().Be(expected); } + [Test] + [TestCase(null, false)] + [TestCase(typeof(int), false)] + [TestCase(typeof(int?), false)] + [TestCase(typeof(DateOnly), true)] + [TestCase(typeof(DateOnly?), true)] + [TestCase(typeof(DateTime), false)] + [TestCase(typeof(DateTime?), false)] + public void IsDateOnly_Test(Type type, bool expected) + { + var isDateOnly = TypeIdentifier.IsDateOnly(type); + isDateOnly.Should().Be(expected); + } + [Test] [TestCase(null, false)] [TestCase(typeof(int), false)] diff --git a/src/MudBlazor/Components/Autocomplete/MudAutocomplete.razor.cs b/src/MudBlazor/Components/Autocomplete/MudAutocomplete.razor.cs index 79a8264be093..ca5e91eba4e5 100644 --- a/src/MudBlazor/Components/Autocomplete/MudAutocomplete.razor.cs +++ b/src/MudBlazor/Components/Autocomplete/MudAutocomplete.razor.cs @@ -742,10 +742,25 @@ public async Task OpenMenuAsync() // Get range of items based off selected item so the selected item can be scrolled to when strict is set to false if (!Strict && searchedItems.Length != 0 && !EqualityComparer.Default.Equals(Value, default(T))) { - int split = (MaxItems.Value / 2) + 1; + int maxItems = MaxItems.Value; int valueIndex = Array.IndexOf(searchedItems, Value); - int endIndex = Math.Min(valueIndex + split, searchedItems.Length); - int startIndex = endIndex - Math.Min(MaxItems.Value, searchedItems.Length); + + // Center the selected item in the list if possible + int half = maxItems / 2; + int startIndex = valueIndex - half; + int endIndex = startIndex + maxItems; + + // Adjust if out of bounds + if (startIndex < 0) + { + startIndex = 0; + endIndex = Math.Min(maxItems, searchedItems.Length); + } + else if (endIndex > searchedItems.Length) + { + endIndex = searchedItems.Length; + startIndex = Math.Max(0, endIndex - maxItems); + } searchedItems = searchedItems.Take(new Range(startIndex, endIndex)).ToArray(); } diff --git a/src/MudBlazor/Components/DataGrid/FieldType.cs b/src/MudBlazor/Components/DataGrid/FieldType.cs index 5c9140d711e4..652ba140afce 100644 --- a/src/MudBlazor/Components/DataGrid/FieldType.cs +++ b/src/MudBlazor/Components/DataGrid/FieldType.cs @@ -37,6 +37,11 @@ public class FieldType ///
public bool IsDateTime { get; init; } + /// + /// Whether the represents a date only. + /// + public bool IsDateOnly { get; init; } + /// /// Whether the represents a true/false value. /// @@ -59,6 +64,7 @@ public static FieldType Identify(Type? type) IsNumber = TypeIdentifier.IsNumber(type), IsEnum = TypeIdentifier.IsEnum(type), IsDateTime = TypeIdentifier.IsDateTime(type), + IsDateOnly = TypeIdentifier.IsDateOnly(type), IsBoolean = TypeIdentifier.IsBoolean(type), IsGuid = TypeIdentifier.IsGuid(type) }; diff --git a/src/MudBlazor/Components/DataGrid/FilterExpressionGenerator.cs b/src/MudBlazor/Components/DataGrid/FilterExpressionGenerator.cs index 8f314cf37cbd..e2b24cfc3f29 100644 --- a/src/MudBlazor/Components/DataGrid/FilterExpressionGenerator.cs +++ b/src/MudBlazor/Components/DataGrid/FilterExpressionGenerator.cs @@ -103,6 +103,25 @@ public static class FilterExpressionGenerator }; } + if (fieldType.IsDateOnly) + { + if (filter.Value is null && filter.Operator != FilterOperator.DateOnly.Empty && filter.Operator != FilterOperator.DateOnly.NotEmpty) + return x => true; + + return filter.Operator switch + { + FilterOperator.DateOnly.Is => propertyExpression.GenerateBinary(ExpressionType.Equal, filter.Value), + FilterOperator.DateOnly.IsNot => propertyExpression.GenerateBinary(ExpressionType.NotEqual, filter.Value), + FilterOperator.DateOnly.After => propertyExpression.GenerateBinary(ExpressionType.GreaterThan, filter.Value), + FilterOperator.DateOnly.OnOrAfter => propertyExpression.GenerateBinary(ExpressionType.GreaterThanOrEqual, filter.Value), + FilterOperator.DateOnly.Before => propertyExpression.GenerateBinary(ExpressionType.LessThan, filter.Value), + FilterOperator.DateOnly.OnOrBefore => propertyExpression.GenerateBinary(ExpressionType.LessThanOrEqual, filter.Value), + FilterOperator.DateOnly.Empty => propertyExpression.GenerateBinary(ExpressionType.Equal, null), + FilterOperator.DateOnly.NotEmpty => propertyExpression.GenerateBinary(ExpressionType.NotEqual, null), + _ => x => true + }; + } + if (fieldType.IsDateTime) { if (filter.Value is null && filter.Operator != FilterOperator.DateTime.Empty && filter.Operator != FilterOperator.DateTime.NotEmpty) diff --git a/src/MudBlazor/Components/DataGrid/FilterHeaderCell.razor b/src/MudBlazor/Components/DataGrid/FilterHeaderCell.razor index 02c730712bd6..e75c876ce733 100644 --- a/src/MudBlazor/Components/DataGrid/FilterHeaderCell.razor +++ b/src/MudBlazor/Components/DataGrid/FilterHeaderCell.razor @@ -42,11 +42,15 @@ @Localizer[LanguageResource.MudDataGrid_False] } + else if (fieldType.IsDateOnly && !(@operator ?? "").EndsWith("empty")) + { + + } else if (fieldType.IsDateTime && !(@operator ?? "").EndsWith("empty")) { - + diff --git a/src/MudBlazor/Components/DataGrid/FilterHeaderCell.razor.cs b/src/MudBlazor/Components/DataGrid/FilterHeaderCell.razor.cs index 0a7566adfdfb..d4babc69793e 100644 --- a/src/MudBlazor/Components/DataGrid/FilterHeaderCell.razor.cs +++ b/src/MudBlazor/Components/DataGrid/FilterHeaderCell.razor.cs @@ -75,7 +75,8 @@ private IReadOnlyCollection operators private double? valueNumber => fieldType.IsNumber ? (double?)Column.FilterContext.FilterDefinition.Value : default; private bool? valueBool => fieldType.IsBoolean && Column.FilterContext.FilterDefinition.Value is not null ? (bool?)Column.FilterContext.FilterDefinition.Value : default; private Enum valueEnum => fieldType.IsEnum && Column.FilterContext.FilterDefinition.Value is not null ? (Enum)Column.FilterContext.FilterDefinition.Value : default; - private DateTime? valueDate => fieldType.IsDateTime ? (DateTime?)Column.FilterContext.FilterDefinition.Value : default; + private DateTime? valueDateTimeForPicker => fieldType.IsDateTime ? (DateTime?)Column.FilterContext.FilterDefinition.Value : default; + private DateTime? valueDateOnlyForPicker => fieldType.IsDateOnly && Column.FilterContext.FilterDefinition.Value != null ? ((DateOnly)Column.FilterContext.FilterDefinition.Value).ToDateTime(TimeOnly.MinValue) : null; private TimeSpan? valueTime => fieldType.IsDateTime && Column.FilterContext.FilterDefinition.Value is not null ? ((DateTime?)Column.FilterContext.FilterDefinition.Value).Value.TimeOfDay : null; private string @operator => Column.FilterContext.FilterDefinition.Operator ?? operators.FirstOrDefault(); @@ -118,8 +119,9 @@ internal async Task BoolValueChangedAsync(bool? value) await ApplyFilterAsync(Column.FilterContext.FilterDefinition); } - internal async Task DateValueChangedAsync(DateTime? value) + internal async Task DateTimeValueChangedAsync(DateTime? value) { + // For DateTime fields, handle both date and time components if (value != null) { var date = value.Value.Date; @@ -127,29 +129,44 @@ internal async Task DateValueChangedAsync(DateTime? value) // get the time component and add it to the date. if (valueTime != null) { - date.Add(valueTime.Value); + date = date.Add(valueTime.Value); } Column.FilterContext.FilterDefinition.Value = date; - await ApplyFilterAsync(Column.FilterContext.FilterDefinition); } else { Column.FilterContext.FilterDefinition.Value = value; - await ApplyFilterAsync(Column.FilterContext.FilterDefinition); } + + await ApplyFilterAsync(Column.FilterContext.FilterDefinition); + } + + internal async Task DateOnlyValueChangedAsync(DateTime? value) + { + // For DateOnly fields, convert DateTime to DateOnly + if (value != null) + { + var dateOnly = DateOnly.FromDateTime(value.Value); + Column.FilterContext.FilterDefinition.Value = dateOnly; + } + else + { + Column.FilterContext.FilterDefinition.Value = null; + } + await ApplyFilterAsync(Column.FilterContext.FilterDefinition); } internal async Task TimeValueChangedAsync(TimeSpan? value) { - if (valueDate != null) + if (valueDateTimeForPicker != null) { - var date = valueDate.Value.Date; + var date = valueDateTimeForPicker.Value.Date; // get the time component and add it to the date. - if (valueTime != null) + if (value != null) { - date = date.Add(valueTime.Value); + date = date.Add(value.Value); } Column.FilterContext.FilterDefinition.Value = date; diff --git a/src/MudBlazor/Components/DataGrid/FilterOperator.cs b/src/MudBlazor/Components/DataGrid/FilterOperator.cs index e16f1e0e0df0..68d8f580b016 100644 --- a/src/MudBlazor/Components/DataGrid/FilterOperator.cs +++ b/src/MudBlazor/Components/DataGrid/FilterOperator.cs @@ -184,6 +184,52 @@ public static class DateTime public const string NotEmpty = "is not empty"; } + /// + /// Represents filters which are available for date only values. + /// + public static class DateOnly + { + /// + /// Find values matching the filter date. + /// + public const string Is = "is"; + + /// + /// Find values different from the filter date. + /// + public const string IsNot = "is not"; + + /// + /// Find values after the filter date. + /// + public const string After = "is after"; + + /// + /// Find values on or after the filter date. + /// + public const string OnOrAfter = "is on or after"; + + /// + /// Find values before the filter date. + /// + public const string Before = "is before"; + + /// + /// Find values on or before the filter date. + /// + public const string OnOrBefore = "is on or before"; + + /// + /// Find null values. + /// + public const string Empty = "is empty"; + + /// + /// Find any non-null value. + /// + public const string NotEmpty = "is not empty"; + } + /// /// Represents filters which are available for Guid values. /// @@ -250,6 +296,20 @@ internal static string[] GetOperatorByDataType(FieldType fieldType) Boolean.Is, }; } + if (fieldType.IsDateOnly) + { + return new[] + { + DateOnly.Is, + DateOnly.IsNot, + DateOnly.After, + DateOnly.OnOrAfter, + DateOnly.Before, + DateOnly.OnOrBefore, + DateOnly.Empty, + DateOnly.NotEmpty, + }; + } if (fieldType.IsDateTime) { return new[] diff --git a/src/MudBlazor/Components/DataGrid/HeaderCell.razor.cs b/src/MudBlazor/Components/DataGrid/HeaderCell.razor.cs index f6a79585edfc..9d392712cd61 100644 --- a/src/MudBlazor/Components/DataGrid/HeaderCell.razor.cs +++ b/src/MudBlazor/Components/DataGrid/HeaderCell.razor.cs @@ -376,7 +376,9 @@ internal async Task SortChangedAsync(MouseEventArgs args) SortDirection = SortDirection switch { SortDirection.Ascending => SortDirection.Descending, - SortDirection.Descending => SortDirection.None, + SortDirection.Descending => DataGrid?.AllowUnsorted ?? false + ? SortDirection.None + : SortDirection.Ascending, _ => SortDirection.Ascending }; @@ -386,9 +388,9 @@ internal async Task SortChangedAsync(MouseEventArgs args) return; } - DataGrid.DropContainerHasChanged(); + DataGrid?.DropContainerHasChanged(); - if ((args.MetaKey || args.CtrlKey) && DataGrid.SortMode == SortMode.Multiple) + if ((args.MetaKey || args.CtrlKey) && DataGrid?.SortMode == SortMode.Multiple) await InvokeAsync(() => DataGrid.ExtendSortAsync(Column.PropertyName, SortDirection, Column.GetLocalSortFunc(), Column.Comparer)); else await InvokeAsync(() => DataGrid.SetSortAsync(Column.PropertyName, SortDirection, Column.GetLocalSortFunc(), Column.Comparer)); diff --git a/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs b/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs index 2ccad9b67d51..7ffd277efd98 100644 --- a/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs +++ b/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs @@ -1101,6 +1101,15 @@ public bool Groupable #endregion + /// + /// Allows a sort direction of in addition to and . + /// + /// + /// Defaults to true. When false, the sort mode will only toggle between and . + /// + [Parameter] + public bool AllowUnsorted { get; set; } = true; + #region Properties internal IEnumerable CurrentPageItems diff --git a/src/MudBlazor/Components/DataGrid/TypeIdentifier.cs b/src/MudBlazor/Components/DataGrid/TypeIdentifier.cs index 64d111268504..680824d4c25b 100644 --- a/src/MudBlazor/Components/DataGrid/TypeIdentifier.cs +++ b/src/MudBlazor/Components/DataGrid/TypeIdentifier.cs @@ -88,6 +88,23 @@ public static bool IsDateTime(Type? type) return underlyingType is not null && underlyingType == typeof(DateTime); } + public static bool IsDateOnly(Type? type) + { + if (type is null) + { + return false; + } + + if (type == typeof(DateOnly)) + { + return true; + } + + var underlyingType = Nullable.GetUnderlyingType(type); + + return underlyingType is not null && underlyingType == typeof(DateOnly); + } + public static bool IsBoolean(Type? type) { if (type is null) diff --git a/src/MudBlazor/Components/Dialog/MudDialog.razor.cs b/src/MudBlazor/Components/Dialog/MudDialog.razor.cs index 683bd7b335a4..7eb16b262b1a 100644 --- a/src/MudBlazor/Components/Dialog/MudDialog.razor.cs +++ b/src/MudBlazor/Components/Dialog/MudDialog.razor.cs @@ -275,6 +275,10 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { // Forward render update to instance (_reference.Dialog as IMudStateHasChanged)?.StateHasChanged(); + + //forward render update to instance container + if (_reference.Dialog is MudDialog { DialogInstance: not null } dialog) + await InvokeAsync(dialog.DialogInstance!.StateHasChanged); } else { diff --git a/src/MudBlazor/Components/Dialog/MudDialogContainer.razor b/src/MudBlazor/Components/Dialog/MudDialogContainer.razor index 6ee05cd0298c..eea75629412a 100644 --- a/src/MudBlazor/Components/Dialog/MudDialogContainer.razor +++ b/src/MudBlazor/Components/Dialog/MudDialogContainer.razor @@ -8,8 +8,8 @@ }
- -