Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A11y fixes #8426

Merged
merged 9 commits into from
May 15, 2023
Merged
1 change: 1 addition & 0 deletions changelog.d/8426.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improve keyboard navigation and accessibility when using a screen reader.
3 changes: 3 additions & 0 deletions library/ui-strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1470,6 +1470,9 @@
<string name="reason_colon">Reason: %1$s</string>

<string name="avatar">Avatar</string>
<string name="avatar_of_space">Avatar of space %1$s</string>
<string name="avatar_of_room">Avatar of room %1$s</string>
<string name="avatar_of_user">Profile picture of user %1$s</string>

<!-- Consent modal -->
<string name="dialog_user_consent_content">To continue using the %1$s homeserver you must review and agree to the terms and conditions.</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<style name="Widget.Vector.AppBarLayout" parent="Widget.MaterialComponents.AppBarLayout.Primary">
<item name="android:background">?vctr_toolbar_background</item>
<item name="elevation">4dp</item>

<!-- a11y -->
<item name="android:touchscreenBlocksFocus">false</item>
</style>

</resources>
</resources>
19 changes: 16 additions & 3 deletions library/ui-styles/src/main/res/values/styles_toolbar.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
<item name="subtitleTextAppearance">@style/TextAppearance.Vector.Widget.ActionBarSubTitle</item>

<item name="navigationIconTint">?vctr_content_secondary</item>

<!-- a11y -->
<item name="android:touchscreenBlocksFocus">false</item>
</style>

<!-- Default toolbar style -->
Expand Down Expand Up @@ -39,14 +42,24 @@
<item name="android:textSize">12sp</item>
</style>

<!-- Material 3 -->
<!-- CollapsingToolbar -->

<style name="Widget.Vector.Material3.Toolbar" parent="Widget.Material3.Toolbar" />
<style name="Widget.Vector.CollapsingToolbar" parent="Widget.Material3.CollapsingToolbar">
<!-- a11y -->
<item name="android:touchscreenBlocksFocus">false</item>
</style>

<style name="Widget.Vector.Material3.CollapsingToolbar.Medium" parent="Widget.Material3.CollapsingToolbar.Medium">
<style name="Widget.Vector.CollapsingToolbar.Medium" parent="Widget.Material3.CollapsingToolbar.Medium">
<item name="expandedTitleTextAppearance">@style/TextAppearance.Vector.Title.Medium</item>
<item name="expandedTitleMarginBottom">20dp</item>
<item name="collapsedTitleTextAppearance">@style/TextAppearance.Vector.Headline.Bold</item>
<!-- a11y -->
<item name="android:touchscreenBlocksFocus">false</item>
</style>

<style name="Widget.Vector.CollapsingToolbar.Large" parent="Widget.Material3.CollapsingToolbar.Large">
<!-- a11y -->
<item name="android:touchscreenBlocksFocus">false</item>
</style>

</resources>
3 changes: 3 additions & 0 deletions library/ui-styles/src/main/res/values/theme_dark.xml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@
<item name="android:textViewStyle">@style/Widget.Vector.TextView.Body</item>
<item name="materialButtonStyle">@style/Widget.Vector.Button</item>
<item name="toolbarStyle">@style/Widget.Vector.Toolbar</item>
<item name="collapsingToolbarLayoutStyle">@style/Widget.Vector.CollapsingToolbar</item>
<item name="collapsingToolbarLayoutMediumStyle">@style/Widget.Vector.CollapsingToolbar.Medium</item>
<item name="collapsingToolbarLayoutLargeStyle">@style/Widget.Vector.CollapsingToolbar.Large</item>
<item name="bottomNavigationStyle">@style/BottomNavigation.Vector</item>
<item name="searchViewStyle">@style/Widget.Vector.SearchView</item>
<item name="textInputStyle">@style/Widget.Vector.TextInputLayout</item>
Expand Down
3 changes: 3 additions & 0 deletions library/ui-styles/src/main/res/values/theme_light.xml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@
<item name="android:textViewStyle">@style/Widget.Vector.TextView.Body</item>
<item name="materialButtonStyle">@style/Widget.Vector.Button</item>
<item name="toolbarStyle">@style/Widget.Vector.Toolbar</item>
<item name="collapsingToolbarLayoutStyle">@style/Widget.Vector.CollapsingToolbar</item>
<item name="collapsingToolbarLayoutMediumStyle">@style/Widget.Vector.CollapsingToolbar.Medium</item>
<item name="collapsingToolbarLayoutLargeStyle">@style/Widget.Vector.CollapsingToolbar.Large</item>
<item name="bottomNavigationStyle">@style/BottomNavigation.Vector</item>
<item name="searchViewStyle">@style/Widget.Vector.SearchView</item>
<item name="textInputStyle">@style/Widget.Vector.TextInputLayout</item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import android.graphics.drawable.Drawable
import android.text.InputType
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.EditText
import android.widget.ImageView
import androidx.annotation.AttrRes
Expand All @@ -28,6 +30,7 @@ import androidx.annotation.DrawableRes
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.transition.ChangeBounds
import androidx.transition.Fade
Expand Down Expand Up @@ -97,6 +100,14 @@ fun View.setAttributeBackground(@AttrRes attributeId: Int) {
setBackgroundResource(attribute.resourceId)
}

/**
* Inspired from https://stackoverflow.com/a/64597532/1472514. Safer to call the 2 available API.
*/
fun View.giveAccessibilityFocus() {
ViewCompat.performAccessibilityAction(this, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED)
}

fun ViewGroup.animateLayoutChange(animationDuration: Long, transitionComplete: (() -> Unit)? = null) {
val transition = TransitionSet().apply {
ordering = TransitionSet.ORDERING_SEQUENTIAL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import im.vector.app.R
import im.vector.app.core.di.ActivityEntryPoint
import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.giveAccessibilityFocus
import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.utils.ToolbarConfig
Expand Down Expand Up @@ -318,4 +319,19 @@ abstract class VectorBaseFragment<VB : ViewBinding> : Fragment(), MavericksView
.setPositiveButton(R.string.ok, null)
.show()
}

/* ==========================================================================================
* Accessibility - a11y
* ========================================================================================== */

private var hasBeenAccessibilityFocused = false

/**
* Ensure the View get the accessibility focus. This method has effect only once per fragment instance.
*/
protected fun View.giveAccessibilityFocusOnce() {
if (hasBeenAccessibilityFocused) return
hasBeenAccessibilityFocused = true
giveAccessibilityFocus()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,6 @@ class BootstrapConclusionFragment :
.toSpannable()
.colorizeMatchingText(getString(R.string.recovery_passphrase), colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
.colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
views.bootstrapConclusionText.giveAccessibilityFocusOnce()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class BootstrapConfirmPassphraseFragment :
views.ssssPassphraseSecurityProgress.isGone = true

views.bootstrapDescriptionText.text = getString(R.string.set_a_security_phrase_again_notice)
views.bootstrapDescriptionText.giveAccessibilityFocusOnce()
views.ssssPassphraseEnterEdittext.hint = getString(R.string.set_a_security_phrase_hint)

withState(sharedViewModel) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,6 @@ class BootstrapEnterPassphraseFragment :
}
}
}
views.bootstrapDescriptionText.giveAccessibilityFocusOnce()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ class BootstrapMigrateBackupFragment :

views.bootstrapMigrateUseFile.isVisible = false
}
views.bootstrapDescriptionText.giveAccessibilityFocusOnce()
}

private val importFileStartForActivityResult = registerStartForActivityResult { activityResult ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,6 @@ class BootstrapReAuthFragment :
views.bootstrapCancelButton.isVisible = true
views.bootstrapRetryButton.isVisible = true
}
views.bootstrapDescriptionText.giveAccessibilityFocusOnce()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,6 @@ class BootstrapSaveRecoveryKeyFragment :

views.recoveryContinue.isVisible = step.isSaved
views.bootstrapRecoveryKeyText.text = state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey()
views.bootstrapSaveText.giveAccessibilityFocusOnce()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class BootstrapSetupRecoveryKeyFragment :
// Choose between create a passphrase or use a recovery key
renderBackupMethodActions(firstFormStep.methods)
}
views.bootstrapSetupSecureText.giveAccessibilityFocusOnce()
}

private fun renderStateWithExistingKeyBackup() = with(views) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@ class BootstrapWaitingFragment :
views.bootstrapDescriptionText.isVisible = false
}
}
views.bootstrapDescriptionText.giveAccessibilityFocusOnce()
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,18 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.giveAccessibilityFocus
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
Expand All @@ -36,15 +42,26 @@ import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding
import im.vector.app.features.crypto.verification.VerificationAction
import im.vector.app.features.qrcode.QrCodeScannerActivity
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import timber.log.Timber
import javax.inject.Inject

@AndroidEntryPoint
class SelfVerificationFragment : VectorBaseFragment<BottomSheetVerificationChildFragmentBinding>(),
class SelfVerificationFragment : VectorBaseFragment<BottomSheetVerificationChildFragmentBinding>(),
SelfVerificationController.InteractionListener {

@Inject lateinit var controller: SelfVerificationController

private var requestAccessibilityFocus: Boolean = false
private val modelBuildListener: OnModelBuildFinishedListener = OnModelBuildFinishedListener {
if (requestAccessibilityFocus) {
// Do not use giveAccessibilityFocusOnce() here.
views.bottomSheetVerificationRecyclerView.layoutManager?.findViewByPosition(0)?.giveAccessibilityFocus()
requestAccessibilityFocus = false
// Note: it does not work when the recycler view is displayed for the first time, because findViewByPosition(0) is null
}
}

private val viewModel by parentFragmentViewModel(SelfVerificationViewModel::class)

override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetVerificationChildFragmentBinding {
Expand All @@ -58,17 +75,22 @@ class SelfVerificationFragment : VectorBaseFragment<BottomSheetVerificationChil

override fun onDestroyView() {
views.bottomSheetVerificationRecyclerView.cleanup()
controller.removeModelBuildListener(modelBuildListener)
controller.listener = null
super.onDestroyView()
}

private fun setupRecyclerView() {
views.bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
controller.addModelBuildListener(modelBuildListener)
controller.listener = this
}

override fun invalidate() = withState(viewModel) { state ->
// Timber.w("VALR: invalidate with State: ${state.pendingRequest}")
if (state.isNewScreen()) {
requestAccessibilityFocus = true
}
controller.update(state)
}

Expand Down Expand Up @@ -176,4 +198,41 @@ class SelfVerificationFragment : VectorBaseFragment<BottomSheetVerificationChil
override fun declineRequest() {
viewModel.handle(VerificationAction.CancelPendingVerification)
}

private var currentScreenIndex = -1

private fun SelfVerificationViewState.isNewScreen(): Boolean {
val newIndex = toScreenIndex()
if (currentScreenIndex == newIndex) {
return false
}
currentScreenIndex = newIndex
return true
}

private fun SelfVerificationViewState.toScreenIndex(): Int {
return if (activeAction !is UserAction.None) {
when (activeAction) {
UserAction.ConfirmCancel -> 30
UserAction.None -> 31
}
} else {
when (pendingRequest) {
is Fail -> 0
is Loading -> 1
is Success -> when (pendingRequest.invoke().state) {
EVerificationState.WaitingForReady -> 10
EVerificationState.Requested -> 11
EVerificationState.Ready -> 12
EVerificationState.Started -> 13
EVerificationState.WeStarted -> 14
EVerificationState.WaitingForDone -> 15
EVerificationState.Done -> 16
EVerificationState.Cancelled -> 17
EVerificationState.HandledByOtherSession -> 18
}
Uninitialized -> 2
}
}
}
}
Loading