Improve screen reader announcements for search box (#19726)

## Summary of the Pull Request
Improves the notification read out by a screen reader when a search is
done in the terminal. This is done across several scenarios:
- previously, the results were only read occasionally. Moving the block
out of `if (results.SearchInvalidated)` fixes that.
- previously, we read "Results found" or "No results found". Now, we
read an accessible version of the written status message.

In order to maintain consistency with the written status message,
`_FormatText()` now takes an `isAccessible` parameter to output a more
accessible version of the text. Specifically...
- `CurrentIndexTooHighStatus` (aka `?`) would not be read, so we replace
it with `TermControl_UnknownSearchResultIndex` (aka `unknown`)
- `TermControl_TooManySearchResults` (aka `999+`) would drop the `+`
when read, so we replace it with `TermControl_TooManySearchResults` (aka
`over 999`)
- `TermControl_NumResults` (aka `{0}/{1}``) would be read as a fraction,
so we replace it with `TermControl_NumResultsAccessible` (aka `{0} of
{1}`).

## Validation Steps Performed
 Announcements are read out when interacting with search box (i.e.
next, prev, regex toggle, etc.)
 "4 of 5" read out by screen reader (or something similar) when search
is performed

Closes #19691
This commit is contained in:
Carlos Zamora
2026-01-13 16:19:16 -08:00
committed by GitHub
parent dfd11cf0cf
commit b668fd5188
4 changed files with 42 additions and 16 deletions

View File

@@ -217,14 +217,6 @@
<data name="TermControlReadOnly" xml:space="preserve">
<value>Read-only mode is enabled.</value>
</data>
<data name="SearchBox_MatchesAvailable" xml:space="preserve">
<value>Results found</value>
<comment>Announced to a screen reader when the user searches for some text and there are matches for that text in the terminal.</comment>
</data>
<data name="SearchBox_NoMatches" xml:space="preserve">
<value>No results found</value>
<comment>Announced to a screen reader when the user searches for some text and there are no matches for that text in the terminal.</comment>
</data>
<data name="PasteCommandButton.Label" xml:space="preserve">
<value>Paste</value>
<comment>The label of a button for pasting the contents of the clipboard.</comment>
@@ -330,4 +322,16 @@
<value>Suggested input: {0}</value>
<comment>{Locked="{0}"} {0} will be replaced with a string of input that is suggested for the user to input</comment>
</data>
</root>
<data name="TermControl_NumResultsAccessible" xml:space="preserve">
<value>{0} of {1}</value>
<comment>{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.</comment>
</data>
<data name="TermControl_UnknownSearchResultIndex" xml:space="preserve">
<value>unknown</value>
<comment>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.</comment>
</data>
<data name="TermControl_TooManySearchResults" xml:space="preserve">
<value>over 999</value>
<comment>Will be read out by a screen reader when a search returns over 999 search results.</comment>
</data>
</root>

View File

@@ -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) };
}
@@ -557,10 +562,22 @@ 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()
{
StatusBox().Text(L"");
StatusBox().Text({});
}
}

View File

@@ -44,6 +44,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void PopulateTextbox(const winrt::hstring& text);
bool ContainsFocus();
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*/);
@@ -77,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();

View File

@@ -3754,13 +3754,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation
};
_updateScrollBar->Run(update);
}
}
if (auto automationPeer{ FrameworkElementAutomationPeer::FromElement(*this) })
if (auto automationPeer{ FrameworkElementAutomationPeer::FromElement(*this) })
{
const auto status = _searchBox->GetAccessibleStatus(results.TotalMatches, results.CurrentMatch, results.SearchRegexInvalid);
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 */);
}
}