Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Make loading page progress bar accessible to TalkBack. #2526

Merged
merged 1 commit into from
Mar 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<DisplayAction> = mutableListOf()
Expand Down Expand Up @@ -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
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
<!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
<string name="mozac_browser_toolbar_menu_button">Menü</string>
<string name="mozac_clear_button_description">Leeren</string>
<string name="mozac_browser_toolbar_progress_loading">Wird geladen</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
<!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
<string name="mozac_browser_toolbar_menu_button">菜单</string>
<string name="mozac_clear_button_description">清除</string>
<string name="mozac_browser_toolbar_progress_loading">正在加载</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
<!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
<string name="mozac_browser_toolbar_menu_button">選單</string>
<string name="mozac_clear_button_description">清除</string>
<string name="mozac_browser_toolbar_progress_loading">載入中</string>
</resources>
2 changes: 2 additions & 0 deletions components/browser/toolbar/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@
<!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
<string name="mozac_browser_toolbar_menu_button">Menu</string>
<string name="mozac_clear_button_description">Clear</string>
<!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
<string name="mozac_browser_toolbar_progress_loading">Loading</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,15 +35,18 @@ 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
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 {
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down