From 4513a429bf9e48b1b354f8b5bd2d9346016e2271 Mon Sep 17 00:00:00 2001 From: Dinah Xiaoman G <116714259+DinahK-2SO@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:39:36 +0800 Subject: [PATCH 1/5] Fix focus not restored after Storage Pickers dialog closes (#6544) **Summary:** Issue reported in #6505. Storage Pickers leave the WinUI window without keyboard focus after their dialog closes, so the keyboard stops working until the user clicks the window again. **Repro:** 1. in code map a button's Click handler with a picker behavior. 2. Click the button (dialog opens), pick a file and confirm (dialog closes), 3. then press Enter. 4. Before the fix Enter does nothing; After the fix Enter re-invokes the button and the dialog reopens. **Fix:** An RAII helper `DialogFocusRestorer` captures the focused window before the dialog opens and restores focus to it once the dialog closes. Applied to all five pick methods. **Commits:** * Restore keyboard focus after Storage Pickers dialog closes (#6505) The COM file dialog runs on a background thread, so SetFocus on close is ignored. Capture the focused window on the UI thread and marshal SetFocus back via the DispatcherQueue. Adds DialogFocusRestorer, applied to all five pick entry points. * add a manual test case --- dev/Interop/StoragePickers/FileOpenPicker.cpp | 6 +++++ dev/Interop/StoragePickers/FileSavePicker.cpp | 3 +++ dev/Interop/StoragePickers/FolderPicker.cpp | 5 ++++- .../StoragePickers/ManualTests/README.md | 16 ++++++++++++++ dev/Interop/StoragePickers/PickerCommon.cpp | 22 +++++++++++++++++++ dev/Interop/StoragePickers/PickerCommon.h | 22 +++++++++++++++++++ 6 files changed, 73 insertions(+), 1 deletion(-) diff --git a/dev/Interop/StoragePickers/FileOpenPicker.cpp b/dev/Interop/StoragePickers/FileOpenPicker.cpp index 775e6411ab..5b4561fd1b 100644 --- a/dev/Interop/StoragePickers/FileOpenPicker.cpp +++ b/dev/Interop/StoragePickers/FileOpenPicker.cpp @@ -74,6 +74,9 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + PickerCommon::DialogFocusRestorer focusRestorer{}; + auto cancellationToken = co_await winrt::get_cancellation_token(); cancellationToken.enable_propagation(true); co_await winrt::resume_background(); @@ -127,6 +130,9 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + PickerCommon::DialogFocusRestorer focusRestorer{}; + auto cancellationToken = co_await winrt::get_cancellation_token(); cancellationToken.enable_propagation(true); co_await winrt::resume_background(); diff --git a/dev/Interop/StoragePickers/FileSavePicker.cpp b/dev/Interop/StoragePickers/FileSavePicker.cpp index dad4ef48f6..62858078fc 100644 --- a/dev/Interop/StoragePickers/FileSavePicker.cpp +++ b/dev/Interop/StoragePickers/FileSavePicker.cpp @@ -104,6 +104,9 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + PickerCommon::DialogFocusRestorer focusRestorer{}; + auto defaultFileExtension = m_defaultFileExtension; auto suggestedFolder = m_suggestedFolder; auto suggestedFileName = m_suggestedFileName; diff --git a/dev/Interop/StoragePickers/FolderPicker.cpp b/dev/Interop/StoragePickers/FolderPicker.cpp index c95a581607..0f20f77ae9 100644 --- a/dev/Interop/StoragePickers/FolderPicker.cpp +++ b/dev/Interop/StoragePickers/FolderPicker.cpp @@ -67,7 +67,10 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation parameters.AllFilesText = PickerLocalization::GetStoragePickersLocalizationText(PickerCommon::AllFilesLocalizationKey); CaptureParameters(parameters); - + + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + PickerCommon::DialogFocusRestorer focusRestorer{}; + auto cancellationToken = co_await winrt::get_cancellation_token(); cancellationToken.enable_propagation(true); co_await winrt::resume_background(); diff --git a/dev/Interop/StoragePickers/ManualTests/README.md b/dev/Interop/StoragePickers/ManualTests/README.md index b89e6aaade..69f6bfef93 100644 --- a/dev/Interop/StoragePickers/ManualTests/README.md +++ b/dev/Interop/StoragePickers/ManualTests/README.md @@ -56,5 +56,21 @@ auto& file = co_await picker.PickSaveFileAsync(); **Example of unexpected behavior - without the fix:** ![alter-extension-with-choices-beforefix](./media/alter-extension-with-choices-beforefix.gif) +### Keyboard focus is restored after the dialog closes +Run from a WinUI 3 app with a button whose Click handler opens a picker. This applies to every pick +method: `FileOpenPicker.PickSingleFileAsync` / `PickMultipleFilesAsync`, `FileSavePicker.PickSaveFileAsync`, +and `FolderPicker.PickSingleFolderAsync` / `PickMultipleFoldersAsync`. +Test code (C++), in the button's Click handler: +```C++ +winrt::Microsoft::Windows::Storage::Pickers::FileOpenPicker picker{ AppWindow().Id() }; +co_await picker.PickSingleFileAsync(); +``` + +1. Click the button. The file dialog opens. +2. Pick a file and confirm. The dialog closes. +3. Without touching the mouse, press Enter. + + **Expected behavior - with the fix:** Enter re-invokes the button and the dialog reopens. + **Example of unexpected behavior - without the fix:** Enter does nothing. diff --git a/dev/Interop/StoragePickers/PickerCommon.cpp b/dev/Interop/StoragePickers/PickerCommon.cpp index c0811a0edf..08ca2249f9 100644 --- a/dev/Interop/StoragePickers/PickerCommon.cpp +++ b/dev/Interop/StoragePickers/PickerCommon.cpp @@ -105,6 +105,28 @@ namespace { namespace PickerCommon { using namespace winrt; + + DialogFocusRestorer::DialogFocusRestorer() + { + // Capture on the UI thread: GetFocus returns the focused window of the calling + // thread's message queue, which is the WinUI content island hosting the focused element. + m_focusedWindow = ::GetFocus(); + m_dispatcherQueue = winrt::Microsoft::UI::Dispatching::DispatcherQueue::GetForCurrentThread(); + } + + DialogFocusRestorer::~DialogFocusRestorer() + { + if (m_focusedWindow && m_dispatcherQueue) + { + HWND focusedWindow = m_focusedWindow; + // Marshal back to the UI thread so SetFocus runs on the thread that owns the window. + m_dispatcherQueue.TryEnqueue([focusedWindow]() + { + ::SetFocus(focusedWindow); + }); + } + } + bool IsHStringNullOrEmpty(winrt::hstring value) { return value.empty(); diff --git a/dev/Interop/StoragePickers/PickerCommon.h b/dev/Interop/StoragePickers/PickerCommon.h index 124a9eb261..297c2e41e0 100644 --- a/dev/Interop/StoragePickers/PickerCommon.h +++ b/dev/Interop/StoragePickers/PickerCommon.h @@ -8,6 +8,7 @@ #include #include #include +#include namespace PickerCommon { winrt::hstring GetPathFromShellItem(winrt::com_ptr shellItem); @@ -28,6 +29,27 @@ namespace PickerCommon { void ValidateSuggestedFileName(winrt::hstring const& suggestedFileName); void ValidateSuggestedFolder(winrt::hstring const& path); + // Restores keyboard focus to the WinUI window after a COM file dialog closes. + // + // These dialogs run on a background thread (winrt::resume_background), so the SetFocus they + // perform on close comes from a thread that does not own the window and is ignored, leaving the + // window unable to receive key presses until the user clicks it again (issue #6505). + // + // Construct this RAII helper on the UI thread before switching to the background thread: it + // captures the focused window at construction and, on destruction, marshals SetFocus back to + // the UI thread via its DispatcherQueue, restoring focus once the dialog has closed. + struct DialogFocusRestorer + { + DialogFocusRestorer(); + ~DialogFocusRestorer(); + DialogFocusRestorer(DialogFocusRestorer const&) = delete; + DialogFocusRestorer& operator=(DialogFocusRestorer const&) = delete; + + private: + HWND m_focusedWindow{ nullptr }; + winrt::Microsoft::UI::Dispatching::DispatcherQueue m_dispatcherQueue{ nullptr }; + }; + struct PickerParameters { HWND HWnd{}; winrt::hstring CommitButtonText; From 2dbbeb57d5d0ba1177b6969181df8c644c7ddde6 Mon Sep 17 00:00:00 2001 From: Dinah Xiaomgao G <116714259+DinahK-2SO@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:23:15 +0800 Subject: [PATCH 2/5] add containment --- dev/Interop/StoragePickers/FileOpenPicker.cpp | 20 ++++++++++++++----- dev/Interop/StoragePickers/FileSavePicker.cpp | 10 ++++++++-- dev/Interop/StoragePickers/FolderPicker.cpp | 13 +++++++++--- .../RuntimeCompatibilityOptions.idl | 1 + 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/dev/Interop/StoragePickers/FileOpenPicker.cpp b/dev/Interop/StoragePickers/FileOpenPicker.cpp index 5b4561fd1b..cf8cfe5ea9 100644 --- a/dev/Interop/StoragePickers/FileOpenPicker.cpp +++ b/dev/Interop/StoragePickers/FileOpenPicker.cpp @@ -13,6 +13,10 @@ #include "PickerCommon.h" #include "PickFileResult.h" #include "PickerLocalization.h" +#include + +// Bug 63006043: [1.8 servicing] Fix focus not restored after Storage Pickers dialog closes +#define WINAPPSDK_CHANGEID_63006043 63006043, WinAppSDK_1_8_10 namespace winrt::Microsoft::Windows::Storage::Pickers::implementation { @@ -74,9 +78,12 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); - // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). - PickerCommon::DialogFocusRestorer focusRestorer{}; - + if (WinAppSdk::Containment::IsChangeEnabled()) + { + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + PickerCommon::DialogFocusRestorer focusRestorer{}; + } + auto cancellationToken = co_await winrt::get_cancellation_token(); cancellationToken.enable_propagation(true); co_await winrt::resume_background(); @@ -130,8 +137,11 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); - // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). - PickerCommon::DialogFocusRestorer focusRestorer{}; + if (WinAppSdk::Containment::IsChangeEnabled()) + { + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + PickerCommon::DialogFocusRestorer focusRestorer{}; + } auto cancellationToken = co_await winrt::get_cancellation_token(); cancellationToken.enable_propagation(true); diff --git a/dev/Interop/StoragePickers/FileSavePicker.cpp b/dev/Interop/StoragePickers/FileSavePicker.cpp index 62858078fc..c771bc7220 100644 --- a/dev/Interop/StoragePickers/FileSavePicker.cpp +++ b/dev/Interop/StoragePickers/FileSavePicker.cpp @@ -23,6 +23,9 @@ // Bug 60257559: [1.8 servicing] Bugfix: FileSavePicker.PickSaveFileAsync() should not truncate file when the picked file exists. #define WINAPPSDK_CHANGEID_60257559 60257559, WinAppSDK_1_8_4 +// Bug 63006043: [1.8 servicing] Fix focus not restored after Storage Pickers dialog closes +#define WINAPPSDK_CHANGEID_63006043 63006043, WinAppSDK_1_8_10 + namespace winrt::Microsoft::Windows::Storage::Pickers::implementation { @@ -104,8 +107,11 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); - // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). - PickerCommon::DialogFocusRestorer focusRestorer{}; + if (WinAppSdk::Containment::IsChangeEnabled()) + { + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + PickerCommon::DialogFocusRestorer focusRestorer{}; + } auto defaultFileExtension = m_defaultFileExtension; auto suggestedFolder = m_suggestedFolder; diff --git a/dev/Interop/StoragePickers/FolderPicker.cpp b/dev/Interop/StoragePickers/FolderPicker.cpp index 0f20f77ae9..d7703f31bb 100644 --- a/dev/Interop/StoragePickers/FolderPicker.cpp +++ b/dev/Interop/StoragePickers/FolderPicker.cpp @@ -11,7 +11,11 @@ #include "TerminalVelocityFeatures-StoragePickers.h" #include "PickerCommon.h" #include "PickFolderResult.h" -#include "PickerLocalization.h" +#include "PickerLocalization.h" +#include + +// Bug 63006043: [1.8 servicing] Fix focus not restored after Storage Pickers dialog closes +#define WINAPPSDK_CHANGEID_63006043 63006043, WinAppSDK_1_8_10 namespace winrt::Microsoft::Windows::Storage::Pickers::implementation { @@ -68,8 +72,11 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); - // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). - PickerCommon::DialogFocusRestorer focusRestorer{}; + if (WinAppSdk::Containment::IsChangeEnabled()) + { + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + PickerCommon::DialogFocusRestorer focusRestorer{}; + } auto cancellationToken = co_await winrt::get_cancellation_token(); cancellationToken.enable_propagation(true); diff --git a/dev/RuntimeCompatibilityOptions/RuntimeCompatibilityOptions.idl b/dev/RuntimeCompatibilityOptions/RuntimeCompatibilityOptions.idl index debf9a1634..9150233076 100644 --- a/dev/RuntimeCompatibilityOptions/RuntimeCompatibilityOptions.idl +++ b/dev/RuntimeCompatibilityOptions/RuntimeCompatibilityOptions.idl @@ -60,6 +60,7 @@ namespace Microsoft.Windows.ApplicationModel.WindowsAppRuntime // 1.8.10 ThemeSettings_OffThreadDestructorFix = 62451730, + StoragePickers_RestoreFocusAfterDialogCloses = 63006043, }; /// Represents a version of the Windows App Runtime. From 49be3ccebe04cee241076e49a28daa409865c3c1 Mon Sep 17 00:00:00 2001 From: Dinah Xiaomgao G <116714259+DinahK-2SO@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:10:37 +0800 Subject: [PATCH 3/5] fix scope --- dev/Interop/StoragePickers/FileOpenPicker.cpp | 6 ++++-- dev/Interop/StoragePickers/FileSavePicker.cpp | 3 ++- dev/Interop/StoragePickers/FolderPicker.cpp | 3 ++- dev/Interop/StoragePickers/PickerCommon.h | 1 + 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/dev/Interop/StoragePickers/FileOpenPicker.cpp b/dev/Interop/StoragePickers/FileOpenPicker.cpp index cf8cfe5ea9..dad13aa63f 100644 --- a/dev/Interop/StoragePickers/FileOpenPicker.cpp +++ b/dev/Interop/StoragePickers/FileOpenPicker.cpp @@ -78,10 +78,11 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); + std::optional focusRestorer; if (WinAppSdk::Containment::IsChangeEnabled()) { // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). - PickerCommon::DialogFocusRestorer focusRestorer{}; + focusRestorer.emplace(); } auto cancellationToken = co_await winrt::get_cancellation_token(); @@ -137,10 +138,11 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); + std::optional focusRestorer; if (WinAppSdk::Containment::IsChangeEnabled()) { // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). - PickerCommon::DialogFocusRestorer focusRestorer{}; + focusRestorer.emplace(); } auto cancellationToken = co_await winrt::get_cancellation_token(); diff --git a/dev/Interop/StoragePickers/FileSavePicker.cpp b/dev/Interop/StoragePickers/FileSavePicker.cpp index c771bc7220..6606a1d1bd 100644 --- a/dev/Interop/StoragePickers/FileSavePicker.cpp +++ b/dev/Interop/StoragePickers/FileSavePicker.cpp @@ -107,10 +107,11 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); + std::optional focusRestorer; if (WinAppSdk::Containment::IsChangeEnabled()) { // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). - PickerCommon::DialogFocusRestorer focusRestorer{}; + focusRestorer.emplace(); } auto defaultFileExtension = m_defaultFileExtension; diff --git a/dev/Interop/StoragePickers/FolderPicker.cpp b/dev/Interop/StoragePickers/FolderPicker.cpp index d7703f31bb..8b1334fe31 100644 --- a/dev/Interop/StoragePickers/FolderPicker.cpp +++ b/dev/Interop/StoragePickers/FolderPicker.cpp @@ -72,10 +72,11 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); + std::optional focusRestorer; if (WinAppSdk::Containment::IsChangeEnabled()) { // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). - PickerCommon::DialogFocusRestorer focusRestorer{}; + focusRestorer.emplace(); } auto cancellationToken = co_await winrt::get_cancellation_token(); diff --git a/dev/Interop/StoragePickers/PickerCommon.h b/dev/Interop/StoragePickers/PickerCommon.h index 297c2e41e0..3f29156584 100644 --- a/dev/Interop/StoragePickers/PickerCommon.h +++ b/dev/Interop/StoragePickers/PickerCommon.h @@ -9,6 +9,7 @@ #include #include #include +#include namespace PickerCommon { winrt::hstring GetPathFromShellItem(winrt::com_ptr shellItem); From 77b4c60bb69050cf2b7a57267dce3c0e9401b221 Mon Sep 17 00:00:00 2001 From: Dinah Xiaomgao G <116714259+DinahK-2SO@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:22:38 +0800 Subject: [PATCH 4/5] update manual test description --- dev/Interop/StoragePickers/ManualTests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/Interop/StoragePickers/ManualTests/README.md b/dev/Interop/StoragePickers/ManualTests/README.md index 69f6bfef93..26ffa30120 100644 --- a/dev/Interop/StoragePickers/ManualTests/README.md +++ b/dev/Interop/StoragePickers/ManualTests/README.md @@ -59,7 +59,7 @@ auto& file = co_await picker.PickSaveFileAsync(); ### Keyboard focus is restored after the dialog closes Run from a WinUI 3 app with a button whose Click handler opens a picker. This applies to every pick method: `FileOpenPicker.PickSingleFileAsync` / `PickMultipleFilesAsync`, `FileSavePicker.PickSaveFileAsync`, -and `FolderPicker.PickSingleFolderAsync` / `PickMultipleFoldersAsync`. +and `FolderPicker.PickSingleFolderAsync`. Test code (C++), in the button's Click handler: ```C++ From 6a86b5d7ba3fc2d399c2552fe772248277594f9c Mon Sep 17 00:00:00 2001 From: Dinah Xiaomgao G <116714259+DinahK-2SO@users.noreply.github.com> Date: Mon, 13 Jul 2026 23:16:08 +0800 Subject: [PATCH 5/5] increase the version number --- dev/Interop/StoragePickers/FileOpenPicker.cpp | 2 +- dev/Interop/StoragePickers/FileSavePicker.cpp | 2 +- dev/Interop/StoragePickers/FolderPicker.cpp | 2 +- dev/RuntimeCompatibilityOptions/RuntimeCompatibilityOptions.idl | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/dev/Interop/StoragePickers/FileOpenPicker.cpp b/dev/Interop/StoragePickers/FileOpenPicker.cpp index dad13aa63f..74cd25d930 100644 --- a/dev/Interop/StoragePickers/FileOpenPicker.cpp +++ b/dev/Interop/StoragePickers/FileOpenPicker.cpp @@ -16,7 +16,7 @@ #include // Bug 63006043: [1.8 servicing] Fix focus not restored after Storage Pickers dialog closes -#define WINAPPSDK_CHANGEID_63006043 63006043, WinAppSDK_1_8_10 +#define WINAPPSDK_CHANGEID_63006043 63006043, WinAppSDK_1_8_11 namespace winrt::Microsoft::Windows::Storage::Pickers::implementation { diff --git a/dev/Interop/StoragePickers/FileSavePicker.cpp b/dev/Interop/StoragePickers/FileSavePicker.cpp index 6606a1d1bd..d89260990c 100644 --- a/dev/Interop/StoragePickers/FileSavePicker.cpp +++ b/dev/Interop/StoragePickers/FileSavePicker.cpp @@ -24,7 +24,7 @@ #define WINAPPSDK_CHANGEID_60257559 60257559, WinAppSDK_1_8_4 // Bug 63006043: [1.8 servicing] Fix focus not restored after Storage Pickers dialog closes -#define WINAPPSDK_CHANGEID_63006043 63006043, WinAppSDK_1_8_10 +#define WINAPPSDK_CHANGEID_63006043 63006043, WinAppSDK_1_8_11 namespace winrt::Microsoft::Windows::Storage::Pickers::implementation { diff --git a/dev/Interop/StoragePickers/FolderPicker.cpp b/dev/Interop/StoragePickers/FolderPicker.cpp index 8b1334fe31..fa1dd5f0e4 100644 --- a/dev/Interop/StoragePickers/FolderPicker.cpp +++ b/dev/Interop/StoragePickers/FolderPicker.cpp @@ -15,7 +15,7 @@ #include // Bug 63006043: [1.8 servicing] Fix focus not restored after Storage Pickers dialog closes -#define WINAPPSDK_CHANGEID_63006043 63006043, WinAppSDK_1_8_10 +#define WINAPPSDK_CHANGEID_63006043 63006043, WinAppSDK_1_8_11 namespace winrt::Microsoft::Windows::Storage::Pickers::implementation { diff --git a/dev/RuntimeCompatibilityOptions/RuntimeCompatibilityOptions.idl b/dev/RuntimeCompatibilityOptions/RuntimeCompatibilityOptions.idl index 9150233076..186fc1434d 100644 --- a/dev/RuntimeCompatibilityOptions/RuntimeCompatibilityOptions.idl +++ b/dev/RuntimeCompatibilityOptions/RuntimeCompatibilityOptions.idl @@ -60,6 +60,8 @@ namespace Microsoft.Windows.ApplicationModel.WindowsAppRuntime // 1.8.10 ThemeSettings_OffThreadDestructorFix = 62451730, + + // 1.8.11 StoragePickers_RestoreFocusAfterDialogCloses = 63006043, };