Skip to content

Commit ce134e1

Browse files
committed
move example to playground
1 parent 2fcff20 commit ce134e1

127 files changed

Lines changed: 852 additions & 1042 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ coverage
4848
!.yarn/sdks
4949
!.yarn/versions
5050
example/.yarn
51+
playground/.yarn/cache/

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ export const App = () => {
6363
};
6464
```
6565

66+
### Playground
67+
68+
You can use the playground app within this repository to see how AMA checks work in practice.
69+
The playground can also be used to learn how to fix accessibility issues in your app.
70+
71+
6672
## 📃 [Documentation](https://commerce.nearform.com/open-source/react-native-ama)
6773

6874
The documentation contains everything you need to know about `@react-native-ama/...`. It contains several sections in order of importance when you first get started:

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
"repository": "https://github.com/FormidableLabs/react-native-ama.git",
77
"homepage": "https://commerce.nearform.com/open-source/react-native-ama/",
88
"scripts": {
9-
"example:bare": "yarn --cwd examples/bare",
10-
"example:expo": "yarn --cwd examples/expo",
9+
"playground": "yarn --cwd playground",
10+
"playground:ios": "yarn --cwd playground run ios",
1111
"build:animations": "yarn workspace @react-native-ama/animations build",
1212
"build:core": "yarn workspace @react-native-ama/core build",
1313
"build:extras": "yarn workspace @react-native-ama/extras build",
@@ -20,7 +20,8 @@
2020
"ts:check": "tsc --noEmit",
2121
"lint": "eslint --ext .js,.ts,.tsx ./packages",
2222
"version": "yarn changeset version && yarn install --no-frozen-lockfile",
23-
"build": "yarn build:internal && yarn build:core && yarn build:animations && yarn build:react-native && yarn build:extras && yarn build:forms && yarn build:lists"
23+
"build": "yarn build:internal && yarn build:core && yarn build:animations && yarn build:react-native && yarn build:extras && yarn build:forms && yarn build:lists",
24+
"prettier": "yarn prettier --write 'packages/**/*.{js,jsx,ts,tsx}'"
2425
},
2526
"workspaces": [
2627
"packages/animations",

packages/core/android/src/debug/java/expo/modules/ama/ReactNativeAmaModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ class ReactNativeAmaModule : Module() {
9898

9999
private fun performA11yChecks() {
100100
Logger.info("performA11yChecks", "doing the job")
101-
val issues = a11yChecker.performA11yChecks(currentDecorView)
101+
val issues = a11yChecker.performA11yChecks(currentDecorView!!)
102102

103103
sendEvent(
104104
"onA11yIssues",

packages/core/android/src/debug/java/expo/modules/ama/a11yChecker.kt

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package expo.modules.ama
22

33
import android.graphics.Color
4+
import android.graphics.Rect
45
import android.graphics.drawable.ColorDrawable
6+
import android.util.DisplayMetrics
57
import android.view.View
68
import android.view.ViewGroup
79
import android.widget.TextView
@@ -13,7 +15,9 @@ import kotlin.math.pow
1315
data 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> =
6973
class 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+
}

packages/core/android/src/debug/java/expo/modules/ama/highlight.kt

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,27 +40,62 @@ class Highlight(private val appContext: AppContext) {
4040
}
4141
}
4242

43+
private fun applyGridBackground(view: View, color: Int?) {
44+
val stripeSize = 40
45+
val bmp = Bitmap.createBitmap(stripeSize * 2, stripeSize * 2, Bitmap.Config.ARGB_8888)
46+
val canvas = Canvas(bmp)
47+
val paint =
48+
Paint().apply {
49+
style = Paint.Style.FILL
50+
}
51+
canvas.drawRect(0f, 0f, stripeSize.toFloat(), stripeSize.toFloat(), paint)
52+
canvas.drawRect(
53+
stripeSize.toFloat(),
54+
stripeSize.toFloat(),
55+
stripeSize * 2f,
56+
stripeSize * 2f,
57+
paint
58+
)
59+
60+
val shader = BitmapShader(bmp, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
61+
val bgPaint = Paint().apply { this.shader = shader }
62+
val drawable =
63+
object : Drawable() {
64+
override fun draw(c: Canvas) = c.drawRect(bounds, bgPaint)
65+
override fun setAlpha(alpha: Int) {
66+
bgPaint.alpha = alpha
67+
}
68+
override fun setColorFilter(cf: ColorFilter?) {
69+
bgPaint.colorFilter = cf
70+
}
71+
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
72+
}
73+
74+
view.background = drawable
75+
}
76+
4377
private fun applyStripyBackground(view: View, color: Int?) {
4478
val originalBackground = view.background
4579

46-
val stripeWidth = 25f // The width of a single black stripe
47-
val gapWidth = 25f // The width of the transparent gap between stripes
48-
val bmpSize = 150 // The size of the bitmap tile we'll create (e.g., 100x100 pixels)
80+
val stripeWidth = 5f
81+
val gapWidth = 25f
82+
val bmpWidth = 150
83+
val bmpHeight = bmpWidth
4984

50-
val bmp = Bitmap.createBitmap(bmpSize, bmpSize * 2, Bitmap.Config.ARGB_8888)
85+
val bmp = Bitmap.createBitmap(bmpWidth, bmpHeight, Bitmap.Config.ARGB_8888)
5186
val canvas = Canvas(bmp)
5287

5388
val stripePaint =
5489
Paint().apply {
5590
this.color = color ?: Color.RED
56-
isAntiAlias = true // For smooth edges
91+
isAntiAlias = true
5792
}
5893

59-
val canvasCenter = bmpSize / 2f
94+
val canvasCenter = bmpWidth / 2f
6095
canvas.save()
6196
canvas.rotate(-45f, canvasCenter, canvasCenter)
6297

63-
val extendedSize = (bmpSize * 1.5f)
98+
val extendedSize = (bmpWidth * 1.5f)
6499
var y = -extendedSize
65100
while (y < extendedSize) {
66101
canvas.drawRect(-extendedSize, y, extendedSize, y + stripeWidth, stripePaint)
@@ -69,7 +104,7 @@ class Highlight(private val appContext: AppContext) {
69104

70105
canvas.restore()
71106

72-
val shader = BitmapShader(bmp, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
107+
val shader = BitmapShader(bmp, Shader.TileMode.MIRROR, Shader.TileMode.REPEAT)
73108

74109
val backgroundPaint = Paint().apply { this.shader = shader }
75110

@@ -103,7 +138,7 @@ class Highlight(private val appContext: AppContext) {
103138
private fun applyRedBorderOverlay(view: View, color: Int?) {
104139
view.overlay.clear()
105140

106-
val stroke = 6f // px
141+
val stroke = 6f
107142
val half = (stroke / 2).toInt()
108143
val shape =
109144
ShapeDrawable(RectShape()).apply {

0 commit comments

Comments
 (0)