From 72d8725c6fdfefad185ec0c05be6f479030dac63 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Jul 2026 06:49:42 +0000
Subject: [PATCH 1/2] Initial plan
From b178f59531189b63ef3761630c22876b7948a53f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Jul 2026 07:05:11 +0000
Subject: [PATCH 2/2] fix: auto-scroll FluentTimePicker dropdown selection into
view
---
.../DateTime/FluentTimePicker.razor.cs | 53 +++++++++
.../DateTime/FluentTimePicker.razor.ts | 105 ++++++++++++++++++
.../DateTimes/FluentTimePickerTests.razor | 27 +++++
3 files changed, 185 insertions(+)
create mode 100644 src/Core/Components/DateTime/FluentTimePicker.razor.ts
diff --git a/src/Core/Components/DateTime/FluentTimePicker.razor.cs b/src/Core/Components/DateTime/FluentTimePicker.razor.cs
index ccbdac17c9..b3f1776463 100644
--- a/src/Core/Components/DateTime/FluentTimePicker.razor.cs
+++ b/src/Core/Components/DateTime/FluentTimePicker.razor.cs
@@ -6,6 +6,7 @@
using System.Globalization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
+using Microsoft.Extensions.Logging;
using Microsoft.FluentUI.AspNetCore.Components.Calendar;
using Microsoft.JSInterop;
@@ -14,9 +15,13 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
///
public partial class FluentTimePicker : FluentInputBase
{
+ private const string JavaScriptFile = FluentJSModule.JAVASCRIPT_ROOT + "DateTime/FluentTimePicker.razor.js";
+
private static readonly IEqualityComparer TimeComparer = new TimeEqualityComparer();
private DateTime DefaultTime => Culture.Calendar.MinSupportedDateTime;
private FluentCombobox _fluentCombobox = default!;
+ private string? _lastScrolledValue;
+ private bool _scrollModuleInitialized;
///
public FluentTimePicker(LibraryConfiguration configuration) : base(configuration)
@@ -172,6 +177,54 @@ await JSRuntime.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Utilities.Attributes.
"[part='control']",
"step", Increment);
}
+
+ if (IsFluentUIStyle)
+ {
+ try
+ {
+ if (!_scrollModuleInitialized)
+ {
+ var module = await JSModule.ImportJavaScriptModuleAsync(JavaScriptFile);
+ await module.InvokeVoidAsync("Microsoft.FluentUI.Blazor.TimePicker.Initialize", Id);
+ _scrollModuleInitialized = true;
+ }
+
+ var selectedValue = FormatValueAsString(SelectedValue);
+ if (firstRender || !string.Equals(selectedValue, _lastScrolledValue, StringComparison.Ordinal))
+ {
+ var module = JSModule.ObjectReference;
+ if (module is null)
+ {
+ throw new InvalidOperationException("The FluentTimePicker JavaScript module is not initialized.");
+ }
+
+ await module.InvokeVoidAsync("Microsoft.FluentUI.Blazor.TimePicker.ScrollToSelectedValue", Id, selectedValue);
+ _lastScrolledValue = selectedValue;
+ }
+ }
+ catch (JSException exception)
+ {
+ var loggerFactory = GetCachedServiceOrNull();
+ loggerFactory?.CreateLogger(GetType()).LogError(exception, "Unable to initialize FluentTimePicker auto-scroll.");
+ }
+ }
+ }
+
+ ///
+ protected override async ValueTask DisposeAsync(IJSObjectReference jsModule)
+ {
+ if (_scrollModuleInitialized)
+ {
+ try
+ {
+ await jsModule.InvokeVoidAsync("Microsoft.FluentUI.Blazor.TimePicker.Dispose", Id);
+ }
+ catch (JSException exception)
+ {
+ var loggerFactory = GetCachedServiceOrNull();
+ loggerFactory?.CreateLogger(GetType()).LogError(exception, "Unable to dispose FluentTimePicker auto-scroll.");
+ }
+ }
}
///
diff --git a/src/Core/Components/DateTime/FluentTimePicker.razor.ts b/src/Core/Components/DateTime/FluentTimePicker.razor.ts
new file mode 100644
index 0000000000..8357c59953
--- /dev/null
+++ b/src/Core/Components/DateTime/FluentTimePicker.razor.ts
@@ -0,0 +1,105 @@
+export namespace Microsoft {
+ export namespace FluentUI {
+ export namespace Blazor {
+ export namespace TimePicker {
+
+ const pendingScrolls = new Map();
+ const initializedDropdowns = new WeakMap();
+ const cleanupCallbacks = new WeakMap void>();
+ const defaultScrollDelayMs = 50;
+
+ function scheduleScroll(id: string, value: string | null | undefined, delay: number = 0) {
+ const key = `${id}:${value ?? ""}`;
+ const existing = pendingScrolls.get(key);
+ if (existing !== undefined) {
+ window.clearTimeout(existing);
+ }
+
+ const timeoutId = window.setTimeout(() => {
+ pendingScrolls.delete(key);
+ ScrollToSelectedValue(id, value);
+ }, delay);
+
+ pendingScrolls.set(key, timeoutId);
+ }
+
+ export function Initialize(id: string) {
+ const dropdown = document.getElementById(id) as HTMLElement | null;
+ if (!dropdown || initializedDropdowns.has(dropdown)) {
+ return;
+ }
+
+ initializedDropdowns.set(dropdown, true);
+
+ // Use the DOM-selected option when the dropdown is opened or edited.
+ const handleOpen = () => scheduleScroll(id, null, defaultScrollDelayMs);
+ const handleInput = () => scheduleScroll(id, null);
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (["ArrowDown", "ArrowUp", "PageDown", "PageUp", "Home", "End"].includes(event.key)) {
+ scheduleScroll(id, null);
+ }
+ };
+
+ dropdown.addEventListener("click", handleOpen);
+ dropdown.addEventListener("focusin", handleOpen);
+ dropdown.addEventListener("input", handleInput);
+ dropdown.addEventListener("change", handleInput);
+ dropdown.addEventListener("keydown", handleKeyDown);
+
+ cleanupCallbacks.set(dropdown, () => {
+ dropdown.removeEventListener("click", handleOpen);
+ dropdown.removeEventListener("focusin", handleOpen);
+ dropdown.removeEventListener("input", handleInput);
+ dropdown.removeEventListener("change", handleInput);
+ dropdown.removeEventListener("keydown", handleKeyDown);
+ });
+
+ scheduleScroll(id, null, defaultScrollDelayMs);
+ }
+
+ export function Dispose(id: string) {
+ const dropdown = document.getElementById(id) as HTMLElement | null;
+ if (!dropdown) {
+ return;
+ }
+
+ const cleanup = cleanupCallbacks.get(dropdown);
+ cleanup?.();
+
+ cleanupCallbacks.delete(dropdown);
+ initializedDropdowns.delete(dropdown);
+ }
+
+ export function ScrollToSelectedValue(id: string, value: string | null | undefined) {
+ const dropdown = document.getElementById(id) as HTMLElement | null;
+ if (!dropdown) {
+ return;
+ }
+
+ const listbox = dropdown.querySelector("fluent-listbox") as HTMLElement | null;
+ if (!listbox) {
+ return;
+ }
+
+ const options = Array.from(listbox.querySelectorAll("fluent-option")) as HTMLElement[];
+ const selectedOption = findSelectedOption(options, value) ?? findSelectedOption(options, null);
+
+ if (selectedOption) {
+ selectedOption.scrollIntoView({ block: "start", inline: "nearest" });
+ }
+ }
+
+ function findSelectedOption(options: HTMLElement[], value: string | null | undefined) {
+ if (value) {
+ const matchByValue = options.find(option => option.getAttribute("value") === value);
+ if (matchByValue) {
+ return matchByValue;
+ }
+ }
+
+ return options.find(option => option.hasAttribute("selected") || option.getAttribute("aria-selected") === "true") ?? null;
+ }
+ }
+ }
+ }
+}
diff --git a/tests/Core/Components/DateTimes/FluentTimePickerTests.razor b/tests/Core/Components/DateTimes/FluentTimePickerTests.razor
index 5499616076..835fa15d09 100644
--- a/tests/Core/Components/DateTimes/FluentTimePickerTests.razor
+++ b/tests/Core/Components/DateTimes/FluentTimePickerTests.razor
@@ -17,6 +17,11 @@
Services.AddFluentUIComponents();
}
+ private static bool IsTimePickerJsModule(object? argument)
+ {
+ return argument?.ToString()?.EndsWith("DateTime/FluentTimePicker.razor.js") == true;
+ }
+
[Fact]
public void FluentTimePicker_Default()
{
@@ -240,6 +245,28 @@
Assert.Empty(timeInputs);
}
+ [Fact]
+ public void FluentTimePicker_FluentUIStyle_InitializesDropdownScrollHook()
+ {
+ // Arrange
+ var module = JSInterop.SetupModule(matcher => matcher.Arguments.Any(IsTimePickerJsModule));
+ module.SetupVoid("Microsoft.FluentUI.Blazor.TimePicker.Initialize", _ => true);
+ module.SetupVoid("Microsoft.FluentUI.Blazor.TimePicker.ScrollToSelectedValue", _ => true);
+ module.SetupVoid("Microsoft.FluentUI.Blazor.TimePicker.Dispose", _ => true);
+
+ // Act
+ var cut = Render(@);
+ cut.Dispose();
+
+ // Assert
+ Assert.Contains(module.Invocations, invocation => invocation.Identifier == "Microsoft.FluentUI.Blazor.TimePicker.Initialize");
+ Assert.Contains(module.Invocations, invocation => invocation.Identifier == "Microsoft.FluentUI.Blazor.TimePicker.ScrollToSelectedValue");
+ Assert.Contains(module.Invocations, invocation => invocation.Identifier == "Microsoft.FluentUI.Blazor.TimePicker.Dispose");
+ }
+
[Fact]
public void FluentTimePicker_NativeStyle_ShowsTextInputWithTimeType()
{