diff --git a/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt b/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt index e1de8d2a594..fb0070927cc 100644 --- a/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt +++ b/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt @@ -15,6 +15,7 @@ import android.util.TypedValue import android.view.Gravity import android.view.View import android.view.ViewGroup +import android.view.accessibility.AccessibilityEvent import android.widget.ProgressBar import mozilla.components.browser.menu.BrowserMenu import mozilla.components.browser.menu.BrowserMenuBuilder @@ -130,6 +131,17 @@ internal class DisplayToolbar( context, null, android.R.attr.progressBarStyleHorizontal ).apply { visibility = View.GONE + setAccessibilityDelegate(object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityEvent(host: View?, event: AccessibilityEvent?) { + super.onInitializeAccessibilityEvent(host, event) + if (event?.eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + // Populate the scroll event with the current progress. + // See accessibility note in `updateProgress()`. + event.scrollY = progress + event.maxScrollY = max + } + } + }) } private val browserActions: MutableList = mutableListOf() @@ -185,11 +197,42 @@ internal class DisplayToolbar( /** * Updates the progress to be displayed. + * + * Accessibility note: + * ProgressBars can be made accessible to TalkBack by setting `android:accessibilityLiveRegion`. + * They will emit TYPE_VIEW_SELECTED events. TalkBack will format those events into percentage + * announcements along with a pitch-change earcon. We are not using that feature here for + * several reasons: + * 1. They are dispatched via a 200ms timeout. Since loading a page can be a short process, + * and since we only update the bar a handful of times, these events often never fire and + * they don't give the user a true sense of the progress. + * 2. The last 100% event is dispatched after the view is hidden. This prevents the event + * from being fired, so the user never gets a "complete" event. + * 3. Live regions in TalkBack have their role announced, so the user will hear + * "Progress bar, 25%". For a common feature like page load this is very chatty and unintuitive. + * 4. We can provide custom strings instead of the less useful percentage utterance, but + * TalkBack will not play an earcon if an event has its own text. + * + * For all those reasons, we are going another route here with a "loading" announcement + * when the progress bar first appears along with scroll events that have the same + * pitch-change earcon in TalkBack (although they are a bit louder). This gives a concise and + * consistent feedback to the user that they can depend on. + * */ fun updateProgress(progress: Int) { + if (!progressView.isVisible() && progress > 0) { + // Loading has just started, make visible and announce "loading" for accessibility. + progressView.visibility = View.VISIBLE + progressView.announceForAccessibility(context.getString(R.string.mozac_browser_toolbar_progress_loading)) + } + progressView.progress = progress + progressView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED) - progressView.visibility = if (progress < progressView.max && progress > 0) View.VISIBLE else View.GONE + if (progress >= progressView.max) { + // Loading is done, hide progress bar. + progressView.visibility = View.GONE + } } /** diff --git a/components/browser/toolbar/src/main/res/values-de/strings.xml b/components/browser/toolbar/src/main/res/values-de/strings.xml index af975fa4e70..de5c826bb5c 100644 --- a/components/browser/toolbar/src/main/res/values-de/strings.xml +++ b/components/browser/toolbar/src/main/res/values-de/strings.xml @@ -3,4 +3,5 @@ Menü Leeren + Wird geladen diff --git a/components/browser/toolbar/src/main/res/values-zh-rCN/strings.xml b/components/browser/toolbar/src/main/res/values-zh-rCN/strings.xml index 7c4e4fb3636..05322ea148d 100644 --- a/components/browser/toolbar/src/main/res/values-zh-rCN/strings.xml +++ b/components/browser/toolbar/src/main/res/values-zh-rCN/strings.xml @@ -3,4 +3,5 @@ 菜单 清除 + 正在加载 diff --git a/components/browser/toolbar/src/main/res/values-zh-rTW/strings.xml b/components/browser/toolbar/src/main/res/values-zh-rTW/strings.xml index 04874baf513..8a4ec5df6d6 100644 --- a/components/browser/toolbar/src/main/res/values-zh-rTW/strings.xml +++ b/components/browser/toolbar/src/main/res/values-zh-rTW/strings.xml @@ -3,4 +3,5 @@ 選單 清除 + 載入中 diff --git a/components/browser/toolbar/src/main/res/values/strings.xml b/components/browser/toolbar/src/main/res/values/strings.xml index ad98c3500be..3ed9b476f49 100644 --- a/components/browser/toolbar/src/main/res/values/strings.xml +++ b/components/browser/toolbar/src/main/res/values/strings.xml @@ -6,4 +6,6 @@ Menu Clear + + Loading diff --git a/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt b/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt index 6e8d61a35d5..e5bb9fbf619 100644 --- a/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt +++ b/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt @@ -11,6 +11,9 @@ import android.graphics.drawable.Drawable import android.support.v13.view.inputmethod.EditorInfoCompat import android.util.AttributeSet import android.view.View +import android.view.ViewParent +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityManager import android.widget.ImageButton import android.widget.LinearLayout import androidx.test.core.app.ApplicationProvider @@ -32,8 +35,10 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers import org.mockito.Mockito.`when` +import org.mockito.Mockito.any import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.spy @@ -41,6 +46,7 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows @RunWith(RobolectricTestRunner::class) class BrowserToolbarTest { @@ -135,6 +141,39 @@ class BrowserToolbarTest { verify(ediToolbar).updateUrl("https://www.mozilla.org") } + @Test + fun `displayProgress will send accessibility events`() { + val toolbar = BrowserToolbar(context) + val root = mock(ViewParent::class.java) + Shadows.shadowOf(toolbar).setMyParent(root) + `when`(root.requestSendAccessibilityEvent(any(), any())).thenReturn(false) + + Shadows.shadowOf(context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager).setEnabled(true) + + toolbar.displayProgress(10) + toolbar.displayProgress(50) + toolbar.displayProgress(100) + + val captor = ArgumentCaptor.forClass(AccessibilityEvent::class.java) + + verify(root, times(4)).requestSendAccessibilityEvent(any(), captor.capture()) + + assertEquals(AccessibilityEvent.TYPE_ANNOUNCEMENT, captor.allValues[0].eventType) + assertEquals(context.getString(R.string.mozac_browser_toolbar_progress_loading), captor.allValues[0].text[0]) + + assertEquals(AccessibilityEvent.TYPE_VIEW_SCROLLED, captor.allValues[1].eventType) + assertEquals(10, captor.allValues[1].scrollY) + assertEquals(100, captor.allValues[1].maxScrollY) + + assertEquals(AccessibilityEvent.TYPE_VIEW_SCROLLED, captor.allValues[2].eventType) + assertEquals(50, captor.allValues[2].scrollY) + assertEquals(100, captor.allValues[2].maxScrollY) + + assertEquals(AccessibilityEvent.TYPE_VIEW_SCROLLED, captor.allValues[3].eventType) + assertEquals(100, captor.allValues[3].scrollY) + assertEquals(100, captor.allValues[3].maxScrollY) + } + @Test fun `displayProgress will be forwarded to display toolbar`() { val toolbar = BrowserToolbar(context) diff --git a/docs/changelog.md b/docs/changelog.md index bb19adc289d..929ae5be427 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -25,6 +25,9 @@ permalink: /changelog/ * **browser-storage-sync**, **browser-storage-memory** * Implementations of `concept-storage`/`HistoryStorage` expose newly added `deleteVisit`. +* **browser-toolbar** + * Add TalkBack support for page load status. + # 0.48.0 * [Commits](https://github.com/mozilla-mobile/android-components/compare/v0.47.0...v0.48.0)