-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathTextScalingRule.kt
More file actions
59 lines (51 loc) · 2.66 KB
/
Copy pathTextScalingRule.kt
File metadata and controls
59 lines (51 loc) · 2.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.composea11yscanner.rules
import com.composea11yscanner.core.model.A11yIssue
import com.composea11yscanner.core.model.A11yNode
import com.composea11yscanner.core.model.A11ySeverity
import com.composea11yscanner.core.rule.BaseScanRule
import kotlin.math.roundToInt
/**
* @param screenDensity Display density from DisplayMetrics.density.
* @param scaleFactor Font scale to simulate (default 1.3×).
*/
class TextScalingRule(
private val screenDensity: Float,
private val scaleFactor: Float = 1.3f,
) : BaseScanRule() {
override val ruleId = "text-scaling"
override val ruleName = "Text Scaling"
override val severity = A11ySeverity.Warning
override val wcagReference = "WCAG 1.4.4 Resize Text (Level AA)"
override fun evaluateAll(nodes: List<A11yNode>): List<A11yIssue> =
nodes
.filter { it.composableName.contains("Text", ignoreCase = true) }
.mapNotNull { textNode ->
val parent = findParent(textNode, nodes) ?: return@mapNotNull null
val scaledHeight = textNode.bounds.height * scaleFactor
val overflowPx = (textNode.bounds.top + scaledHeight) - parent.bounds.bottom
if (overflowPx <= 0f) return@mapNotNull null
val originalDp = (textNode.bounds.height / screenDensity).roundToInt()
val scaledDp = (scaledHeight / screenDensity).roundToInt()
val overflowDp = (overflowPx / screenDensity).roundToInt()
issue(
node = textNode,
message = "'${textNode.composableName}' may clip at ${scaleFactor}× font scale: " +
"height grows from ${originalDp}dp to ${scaledDp}dp, " +
"overflowing its container by ${overflowDp}dp.",
howToFix = "Remove fixed heights from the parent container, use " +
"wrapContentHeight(), or wrap the content in a verticalScroll " +
"so text can reflow without clipping.",
)
}
// Tightest enclosing node at depth - 1 (smallest area that still fully contains the text node).
private fun findParent(node: A11yNode, allNodes: List<A11yNode>): A11yNode? =
allNodes
.filter { candidate ->
candidate.depth == node.depth - 1 &&
candidate.bounds.left <= node.bounds.left &&
candidate.bounds.top <= node.bounds.top &&
candidate.bounds.right >= node.bounds.right &&
candidate.bounds.bottom >= node.bounds.bottom
}
.minByOrNull { it.bounds.width * it.bounds.height }
}