Commit e28c4919 authored by Mei Liang's avatar Mei Liang Committed by Commit Bot

[a11y] Add a11y support to GTS

This CL adds the following a11y support to the GTS:
  * Requests focus to the currently selected tab after GTS is finished
    showing.
  * Updates the close button content description for individual tabs and
    tab groups with or without group name.

This CL also updates the placeholder for the
accessibility_close_tab_group_button_with_group_name string.

Note: set `Skip-Translation-Screenshots-Check: True` to bypass error
since the screenshot is the same before and after editing the string.

Everything introduced in this CL is gated by by Finch parameter
"enable_launch_polish" under flag "enable-tab-grid-layout". The changes
in TabSwitcherCoordinator is also gated by the Finch flag
"enable-tab-groups-continuation". Most of the code is verified to be
behind the gating function TabUiFeatureUtilities#isLaunchPolishEnabled
by formal equivalence checking tool here: http://crrev.com/c/1934235.
The changes in following files can't be verified by the tool, but it is
expected and no-op without both flags:
  * new property in TabProperties
  * StartSurfaceLayout#canHostBeFocusable is the only diff
  * Lots of diffs in language files due to the string placeholder
    change

Skip-Translation-Screenshots-Check: True
Bug: 1124921
Change-Id: If665464d82f83596c93449e8910878b4c00d02cb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2402302
Commit-Queue: Mei Liang <meiliang@chromium.org>
Reviewed-by: default avatarWei-Yin Chen (陳威尹) <wychen@chromium.org>
Cr-Commit-Position: refs/heads/master@{#806719}
parent 15ae5bf3
......@@ -73,6 +73,7 @@ android_library("java") {
"//chrome/browser/tab:java",
"//chrome/browser/tabmodel:java",
"//chrome/browser/ui/messages/android:java",
"//chrome/browser/util:java",
"//components/browser_ui/android/bottomsheet:java",
"//components/browser_ui/widget/android:java",
"//components/prefs/android:java",
......
......@@ -37,6 +37,7 @@ import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tasks.tab_management.TabSwitcher;
import org.chromium.chrome.browser.tasks.tab_management.TabUiFeatureUtilities;
import org.chromium.chrome.browser.util.ChromeAccessibilityUtil;
import org.chromium.components.browser_ui.widget.animation.Interpolators;
import org.chromium.ui.resources.ResourceManager;
......@@ -186,6 +187,14 @@ public class StartSurfaceLayout extends Layout implements StartSurface.OverviewM
if (skipSlowZooming) {
showShrinkingAnimation &= quick;
}
if (TabUiFeatureUtilities.isLaunchPolishEnabled()) {
// Intentionally disable the shrinking animation when accessibility is enabled. During
// the shrinking animation, since the ComponsitorViewHolder is not focusable, I think
// we are in a temporary no "valid" focus target state, so the focus shifts to the
// omnibox and triggers an accessibility announcement of the URL and a keyboard hiding
// event. Disable the animation to avoid this temporary state.
showShrinkingAnimation &= !ChromeAccessibilityUtil.get().isAccessibilityEnabled();
}
// Keep the current tab in mLayoutTabs even if we are not going to show the shrinking
// animation so that thumbnail taking is not blocked.
......@@ -491,4 +500,16 @@ public class StartSurfaceLayout extends Layout implements StartSurface.OverviewM
public boolean onUpdateAnimation(long time, boolean jumpToEnd) {
return mTabToSwitcherAnimation == null && !mIsAnimating;
}
@Override
public boolean canHostBeFocusable() {
if (TabUiFeatureUtilities.isLaunchPolishEnabled()
&& ChromeAccessibilityUtil.get().isAccessibilityEnabled()) {
// We don't allow this layout to gain focus when accessibility is enabled so that the
// CompositorViewHolder doesn't steal focus when entering tab switcher.
// (crbug.com/1125185).
return false;
}
return super.canHostBeFocusable();
}
}
......@@ -1450,6 +1450,58 @@ public class StartSurfaceLayoutTest {
verifyTabModelTabCount(cta, 0, 0);
}
@Test
@MediumTest
// clang-format off
@EnableFeatures({ChromeFeatureList.TAB_GROUPS_ANDROID,
ChromeFeatureList.TAB_GROUPS_CONTINUATION_ANDROID})
@CommandLineFlags.Add({BASE_PARAMS + "/enable_launch_polish/true"})
public void testCloseButtonDescription() {
String expectedDescription = "Close New tab tab";
// clang-format on
ChromeTabbedActivity cta = mActivityTestRule.getActivity();
enterTabSwitcher(cta);
// Test single tab.
onView(allOf(withParent(withId(R.id.content_view)), withId(R.id.action_button),
withEffectiveVisibility(VISIBLE)))
.check(ViewContentDescription.havingDescription(expectedDescription));
// Create 2 tabs and merge them into one group.
createTabs(cta, false, 2);
enterTabSwitcher(cta);
TabModel normalTabModel = cta.getTabModelSelector().getModel(false);
List<Tab> tabGroup = new ArrayList<>(
Arrays.asList(normalTabModel.getTabAt(0), normalTabModel.getTabAt(1)));
createTabGroup(cta, false, tabGroup);
verifyTabSwitcherCardCount(cta, 1);
// Test group tab.
expectedDescription = "Close tab group with 2 tabs";
onView(allOf(withParent(withId(R.id.content_view)), withId(R.id.action_button),
withEffectiveVisibility(VISIBLE)))
.check(ViewContentDescription.havingDescription(expectedDescription));
}
private static class ViewContentDescription implements ViewAssertion {
private String mExpectedDescription;
public static ViewContentDescription havingDescription(String description) {
return new ViewContentDescription(description);
}
public ViewContentDescription(String description) {
mExpectedDescription = description;
}
@Override
public void check(View view, NoMatchingViewException noMatchException) {
if (noMatchException != null) throw noMatchException;
assertEquals(mExpectedDescription, view.getContentDescription());
}
}
private static class TabCountAssertion implements ViewAssertion {
private int mExpectedCount;
......
......@@ -189,6 +189,7 @@ class TabGridViewBinder {
} else if (CARD_ALPHA == propertyKey) {
view.setAlpha(model.get(CARD_ALPHA));
} else if (TabProperties.TITLE == propertyKey) {
if (TabUiFeatureUtilities.isLaunchPolishEnabled()) return;
String title = model.get(TabProperties.TITLE);
view.fastFindViewById(R.id.action_button)
.setContentDescription(view.getResources().getString(
......@@ -232,6 +233,11 @@ class TabGridViewBinder {
searchButton.setIcon(iconDrawableId, shouldTint);
} else if (TabProperties.IS_SELECTED == propertyKey) {
view.setSelected(model.get(TabProperties.IS_SELECTED));
} else if (TabUiFeatureUtilities.isLaunchPolishEnabled()
&& TabProperties.CLOSE_BUTTON_DESCRIPTION_STRING == propertyKey) {
view.fastFindViewById(R.id.action_button)
.setContentDescription(
model.get(TabProperties.CLOSE_BUTTON_DESCRIPTION_STRING));
}
}
......
......@@ -8,6 +8,7 @@ import static org.chromium.chrome.browser.tasks.tab_management.MessageCardViewPr
import static org.chromium.chrome.browser.tasks.tab_management.TabListModel.CardProperties.CARD_ALPHA;
import static org.chromium.chrome.browser.tasks.tab_management.TabListModel.CardProperties.CARD_TYPE;
import static org.chromium.chrome.browser.tasks.tab_management.TabListModel.CardProperties.ModelType.TAB;
import static org.chromium.chrome.browser.tasks.tab_management.TabProperties.CLOSE_BUTTON_DESCRIPTION_STRING;
import android.app.Activity;
import android.content.ComponentCallbacks;
......@@ -778,6 +779,10 @@ class TabListMediator {
if (index == TabModel.INVALID_TAB_INDEX) return;
mModel.get(index).model.set(TabProperties.TITLE, title);
updateDescriptionString(PseudoTab.fromTab(tab), mModel.get(index).model);
if (TabUiFeatureUtilities.isLaunchPolishEnabled()) {
updateCloseButtonDescriptionString(
PseudoTab.fromTab(tab), mModel.get(index).model);
}
}
@Override
......@@ -1018,6 +1023,9 @@ class TabListMediator {
mModel.get(index).model.set(TabProperties.IS_SELECTED, isSelected);
mModel.get(index).model.set(TabProperties.TITLE, getLatestTitleForTab(pseudoTab));
updateDescriptionString(pseudoTab, mModel.get(index).model);
if (TabUiFeatureUtilities.isLaunchPolishEnabled()) {
updateCloseButtonDescriptionString(pseudoTab, mModel.get(index).model);
}
if (isRealTab) {
mModel.get(index).model.set(
TabProperties.URL_DOMAIN, getDomainForTab(pseudoTab.getTab()));
......@@ -1276,6 +1284,9 @@ class TabListMediator {
tabInfo.set(TabProperties.TAB_SELECTED_LISTENER, tabSelectedListener);
tabInfo.set(TabProperties.TAB_CLOSED_LISTENER, isRealTab ? mTabClosedListener : null);
updateDescriptionString(pseudoTab, tabInfo);
if (TabUiFeatureUtilities.isLaunchPolishEnabled()) {
updateCloseButtonDescriptionString(pseudoTab, tabInfo);
}
}
if (index >= mModel.size()) {
......@@ -1350,6 +1361,33 @@ class TabListMediator {
}
}
private void updateCloseButtonDescriptionString(PseudoTab pseudoTab, PropertyModel model) {
if (!TabUiFeatureUtilities.isLaunchPolishEnabled()) return;
if (mActionsOnAllRelatedTabs) {
int numOfRelatedTabs = getRelatedTabsForId(pseudoTab.getId()).size();
if (numOfRelatedTabs > 1) {
String title = getLatestTitleForTab(pseudoTab);
title = title.equals(pseudoTab.getTitle(mTitleProvider)) ? "" : title;
if (title.isEmpty()) {
model.set(TabProperties.CLOSE_BUTTON_DESCRIPTION_STRING,
mContext.getString(R.string.accessibility_close_tab_group_button,
String.valueOf(numOfRelatedTabs)));
} else {
model.set(TabProperties.CLOSE_BUTTON_DESCRIPTION_STRING,
mContext.getString(
R.string.accessibility_close_tab_group_button_with_group_name,
title, String.valueOf(numOfRelatedTabs)));
}
return;
}
}
model.set(CLOSE_BUTTON_DESCRIPTION_STRING,
mContext.getString(
R.string.accessibility_tabstrip_btn_close_tab, pseudoTab.getTitle()));
}
@VisibleForTesting
protected static String getDomain(Tab tab) {
// TODO(crbug.com/1116613) Investigate how uninitialized Tabs are appearing
......
......@@ -112,6 +112,9 @@ public class TabProperties {
public static final WritableObjectPropertyKey<String> CONTENT_DESCRIPTION_STRING =
new WritableObjectPropertyKey<>();
public static final WritableObjectPropertyKey<String> CLOSE_BUTTON_DESCRIPTION_STRING =
new WritableObjectPropertyKey<>();
public static final PropertyKey[] ALL_KEYS_TAB_GRID = new PropertyKey[] {TAB_ID,
TAB_SELECTED_LISTENER, TAB_CLOSED_LISTENER, FAVICON, THUMBNAIL_FETCHER, IPH_PROVIDER,
TITLE, IS_SELECTED, CHECKED_DRAWABLE_STATE_LIST, CREATE_GROUP_LISTENER, CARD_ALPHA,
......@@ -120,7 +123,7 @@ public class TabProperties {
SELECTABLE_TAB_ACTION_BUTTON_BACKGROUND,
SELECTABLE_TAB_ACTION_BUTTON_SELECTED_BACKGROUND, URL_DOMAIN, ACCESSIBILITY_DELEGATE,
SEARCH_QUERY, SEARCH_LISTENER, SEARCH_CHIP_ICON_DRAWABLE_ID, CARD_TYPE,
CONTENT_DESCRIPTION_STRING};
CONTENT_DESCRIPTION_STRING, CLOSE_BUTTON_DESCRIPTION_STRING};
public static final PropertyKey[] ALL_KEYS_TAB_STRIP =
new PropertyKey[] {TAB_ID, TAB_SELECTED_LISTENER, TAB_CLOSED_LISTENER, FAVICON,
......
......@@ -9,6 +9,7 @@ import android.graphics.Bitmap;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
......@@ -171,6 +172,36 @@ public class TabSwitcherCoordinator
mContainerViewChangeProcessor = PropertyModelChangeProcessor.create(containerViewModel,
mTabListCoordinator.getContainerView(), TabListContainerViewBinder::bind);
if (TabUiFeatureUtilities.isLaunchPolishEnabled()
&& TabUiFeatureUtilities.isTabGroupsAndroidContinuationEnabled()) {
mMediator.addOverviewModeObserver(new OverviewModeObserver() {
@Override
public void startedShowing() {}
@Override
public void finishedShowing() {
int selectedIndex = mTabModelSelector.getTabModelFilterProvider()
.getCurrentTabModelFilter()
.index();
ViewHolder selectedViewHolder =
mTabListCoordinator.getContainerView().findViewHolderForAdapterPosition(
selectedIndex);
if (selectedViewHolder == null) return;
View focusView = selectedViewHolder.itemView;
focusView.requestFocus();
focusView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
}
@Override
public void startedHiding() {}
@Override
public void finishedHiding() {}
});
}
mMessageCardProviderCoordinator =
new MessageCardProviderCoordinator(context, (identifier) -> {
mTabListCoordinator.removeSpecialListItem(
......
......@@ -221,7 +221,7 @@
Close tab group with <ph name="NUMBER_OF_TABS">%1$s<ex>2</ex></ph> tabs
</message>
<message name="IDS_ACCESSIBILITY_CLOSE_TAB_GROUP_BUTTON_WITH_GROUP_NAME" desc="The accessibility text to read when the close button on a card representing a tab group is focused in grid tab switcher. When this close button is tapped, all tabs within the tab group will be closed. TITLE_OF_GROUP is the title of the group. NUMBER_OF_TABS is the number of tabs within this group. Note that there are always at least two tabs in a group so plural form should always be used.">
Close <ph name="TITLE_OF_GROUP">%1$s<ex>shopping</ex></ph> group with <ph name="NUMBER_OF_TABS">%1$s<ex>2</ex></ph> tabs
Close <ph name="TITLE_OF_GROUP">%1$s<ex>shopping</ex></ph> group with <ph name="NUMBER_OF_TABS">%2$s<ex>2</ex></ph> tabs
</message>
<!-- Tab Selection Editor strings -->
......
......@@ -788,31 +788,40 @@ public class TabGridDialogTest {
openDialogFromTabSwitcherAndVerify(cta, 3, null);
verifyDialogBackButtonContentDescription(cta, collapseTargetString);
editDialogTitle(cta, CUSTOMIZED_TITLE1);
collapseTargetString =
String.format("Collapse %s tab group with 3 tabs.", CUSTOMIZED_TITLE1);
collapseTargetString = "Collapse " + CUSTOMIZED_TITLE1 + " tab group with 3 tabs.";
verifyDialogBackButtonContentDescription(cta, collapseTargetString);
// Group card content description should update with group title.
clickScrimToExitDialog(cta);
waitForDialogHidingAnimationInTabSwitcher(cta);
verifyFirstCardTitle(CUSTOMIZED_TITLE1);
expandTargetString = String.format("Expand %s tab group with 3 tabs.", CUSTOMIZED_TITLE1);
expandTargetString = "Expand " + CUSTOMIZED_TITLE1 + " tab group with 3 tabs.";
assertEquals(expandTargetString, firstItem.getContentDescription());
// Verify the TabSwitcher group card close button content description should update with
// group title.
View closeButton = firstItem.findViewById(R.id.action_button);
String closeButtonTargetString = "Close " + CUSTOMIZED_TITLE1 + " group with 3 tabs";
assertEquals(closeButtonTargetString, closeButton.getContentDescription());
// Back button content description should update with group count change.
openDialogFromTabSwitcherAndVerify(cta, 3, CUSTOMIZED_TITLE1);
closeFirstTabInDialog();
verifyShowingDialog(cta, 2, CUSTOMIZED_TITLE1);
collapseTargetString =
String.format("Collapse %s tab group with 2 tabs.", CUSTOMIZED_TITLE1);
collapseTargetString = "Collapse " + CUSTOMIZED_TITLE1 + " tab group with 2 tabs.";
verifyDialogBackButtonContentDescription(cta, collapseTargetString);
// Group card content description should update with group count change.
clickScrimToExitDialog(cta);
waitForDialogHidingAnimationInTabSwitcher(cta);
expandTargetString = String.format("Expand %s tab group with 2 tabs.", CUSTOMIZED_TITLE1);
expandTargetString = "Expand " + CUSTOMIZED_TITLE1 + " tab group with 2 tabs.";
assertEquals(expandTargetString, firstItem.getContentDescription());
// TabSwitcher group card Close button content description should update with group count
// change.
closeButtonTargetString = "Close " + CUSTOMIZED_TITLE1 + " group with 2 tabs";
assertEquals(closeButtonTargetString, closeButton.getContentDescription());
// Back button content description should restore when the group loses customized title.
openDialogFromTabSwitcherAndVerify(cta, 2, CUSTOMIZED_TITLE1);
editDialogTitle(cta, "");
......@@ -830,6 +839,11 @@ public class TabGridDialogTest {
clickScrimToExitDialog(cta);
waitForDialogHidingAnimationInTabSwitcher(cta);
assertEquals(null, firstItem.getContentDescription());
// TabSwitcher Group card Close button content description should restore when the group
// becomes a single tab.
closeButtonTargetString = "Close New tab tab";
assertEquals(closeButtonTargetString, closeButton.getContentDescription());
}
@Test
......
......@@ -63,6 +63,7 @@ import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import org.junit.After;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
......@@ -2240,6 +2241,70 @@ public class TabListMediatorUnitTest {
equalTo(targetString));
}
@Test
@Features.EnableFeatures({TAB_GROUPS_CONTINUATION_ANDROID})
public void testCloseButtonDescriptionStringSetup_TabSwitcher() {
Assume.assumeTrue("The close button changes are gated by a Chrome fast path fieldtrials"
+ "flag. Remove this assumption after the fast path flag is removed.",
TabUiFeatureUtilities.isLaunchPolishEnabled());
setUpForTabGroupOperation(TabListMediatorType.TAB_SWITCHER);
setUpCloseButtonDescriptionString(false);
String targetString = "Close Tab1 tab";
List<Tab> tabs = new ArrayList<>();
for (int i = 0; i < mTabModel.getCount(); i++) {
tabs.add(mTabModel.getTabAt(i));
}
mMediator.resetWithListOfTabs(PseudoTab.getListOfPseudoTab(tabs), false, false);
assertThat(mModel.get(POSITION1).model.get(TabProperties.CLOSE_BUTTON_DESCRIPTION_STRING),
equalTo(targetString));
// Create tab group.
TabImpl tab3 = prepareTab(TAB3_ID, TAB3_TITLE, TAB3_URL);
List<Tab> group1 = new ArrayList<>(Arrays.asList(mTab1, tab3));
createTabGroup(group1, TAB1_ID);
setUpCloseButtonDescriptionString(true);
targetString = "Close tab group with 2 tabs.";
mMediator.resetWithListOfTabs(PseudoTab.getListOfPseudoTab(tabs), false, false);
assertThat(mModel.get(POSITION1).model.get(TabProperties.CLOSE_BUTTON_DESCRIPTION_STRING),
equalTo(targetString));
// Set group name.
targetString = String.format("Close %s group with 2 tabs.", CUSTOMIZED_DIALOG_TITLE1);
mMediator.getTabGroupTitleEditor().storeTabGroupTitle(TAB1_ID, CUSTOMIZED_DIALOG_TITLE1);
mMediator.getTabGroupTitleEditor().updateTabGroupTitle(mTab1, CUSTOMIZED_DIALOG_TITLE1);
assertThat(mModel.get(POSITION1).model.get(TabProperties.CLOSE_BUTTON_DESCRIPTION_STRING),
equalTo(targetString));
}
private void setUpCloseButtonDescriptionString(boolean isGroup) {
if (isGroup) {
doAnswer(invocation -> {
String title = invocation.getArgument(1);
String num = invocation.getArgument(2);
return String.format("Close %s group with %s tabs.", title, num);
})
.when(mContext)
.getString(anyInt(), anyString(), anyString());
doAnswer(invocation -> {
String num = invocation.getArgument(1);
return String.format("Close tab group with %s tabs.", num);
})
.when(mContext)
.getString(anyInt(), anyString());
} else {
doAnswer(invocation -> {
String title = invocation.getArgument(1);
return String.format("Close %s, tab.", title);
})
.when(mContext)
.getString(anyInt(), anyString());
}
}
private void setUpTabGroupCardDescriptionString() {
doAnswer(invocation -> {
String title = invocation.getArgument(1);
......
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