diff --git a/lib/java/com/google/android/material/internal/NavigationMenuPresenter.java b/lib/java/com/google/android/material/internal/NavigationMenuPresenter.java index a929f62b080..53c64e69173 100644 --- a/lib/java/com/google/android/material/internal/NavigationMenuPresenter.java +++ b/lib/java/com/google/android/material/internal/NavigationMenuPresenter.java @@ -48,10 +48,12 @@ import androidx.annotation.Px; import androidx.annotation.RestrictTo; import androidx.annotation.StyleRes; +import androidx.core.view.AccessibilityDelegateCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat; import androidx.core.widget.TextViewCompat; import java.util.ArrayList; @@ -596,6 +598,7 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) { } itemView.setMaxLines(itemMaxLines); itemView.initialize(item.getMenuItem(), 0); + setAccessibilityDelegate(itemView, position, false); break; } case VIEW_TYPE_SUBHEADER: @@ -615,6 +618,7 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) { if (subheaderColor != null) { subHeader.setTextColor(subheaderColor); } + setAccessibilityDelegate(subHeader, position, true); break; } case VIEW_TYPE_SEPARATOR: @@ -629,11 +633,46 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) { } case VIEW_TYPE_HEADER: { + setAccessibilityDelegate(holder.itemView, position, true); break; } } } + private void setAccessibilityDelegate(View view, int position, boolean isHeader) { + ViewCompat.setAccessibilityDelegate( + view, + new AccessibilityDelegateCompat() { + @Override + public void onInitializeAccessibilityNodeInfo( + @NonNull View host, @NonNull AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setCollectionItemInfo( + CollectionItemInfoCompat.obtain( + /* rowIndex= */ adjustItemPositionForA11yDelegate(position), + /* rowSpan= */ 1, + /* columnIndex =*/ 1, + /* columnSpan= */ 1, + /* heading= */ isHeader, + /* selected= */ host.isSelected())); + } + }); + } + + /** Adjusts position based on the presence of separators and header. */ + private int adjustItemPositionForA11yDelegate(int position) { + int adjustedPosition = position; + for (int i = 0; i < position; i++) { + if (adapter.getItemViewType(i) == VIEW_TYPE_SEPARATOR) { + adjustedPosition--; + } + } + if (headerLayout.getChildCount() == 0) { // no header + adjustedPosition--; + } + return adjustedPosition; + } + @Override public void onViewRecycled(ViewHolder holder) { if (holder instanceof NormalViewHolder) { @@ -816,7 +855,8 @@ public void setUpdateSuspended(boolean updateSuspended) { int getRowCount() { int itemCount = headerLayout.getChildCount() == 0 ? 0 : 1; for (int i = 0; i < adapter.getItemCount(); i++) { - if (adapter.getItemViewType(i) == VIEW_TYPE_NORMAL) { + int type = adapter.getItemViewType(i); + if (type == VIEW_TYPE_NORMAL || type == VIEW_TYPE_SUBHEADER) { itemCount++; } } @@ -880,7 +920,9 @@ private class NavigationMenuViewAccessibilityDelegate extends RecyclerViewAccess public void onInitializeAccessibilityNodeInfo( View host, @NonNull AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(host, info); - info.setCollectionInfo(CollectionInfoCompat.obtain(adapter.getRowCount(), 0, false)); + info.setCollectionInfo( + CollectionInfoCompat.obtain( + adapter.getRowCount(), /* columnCount= */ 1, /* hierarchical= */ false)); } } } diff --git a/testing/java/com/google/android/material/testapp/res/menu/navigation_view_content.xml b/testing/java/com/google/android/material/testapp/res/menu/navigation_view_content.xml index 7c8ab8df1b6..c801daf3b42 100644 --- a/testing/java/com/google/android/material/testapp/res/menu/navigation_view_content.xml +++ b/testing/java/com/google/android/material/testapp/res/menu/navigation_view_content.xml @@ -1,5 +1,4 @@ - - - - - - - - - - + + + + + + + + + + + + + diff --git a/testing/java/com/google/android/material/testapp/res/values/strings.xml b/testing/java/com/google/android/material/testapp/res/values/strings.xml index 7bd472214da..2d85fc07f3a 100644 --- a/testing/java/com/google/android/material/testapp/res/values/strings.xml +++ b/testing/java/com/google/android/material/testapp/res/values/strings.xml @@ -21,6 +21,8 @@ People Custom Settings + Subheader + Subheader item This is a test message Undo diff --git a/tests/javatests/com/google/android/material/navigation/NavigationViewTest.java b/tests/javatests/com/google/android/material/navigation/NavigationViewTest.java index 15b9ca2a63f..53b8a94449a 100644 --- a/tests/javatests/com/google/android/material/navigation/NavigationViewTest.java +++ b/tests/javatests/com/google/android/material/navigation/NavigationViewTest.java @@ -15,6 +15,7 @@ */ package com.google.android.material.navigation; +import static android.os.Build.VERSION_CODES.KITKAT; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.assertion.ViewAssertions.matches; @@ -61,6 +62,7 @@ import android.content.res.Resources; import android.os.Build; +import android.os.Build.VERSION; import android.os.Parcelable; import androidx.recyclerview.widget.RecyclerView; import androidx.appcompat.widget.SwitchCompat; @@ -74,11 +76,16 @@ import androidx.annotation.IdRes; import androidx.core.content.res.ResourcesCompat; import androidx.core.view.GravityCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.test.espresso.matcher.ViewMatchers.Visibility; import androidx.test.filters.MediumTest; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; +import com.google.android.material.internal.NavigationMenuView; import com.google.android.material.testapp.NavigationViewActivity; import com.google.android.material.testapp.R; import com.google.android.material.testapp.custom.NavigationTestView; @@ -135,7 +142,7 @@ public void testBasics() { final Menu menu = navigationView.getMenu(); assertNotNull("Menu should not be null", menu); assertEquals( - "Should have matching number of items", MENU_CONTENT_ITEM_IDS.length + 1, menu.size()); + "Should have matching number of items", MENU_CONTENT_ITEM_IDS.length + 2, menu.size()); for (int i = 0; i < MENU_CONTENT_ITEM_IDS.length; i++) { final MenuItem currItem = menu.getItem(i); assertEquals("ID for Item #" + i, MENU_CONTENT_ITEM_IDS[i], currItem.getItemId()); @@ -707,4 +714,51 @@ public void testActionLayout() { allOf(isAssignableFrom(TextView.class), withEffectiveVisibility(Visibility.GONE)))); onView(customItemMatcher).perform(click()); } + + @Test + public void testAccessibility() throws Throwable { + if (VERSION.SDK_INT < KITKAT) { + // CollectionInfo and CollectionItemInfo only available on API 19+. + return; + } + + // Open our drawer + onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START)); + + // Add header + onView(withId(R.id.start_drawer)) + .perform(inflateHeaderView(R.layout.design_navigation_view_header1)); + + final NavigationViewActivity activity = activityTestRule.getActivity(); + NavigationMenuView navigationMenuView = activity.findViewById(R.id.design_navigation_view); + AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain(); + ViewCompat.onInitializeAccessibilityNodeInfo(navigationMenuView, nodeInfo); + + CollectionInfoCompat collectionInfo = nodeInfo.getCollectionInfo(); + assertEquals(8, collectionInfo.getRowCount()); + assertEquals(1, collectionInfo.getColumnCount()); + + activityTestRule.runOnUiThread( + () -> { + verifyItemInfo(navigationMenuView.getChildAt(0), 0, true); + verifyItemInfo(navigationMenuView.getChildAt(1), 1, false); + verifyItemInfo(navigationMenuView.getChildAt(2), 2, false); + verifyItemInfo(navigationMenuView.getChildAt(3), 3, false); + verifyItemInfo(navigationMenuView.getChildAt(4), 4, false); + // index 5 = separator + verifyItemInfo(navigationMenuView.getChildAt(6), 5, true); + verifyItemInfo(navigationMenuView.getChildAt(7), 6, false); + verifyItemInfo(navigationMenuView.getChildAt(8), 7, false); + }); + } + + private void verifyItemInfo(View view, int rowIndex, boolean isHeader) { + AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain(); + ViewCompat.onInitializeAccessibilityNodeInfo(view, nodeInfo); + + CollectionItemInfoCompat itemInfo = nodeInfo.getCollectionItemInfo(); + assertEquals(rowIndex, itemInfo.getRowIndex()); + assertEquals(1, itemInfo.getColumnIndex()); + assertEquals(isHeader, nodeInfo.isHeading()); + } }