From b36267a0860e1d16b2f8adb2fb69244aff4bf873 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Fri, 9 Jan 2026 14:41:08 -0800 Subject: [PATCH 1/3] Improve screen reader announcements for search box --- .../TerminalControl/Resources/en-US/Resources.resw | 14 +++++--------- src/cascadia/TerminalControl/SearchBoxControl.cpp | 7 ++++++- src/cascadia/TerminalControl/SearchBoxControl.h | 1 + src/cascadia/TerminalControl/TermControl.cpp | 13 +++++++++++-- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw index 85446823b5..076ddb2868 100644 --- a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw @@ -229,14 +229,6 @@ Please either install the missing font or choose another one. Read-only mode is enabled. - - Results found - Announced to a screen reader when the user searches for some text and there are matches for that text in the terminal. - - - No results found - Announced to a screen reader when the user searches for some text and there are no matches for that text in the terminal. - Paste The label of a button for pasting the contents of the clipboard. @@ -342,4 +334,8 @@ Please either install the missing font or choose another one. Suggested input: {0} {Locked="{0}"} {0} will be replaced with a string of input that is suggested for the user to input - + + {} of {} + {Locked="{}"} Read out by the screen reader to announce number of results from a search query. First "{}" is replaced with index of search result. Second "{}" is replaced by total number of results. + + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/SearchBoxControl.cpp b/src/cascadia/TerminalControl/SearchBoxControl.cpp index b788180df0..5f4e33ae23 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.cpp +++ b/src/cascadia/TerminalControl/SearchBoxControl.cpp @@ -535,6 +535,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation return maxLength; } + winrt::hstring SearchBoxControl::GetStatusText() + { + return StatusBox().Text(); + } + // Method Description: // - Formats and sets the status message in the status box. // Increases the size of the box if required. @@ -561,6 +566,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - Removes the status message in the status box. void SearchBoxControl::ClearStatus() { - StatusBox().Text(L""); + StatusBox().Text({}); } } diff --git a/src/cascadia/TerminalControl/SearchBoxControl.h b/src/cascadia/TerminalControl/SearchBoxControl.h index ab6af68c4e..5d57923ec3 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.h +++ b/src/cascadia/TerminalControl/SearchBoxControl.h @@ -43,6 +43,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void SetFocusOnTextbox(); void PopulateTextbox(const winrt::hstring& text); bool ContainsFocus(); + winrt::hstring GetStatusText(); void SetStatus(int32_t totalMatches, int32_t currentMatch, bool searchRegexInvalid); void ClearStatus(); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 12eab22639..e0f51eb286 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -3754,13 +3754,22 @@ namespace winrt::Microsoft::Terminal::Control::implementation }; _updateScrollBar->Run(update); } + } - if (auto automationPeer{ FrameworkElementAutomationPeer::FromElement(*this) }) + if (auto automationPeer{ FrameworkElementAutomationPeer::FromElement(*this) }) + { + auto status = _searchBox->GetStatusText(); + if (const auto i = status.size() / 2; !status.empty() && status[i] == '/') + { + status = RS_fmt(L"TermControl_NumResultsAccessible", results.CurrentMatch + 1, results.TotalMatches); + } + + if (!status.empty()) { automationPeer.RaiseNotificationEvent( AutomationNotificationKind::ActionCompleted, AutomationNotificationProcessing::ImportantMostRecent, - results.TotalMatches > 0 ? RS_(L"SearchBox_MatchesAvailable") : RS_(L"SearchBox_NoMatches"), // what to announce if results were found + status, L"SearchBoxResultAnnouncement" /* unique name for this group of notifications */); } } From f7fe9e78ddeaa8092f54dc76635265363e311ac0 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Fri, 9 Jan 2026 15:17:02 -0800 Subject: [PATCH 2/3] move logic to SearchBoxControl for better handling --- .../Resources/en-US/Resources.resw | 12 ++++++-- .../TerminalControl/SearchBoxControl.cpp | 28 +++++++++++++------ .../TerminalControl/SearchBoxControl.h | 4 +-- src/cascadia/TerminalControl/TermControl.cpp | 7 +---- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw index 076ddb2868..c4f80ce7e3 100644 --- a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw @@ -335,7 +335,15 @@ Please either install the missing font or choose another one. {Locked="{0}"} {0} will be replaced with a string of input that is suggested for the user to input - {} of {} - {Locked="{}"} Read out by the screen reader to announce number of results from a search query. First "{}" is replaced with index of search result. Second "{}" is replaced by total number of results. + {0} of {1} + {Locked="{0}"}{Locked="{1}"} Read out by the screen reader to announce number of results from a search query. First "{}" is replaced with index of search result. Second "{}" is replaced by total number of results. + + + unknown + Will be read out by a screen reader when a value for the index of a search result is mismatched and unclear. Displayed as a part of TermControl_NumResultsAccessible. + + + over 999 + Will be read out by a screen reader when a search returns over 999 search results. \ No newline at end of file diff --git a/src/cascadia/TerminalControl/SearchBoxControl.cpp b/src/cascadia/TerminalControl/SearchBoxControl.cpp index 5f4e33ae23..e9a7620316 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.cpp +++ b/src/cascadia/TerminalControl/SearchBoxControl.cpp @@ -463,9 +463,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Arguments: // - totalMatches - total number of matches (search results) // - currentMatch - the index of the current match (0-based) + // - isAccessible - if true, format the string for screen readers. Defaults to false. // Return Value: // - status message - winrt::hstring SearchBoxControl::_FormatStatus(int32_t totalMatches, int32_t currentMatch) + winrt::hstring SearchBoxControl::_FormatStatus(int32_t totalMatches, int32_t currentMatch, bool isAccessible) { if (totalMatches < 0) { @@ -482,7 +483,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (currentMatch < 0 || currentMatch > (MaximumTotalResultsToShowInStatus - 1)) { - currentString = CurrentIndexTooHighStatus; + currentString = isAccessible ? RS_(L"TermControl_UnknownSearchResultIndex") : CurrentIndexTooHighStatus; } else { @@ -491,13 +492,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (totalMatches > MaximumTotalResultsToShowInStatus) { - totalString = TotalResultsTooHighStatus; + totalString = isAccessible ? RS_(L"TermControl_TooManySearchResults") : TotalResultsTooHighStatus; } else { totalString = fmt::to_wstring(totalMatches); } + if (isAccessible) + { + return winrt::hstring{ RS_fmt(L"TermControl_NumResultsAccessible", currentString, totalString) }; + } return winrt::hstring{ RS_fmt(L"TermControl_NumResults", currentString, totalString) }; } @@ -535,11 +540,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation return maxLength; } - winrt::hstring SearchBoxControl::GetStatusText() - { - return StatusBox().Text(); - } - // Method Description: // - Formats and sets the status message in the status box. // Increases the size of the box if required. @@ -562,6 +562,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation StatusBox().Text(status); } + // Method Description: + // - Formats and returns an accessible status message representing the search state. + // - Similar to SetStatus but returns a more descriptive string for screen readers. + hstring SearchBoxControl::GetAccessibleStatus(int32_t totalMatches, int32_t currentMatch, bool searchRegexInvalid) + { + if (searchRegexInvalid) + { + return RS_(L"SearchRegexInvalid"); + } + return _FormatStatus(totalMatches, currentMatch, true); + } + // Method Description: // - Removes the status message in the status box. void SearchBoxControl::ClearStatus() diff --git a/src/cascadia/TerminalControl/SearchBoxControl.h b/src/cascadia/TerminalControl/SearchBoxControl.h index 5d57923ec3..57fd319de5 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.h +++ b/src/cascadia/TerminalControl/SearchBoxControl.h @@ -43,8 +43,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation void SetFocusOnTextbox(); void PopulateTextbox(const winrt::hstring& text); bool ContainsFocus(); - winrt::hstring GetStatusText(); void SetStatus(int32_t totalMatches, int32_t currentMatch, bool searchRegexInvalid); + winrt::hstring GetAccessibleStatus(int32_t totalMatches, int32_t currentMatch, bool searchRegexInvalid); void ClearStatus(); void GoBackwardClicked(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::RoutedEventArgs& /*e*/); @@ -78,7 +78,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _PlayCloseAnimation(); bool _AnimationEnabled(); - static winrt::hstring _FormatStatus(int32_t totalMatches, int32_t currentMatch); + static winrt::hstring _FormatStatus(int32_t totalMatches, int32_t currentMatch, bool isAccessible = false); static double _TextWidth(winrt::hstring text, double fontSize); double _GetStatusMaxWidth(); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index e0f51eb286..47a70a6b15 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -3758,12 +3758,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (auto automationPeer{ FrameworkElementAutomationPeer::FromElement(*this) }) { - auto status = _searchBox->GetStatusText(); - if (const auto i = status.size() / 2; !status.empty() && status[i] == '/') - { - status = RS_fmt(L"TermControl_NumResultsAccessible", results.CurrentMatch + 1, results.TotalMatches); - } - + const auto status = _searchBox->GetAccessibleStatus(results.TotalMatches, results.CurrentMatch, results.SearchRegexInvalid); if (!status.empty()) { automationPeer.RaiseNotificationEvent( From 967edaa0e98860d89b9b4d40374cd477b20d8616 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Fri, 9 Jan 2026 15:18:11 -0800 Subject: [PATCH 3/3] improve comment --- src/cascadia/TerminalControl/Resources/en-US/Resources.resw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw index c4f80ce7e3..b4eade9295 100644 --- a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw @@ -336,7 +336,7 @@ Please either install the missing font or choose another one. {0} of {1} - {Locked="{0}"}{Locked="{1}"} Read out by the screen reader to announce number of results from a search query. First "{}" is replaced with index of search result. Second "{}" is replaced by total number of results. + {Locked="{0}"}{Locked="{1}"} Read out by the screen reader to announce number of results from a search query. "{0}" is replaced with index of search result. "{1}" is replaced by total number of results. unknown