Skip to content

Commit 0a799a9

Browse files
authored
TextInputView accessibility improvements (#38)
* Update material version * Update hint content description to include error and counter * Improve counter message * Update content description where text has been entered * Tidy up and update changelog * ensure character count is updated each time * Tidy up * Fix space in annoucement * tidy up
1 parent e43e0fa commit 0a799a9

File tree

6 files changed

+75
-26
lines changed

6 files changed

+75
-26
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ Allowed headings:
1616

1717
## [Unreleased]
1818

19-
### Changed
19+
### Added
2020

2121
* Added ability to set error content descriptions on `TextInputView`
22+
23+
### Changed
24+
25+
* Improved content description for `TextInputView` to announce the error and counter status along with the hint on talkback.
2226
* Upgrade target and compile sdk versions to 30
2327
* Upgrade Gradle plugin to 4.1.3
2428
* Upgrade Kotlin version to 1.4.20

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ multi_column_row.setText1AsHeading(true)
285285
android:layout_width="match_parent"
286286
android:layout_height="wrap_content"
287287
app:text="@string/text"
288-
app:overrideHintContentDescription="@string/content_description"
288+
app:hintContentDescription="@string/content_description"
289289
app:counterEnabled="true"
290290
app:counterMaxLength="@integer/max_length"
291291
app:hintText="@string/hint_text" />

components/src/main/java/uk/gov/hmrc/components/molecule/input/TextInputView.kt

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
package uk.gov.hmrc.components.molecule.input
1717

1818
import android.content.Context
19+
import android.os.Build.VERSION
20+
import android.os.Build.VERSION_CODES.O
1921
import android.os.Bundle
2022
import android.os.Parcelable
2123
import android.text.InputFilter
@@ -52,14 +54,15 @@ open class TextInputView @JvmOverloads constructor(
5254
binding.root.id = value
5355
}
5456

57+
private var hintContentDescription: String? = null
58+
5559
init {
5660

5761
attrs?.let {
5862
val typedArray = context.theme.obtainStyledAttributes(it, R.styleable.TextInputView, 0, 0)
5963

6064
val textString = typedArray.getString(R.styleable.TextInputView_text) ?: ""
61-
val overrideHintContentDescription = typedArray.getString(
62-
R.styleable.TextInputView_overrideHintContentDescription)
65+
val hintTextContentDescription = typedArray.getString(R.styleable.TextInputView_hintContentDescription)
6366
val hintText = typedArray.getString(R.styleable.TextInputView_hintText) ?: ""
6467
val errorText = typedArray.getString(R.styleable.TextInputView_errorText) ?: ""
6568
val counterMaxLength = typedArray.getInt(R.styleable.TextInputView_counterMaxLength, NO_MAX_LENGTH)
@@ -70,8 +73,7 @@ open class TextInputView @JvmOverloads constructor(
7073
val maxLength = typedArray.getInt(R.styleable.TextInputView_android_maxLength, -1)
7174

7275
setText(textString)
73-
overrideHintContentDescription(overrideHintContentDescription)
74-
setHint(hintText)
76+
setHint(hintText, hintTextContentDescription)
7577
setError(errorText)
7678
setCounterMaxLength(counterMaxLength)
7779
setCounterEnabled(counterEnabled)
@@ -108,30 +110,20 @@ open class TextInputView @JvmOverloads constructor(
108110

109111
fun getText(): String? = getEditText().text?.toString()
110112

111-
fun overrideHintContentDescription(contentDescription: CharSequence?) {
112-
contentDescription ?: return
113-
114-
binding.root.apply {
115-
setTextInputAccessibilityDelegate(object : TextInputLayout.AccessibilityDelegate(this) {
116-
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
117-
super.onInitializeAccessibilityNodeInfo(host, info)
118-
val before = info.text.toString()
119-
val after = before.replace(hint.toString(), contentDescription.toString())
120-
info.text = after
121-
}
122-
})
123-
}
124-
}
125-
126-
fun setHint(hint: CharSequence) {
113+
fun setHint(hint: CharSequence, contentDescription: CharSequence? = null) {
127114
binding.root.hint = hint
115+
hintContentDescription = (contentDescription ?: hint).toString()
116+
updateTextInputViewContentDescription()
128117
}
129118

130119
fun getError() = binding.root.error
131120

132121
fun setError(errorText: CharSequence?, errorContentDescription: CharSequence? = null) {
133122
binding.root.error = errorText
134-
binding.root.errorContentDescription = errorContentDescription ?: errorText
123+
binding.root.errorContentDescription = errorContentDescription ?: if (!errorText.isNullOrEmpty()) {
124+
context.getString(R.string.accessibility_error_prefix, errorText)
125+
} else null
126+
updateTextInputViewContentDescription()
135127
}
136128

137129
fun setErrorText(@StringRes error: Int?, @StringRes errorContentDescription: Int? = null) {
@@ -145,10 +137,12 @@ open class TextInputView @JvmOverloads constructor(
145137

146138
fun setCounterMaxLength(maxLength: Int) {
147139
binding.root.counterMaxLength = maxLength
140+
updateTextInputViewContentDescription()
148141
}
149142

150143
fun setCounterEnabled(enabled: Boolean) {
151144
binding.root.isCounterEnabled = enabled
145+
updateTextInputViewContentDescription()
152146
}
153147

154148
fun setPrefixText(prefixText: CharSequence?) {
@@ -177,6 +171,53 @@ open class TextInputView @JvmOverloads constructor(
177171

178172
fun getEditText(): TextInputEditText = binding.root.findViewWithTag("edit_text")
179173

174+
private fun updateTextInputViewContentDescription() {
175+
binding.root.apply {
176+
setTextInputAccessibilityDelegate(object : TextInputLayout.AccessibilityDelegate(this) {
177+
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
178+
super.onInitializeAccessibilityNodeInfo(host, info)
179+
180+
val customHint = hintContentDescription ?: hint
181+
182+
val error = if (errorContentDescription.isNullOrEmpty()) "" else ", $errorContentDescription"
183+
184+
val counter = if (isCounterEnabled) {
185+
val currentChars = if (getText().isNullOrEmpty()) 0 else getText()!!.length
186+
val maxLength = counterMaxLength
187+
188+
val limitExceededText = if (currentChars > maxLength) {
189+
"${context.getString(R.string.accessibility_counter_limit_exceeded)} "
190+
} else ""
191+
192+
val counterText = context.getString(
193+
R.string.accessibility_counter_state,
194+
currentChars.toString(),
195+
maxLength.toString())
196+
197+
", $limitExceededText$counterText"
198+
} else ""
199+
200+
val newContentDescription = "$customHint$error$counter"
201+
202+
val showingText = !getText().isNullOrEmpty()
203+
if (VERSION.SDK_INT >= O) {
204+
if (showingText) {
205+
info.hintText = newContentDescription
206+
} else {
207+
info.text = newContentDescription
208+
}
209+
} else {
210+
// Due to a TalkBack bug, setHintText has no effect in APIs < 26 so we append the hint to
211+
// the text announcement. The resulting announcement is the same as in APIs >= 26.
212+
info.text = if (showingText) {
213+
getText() + ", " + newContentDescription
214+
} else newContentDescription
215+
}
216+
}
217+
})
218+
}
219+
}
220+
180221
companion object {
181222
private const val STATE_TEXT = "STATE_TEXT"
182223
private const val STATE_SUPER = "STATE_SUPER"

components/src/main/res/values/attrs.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
<!-- Text to display. -->
4040
<attr name="text" />
4141
<!-- Hint content description. -->
42-
<attr name="overrideHintContentDescription" format="string" />
42+
<attr name="hintContentDescription" format="string" />
4343
<!-- Hint text. -->
4444
<attr name="hintText" format="string" />
4545
<!-- Error text. -->

components/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
<string name="accessibility_radio_button">radio button</string>
3838
<string name="accessibility_list_position">%1d of %2d</string>
3939
<string name="accessibility_tab_position">tab %1d of %2d</string>
40+
<string name="accessibility_error_prefix">Error: %s</string>
41+
<string name="accessibility_counter_state">%1$s characters of %2$s used</string>
42+
<string name="accessibility_counter_limit_exceeded">Character limit exceeded</string>
4043

4144
<string name="currency_input_prefix">£</string>
4245
</resources>

sample/src/main/res/layout/fragment_text_input.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
android:layout_marginStart="@dimen/hmrc_spacing_16"
3131
android:layout_marginTop="@dimen/hmrc_spacing_16"
3232
android:layout_marginEnd="@dimen/hmrc_spacing_16"
33-
app:contentDescription="@string/text_input_placeholder_hint"
3433
app:counterEnabled="true"
3534
app:counterMaxLength="@integer/text_input_placeholder_max_length"
3635
app:hintText="@string/text_input_placeholder_hint" />
@@ -57,7 +56,9 @@
5756
android:layout_marginStart="@dimen/hmrc_spacing_16"
5857
android:layout_marginTop="@dimen/hmrc_spacing_16"
5958
android:layout_marginEnd="@dimen/hmrc_spacing_16"
60-
app:overrideHintContentDescription="@string/text_input_example_1_content_description"
59+
app:counterEnabled="true"
60+
app:counterMaxLength="@integer/text_input_placeholder_max_length"
61+
app:hintContentDescription="@string/text_input_example_1_content_description"
6162
app:hintText="@string/text_input_example_1_hint" />
6263

6364
<uk.gov.hmrc.components.molecule.input.TextInputView

0 commit comments

Comments
 (0)