Commit c9de1bfb authored by Aaron Leventhal's avatar Aaron Leventhal Committed by Commit Bot

Omnibox suggestions that are friendly to non-Mac screen readers

Provide friendly-sounding suggestions that are more efficient for screen reader use.
For example, read the title of a document before the URL. An example of a friendly
suggestion would be "Gmail https://mail.google.com location from history".

In order to get the friendly suggestions to be announce or shown on a Braille display,
the textfield's accessible text value is replaced with the friendly text, rather than
containing the URL only. If the user begins to edit the text or arrow through it, the
accessible text/value is returned to the plain URL or search text.

In addition, incoming and outgoing cursor/selection offsets are corrected so that
assistive technologies can set the location of the cursor (for cursor routing),
or retrieve the actual text at the current caret position. The caret/selection
indices are offset by the amount of friendly text inserted before the actual
text being shown in the text box.

Bug: 785436
Change-Id: I7fb9071c2df1a4ab188bc8e7b7001396578b1950
Reviewed-on: https://chromium-review.googlesource.com/784591
Commit-Queue: Aaron Leventhal <aleventhal@chromium.org>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Reviewed-by: default avatarJustin Donnelly <jdonnelly@chromium.org>
Cr-Commit-Position: refs/heads/master@{#521769}
parent 60e1e0e9
...@@ -130,6 +130,7 @@ OmniboxViewViews::OmniboxViewViews(OmniboxEditController* controller, ...@@ -130,6 +130,7 @@ OmniboxViewViews::OmniboxViewViews(OmniboxEditController* controller,
select_all_on_mouse_release_(false), select_all_on_mouse_release_(false),
select_all_on_gesture_tap_(false), select_all_on_gesture_tap_(false),
latency_histogram_state_(NOT_ACTIVE), latency_histogram_state_(NOT_ACTIVE),
friendly_suggestion_text_prefix_length_(0),
scoped_observer_(this) { scoped_observer_(this) {
set_id(VIEW_ID_OMNIBOX); set_id(VIEW_ID_OMNIBOX);
SetFontList(font_list); SetFontList(font_list);
...@@ -481,6 +482,10 @@ void OmniboxViewViews::OnTemporaryTextMaybeChanged( ...@@ -481,6 +482,10 @@ void OmniboxViewViews::OnTemporaryTextMaybeChanged(
if (save_original_selection) if (save_original_selection)
saved_temporary_selection_ = GetSelectedRange(); saved_temporary_selection_ = GetSelectedRange();
friendly_suggestion_text_ = AutocompleteMatchType::ToAccessibilityLabel(
match.type, display_text, match.description,
&friendly_suggestion_text_prefix_length_);
SetWindowTextAndCaretPos(display_text, display_text.length(), false, SetWindowTextAndCaretPos(display_text, display_text.length(), false,
notify_text_changed); notify_text_changed);
} }
...@@ -516,10 +521,20 @@ void OmniboxViewViews::OnRevertTemporaryText() { ...@@ -516,10 +521,20 @@ void OmniboxViewViews::OnRevertTemporaryText() {
// warranted. // warranted.
} }
void OmniboxViewViews::ClearAccessibilityLabel() {
friendly_suggestion_text_.clear();
friendly_suggestion_text_prefix_length_ = 0;
}
void OmniboxViewViews::OnBeforePossibleChange() { void OmniboxViewViews::OnBeforePossibleChange() {
// Record our state. // Record our state.
GetState(&state_before_change_); GetState(&state_before_change_);
ime_composing_before_change_ = IsIMEComposing(); ime_composing_before_change_ = IsIMEComposing();
// User is editing or traversing the text, as opposed to moving
// through suggestions. Clear the accessibility label
// so that the screen reader reports the raw text in the field.
ClearAccessibilityLabel();
} }
bool OmniboxViewViews::OnAfterPossibleChange(bool allow_keyword_ui_change) { bool OmniboxViewViews::OnAfterPossibleChange(bool allow_keyword_ui_change) {
...@@ -722,7 +737,20 @@ bool OmniboxViewViews::SkipDefaultKeyEventProcessing( ...@@ -722,7 +737,20 @@ bool OmniboxViewViews::SkipDefaultKeyEventProcessing(
void OmniboxViewViews::GetAccessibleNodeData(ui::AXNodeData* node_data) { void OmniboxViewViews::GetAccessibleNodeData(ui::AXNodeData* node_data) {
node_data->role = ui::AX_ROLE_TEXT_FIELD; node_data->role = ui::AX_ROLE_TEXT_FIELD;
node_data->SetName(l10n_util::GetStringUTF8(IDS_ACCNAME_LOCATION)); node_data->SetName(l10n_util::GetStringUTF8(IDS_ACCNAME_LOCATION));
node_data->SetValue(GetText()); node_data->AddStringAttribute(ui::AX_ATTR_AUTO_COMPLETE, "both");
if (friendly_suggestion_text_.empty()) {
// While user edits text, use the exact text displayed in the omnibox.
node_data->SetValue(GetText());
} else {
// While user navigates omnibox suggestions, use the current editable
// text decorated with additional friendly labelling text, such as the
// title of the page and the type of autocomplete, for example:
// "Google https://google.com location from history".
// The edited text is always a substring of the friendly label, so that
// users can navigate to specific characters in the friendly version using
// Braille display routing keys or other assistive technologies.
node_data->SetValue(friendly_suggestion_text_);
}
node_data->html_attributes.push_back(std::make_pair("type", "url")); node_data->html_attributes.push_back(std::make_pair("type", "url"));
base::string16::size_type entry_start; base::string16::size_type entry_start;
...@@ -735,8 +763,12 @@ void OmniboxViewViews::GetAccessibleNodeData(ui::AXNodeData* node_data) { ...@@ -735,8 +763,12 @@ void OmniboxViewViews::GetAccessibleNodeData(ui::AXNodeData* node_data) {
} else { } else {
GetSelectionBounds(&entry_start, &entry_end); GetSelectionBounds(&entry_start, &entry_end);
} }
node_data->AddIntAttribute(ui::AX_ATTR_TEXT_SEL_START, entry_start); node_data->AddIntAttribute(
node_data->AddIntAttribute(ui::AX_ATTR_TEXT_SEL_END, entry_end); ui::AX_ATTR_TEXT_SEL_START,
entry_start + friendly_suggestion_text_prefix_length_);
node_data->AddIntAttribute(
ui::AX_ATTR_TEXT_SEL_END,
entry_end + friendly_suggestion_text_prefix_length_);
if (popup_window_mode_) { if (popup_window_mode_) {
node_data->AddIntAttribute(ui::AX_ATTR_RESTRICTION, node_data->AddIntAttribute(ui::AX_ATTR_RESTRICTION,
...@@ -763,6 +795,16 @@ bool OmniboxViewViews::HandleAccessibleAction( ...@@ -763,6 +795,16 @@ bool OmniboxViewViews::HandleAccessibleAction(
InsertOrReplaceText(action_data.value); InsertOrReplaceText(action_data.value);
TextChanged(); TextChanged();
return true; return true;
} else if (action_data.action == ui::AX_ACTION_SET_SELECTION) {
// Adjust for friendly text inserted at the start of the url.
ui::AXActionData set_selection_action_data;
set_selection_action_data.action = ui::AX_ACTION_SET_SELECTION;
set_selection_action_data.anchor_node_id = action_data.anchor_node_id;
set_selection_action_data.focus_offset =
action_data.focus_offset - friendly_suggestion_text_prefix_length_;
set_selection_action_data.anchor_offset =
action_data.anchor_offset - friendly_suggestion_text_prefix_length_;
return Textfield::HandleAccessibleAction(set_selection_action_data);
} }
return Textfield::HandleAccessibleAction(action_data); return Textfield::HandleAccessibleAction(action_data);
...@@ -849,6 +891,8 @@ void OmniboxViewViews::OnBlur() { ...@@ -849,6 +891,8 @@ void OmniboxViewViews::OnBlur() {
// The location bar needs to repaint without a focus ring. // The location bar needs to repaint without a focus ring.
location_bar_view_->SchedulePaint(); location_bar_view_->SchedulePaint();
} }
ClearAccessibilityLabel();
} }
bool OmniboxViewViews::IsCommandIdEnabled(int command_id) const { bool OmniboxViewViews::IsCommandIdEnabled(int command_id) const {
......
...@@ -112,6 +112,7 @@ class OmniboxViewViews : public OmniboxView, ...@@ -112,6 +112,7 @@ class OmniboxViewViews : public OmniboxView,
private: private:
FRIEND_TEST_ALL_PREFIXES(OmniboxViewViewsTest, CloseOmniboxPopupOnTextDrag); FRIEND_TEST_ALL_PREFIXES(OmniboxViewViewsTest, CloseOmniboxPopupOnTextDrag);
FRIEND_TEST_ALL_PREFIXES(OmniboxViewViewsTest, FriendlyAccessibleLabel);
FRIEND_TEST_ALL_PREFIXES(OmniboxViewViewsTest, MaintainCursorAfterFocusCycle); FRIEND_TEST_ALL_PREFIXES(OmniboxViewViewsTest, MaintainCursorAfterFocusCycle);
FRIEND_TEST_ALL_PREFIXES(OmniboxViewViewsTest, OnBlur); FRIEND_TEST_ALL_PREFIXES(OmniboxViewViewsTest, OnBlur);
...@@ -135,6 +136,8 @@ class OmniboxViewViews : public OmniboxView, ...@@ -135,6 +136,8 @@ class OmniboxViewViews : public OmniboxView,
// Updates |security_level_| based on the toolbar model's current value. // Updates |security_level_| based on the toolbar model's current value.
void UpdateSecurityLevel(); void UpdateSecurityLevel();
void ClearAccessibilityLabel();
// OmniboxView: // OmniboxView:
void SetWindowTextAndCaretPos(const base::string16& text, void SetWindowTextAndCaretPos(const base::string16& text,
size_t caret_pos, size_t caret_pos,
...@@ -273,6 +276,17 @@ class OmniboxViewViews : public OmniboxView, ...@@ -273,6 +276,17 @@ class OmniboxViewViews : public OmniboxView,
COMPOSITING_STARTED, // Compositing was started. COMPOSITING_STARTED, // Compositing was started.
} latency_histogram_state_; } latency_histogram_state_;
// The currently selected match, if any, with additional labelling text
// such as the document title and the type of search, for example:
// "Google https://google.com location from bookmark", or
// "cats are liquid search suggestion".
base::string16 friendly_suggestion_text_;
// The number of added labelling characters before editable text begins.
// For example, "Google https://google.com location from history",
// this is set to 7 (the length of "Google ").
int friendly_suggestion_text_prefix_length_;
ScopedObserver<ui::Compositor, ui::CompositorObserver> scoped_observer_; ScopedObserver<ui::Compositor, ui::CompositorObserver> scoped_observer_;
DISALLOW_COPY_AND_ASSIGN(OmniboxViewViews); DISALLOW_COPY_AND_ASSIGN(OmniboxViewViews);
......
...@@ -23,6 +23,8 @@ ...@@ -23,6 +23,8 @@
#include "chrome/test/base/interactive_test_utils.h" #include "chrome/test/base/interactive_test_utils.h"
#include "components/omnibox/browser/omnibox_popup_model.h" #include "components/omnibox/browser/omnibox_popup_model.h"
#include "components/omnibox/browser/test_scheme_classifier.h" #include "components/omnibox/browser/test_scheme_classifier.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/clipboard/clipboard.h" #include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h" #include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/base/ime/input_method.h" #include "ui/base/ime/input_method.h"
...@@ -458,3 +460,63 @@ IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, TextElideStatus) { ...@@ -458,3 +460,63 @@ IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, TextElideStatus) {
EXPECT_EQ(omnibox_view_views->GetRenderText()->elide_behavior(), EXPECT_EQ(omnibox_view_views->GetRenderText()->elide_behavior(),
gfx::NO_ELIDE); gfx::NO_ELIDE);
} }
IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, FriendlyAccessibleLabel) {
OmniboxView* omnibox_view = nullptr;
ASSERT_NO_FATAL_FAILURE(GetOmniboxViewForBrowser(browser(), &omnibox_view));
base::string16 match_url = base::ASCIIToUTF16("https://google.com");
AutocompleteMatch match(nullptr, 500, false,
AutocompleteMatchType::HISTORY_TITLE);
match.contents = match_url;
match.contents_class.push_back(
ACMatchClassification(0, ACMatchClassification::URL));
match.destination_url = GURL(match_url);
match.description = base::ASCIIToUTF16("Google");
match.allowed_to_be_default_match = true;
chrome::FocusLocationBar(browser());
OmniboxViewViews* omnibox_view_views =
static_cast<OmniboxViewViews*>(omnibox_view);
omnibox_view_views->SetText(match_url);
omnibox_view_views->OnTemporaryTextMaybeChanged(match_url, match, false,
false);
omnibox_view->SelectAll(true);
const int kFriendlyPrefixLength = match.description.size() + 1;
ui::AXNodeData node_data;
omnibox_view_views->GetAccessibleNodeData(&node_data);
EXPECT_EQ(
base::ASCIIToUTF16("Google https://google.com location from history"),
node_data.GetString16Attribute(ui::AX_ATTR_VALUE));
// Selection offsets are moved over by length the inserted descriptive text
// prefix ("Google") + 1 for the space.
EXPECT_EQ(kFriendlyPrefixLength,
node_data.GetIntAttribute(ui::AX_ATTR_TEXT_SEL_END));
EXPECT_EQ(kFriendlyPrefixLength + static_cast<int>(match_url.size()),
node_data.GetIntAttribute(ui::AX_ATTR_TEXT_SEL_START));
EXPECT_EQ("both", node_data.GetStringAttribute(ui::AX_ATTR_AUTO_COMPLETE));
EXPECT_EQ(ui::AX_ROLE_TEXT_FIELD, node_data.role);
// Select second character -- even though the friendly "Google " prefix is
// part of the exposed accessible text, setting the selection within select
// the intended part of the editable text.
ui::AXActionData set_selection_action_data;
set_selection_action_data.action = ui::AX_ACTION_SET_SELECTION;
set_selection_action_data.anchor_node_id = node_data.id;
set_selection_action_data.focus_offset = kFriendlyPrefixLength + 1;
set_selection_action_data.anchor_offset = kFriendlyPrefixLength + 3;
omnibox_view_views->HandleAccessibleAction(set_selection_action_data);
// Type "x" to replace the selected "tt" with that character.
ASSERT_TRUE(ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_X, false,
false, false, false));
// Second character should be an "x" now.
EXPECT_EQ(base::ASCIIToUTF16("hxps://google.com"),
omnibox_view_views->GetText());
// When editing starts, the accessible value becomes the same as the raw
// edited text.
omnibox_view_views->GetAccessibleNodeData(&node_data);
EXPECT_EQ(base::ASCIIToUTF16("hxps://google.com"),
node_data.GetString16Attribute(ui::AX_ATTR_VALUE));
}
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
#include "base/logging.h" #include "base/logging.h"
#include "base/macros.h" #include "base/macros.h"
#include "base/strings/utf_string_conversions.h"
#include "components/strings/grit/components_strings.h" #include "components/strings/grit/components_strings.h"
#include "ui/base/l10n/l10n_util.h" #include "ui/base/l10n/l10n_util.h"
...@@ -44,10 +45,21 @@ std::string AutocompleteMatchType::ToString(AutocompleteMatchType::Type type) { ...@@ -44,10 +45,21 @@ std::string AutocompleteMatchType::ToString(AutocompleteMatchType::Type type) {
return strings[type]; return strings[type];
} }
static const wchar_t kAccessibilityLabelPrefixEndSentinal[] =
L"\uFFFC"; // Embedded object character.
static int AccessibilityLabelPrefixLength(base::string16 accessibility_label) {
const base::string16 sentinal =
base::WideToUTF16(kAccessibilityLabelPrefixEndSentinal);
auto length = accessibility_label.find(sentinal);
return length == base::string16::npos ? 0 : static_cast<int>(length);
}
base::string16 AutocompleteMatchType::ToAccessibilityLabel( base::string16 AutocompleteMatchType::ToAccessibilityLabel(
AutocompleteMatchType::Type type, AutocompleteMatchType::Type type,
const base::string16& match_text, const base::string16& match_text,
const base::string16& additional_descriptive_text) { const base::string16& additional_descriptive_text,
int* label_prefix_length) {
// Types with a message ID of zero get |text| returned as-is. // Types with a message ID of zero get |text| returned as-is.
static constexpr int message_ids[] = { static constexpr int message_ids[] = {
0, // URL_WHAT_YOU_TYPED 0, // URL_WHAT_YOU_TYPED
...@@ -86,22 +98,35 @@ base::string16 AutocompleteMatchType::ToAccessibilityLabel( ...@@ -86,22 +98,35 @@ base::string16 AutocompleteMatchType::ToAccessibilityLabel(
}; };
static_assert(arraysize(message_ids) == AutocompleteMatchType::NUM_TYPES, static_assert(arraysize(message_ids) == AutocompleteMatchType::NUM_TYPES,
"message_ids must have NUM_TYPES elements"); "message_ids must have NUM_TYPES elements");
if (label_prefix_length)
*label_prefix_length = 0;
int message = message_ids[type]; int message = message_ids[type];
if (!message) if (!message)
return match_text; return match_text;
const base::string16 sentinal =
base::WideToUTF16(kAccessibilityLabelPrefixEndSentinal);
switch (message) { switch (message) {
case IDS_ACC_AUTOCOMPLETE_SEARCH_HISTORY: case IDS_ACC_AUTOCOMPLETE_SEARCH_HISTORY:
case IDS_ACC_AUTOCOMPLETE_SEARCH: case IDS_ACC_AUTOCOMPLETE_SEARCH:
case IDS_ACC_AUTOCOMPLETE_SUGGESTED_SEARCH: case IDS_ACC_AUTOCOMPLETE_SUGGESTED_SEARCH:
// Additional descriptive text NOT relevant. // Additional descriptive text NOT relevant.
return l10n_util::GetStringFUTF16(message_ids[type], match_text); if (label_prefix_length)
*label_prefix_length = AccessibilityLabelPrefixLength(
l10n_util::GetStringFUTF16(message, sentinal));
return l10n_util::GetStringFUTF16(message, match_text);
case IDS_ACC_AUTOCOMPLETE_HISTORY: case IDS_ACC_AUTOCOMPLETE_HISTORY:
case IDS_ACC_AUTOCOMPLETE_BOOKMARK: case IDS_ACC_AUTOCOMPLETE_BOOKMARK:
case IDS_ACC_AUTOCOMPLETE_CLIPBOARD: case IDS_ACC_AUTOCOMPLETE_CLIPBOARD:
// Additional descriptive text relevant. // Additional descriptive text relevant.
return l10n_util::GetStringFUTF16(message_ids[type], match_text, if (label_prefix_length)
*label_prefix_length =
AccessibilityLabelPrefixLength(l10n_util::GetStringFUTF16(
message, sentinal, additional_descriptive_text));
return l10n_util::GetStringFUTF16(message, match_text,
additional_descriptive_text); additional_descriptive_text);
default: default:
break; break;
......
...@@ -70,10 +70,13 @@ struct AutocompleteMatchType { ...@@ -70,10 +70,13 @@ struct AutocompleteMatchType {
// whose text is |match_text| and which may have friendly descriptive text in // whose text is |match_text| and which may have friendly descriptive text in
// |additional_descriptive_text_|. The accessibility label describes the // |additional_descriptive_text_|. The accessibility label describes the
// match for use in a screenreader or other assistive technology. // match for use in a screenreader or other assistive technology.
// The |label_prefix_length| is an optional out param that provides the number
// of characters in the label that were added before the actual match_text.
static base::string16 ToAccessibilityLabel( static base::string16 ToAccessibilityLabel(
AutocompleteMatchType::Type type, AutocompleteMatchType::Type type,
const base::string16& match_text, const base::string16& match_text,
const base::string16& additional_descriptive_text); const base::string16& additional_descriptive_text,
int* label_prefix_length = nullptr);
}; };
#endif // COMPONENTS_OMNIBOX_BROWSER_AUTOCOMPLETE_MATCH_TYPE_H_ #endif // COMPONENTS_OMNIBOX_BROWSER_AUTOCOMPLETE_MATCH_TYPE_H_
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment