Skip to content

Commit 4bda09f

Browse files
Fix #5915: Improve TalkBack content description for concept card links (#6033)
## Explanation Fix #5915: Improve TalkBack reading for concept card links This PR updates the accessibility content description for the `<oppia-noninteractive-skillreview>` custom HTML tag. Earlier, TalkBack skipped the concept card link values. Now, TalkBack consistently reads: “<skill_id> concept card” This improves the screen-reader experience in: - Hint section - Solution section - Flashback solution section ## What this fixes TalkBack was not reading concept-card links correctly because the custom HTML tag did not provide a clear accessibility description. This PR fixes the issue and makes the content accessible for learners using screen readers. ## What was changed Updated `ConceptCardTagHandler.getContentDescription()` → Now returns a clean, consistent description using only the`skill_id.` Updated `SolutionViewModel` → Now uses `CustomHtmlContentHandler.getContentDescription() `together with `ConceptCardTagHandler` to ensure correct accessibility parsing across hint and solution screens. Additional note This PR also includes an auto-format update to `scripts/assets/kdoc_validity_exemptions.textproto` generated automatically by ktlint. No functional changes were made. ## Testing done Tested TalkBack behavior in all places where concept-card links appear: - Hint section - Solution section - Flashback mode TalkBack now reads the concept card link: - Exactly once - In the correct format - With the correct text ## Ready for review Screen recording demonstrating TalkBack reading concept card links correctly in all 3 places: 🔗https://drive.google.com/file/d/1y6pqNhccfw5rjqeOtr_VNsF78jvSTvPN/view?usp=sharing ## Essential Checklist <!-- Please tick the relevant boxes by putting an "x" in them. --> - [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".) - [x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)). - [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-o-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)).
1 parent 8dd48de commit 4bda09f

8 files changed

Lines changed: 40 additions & 23 deletions

File tree

‎app/src/main/java/org/oppia/android/app/hintsandsolution/SolutionViewModel.kt‎

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.oppia.android.app.hintsandsolution
22

3+
import android.view.View
34
import androidx.databinding.ObservableBoolean
45
import org.oppia.android.app.model.Interaction
56
import org.oppia.android.app.model.InteractionObject
@@ -29,6 +30,7 @@ import org.oppia.android.app.translation.AppLanguageResourceHandler
2930
import org.oppia.android.app.utility.math.MathExpressionAccessibilityUtil
3031
import org.oppia.android.app.utility.toAccessibleAnswerString
3132
import org.oppia.android.app.view.models.R
33+
import org.oppia.android.util.logging.ConsoleLogger
3234
import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY
3335
import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult
3436
import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation
@@ -38,6 +40,8 @@ import org.oppia.android.util.math.isApproximatelyEqualTo
3840
import org.oppia.android.util.math.toAnswerString
3941
import org.oppia.android.util.math.toPlainString
4042
import org.oppia.android.util.math.toRawLatex
43+
import org.oppia.android.util.parser.html.CUSTOM_CONCEPT_CARD_TAG
44+
import org.oppia.android.util.parser.html.ConceptCardTagHandler
4145
import org.oppia.android.util.parser.html.CustomHtmlContentHandler
4246
import javax.inject.Inject
4347

@@ -61,18 +65,24 @@ class SolutionViewModel private constructor(
6165
private val mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil,
6266
val explorationId: String,
6367
val isFlashback: Boolean,
64-
val solutionBoxStrokeWidth: Int
68+
val solutionBoxStrokeWidth: Int,
69+
private val consoleLogger: ConsoleLogger
6570
) {
66-
/**
67-
* A screenreader-friendly version of [solutionSummary] that should be used for readout, in place
68-
* of the original summary.
69-
*/
71+
/** Lazily computes the accessibility content description for the solution summary HTML content. */
7072
val solutionSummaryContentDescription by lazy {
71-
CustomHtmlContentHandler.fromHtml(
73+
CustomHtmlContentHandler.getContentDescription(
7274
solutionSummary,
73-
imageRetriever = null,
74-
customTagHandlers = mapOf()
75-
).toString()
75+
customTagHandlers = mapOf(
76+
CUSTOM_CONCEPT_CARD_TAG to ConceptCardTagHandler(
77+
listener = object : ConceptCardTagHandler.ConceptCardLinkClickListener {
78+
override fun onConceptCardLinkClicked(view: View, skillId: String) {
79+
// No action for TalkBack readout
80+
}
81+
},
82+
consoleLogger = consoleLogger
83+
)
84+
)
85+
)
7686
}
7787

7888
/** A displayable HTML representation of the correct answer presented by this model's solution. */
@@ -239,7 +249,8 @@ class SolutionViewModel private constructor(
239249
/** Application-injectable factory to create [SolutionViewModel]s (see [create]). */
240250
class Factory @Inject constructor(
241251
private val appLanguageResourceHandler: AppLanguageResourceHandler,
242-
private val mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil
252+
private val mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil,
253+
private val consoleLogger: ConsoleLogger
243254
) {
244255
/**
245256
* Returns a new [SolutionViewModel] with the specified summary HTML text, correct answer,
@@ -269,7 +280,8 @@ class SolutionViewModel private constructor(
269280
mathExpressionAccessibilityUtil,
270281
explorationId,
271282
isFlashback,
272-
solutionBoxStrokeWidth
283+
solutionBoxStrokeWidth,
284+
consoleLogger
273285
)
274286
}
275287

‎app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1682,8 +1682,8 @@ class ExplorationActivityTest {
16821682
.check(
16831683
matches(
16841684
withContentDescription(
1685-
"Remember that two halves, when added together, make one whole." +
1686-
"\nClick on this test_skill_id_1 concept card."
1685+
"Remember that two halves, when added together, make one whole.\n" +
1686+
"Click on this test_skill_id_1 concept card."
16871687
)
16881688
)
16891689
)

‎app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5600,7 +5600,7 @@ class StateFragmentTest {
56005600
onView(withId(R.id.solution_summary_label)).check(matches(withText("Explanation:")))
56015601

56025602
val expectedSolutionSummary = "Half of something has one part in the numerator for" +
5603-
" every two parts in the denominator."
5603+
" every two parts in the denominator.\n\n"
56045604
onView(withId(R.id.solution_summary))
56055605
.check(matches(withText(containsString(expectedSolutionSummary))))
56065606

@@ -5652,7 +5652,7 @@ class StateFragmentTest {
56525652
onView(withId(R.id.solution_summary_label)).check(matches(withText("Explanation:")))
56535653

56545654
val expectedSolutionSummary = "Half of something has one part in the numerator for" +
5655-
" every two parts in the denominator."
5655+
" every two parts in the denominator.\n\n"
56565656
onView(withId(R.id.solution_summary))
56575657
.check(matches(withText(containsString(expectedSolutionSummary))))
56585658

‎app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,8 +1317,8 @@ class StateFragmentLocalTest {
13171317
onView(withId(R.id.solution_summary)).check(
13181318
matches(
13191319
withContentDescription(
1320-
"Start by dividing the cake into equal parts:\n\nThree of " +
1321-
"the four equal parts are red. So, the answer is 3/4.\n\n"
1320+
"Start by dividing the cake into equal parts:" +
1321+
"\nThree of the four equal parts are red. So, the answer is 3/4."
13221322
)
13231323
)
13241324
)

‎scripts/assets/kdoc_validity_exemptions.textproto‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,6 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/threading/T
299299
exempted_file_path: "testing/src/main/java/org/oppia/android/testing/threading/TestCoroutineDispatchersRobolectricImpl.kt"
300300
exempted_file_path: "testing/src/test/java/org/oppia/android/testing/threading/TestCoroutineDispatcherTestBase.kt"
301301
exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/LogLevel.kt"
302-
exempted_file_path: "utility/src/main/java/org/oppia/android/util/parser/html/ConceptCardTagHandler.kt"
303302
exempted_file_path: "utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt"
304303
exempted_file_path: "utility/src/main/java/org/oppia/android/util/parser/image/UrlImageParser.kt"
305304
exempted_file_path: "utility/src/main/java/org/oppia/android/util/parser/svg/ScalableVectorGraphic.kt"

‎utility/src/main/java/org/oppia/android/util/parser/html/ConceptCardTagHandler.kt‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@ import javax.inject.Inject
1111

1212
/** The custom tag corresponding to [ConceptCardTagHandler]. */
1313
const val CUSTOM_CONCEPT_CARD_TAG = "oppia-noninteractive-skillreview"
14+
/** The attribute key used to extract the concept card skill ID from the custom tag. */
1415
const val CUSTOM_CONCEPT_CARD_SKILL_ID = "skill_id-with-value"
16+
/** The attribute key used to extract the visible concept card text from the custom tag. */
1517
const val CUSTOM_CONCEPT_CARD_TEXT_VALUE = "text-with-value"
1618

1719
// https://mohammedlakkadshaw.com/blog/handling-custom-tags-in-android-using-html-taghandler.html/
20+
/**
21+
* A custom tag handler responsible for converting concept card custom tags into clickable spans and
22+
* generating the appropriate accessibility content description.
23+
*/
1824
class ConceptCardTagHandler(
1925
private val listener: ConceptCardLinkClickListener,
2026
private val consoleLogger: ConsoleLogger
@@ -53,9 +59,9 @@ class ConceptCardTagHandler(
5359
}
5460

5561
override fun getContentDescription(attributes: Attributes): String? {
56-
val text = attributes.getJsonStringValue(CUSTOM_CONCEPT_CARD_TEXT_VALUE)
57-
return if (!text.isNullOrBlank()) {
58-
text
62+
val skillId = attributes.getJsonStringValue(CUSTOM_CONCEPT_CARD_SKILL_ID)
63+
return if (!skillId.isNullOrBlank()) {
64+
"$skillId concept card"
5965
} else ""
6066
}
6167

‎utility/src/test/java/org/oppia/android/util/parser/html/ConceptCardTagHandlerTest.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ class ConceptCardTagHandlerTest {
124124
html = CONCEPT_CARD_LINK_MARKUP_1,
125125
customTagHandlers = tagHandlersWithConceptCardSupport
126126
)
127-
assertThat(contentDescription).isEqualTo("refresher lesson")
127+
assertThat(contentDescription).isEqualTo("skill_id_1 concept card")
128128
}
129129

130130
@Test

‎utility/src/test/java/org/oppia/android/util/parser/html/HtmlParserTest.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -895,7 +895,7 @@ class HtmlParserTest {
895895
textView.text = htmlResult
896896

897897
assertThat(textView.contentDescription.toString()).isEqualTo(
898-
"Visit refresher lesson to learn more."
898+
"Visit skill_id_1 concept card to learn more."
899899
)
900900
}
901901
}

0 commit comments

Comments
 (0)