11package expo.modules.ama
22
33import android.graphics.Color
4+ import android.graphics.Rect
45import android.graphics.drawable.ColorDrawable
6+ import android.util.DisplayMetrics
57import android.view.View
68import android.view.ViewGroup
79import android.widget.TextView
@@ -13,7 +15,9 @@ import kotlin.math.pow
1315data class A11yIssue (
1416 val type : RuleAction ,
1517 val rule : Rule ,
16- val message : String? = null ,
18+ val label : String? = null ,
19+ val reason : String? = null ,
20+ val bounds : List <Int >,
1721 val viewId : Int? = null ,
1822)
1923
@@ -69,11 +73,14 @@ val LOGGER_RULES: Map<Rule, RuleAction> =
6973class A11yChecker (private val appContext : AppContext , private val config : AMAConfig ) {
7074 private val issues = Collections .synchronizedList(mutableListOf<A11yIssue >())
7175 private val highlighter = Highlight (appContext)
76+ private lateinit var rootView: View
7277
73- public fun performA11yChecks (rootView : View ? ): List <Map <String , Any ?>> {
78+ public fun performA11yChecks (rootView : View ): List <Map <String , Any ?>> {
7479 Logger .info(" performA11yChecks" , " Performing a11y checks" )
7580
76- rootView?.let { root ->
81+ this .rootView = rootView
82+
83+ rootView.let { root ->
7784 traverseAndCheck(root)
7885
7986 if (issues.isNotEmpty()) {
@@ -83,7 +90,10 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
8390 issues.map { issue ->
8491 mapOf (
8592 " type" to issue.type,
86- " message" to (issue.message ? : " " ),
93+ " rule" to issue.rule,
94+ " label" to (issue.label ? : " " ),
95+ " reason" to (issue.reason ? : " " ),
96+ " bounds" to issue.bounds,
8797 " viewId" to issue.viewId
8898 )
8999 }
@@ -114,9 +124,7 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
114124 }
115125 }
116126
117- private fun getRuleSeverity (rule : String ) {}
118-
119- private fun addIssue (rule : Rule , message : String , view : View ) {
127+ private fun addIssue (rule : Rule , label : String , reason : String , view : View ) {
120128 val action = getRuleAction(rule)
121129
122130 if (action == RuleAction .IGNORE ) {
@@ -128,7 +136,16 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
128136 if (existingIssue == null ) {
129137 view.id.let { id -> highlighter.highlight(id, config.highlight, action) }
130138
131- issues.add(A11yIssue (type = action, rule = rule, message = message, viewId = view.id))
139+ issues.add(
140+ A11yIssue (
141+ type = action,
142+ rule = rule,
143+ label = label,
144+ reason = reason,
145+ bounds = view.getGlobalDpBounds(this .rootView),
146+ viewId = view.id
147+ )
148+ )
132149 }
133150 }
134151
@@ -157,7 +174,8 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
157174
158175 addIssue(
159176 rule = Rule .NO_ACCESSIBILITY_LABEL ,
160- message = view.getTextOrContent() + " is missing the accessibility label." ,
177+ label = view.getTextOrContent(),
178+ reason = " is missing the accessibility label." ,
161179 view = view
162180 )
163181 }
@@ -182,7 +200,8 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
182200
183201 addIssue(
184202 rule = Rule .NO_ACCESSIBILITY_ROLE ,
185- message = view.getTextOrContent() + " is missing the accessibility role." ,
203+ label = view.getTextOrContent(),
204+ reason = " is missing the accessibility role." ,
186205 view = view
187206 )
188207 }
@@ -194,7 +213,8 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
194213
195214 addIssue(
196215 rule = Rule .MINIMUM_SIZE ,
197- message =
216+ label = view.toString(),
217+ reason =
198218 " The touchable area must have a minimum size of 48x48, found ${view.width} x${view.height} instead" ,
199219 view = view
200220 )
@@ -222,7 +242,8 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
222242 if (contrastRatio < minContrast) {
223243 addIssue(
224244 rule = Rule .CONTRAST_FAILED ,
225- message =
245+ label = textView.toString(),
246+ reason =
226247 " Color contrast ratio ${String .format(" %.2f" , contrastRatio)} is below minimum ${minContrast} (WCAG AA)" ,
227248 view = textView
228249 )
@@ -323,3 +344,25 @@ fun View.getTextOrContent(): String {
323344
324345 return a11yInfo.contentDescription?.toString().orEmpty()
325346}
347+
348+ private fun View.getGlobalDpBounds (rootView : View ): List <Int > {
349+ // 1) grab absolute screen bounds
350+ val abs = Rect ().also { createAccessibilityNodeInfo().getBoundsInScreen(it) }
351+
352+ // 2) find your root’s absolute origin
353+ val origin = IntArray (2 ).also { rootView.getLocationOnScreen(it) }
354+ val relLeftPx = abs.left - origin[0 ]
355+ val relTopPx = abs.top - origin[1 ]
356+ val widthPx = abs.width()
357+ val heightPx = abs.height()
358+
359+ // 3) convert px → dp
360+ val metrics: DisplayMetrics = resources.displayMetrics
361+ val d = metrics.density
362+ val leftDp = (relLeftPx / d).toInt()
363+ val topDp = (relTopPx / d).toInt()
364+ val widthDp = (widthPx / d).toInt()
365+ val heightDp = (heightPx / d).toInt()
366+
367+ return listOf (leftDp, topDp, widthDp, heightDp)
368+ }
0 commit comments