Add Event feature to BitCalendar (#12261)#12262
Add Event feature to BitCalendar (#12261)#12262msynk wants to merge 1 commit intobitfoundation:developfrom
Conversation
WalkthroughThe pull request introduces an event feature to the BitCalendar component, allowing users to display custom events for each calendar day via a new Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor.cs (1)
1006-1011: Index events once instead of filtering them per cell render.
GetDayEventswalks the fullEventssequence for every visible day button, so each rerender becomes ~42 full enumerations plus list allocations. Precomputing aDictionary<DateOnly, List<BitCalendarEvent>>whenEventschanges will keep the render path flat and avoid scaling cost with larger event sets.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor.cs` around lines 1006 - 1011, GetDayEvents currently scans the entire Events sequence for each day button causing O(n*m) cost; instead, build a Dictionary<DateOnly, List<BitCalendarEvent>> when Events changes and have GetDayEvents simply look up the date key. Add a private field (e.g. _eventsByDate) and populate it in the component's parameter-set/update path (e.g. in the Events setter or OnParametersSet/OnParametersSetAsync) by grouping Events by DateOnly.FromDateTime(e.Date) or e.Date if already DateOnly; ensure null/empty Events clears the dictionary; then change GetDayEvents to return the list from _eventsByDate.TryGetValue(dateOnly, out var list) ? list : an empty list to avoid per-cell enumeration and allocations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor`:
- Around line 589-616: Replace hard-coded date/time formats and English labels
in the modal and the tooltip formatter with a single culture-aware
formatter/localizer: change the _eventModalDate.ToString("MMMM d, yyyy") and the
time renderings for evt.StartTime/evt.EndTime to call a shared method (e.g.,
Create a FormatEventDate(DateTime) and FormatEventTime(TimeSpan?/DateTime?) or a
single FormatEventRange(start,end)) that uses CultureInfo.CurrentCulture (and
the BitTimeFormat setting to choose 12/24-hour patterns) and returns localized
"From"/"Until" text via resources; also update the tooltip formatter in the
BitCalendar class to call the same formatter so all date/time displays use
consistent, culture-aware formatting.
- Around line 579-625: The event popup markup controlled by _showEventModal
should be turned into an accessible dialog: add role="dialog" and
aria-modal="true" on the modal container and set aria-labelledby to the header
span (give the header span a unique id, e.g., eventModalTitle) which should
reference `@_eventModalDate` display; make the container focusable (tabindex="0")
and capture an ElementReference (e.g., _eventModalRef) so when _showEventModal
becomes true you call FocusAsync on that ref and save/restore the previously
focused element; wire an `@onkeydown` handler on the dialog container to close via
Escape (call CloseEventModal) and to trap Tab/Shift+Tab (or call a small JS
focus-trap helper via IJSRuntime) so keyboard users cannot tab out; ensure
CloseEventModal returns focus to the saved element when closing and keep
references to _eventModalEvents and CloseEventModal in these changes to locate
the code.
---
Nitpick comments:
In `@src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor.cs`:
- Around line 1006-1011: GetDayEvents currently scans the entire Events sequence
for each day button causing O(n*m) cost; instead, build a Dictionary<DateOnly,
List<BitCalendarEvent>> when Events changes and have GetDayEvents simply look up
the date key. Add a private field (e.g. _eventsByDate) and populate it in the
component's parameter-set/update path (e.g. in the Events setter or
OnParametersSet/OnParametersSetAsync) by grouping Events by
DateOnly.FromDateTime(e.Date) or e.Date if already DateOnly; ensure null/empty
Events clears the dictionary; then change GetDayEvents to return the list from
_eventsByDate.TryGetValue(dateOnly, out var list) ? list : an empty list to
avoid per-cell enumeration and allocations.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 646fab1c-3a88-44be-8c98-a33934be6383
📒 Files selected for processing (8)
src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razorsrc/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor.cssrc/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.scsssrc/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendarClassStyles.cssrc/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendarEvent.cssrc/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Calendar/BitCalendarDemo.razorsrc/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Calendar/BitCalendarDemo.razor.cssrc/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Calendar/BitCalendarDemo.razor.samples.cs
| @if (_showEventModal) | ||
| { | ||
| <div class="bit-cal-eov @Classes?.EventModalOverlay" | ||
| style="@Styles?.EventModalOverlay" | ||
| @onclick="CloseEventModal"> | ||
| <div class="bit-cal-emc @Classes?.EventModalContainer" | ||
| style="@Styles?.EventModalContainer" | ||
| @onclick:stopPropagation="true"> | ||
| <div class="bit-cal-emh @Classes?.EventModalHeader" | ||
| style="@Styles?.EventModalHeader"> | ||
| <span>@_eventModalDate.ToString("MMMM d, yyyy")</span> | ||
| <button type="button" | ||
| @onclick="CloseEventModal" | ||
| style="@Styles?.EventModalCloseButton" | ||
| class="bit-cal-emx @Classes?.EventModalCloseButton">×</button> | ||
| </div> | ||
| @foreach (var evt in _eventModalEvents) | ||
| { | ||
| <div class="bit-cal-emi @Classes?.EventItem" | ||
| style="@Styles?.EventItem"> | ||
| <div class="bit-cal-eit @Classes?.EventItemTitle" | ||
| style="@Styles?.EventItemTitle">@evt.Title</div> | ||
| @if (evt.StartTime.HasValue || evt.EndTime.HasValue) | ||
| { | ||
| <div class="bit-cal-eis @Classes?.EventItemTime" | ||
| style="@Styles?.EventItemTime"> | ||
| @if (evt.StartTime.HasValue && evt.EndTime.HasValue) | ||
| { | ||
| @($"{evt.StartTime:HH:mm} \u2013 {evt.EndTime:HH:mm}") | ||
| } | ||
| else if (evt.StartTime.HasValue) | ||
| { | ||
| @($"From {evt.StartTime:HH:mm}") | ||
| } | ||
| else | ||
| { | ||
| @($"Until {evt.EndTime:HH:mm}") | ||
| } | ||
| </div> | ||
| } | ||
| <div class="bit-cal-eib @Classes?.EventItemBody" | ||
| style="@Styles?.EventItemBody">@evt.Body</div> | ||
| </div> | ||
| } | ||
| </div> | ||
| </div> | ||
| } |
There was a problem hiding this comment.
Make the event popup an actual dialog.
This is visually a modal, but it has no role="dialog", aria-modal, labelled title, Escape handling, or focus handoff when it opens. Keyboard and screen-reader users can remain on the underlying calendar and miss the popup entirely.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor`
around lines 579 - 625, The event popup markup controlled by _showEventModal
should be turned into an accessible dialog: add role="dialog" and
aria-modal="true" on the modal container and set aria-labelledby to the header
span (give the header span a unique id, e.g., eventModalTitle) which should
reference `@_eventModalDate` display; make the container focusable (tabindex="0")
and capture an ElementReference (e.g., _eventModalRef) so when _showEventModal
becomes true you call FocusAsync on that ref and save/restore the previously
focused element; wire an `@onkeydown` handler on the dialog container to close via
Escape (call CloseEventModal) and to trap Tab/Shift+Tab (or call a small JS
focus-trap helper via IJSRuntime) so keyboard users cannot tab out; ensure
CloseEventModal returns focus to the saved element when closing and keep
references to _eventModalEvents and CloseEventModal in these changes to locate
the code.
| <span>@_eventModalDate.ToString("MMMM d, yyyy")</span> | ||
| <button type="button" | ||
| @onclick="CloseEventModal" | ||
| style="@Styles?.EventModalCloseButton" | ||
| class="bit-cal-emx @Classes?.EventModalCloseButton">×</button> | ||
| </div> | ||
| @foreach (var evt in _eventModalEvents) | ||
| { | ||
| <div class="bit-cal-emi @Classes?.EventItem" | ||
| style="@Styles?.EventItem"> | ||
| <div class="bit-cal-eit @Classes?.EventItemTitle" | ||
| style="@Styles?.EventItemTitle">@evt.Title</div> | ||
| @if (evt.StartTime.HasValue || evt.EndTime.HasValue) | ||
| { | ||
| <div class="bit-cal-eis @Classes?.EventItemTime" | ||
| style="@Styles?.EventItemTime"> | ||
| @if (evt.StartTime.HasValue && evt.EndTime.HasValue) | ||
| { | ||
| @($"{evt.StartTime:HH:mm} \u2013 {evt.EndTime:HH:mm}") | ||
| } | ||
| else if (evt.StartTime.HasValue) | ||
| { | ||
| @($"From {evt.StartTime:HH:mm}") | ||
| } | ||
| else | ||
| { | ||
| @($"Until {evt.EndTime:HH:mm}") | ||
| } |
There was a problem hiding this comment.
Render event date/time with the calendar’s culture and time format.
The modal hard-codes "MMMM d, yyyy", "HH:mm", and English "From"/"Until" text, so localized calendars and BitTimeFormat.TwelveHours show event details in the wrong format. The tooltip formatter in src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor.cs:1013-1017 has the same problem, so this should come from one culture-aware formatter/localizer.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor`
around lines 589 - 616, Replace hard-coded date/time formats and English labels
in the modal and the tooltip formatter with a single culture-aware
formatter/localizer: change the _eventModalDate.ToString("MMMM d, yyyy") and the
time renderings for evt.StartTime/evt.EndTime to call a shared method (e.g.,
Create a FormatEventDate(DateTime) and FormatEventTime(TimeSpan?/DateTime?) or a
single FormatEventRange(start,end)) that uses CultureInfo.CurrentCulture (and
the BitTimeFormat setting to choose 12/24-hour patterns) and returns localized
"From"/"Until" text via resources; also update the tooltip formatter in the
BitCalendar class to call the same formatter so all date/time displays use
consistent, culture-aware formatting.
There was a problem hiding this comment.
Pull request overview
Adds an “Events” feature to BitCalendar, enabling days to display an event indicator and show event details via tooltip + modal, and updates the demo/docs accordingly.
Changes:
- Introduces
BitCalendarEventmodel and a newEventsparameter onBitCalendar. - Renders an event indicator dot on days with events and shows an overlay modal with event details on click.
- Updates the Calendar demo page, sample snippets, and parameter/class-style documentation to include the new feature.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Calendar/BitCalendarDemo.razor.samples.cs | Adds sample snippet/code for Events and shifts example numbering. |
| src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Calendar/BitCalendarDemo.razor.cs | Documents new Events parameter, event-related class/style hooks, and BitCalendarEvent metadata. |
| src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Calendar/BitCalendarDemo.razor | Adds an “Events” demo section and updates subsequent example references/IDs. |
| src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendarEvent.cs | Introduces the new event DTO used by the calendar. |
| src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendarClassStyles.cs | Adds class/style extension points for the new indicator and modal UI. |
| src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.scss | Adds styles for the event indicator and modal overlay/container/items. |
| src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor.cs | Adds Events parameter and supporting logic to retrieve events, build tooltips, and manage modal state. |
| src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor | Renders event indicator dot per day, sets tooltip, and adds the event details modal markup. |
| /// <summary> | ||
| /// The full body/description of the event. | ||
| /// </summary> | ||
| public required string Body { get; set; } |
There was a problem hiding this comment.
BitCalendarEvent.Body is marked as required, which forces consumers to always provide a body even when they only want a title/time (and the UI could reasonably omit an empty description). Consider making Body optional (e.g., string?) or default it to string.Empty and render it conditionally, to keep the API lightweight and avoid unnecessary boilerplate for callers.
| public required string Body { get; set; } | |
| public string Body { get; set; } = string.Empty; |
| DefaultValue = "", | ||
| Description = "The title of the event." | ||
| }, | ||
| new() | ||
| { | ||
| Name = "Body", | ||
| Type = "string", | ||
| DefaultValue = "", |
There was a problem hiding this comment.
In the demo metadata for BitCalendarEvent, Title and Body have DefaultValue = "". These properties are modeled as required in BitCalendarEvent, so showing an empty-string default is misleading in the generated docs. Consider marking them as “required” (or string.Empty if you change the model), so the docs accurately reflect how consumers should construct events.
| DefaultValue = "", | |
| Description = "The title of the event." | |
| }, | |
| new() | |
| { | |
| Name = "Body", | |
| Type = "string", | |
| DefaultValue = "", | |
| DefaultValue = "required", | |
| Description = "The title of the event." | |
| }, | |
| new() | |
| { | |
| Name = "Body", | |
| Type = "string", | |
| DefaultValue = "required", |
| private List<BitCalendarEvent> GetDayEvents(DateTime date) | ||
| { | ||
| if (Events is null) return []; | ||
| var dateOnly = DateOnly.FromDateTime(date); | ||
| return Events.Where(e => e.Date == dateOnly).ToList(); | ||
| } |
There was a problem hiding this comment.
GetDayEvents filters Events via Where(...).ToList() for every rendered day cell. This repeatedly enumerates the IEnumerable and results in O(days × events) work per render (and can be problematic if Events is a non-materialized enumerable). Consider materializing/grouping once (e.g., build a Dictionary<DateOnly, List<BitCalendarEvent>> in OnParametersSet) and then do O(1) lookups per day.
| /// <summary> | ||
| /// The list of events to display on calendar days. | ||
| /// </summary> | ||
| [Parameter] public IEnumerable<BitCalendarEvent>? Events { get; set; } | ||
|
|
There was a problem hiding this comment.
The new events feature is not covered by the existing bUnit test suite for BitCalendar. Please add tests that verify (1) days with events render an indicator, (2) the title tooltip text is set, and (3) clicking a day with events opens/closes the modal (including overlay click/close button).
| @onclick:stopPropagation="true"> | ||
| <div class="bit-cal-emh @Classes?.EventModalHeader" | ||
| style="@Styles?.EventModalHeader"> | ||
| <span>@_eventModalDate.ToString("MMMM d, yyyy")</span> |
There was a problem hiding this comment.
The event modal date header uses _eventModalDate.ToString("MMMM d, yyyy"), which formats using the current thread culture rather than the calendar’s configured culture (_culture). When consumers pass a custom Culture to BitCalendar, the modal header can display in a different language/format than the calendar itself. Use an overload that specifies _culture (or reuse the same formatting logic used elsewhere in the component).
| <span>@_eventModalDate.ToString("MMMM d, yyyy")</span> | |
| <span>@_eventModalDate.ToString("MMMM d, yyyy", _culture)</span> |
| @onclick="CloseEventModal"> | ||
| <div class="bit-cal-emc @Classes?.EventModalContainer" | ||
| style="@Styles?.EventModalContainer" | ||
| @onclick:stopPropagation="true"> | ||
| <div class="bit-cal-emh @Classes?.EventModalHeader" | ||
| style="@Styles?.EventModalHeader"> | ||
| <span>@_eventModalDate.ToString("MMMM d, yyyy")</span> | ||
| <button type="button" | ||
| @onclick="CloseEventModal" | ||
| style="@Styles?.EventModalCloseButton" | ||
| class="bit-cal-emx @Classes?.EventModalCloseButton">×</button> | ||
| </div> | ||
| @foreach (var evt in _eventModalEvents) | ||
| { | ||
| <div class="bit-cal-emi @Classes?.EventItem" | ||
| style="@Styles?.EventItem"> | ||
| <div class="bit-cal-eit @Classes?.EventItemTitle" | ||
| style="@Styles?.EventItemTitle">@evt.Title</div> | ||
| @if (evt.StartTime.HasValue || evt.EndTime.HasValue) | ||
| { | ||
| <div class="bit-cal-eis @Classes?.EventItemTime" | ||
| style="@Styles?.EventItemTime"> | ||
| @if (evt.StartTime.HasValue && evt.EndTime.HasValue) | ||
| { | ||
| @($"{evt.StartTime:HH:mm} \u2013 {evt.EndTime:HH:mm}") | ||
| } | ||
| else if (evt.StartTime.HasValue) | ||
| { | ||
| @($"From {evt.StartTime:HH:mm}") | ||
| } | ||
| else | ||
| { | ||
| @($"Until {evt.EndTime:HH:mm}") | ||
| } | ||
| </div> | ||
| } | ||
| <div class="bit-cal-eib @Classes?.EventItemBody" | ||
| style="@Styles?.EventItemBody">@evt.Body</div> | ||
| </div> | ||
| } |
There was a problem hiding this comment.
The event modal markup is missing key accessibility semantics (e.g., role="dialog", aria-modal="true", and labeling via aria-labelledby/aria-label). It also doesn’t handle keyboard dismissal (Escape) or focus management when the modal opens/closes. Please update the modal container to follow accessible dialog patterns so keyboard and screen reader users can use the feature.
| @onclick="CloseEventModal"> | |
| <div class="bit-cal-emc @Classes?.EventModalContainer" | |
| style="@Styles?.EventModalContainer" | |
| @onclick:stopPropagation="true"> | |
| <div class="bit-cal-emh @Classes?.EventModalHeader" | |
| style="@Styles?.EventModalHeader"> | |
| <span>@_eventModalDate.ToString("MMMM d, yyyy")</span> | |
| <button type="button" | |
| @onclick="CloseEventModal" | |
| style="@Styles?.EventModalCloseButton" | |
| class="bit-cal-emx @Classes?.EventModalCloseButton">×</button> | |
| </div> | |
| @foreach (var evt in _eventModalEvents) | |
| { | |
| <div class="bit-cal-emi @Classes?.EventItem" | |
| style="@Styles?.EventItem"> | |
| <div class="bit-cal-eit @Classes?.EventItemTitle" | |
| style="@Styles?.EventItemTitle">@evt.Title</div> | |
| @if (evt.StartTime.HasValue || evt.EndTime.HasValue) | |
| { | |
| <div class="bit-cal-eis @Classes?.EventItemTime" | |
| style="@Styles?.EventItemTime"> | |
| @if (evt.StartTime.HasValue && evt.EndTime.HasValue) | |
| { | |
| @($"{evt.StartTime:HH:mm} \u2013 {evt.EndTime:HH:mm}") | |
| } | |
| else if (evt.StartTime.HasValue) | |
| { | |
| @($"From {evt.StartTime:HH:mm}") | |
| } | |
| else | |
| { | |
| @($"Until {evt.EndTime:HH:mm}") | |
| } | |
| </div> | |
| } | |
| <div class="bit-cal-eib @Classes?.EventItemBody" | |
| style="@Styles?.EventItemBody">@evt.Body</div> | |
| </div> | |
| } | |
| @onclick="async () => { CloseEventModal(); await RootElement.FocusAsync(); }" | |
| @onkeydown="async args => { if (args.Key is \"Escape\") { CloseEventModal(); await RootElement.FocusAsync(); } }" | |
| tabindex="-1"> | |
| <div class="bit-cal-emc @Classes?.EventModalContainer" | |
| style="@Styles?.EventModalContainer" | |
| role="dialog" | |
| aria-modal="true" | |
| aria-labelledby="@($"{_Id}-event-modal-title")" | |
| aria-describedby="@($"{_Id}-event-modal-content")" | |
| tabindex="-1" | |
| @onclick:stopPropagation="true" | |
| @onkeydown="async args => { if (args.Key is \"Escape\") { CloseEventModal(); await RootElement.FocusAsync(); } }"> | |
| <div class="bit-cal-emh @Classes?.EventModalHeader" | |
| style="@Styles?.EventModalHeader"> | |
| <span id="@($"{_Id}-event-modal-title")">@_eventModalDate.ToString("MMMM d, yyyy")</span> | |
| <button type="button" | |
| aria-label="Close event details" | |
| autofocus | |
| @onclick="async () => { CloseEventModal(); await RootElement.FocusAsync(); }" | |
| style="@Styles?.EventModalCloseButton" | |
| class="bit-cal-emx @Classes?.EventModalCloseButton">×</button> | |
| </div> | |
| <div id="@($"{_Id}-event-modal-content")"> | |
| @foreach (var evt in _eventModalEvents) | |
| { | |
| <div class="bit-cal-emi @Classes?.EventItem" | |
| style="@Styles?.EventItem"> | |
| <div class="bit-cal-eit @Classes?.EventItemTitle" | |
| style="@Styles?.EventItemTitle">@evt.Title</div> | |
| @if (evt.StartTime.HasValue || evt.EndTime.HasValue) | |
| { | |
| <div class="bit-cal-eis @Classes?.EventItemTime" | |
| style="@Styles?.EventItemTime"> | |
| @if (evt.StartTime.HasValue && evt.EndTime.HasValue) | |
| { | |
| @($"{evt.StartTime:HH:mm} \u2013 {evt.EndTime:HH:mm}") | |
| } | |
| else if (evt.StartTime.HasValue) | |
| { | |
| @($"From {evt.StartTime:HH:mm}") | |
| } | |
| else | |
| { | |
| @($"Until {evt.EndTime:HH:mm}") | |
| } | |
| </div> | |
| } | |
| <div class="bit-cal-eib @Classes?.EventItemBody" | |
| style="@Styles?.EventItemBody">@evt.Body</div> | |
| </div> | |
| } | |
| </div> |
| style="@Styles?.EventModalHeader"> | ||
| <span>@_eventModalDate.ToString("MMMM d, yyyy")</span> | ||
| <button type="button" | ||
| @onclick="CloseEventModal" |
There was a problem hiding this comment.
The close button only renders "×" and has no accessible name. Add an aria-label (and/or visually hidden text) so screen readers announce it as “Close” (or equivalent), and ensure it’s reachable/obvious for keyboard users.
| @onclick="CloseEventModal" | |
| @onclick="CloseEventModal" | |
| aria-label="Close" |
closes #12261
Summary by CodeRabbit