diff --git a/README.md b/README.md
index 331d11b..e1ecc62 100644
--- a/README.md
+++ b/README.md
@@ -102,7 +102,6 @@ See [RULES.md](RULES.md) for complete behavior, fixes, WCAG references, and exam
| Rule ID | Name | Severity | Details |
| --- | --- | --- | --- |
-| `touch-target-size` | Touch Target Size | Error | [RULES.md](RULES.md#touch-target-size---touch-target-size) |
| `missing-content-description` | Missing Content Description | Error | [RULES.md](RULES.md#missing-content-description---missing-content-description) |
| `duplicate-content-description` | Duplicate Content Description | Warning | [RULES.md](RULES.md#duplicate-content-description---duplicate-content-description) |
| `focus-order` | Focus Order | Error | [RULES.md](RULES.md#focus-order---focus-order) |
diff --git a/RULES.md b/RULES.md
index 251bba9..7882440 100644
--- a/RULES.md
+++ b/RULES.md
@@ -2,43 +2,18 @@
Compose A11y Scanner ships the built-in rules registered by `ScannerRules.allRuleIds()`.
-Implementation note: the current codebase exposes 7 built-in rules, not 8. This document covers every implemented built-in rule in `scanner-rules`.
+Implementation note: the current codebase exposes 6 built-in rules. This document covers every implemented built-in rule in `scanner-rules`.
## Summary
| Rule ID | Name | Severity | What It Checks | How To Fix | WCAG Reference |
| --- | --- | --- | --- | --- | --- |
-| `touch-target-size` | Touch Target Size | Error | Clickable/touch target nodes whose measured width or height is smaller than the configured minimum, 48dp by default. | Use `Modifier.minimumInteractiveComponentSize()` or add padding so the interactive area is at least the configured minimum in both dimensions. | WCAG 2.5.5 Target Size (Level AA) |
| `missing-content-description` | Missing Content Description | Error | Interactive nodes and image-like nodes that do not expose a non-empty content description. | Add a meaningful `contentDescription` through semantics, or pass one directly to image composables that support it. | WCAG 1.1.1 Non-text Content (Level A) |
| `duplicate-content-description` | Duplicate Content Description | Warning | Non-merged nodes at the same semantics depth that reuse the same non-empty content description. | Give each control or item a label that identifies its specific action, state, or content. | WCAG 2.4.6 Headings and Labels (Level AA) |
| `focus-order` | Focus Order | Error | Focusable nodes whose semantics traversal jumps upward compared with the previous focusable node's visual position. | Reorder composables so focus follows the visual reading order, or set explicit traversal order with semantics. | WCAG 2.4.3 Focus Order (Level A) |
| `text-scaling` | Text Scaling | Warning | Text nodes that may overflow or clip inside their parent when simulated at a larger font scale. | Avoid fixed-height containers for text; use flexible height, wrapping, or scrolling so scaled text can reflow. | WCAG 1.4.4 Resize Text (Level AA) |
| `image-text-overlay` | Image With Text Overlay | Warning | Text nodes that significantly overlap image nodes, creating a contrast risk across dynamic images. | Add a scrim or solid text background, or otherwise guarantee sufficient contrast for every image state. | WCAG 1.4.3 Contrast Minimum (Level AA) |
-| `clickable-role` | Clickable Role | Error | Clickable/touch target nodes that do not expose a semantic role, and clickable image roles without a content description. | Add the appropriate role, such as `Role.Button`, `Role.Checkbox`, or `Role.Image`; provide labels for clickable images. | WCAG 4.1.2 Name, Role, Value (Level A) |
-
-## `touch-target-size` - Touch Target Size
-
-**Severity:** Error
-
-**What it checks:** This rule evaluates clickable/touch target nodes that are not merged descendants. It reports a node when either its measured width or height is smaller than `ScannerConfig.minTouchTargetDp`, which defaults to 48dp.
-
-**How to fix:** Ensure the interactive area reaches the minimum size in both dimensions. Prefer Material's `Modifier.minimumInteractiveComponentSize()` for Material controls, or add padding around custom controls.
-
-**WCAG reference:** WCAG 2.5.5 Target Size (Level AA)
-
-**Code example:**
-
-```kotlin
-IconButton(
- onClick = onClose,
- modifier = Modifier.minimumInteractiveComponentSize(),
-) {
- Icon(
- imageVector = Icons.Default.Close,
- contentDescription = "Close",
- )
-}
-```
+| `clickable-role` | Clickable Role | Error | Clickable nodes with role-specific semantic requirements, currently clickable images without a content description. | Provide a meaningful label for clickable images. A generic clickable does not require a role when no predefined Compose role accurately applies. | WCAG 4.1.2 Name, Role, Value (Level A) |
## `missing-content-description` - Missing Content Description
@@ -196,9 +171,9 @@ Box {
**Severity:** Error
-**What it checks:** This rule reports clickable/touch target nodes that are not merged descendants and do not expose a semantic role. It also reports clickable image roles when the content description is empty.
+**What it checks:** This rule reports clickable image nodes when their content description is empty. A generic clickable node is not reported merely because its role is null: Compose permits an unspecified role when none of its predefined roles accurately describes the element, such as a clickable list row.
-**How to fix:** Use Material components when possible because they usually provide roles automatically. For custom click targets, set the appropriate role through semantics or use clickable APIs that expose the role.
+**How to fix:** Provide a meaningful, non-empty content description for a clickable image. For controls with a well-defined role, use the corresponding semantic API—for example, `selectable(role = Role.Tab, ...)` for a custom tab—but do not assign `Role.Button` to generic click targets solely to satisfy this rule.
**WCAG reference:** WCAG 4.1.2 Name, Role, Value (Level A)
@@ -211,8 +186,7 @@ Box(
role = Role.Button
contentDescription = "Retry loading"
}
- .clickable(onClick = onRetry)
- .minimumInteractiveComponentSize(),
+ .clickable(onClick = onRetry),
) {
Text("Retry")
}
diff --git a/sample/src/androidTest/java/com/composea11yscanner/sample/FixedScreenAccessibilityTest.kt b/sample/src/androidTest/java/com/composea11yscanner/sample/FixedScreenAccessibilityTest.kt
index dd43e96..959ee96 100644
--- a/sample/src/androidTest/java/com/composea11yscanner/sample/FixedScreenAccessibilityTest.kt
+++ b/sample/src/androidTest/java/com/composea11yscanner/sample/FixedScreenAccessibilityTest.kt
@@ -3,7 +3,6 @@ package com.composea11yscanner.sample
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.unit.Density
import com.composea11yscanner.core.model.ScannerConfig
import com.composea11yscanner.rules.ScannerRules
import com.composea11yscanner.sample.ui.theme.ScannerTheme
@@ -50,7 +49,6 @@ class FixedScreenAccessibilityTest {
val config = ScannerConfig(enabledRules = ScannerRules.allRuleIds().toSet())
val scanner = A11yScanner(
rules = ScannerRules.buildRules(config, screenDensity = 1f),
- density = Density(1f),
)
val result = scanner.scan(composeRule.onRoot(useUnmergedTree = true).fetchSemanticsNode())
diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml
index 757cbe8..fce926b 100644
--- a/sample/src/main/AndroidManifest.xml
+++ b/sample/src/main/AndroidManifest.xml
@@ -10,9 +10,6 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ComposeA11yScanner">
-
diff --git a/sample/src/main/java/com/composea11yscanner/sample/LoginScreens.kt b/sample/src/main/java/com/composea11yscanner/sample/LoginScreens.kt
index 3a0ef1b..09ab23e 100644
--- a/sample/src/main/java/com/composea11yscanner/sample/LoginScreens.kt
+++ b/sample/src/main/java/com/composea11yscanner/sample/LoginScreens.kt
@@ -47,7 +47,7 @@ fun BrokenLoginScreen(onViewFixed: (() -> Unit)? = null) {
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(18.dp),
) {
- SecureBankHeader()
+ SecureBankHeader(isFixed = false)
LoginIntro()
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
@@ -135,7 +135,7 @@ fun FixedLoginScreen(onViewBroken: (() -> Unit)? = null) {
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(18.dp),
) {
- SecureBankHeader()
+ SecureBankHeader(isFixed = true)
LoginIntro()
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
@@ -239,13 +239,22 @@ fun FixedLoginScreen(onViewBroken: (() -> Unit)? = null) {
}
@Composable
-private fun SecureBankHeader() {
+private fun SecureBankHeader(isFixed: Boolean) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
- BankLogoMark()
+ BankLogoMark(
+ modifier = if (isFixed) {
+ Modifier
+ .semantics { contentDescription = "Open SecureBank home" }
+ .clickable(role = Role.Image, onClick = {})
+ } else {
+ // Intentionally broken: an actionable image needs an accessible description.
+ Modifier.clickable(role = Role.Image, onClick = {})
+ },
+ )
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text = "SecureBank",
diff --git a/sample/src/main/java/com/composea11yscanner/sample/SampleComponents.kt b/sample/src/main/java/com/composea11yscanner/sample/SampleComponents.kt
index 4252950..35dd3e3 100644
--- a/sample/src/main/java/com/composea11yscanner/sample/SampleComponents.kt
+++ b/sample/src/main/java/com/composea11yscanner/sample/SampleComponents.kt
@@ -56,9 +56,9 @@ fun BrokenScreenCard(
}
@Composable
-fun BankLogoMark() {
+fun BankLogoMark(modifier: Modifier = Modifier) {
Box(
- modifier = Modifier
+ modifier = modifier
.size(48.dp)
.clip(RoundedCornerShape(14.dp))
.background(Color(0xFF6C63FF)),
diff --git a/sample/src/main/java/com/composea11yscanner/sample/SampleSemanticsExtractor.kt b/sample/src/main/java/com/composea11yscanner/sample/SampleSemanticsExtractor.kt
index f68eb59..aa0a7d1 100644
--- a/sample/src/main/java/com/composea11yscanner/sample/SampleSemanticsExtractor.kt
+++ b/sample/src/main/java/com/composea11yscanner/sample/SampleSemanticsExtractor.kt
@@ -8,7 +8,6 @@ import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.getOrNull
-import androidx.compose.ui.unit.Density
import com.composea11yscanner.core.model.A11yNode
import com.composea11yscanner.core.model.Rect
import com.composea11yscanner.ui.A11yNodeExtractor
@@ -33,7 +32,7 @@ internal fun ComponentActivity.extractBrokenSampleNodes(): List =
?: sampleRoot.boundsInRoot.let {
Rect(it.left.roundToInt(), it.top.roundToInt(), it.right.roundToInt(), it.bottom.roundToInt())
}
- A11yNodeExtractor(Density(this))
+ A11yNodeExtractor()
.extract(sampleRoot)
.filterVisibleIn(viewport)
}.getOrDefault(emptyList())
diff --git a/scanner-core/src/main/java/com/composea11yscanner/core/model/A11yNode.kt b/scanner-core/src/main/java/com/composea11yscanner/core/model/A11yNode.kt
index 89a8d23..899b9ef 100644
--- a/scanner-core/src/main/java/com/composea11yscanner/core/model/A11yNode.kt
+++ b/scanner-core/src/main/java/com/composea11yscanner/core/model/A11yNode.kt
@@ -8,7 +8,6 @@ package com.composea11yscanner.core.model
* @property bounds Pixel bounds relative to the scanned root.
* @property contentDescription Accessible label exposed by the node, if any.
* @property isTouchTarget True when the node exposes a click action.
- * @property touchTargetSize Size used by touch target rules, expressed in dp.
* @property textColor Foreground text color when it can be extracted.
* @property backgroundColors Candidate background colors sampled behind the node.
* @property isFocusable True when the node can participate in focus traversal.
@@ -22,7 +21,6 @@ data class A11yNode(
val bounds: Rect,
val contentDescription: String?,
val isTouchTarget: Boolean,
- val touchTargetSize: DpSize,
val textColor: Color?,
val backgroundColors: List,
val isFocusable: Boolean,
diff --git a/scanner-core/src/main/java/com/composea11yscanner/core/model/DpSize.kt b/scanner-core/src/main/java/com/composea11yscanner/core/model/DpSize.kt
deleted file mode 100644
index 2602234..0000000
--- a/scanner-core/src/main/java/com/composea11yscanner/core/model/DpSize.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.composea11yscanner.core.model
-
-/**
- * Density-independent size in dp units. Mirrors the Compose DpSize API
- * without pulling in the Compose dependency.
- *
- * @property width Width in dp.
- * @property height Height in dp.
- */
-data class DpSize(val width: Float, val height: Float) {
- companion object {
- /** A zero-width and zero-height size. */
- val Zero = DpSize(0f, 0f)
-
- /** Sentinel used when a size could not be measured. */
- val Unspecified = DpSize(Float.NaN, Float.NaN)
- }
-}
diff --git a/scanner-core/src/main/java/com/composea11yscanner/core/model/ScannerConfig.kt b/scanner-core/src/main/java/com/composea11yscanner/core/model/ScannerConfig.kt
index 1f8bd51..be109d6 100644
--- a/scanner-core/src/main/java/com/composea11yscanner/core/model/ScannerConfig.kt
+++ b/scanner-core/src/main/java/com/composea11yscanner/core/model/ScannerConfig.kt
@@ -4,14 +4,12 @@ package com.composea11yscanner.core.model
* Runtime configuration used by the scanner engine and UI integration.
*
* @property enabledRules Rule ids that should be evaluated.
- * @property minTouchTargetDp Minimum touch target size in dp.
* @property minContrastRatio Minimum text contrast ratio used by contrast-related rules.
* @property debugOverlay Whether scanner UI should display issue overlays.
* @property autoScan Whether scanning should start automatically when the scanner attaches.
*/
data class ScannerConfig(
val enabledRules: Set,
- val minTouchTargetDp: Int = 48,
val minContrastRatio: Float = 4.5f,
val debugOverlay: Boolean = true,
val autoScan: Boolean = true,
diff --git a/scanner-core/src/test/java/com/composea11yscanner/core/A11yScanEngineTest.kt b/scanner-core/src/test/java/com/composea11yscanner/core/A11yScanEngineTest.kt
index 6c0288a..db95426 100644
--- a/scanner-core/src/test/java/com/composea11yscanner/core/A11yScanEngineTest.kt
+++ b/scanner-core/src/test/java/com/composea11yscanner/core/A11yScanEngineTest.kt
@@ -4,7 +4,6 @@ import app.cash.turbine.test
import com.composea11yscanner.core.model.A11yIssue
import com.composea11yscanner.core.model.A11yNode
import com.composea11yscanner.core.model.A11ySeverity
-import com.composea11yscanner.core.model.DpSize
import com.composea11yscanner.core.model.Rect
import com.composea11yscanner.core.model.ScannerConfig
import com.composea11yscanner.core.model.ScannerState
@@ -29,7 +28,6 @@ class A11yScanEngineTest {
bounds = Rect(0, 0, 100, 100),
contentDescription = null,
isTouchTarget = false,
- touchTargetSize = DpSize(48f, 48f),
textColor = null,
backgroundColors = emptyList(),
isFocusable = false,
diff --git a/scanner-core/src/test/java/com/composea11yscanner/core/CoreModelAndRuleTest.kt b/scanner-core/src/test/java/com/composea11yscanner/core/CoreModelAndRuleTest.kt
index 25cafce..5ea3e40 100644
--- a/scanner-core/src/test/java/com/composea11yscanner/core/CoreModelAndRuleTest.kt
+++ b/scanner-core/src/test/java/com/composea11yscanner/core/CoreModelAndRuleTest.kt
@@ -4,7 +4,6 @@ import com.composea11yscanner.core.model.A11yNode
import com.composea11yscanner.core.model.A11yRole
import com.composea11yscanner.core.model.A11ySeverity
import com.composea11yscanner.core.model.Color
-import com.composea11yscanner.core.model.DpSize
import com.composea11yscanner.core.model.Rect
import com.composea11yscanner.core.model.ScanResult
import com.composea11yscanner.core.rule.BaseA11yRule
@@ -24,7 +23,6 @@ class CoreModelAndRuleTest {
bounds = Rect(0, 0, 100, 48),
contentDescription = "Submit",
isTouchTarget = true,
- touchTargetSize = DpSize(100f, 48f),
textColor = Color(0xFF000000),
backgroundColors = emptyList(),
isFocusable = true,
diff --git a/scanner-rules/src/main/java/com/composea11yscanner/rules/ClickableRoleRule.kt b/scanner-rules/src/main/java/com/composea11yscanner/rules/ClickableRoleRule.kt
index 1d6c515..48edefb 100644
--- a/scanner-rules/src/main/java/com/composea11yscanner/rules/ClickableRoleRule.kt
+++ b/scanner-rules/src/main/java/com/composea11yscanner/rules/ClickableRoleRule.kt
@@ -6,7 +6,7 @@ import com.composea11yscanner.core.model.A11yRole
import com.composea11yscanner.core.model.A11ySeverity
import com.composea11yscanner.core.rule.BaseA11yRule
-/** Flags clickable nodes that do not expose an accessibility role. */
+/** Flags clickable images that do not expose an accessible description. */
class ClickableRoleRule : BaseA11yRule() {
/** Stable id for the clickable role rule. */
@@ -15,30 +15,26 @@ class ClickableRoleRule : BaseA11yRule() {
/** Human-readable rule name. */
override val ruleName = "Clickable Role"
- /** Severity assigned to missing roles. */
+ /** Severity assigned to invalid clickable-role semantics. */
override val severity = A11ySeverity.Error
/** WCAG criterion associated with name, role, and value. */
override val wcagReference = "WCAG 4.1.2 Name, Role, Value (Level A)"
- /** Evaluates a single node for role metadata. */
+ /** Evaluates role-specific requirements for a clickable node. */
override fun check(node: A11yNode): A11yIssue? {
if (node.isMergedDescendant) return null
if (!node.isTouchTarget) return null
- val missingRole = node.role == null
- val imageWithoutDescription =
- node.role == A11yRole.Image && node.contentDescription.isNullOrBlank()
-
- if (!missingRole && !imageWithoutDescription) return null
+ // Compose intentionally permits a null role when none of its predefined roles accurately
+ // describes a custom click target (for example, a clickable list row). Requiring a role for
+ // every OnClick node encourages misleading Role.Button semantics and creates false positives.
+ if (node.role != A11yRole.Image || !node.contentDescription.isNullOrBlank()) return null
return issue(
node = node,
- message = "Clickable element has no semantic role. " +
- "Add Modifier.semantics { role = Role.Button }",
- howToFix = "Apply the appropriate role: Modifier.semantics { role = Role.Button } " +
- "for buttons, Role.Checkbox for toggles, Role.Image for images. " +
- "Clickable images also require a non-empty contentDescription.",
+ message = "Clickable image has no accessible description.",
+ howToFix = "Provide a meaningful, non-empty contentDescription for the clickable image.",
)
}
}
diff --git a/scanner-rules/src/main/java/com/composea11yscanner/rules/ScannerRules.kt b/scanner-rules/src/main/java/com/composea11yscanner/rules/ScannerRules.kt
index ffb4250..1acd8a1 100644
--- a/scanner-rules/src/main/java/com/composea11yscanner/rules/ScannerRules.kt
+++ b/scanner-rules/src/main/java/com/composea11yscanner/rules/ScannerRules.kt
@@ -10,7 +10,6 @@ object ScannerRules {
/** Returns every built-in rule id understood by [buildRules]. */
fun allRuleIds(): List = listOf(
- "touch-target-size",
"missing-content-description",
"duplicate-content-description",
"focus-order",
@@ -27,8 +26,6 @@ object ScannerRules {
* @return Rule instances that should run for the provided configuration.
*/
fun buildRules(config: ScannerConfig, screenDensity: Float): List = buildList {
- if ("touch-target-size" in config.enabledRules)
- add(TouchTargetRule(config.minTouchTargetDp))
if ("missing-content-description" in config.enabledRules)
add(MissingContentDescriptionRule())
if ("duplicate-content-description" in config.enabledRules)
diff --git a/scanner-rules/src/main/java/com/composea11yscanner/rules/TouchTargetRule.kt b/scanner-rules/src/main/java/com/composea11yscanner/rules/TouchTargetRule.kt
deleted file mode 100644
index 254f814..0000000
--- a/scanner-rules/src/main/java/com/composea11yscanner/rules/TouchTargetRule.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-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.BaseA11yRule
-
-/**
- * Flags clickable nodes whose measured touch target is smaller than the configured minimum.
- *
- * @param minTouchTargetDp Minimum accepted width and height in dp.
- */
-class TouchTargetRule(
- private val minTouchTargetDp: Int = 48,
-) : BaseA11yRule() {
- /** Stable id for the touch target size rule. */
- override val ruleId = "touch-target-size"
-
- /** Human-readable rule name. */
- override val ruleName = "Touch Target Size"
-
- /** Severity assigned to undersized touch targets. */
- override val severity = A11ySeverity.Error
-
- /** WCAG criterion associated with target size. */
- override val wcagReference = "WCAG 2.5.5 Target Size (Level AA)"
-
- /** Evaluates a single node for touch target dimensions. */
- override fun check(node: A11yNode): A11yIssue? {
- if (node.isMergedDescendant) return null
- if (!node.isTouchTarget) return null
-
- val w = node.touchTargetSize.width
- val h = node.touchTargetSize.height
- // Pixel bounds converted back to dp can land infinitesimally below an exact dp value
- // (for example, a 48 dp target may be reported as 47.999996 dp).
- if (
- w + MeasurementToleranceDp >= minTouchTargetDp &&
- h + MeasurementToleranceDp >= minTouchTargetDp
- ) return null
-
- return issue(
- node = node,
- message = "Touch target is ${"%.0f".format(w)}x${"%.0f".format(h)}dp. " +
- "Minimum required is ${minTouchTargetDp}x${minTouchTargetDp}dp.",
- howToFix = "Apply Modifier.minimumInteractiveComponentSize() or add padding so the " +
- "composable reaches at least ${minTouchTargetDp}dp in both dimensions.",
- )
- }
-
- private companion object {
- const val MeasurementToleranceDp = 0.01f
- }
-}
diff --git a/scanner-rules/src/test/java/com/composea11yscanner/rules/ClickableRoleRuleTest.kt b/scanner-rules/src/test/java/com/composea11yscanner/rules/ClickableRoleRuleTest.kt
index 9d5bfcc..5403561 100644
--- a/scanner-rules/src/test/java/com/composea11yscanner/rules/ClickableRoleRuleTest.kt
+++ b/scanner-rules/src/test/java/com/composea11yscanner/rules/ClickableRoleRuleTest.kt
@@ -50,8 +50,8 @@ class ClickableRoleRuleTest {
// --- failing cases ---
@Test
- fun `touch target with null role fails`() {
- assertNotNull(rule.evaluate(createNode(isTouchTarget = true, role = null)))
+ fun `touch target with null role passes when no predefined role applies`() {
+ assertNull(rule.evaluate(createNode(isTouchTarget = true, role = null)))
}
@Test
@@ -90,13 +90,16 @@ class ClickableRoleRuleTest {
createNode(isTouchTarget = false),
createNode(isTouchTarget = true, role = A11yRole.Button),
createNode(isTouchTarget = true, role = null),
+ createNode(isTouchTarget = true, role = A11yRole.Image, contentDescription = null),
)
assertEquals(1, rule.evaluateAll(nodes).size)
}
@Test
fun `issue carries correct rule metadata`() {
- val issue = rule.evaluate(createNode(isTouchTarget = true, role = null))!!
+ val issue = rule.evaluate(
+ createNode(isTouchTarget = true, role = A11yRole.Image, contentDescription = null)
+ )!!
assertEquals("clickable-role", issue.ruleId)
assertEquals(A11ySeverity.Error, issue.severity)
assertEquals("WCAG 4.1.2 Name, Role, Value (Level A)", issue.wcagReference)
diff --git a/scanner-rules/src/test/java/com/composea11yscanner/rules/FakeNodeBuilder.kt b/scanner-rules/src/test/java/com/composea11yscanner/rules/FakeNodeBuilder.kt
index 2b2bc40..227d4d5 100644
--- a/scanner-rules/src/test/java/com/composea11yscanner/rules/FakeNodeBuilder.kt
+++ b/scanner-rules/src/test/java/com/composea11yscanner/rules/FakeNodeBuilder.kt
@@ -3,7 +3,6 @@ package com.composea11yscanner.rules
import com.composea11yscanner.core.model.A11yNode
import com.composea11yscanner.core.model.A11yRole
import com.composea11yscanner.core.model.Color
-import com.composea11yscanner.core.model.DpSize
import com.composea11yscanner.core.model.Rect
import java.util.concurrent.atomic.AtomicInteger
@@ -14,7 +13,6 @@ fun createNode(
bounds: Rect = Rect(0, 0, 100, 100),
contentDescription: String? = null,
isTouchTarget: Boolean = false,
- touchTargetSize: DpSize = DpSize(48f, 48f),
textColor: Color? = null,
backgroundColors: List = emptyList(),
isFocusable: Boolean = false,
@@ -28,7 +26,6 @@ fun createNode(
bounds = bounds,
contentDescription = contentDescription,
isTouchTarget = isTouchTarget,
- touchTargetSize = touchTargetSize,
textColor = textColor,
backgroundColors = backgroundColors,
isFocusable = isFocusable,
diff --git a/scanner-rules/src/test/java/com/composea11yscanner/rules/TouchTargetRuleTest.kt b/scanner-rules/src/test/java/com/composea11yscanner/rules/TouchTargetRuleTest.kt
deleted file mode 100644
index bde8244..0000000
--- a/scanner-rules/src/test/java/com/composea11yscanner/rules/TouchTargetRuleTest.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-package com.composea11yscanner.rules
-
-import com.composea11yscanner.core.model.A11ySeverity
-import com.composea11yscanner.core.model.DpSize
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNotNull
-import org.junit.Assert.assertNull
-import org.junit.Assert.assertTrue
-import org.junit.Test
-
-class TouchTargetRuleTest {
-
- private val rule = TouchTargetRule()
-
- // --- passing cases ---
-
- @Test
- fun `non-interactive node is not evaluated`() {
- assertNull(rule.evaluate(createNode(isTouchTarget = false, touchTargetSize = DpSize(40f, 40f))))
- }
-
- @Test
- fun `touch target at exactly minimum size passes`() {
- assertNull(rule.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(48f, 48f))))
- }
-
- @Test
- fun `floating point noise just below minimum size passes`() {
- assertNull(
- rule.evaluate(
- createNode(
- isTouchTarget = true,
- touchTargetSize = DpSize(47.999996f, 47.999996f),
- )
- )
- )
- }
-
- @Test
- fun `touch target larger than minimum passes`() {
- assertNull(rule.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(56f, 64f))))
- }
-
- @Test
- fun `merged descendant touch target is skipped`() {
- assertNull(
- rule.evaluate(
- createNode(
- isTouchTarget = true,
- touchTargetSize = DpSize(32f, 32f),
- isMergedDescendant = true,
- )
- )
- )
- }
-
- // --- failing cases ---
-
- @Test
- fun `width below minimum fails`() {
- assertNotNull(rule.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(44f, 48f))))
- }
-
- @Test
- fun `meaningfully undersized target is not hidden by measurement tolerance`() {
- assertNotNull(
- rule.evaluate(
- createNode(
- isTouchTarget = true,
- touchTargetSize = DpSize(47.98f, 48f),
- )
- )
- )
- }
-
- @Test
- fun `height below minimum fails`() {
- assertNotNull(rule.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(48f, 44f))))
- }
-
- @Test
- fun `both dimensions below minimum fail`() {
- assertNotNull(rule.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(32f, 32f))))
- }
-
- // --- edge cases ---
-
- @Test
- fun `zero-size touch target fails with correct dimensions in message`() {
- val issue = rule.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(0f, 0f)))
- assertNotNull(issue)
- assertTrue(issue!!.message.contains("0x0dp"))
- }
-
- @Test
- fun `message contains actual dimensions`() {
- val issue = rule.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(32f, 40f)))
- assertNotNull(issue)
- assertTrue(issue!!.message.contains("32x40dp"))
- }
-
- @Test
- fun `custom threshold accepts larger node`() {
- val rule36 = TouchTargetRule(minTouchTargetDp = 36)
- assertNull(rule36.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(40f, 40f))))
- }
-
- @Test
- fun `custom threshold rejects node below it`() {
- val rule36 = TouchTargetRule(minTouchTargetDp = 36)
- assertNotNull(rule36.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(30f, 30f))))
- }
-
- @Test
- fun `issue carries correct rule metadata`() {
- val issue = rule.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(40f, 40f)))!!
- assertEquals("touch-target-size", issue.ruleId)
- assertEquals(A11ySeverity.Error, issue.severity)
- assertEquals("WCAG 2.5.5 Target Size (Level AA)", issue.wcagReference)
- }
-
- @Test
- fun `evaluateAll aggregates per-node results`() {
- val nodes = listOf(
- createNode(isTouchTarget = false),
- createNode(isTouchTarget = true, touchTargetSize = DpSize(40f, 40f)),
- createNode(isTouchTarget = true, touchTargetSize = DpSize(48f, 48f)),
- )
- assertEquals(1, rule.evaluateAll(nodes).size)
- }
-}
diff --git a/scanner-ui/src/androidTest/java/com/composea11yscanner/ui/A11yNodeExtractorTest.kt b/scanner-ui/src/androidTest/java/com/composea11yscanner/ui/A11yNodeExtractorTest.kt
index 452c7bf..88b41bc 100644
--- a/scanner-ui/src/androidTest/java/com/composea11yscanner/ui/A11yNodeExtractorTest.kt
+++ b/scanner-ui/src/androidTest/java/com/composea11yscanner/ui/A11yNodeExtractorTest.kt
@@ -8,7 +8,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@@ -30,7 +29,7 @@ class A11yNodeExtractorTest {
composeRule.waitForIdle()
- val nodes = A11yNodeExtractor(Density(1f))
+ val nodes = A11yNodeExtractor()
.extract(composeRule.onRoot(useUnmergedTree = true).fetchSemanticsNode())
val clickableNode = nodes.single { it.isTouchTarget && it.contentDescription == "Go" }
@@ -45,7 +44,7 @@ class A11yNodeExtractorTest {
composeRule.waitForIdle()
- val nodes = A11yNodeExtractor(Density(1f))
+ val nodes = A11yNodeExtractor()
.extract(composeRule.onRoot(useUnmergedTree = true).fetchSemanticsNode())
val clickableNode = nodes.single { it.isTouchTarget }
@@ -53,25 +52,4 @@ class A11yNodeExtractorTest {
assertTrue(clickableNode.contentDescription.isNullOrBlank())
}
- @Test
- fun compactClickable_usesExpandedTouchBounds() {
- lateinit var density: Density
- composeRule.setContent {
- density = LocalDensity.current
- Box(
- modifier = Modifier
- .size(width = 40.dp, height = 48.dp)
- .clickable { },
- )
- }
-
- composeRule.waitForIdle()
-
- val nodes = A11yNodeExtractor(density)
- .extract(composeRule.onRoot(useUnmergedTree = true).fetchSemanticsNode())
- val clickableNode = nodes.single { it.isTouchTarget }
-
- assertTrue(clickableNode.touchTargetSize.width >= 48f)
- assertTrue(clickableNode.touchTargetSize.height >= 48f)
- }
}
diff --git a/scanner-ui/src/main/java/com/composea11yscanner/A11yScannerInitializer.kt b/scanner-ui/src/main/java/com/composea11yscanner/A11yScannerInitializer.kt
index 419e21f..d50ace8 100644
--- a/scanner-ui/src/main/java/com/composea11yscanner/A11yScannerInitializer.kt
+++ b/scanner-ui/src/main/java/com/composea11yscanner/A11yScannerInitializer.kt
@@ -15,7 +15,6 @@ import com.composea11yscanner.rules.ScannerRules
* AndroidX App Startup initializer for installing [ComposeA11yScanner] in debug builds.
*
* Configure the scanner from the app manifest with:
- * - `a11y_scanner_min_touch_target`
* - `a11y_scanner_min_contrast`
* - `a11y_scanner_auto_scan`
*/
@@ -68,10 +67,6 @@ class A11yScannerInitializer : Initializer {
val defaults = ScannerConfig(enabledRules = emptySet())
return ScannerConfig(
enabledRules = ScannerRules.allRuleIds().toSet(),
- minTouchTargetDp = metadata.intValue(
- key = META_MIN_TOUCH_TARGET,
- defaultValue = defaults.minTouchTargetDp,
- ),
minContrastRatio = metadata.floatValue(
key = META_MIN_CONTRAST,
defaultValue = defaults.minContrastRatio,
@@ -88,14 +83,6 @@ class A11yScannerInitializer : Initializer {
return appInfo.metaData ?: Bundle.EMPTY
}
- private fun Bundle.intValue(key: String, defaultValue: Int): Int =
- when (val value = get(key)) {
- is Int -> value
- is Number -> value.toInt()
- is String -> value.toIntOrNull() ?: defaultValue
- else -> defaultValue
- }
-
private fun Bundle.floatValue(key: String, defaultValue: Float): Float =
when (val value = get(key)) {
is Float -> value
@@ -112,7 +99,6 @@ class A11yScannerInitializer : Initializer {
}
private companion object {
- const val META_MIN_TOUCH_TARGET = "a11y_scanner_min_touch_target"
const val META_MIN_CONTRAST = "a11y_scanner_min_contrast"
const val META_AUTO_SCAN = "a11y_scanner_auto_scan"
}
diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ComposeA11yScanner.kt b/scanner-ui/src/main/java/com/composea11yscanner/ComposeA11yScanner.kt
index d4cd275..680a691 100644
--- a/scanner-ui/src/main/java/com/composea11yscanner/ComposeA11yScanner.kt
+++ b/scanner-ui/src/main/java/com/composea11yscanner/ComposeA11yScanner.kt
@@ -28,7 +28,6 @@ import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.semantics.SemanticsOwner
-import androidx.compose.ui.unit.Density
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.composea11yscanner.core.model.A11yIssue
@@ -204,7 +203,7 @@ object ComposeA11yScanner {
?.findFirstAbstractComposeView(excludeView = overlayView)
?: return emptyList()
val semanticsOwner = hostView.findSemanticsOwner() ?: return emptyList()
- return A11yNodeExtractor(Density(activity)).extract(semanticsOwner)
+ return A11yNodeExtractor().extract(semanticsOwner)
}
private fun AbstractComposeView.findSemanticsOwner(): SemanticsOwner? {
diff --git a/scanner-ui/src/main/java/com/composea11yscanner/export/ScanResultExporter.kt b/scanner-ui/src/main/java/com/composea11yscanner/export/ScanResultExporter.kt
index 191025b..a546a55 100644
--- a/scanner-ui/src/main/java/com/composea11yscanner/export/ScanResultExporter.kt
+++ b/scanner-ui/src/main/java/com/composea11yscanner/export/ScanResultExporter.kt
@@ -3,7 +3,6 @@ package com.composea11yscanner.export
import com.composea11yscanner.core.model.A11yNode
import com.composea11yscanner.core.model.A11ySeverity
import com.composea11yscanner.core.model.Color
-import com.composea11yscanner.core.model.DpSize
import com.composea11yscanner.core.model.Rect
import com.composea11yscanner.core.model.ScanResult
import java.util.Locale
@@ -100,8 +99,6 @@ object ScanResultExporter {
append(", ")
appendJsonPair("isTouchTarget", isTouchTarget)
append(", ")
- appendJsonPair("touchTargetSize", touchTargetSize)
- append(", ")
appendJsonPair("textColor", textColor)
append(", ")
append("\"backgroundColors\": [")
@@ -181,13 +178,6 @@ object ScanResultExporter {
append(value.toJson())
}
- private fun StringBuilder.appendJsonPair(name: String, value: DpSize) {
- append("\"")
- append(name.escapeJson())
- append("\": ")
- append(value.toJson())
- }
-
private fun StringBuilder.appendJsonPair(name: String, value: Color?) {
append("\"")
append(name.escapeJson())
@@ -198,15 +188,9 @@ object ScanResultExporter {
private fun Rect.toJson(): String =
"""{"left": $left, "top": $top, "right": $right, "bottom": $bottom, "width": $width, "height": $height}"""
- private fun DpSize.toJson(): String =
- """{"width": ${width.toJsonNumber()}, "height": ${height.toJsonNumber()}}"""
-
private fun Color.toJson(): String =
"""{"value": $value}"""
- private fun Float.toJsonNumber(): String =
- if (isFinite()) toString() else "null"
-
private fun Float.formatPercent(): String =
String.format(Locale.US, "%.1f%%", this)
diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yNodeExtractor.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yNodeExtractor.kt
index 405b748..3e7c956 100644
--- a/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yNodeExtractor.kt
+++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yNodeExtractor.kt
@@ -7,10 +7,8 @@ import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.getOrNull
-import androidx.compose.ui.unit.Density
import com.composea11yscanner.core.model.A11yNode
import com.composea11yscanner.core.model.A11yRole
-import com.composea11yscanner.core.model.DpSize
import com.composea11yscanner.core.model.Rect
import kotlin.math.roundToInt
@@ -21,9 +19,8 @@ import kotlin.math.roundToInt
* - Test: `composeTestRule.onRoot(useUnmergedTree = true).fetchSemanticsNode()`
* - Production: `SemanticsOwner.rootSemanticsNode` via [extract(SemanticsOwner)]
*
- * @param density Density used to convert pixel bounds into dp sizes.
*/
-class A11yNodeExtractor(private val density: Density) {
+class A11yNodeExtractor {
/**
* Recursively extracts all nodes from the tree rooted at [rootNode].
@@ -76,10 +73,6 @@ class A11yNodeExtractor(private val density: Density) {
null
}
val visualBounds = boundsInRoot
- // Compose expands pointer input for small clickables to the platform minimum touch
- // target without changing their visual/layout bounds. Measure the region that actually
- // accepts input so compact controls such as a short FilterChip are not false positives.
- val touchTargetBounds = touchBoundsInRoot
val bounds = visualBounds.toCoreRect()
return A11yNode(
@@ -95,7 +88,6 @@ class A11yNodeExtractor(private val density: Density) {
?.joinToString(separator = ", ")
?: textLabel,
isTouchTarget = isTouchTarget,
- touchTargetSize = touchTargetBounds.toDpSize(),
textColor = null, // not available via semantics
backgroundColors = emptyList(), // not available via semantics
isFocusable = config.contains(SemanticsActions.OnClick)
@@ -164,12 +156,6 @@ class A11yNodeExtractor(private val density: Density) {
bottom = bottom.roundToInt(),
)
- private fun androidx.compose.ui.geometry.Rect.toDpSize(): DpSize = with(density) {
- DpSize(
- width = width.toDp().value,
- height = height.toDp().value,
- )
- }
}
// --- Role mapping ---
diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScanner.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScanner.kt
index 5e5dea0..1e4a7a9 100644
--- a/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScanner.kt
+++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScanner.kt
@@ -3,7 +3,6 @@ package com.composea11yscanner.ui
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsOwner
-import androidx.compose.ui.unit.Density
import com.composea11yscanner.core.model.A11yNode
import com.composea11yscanner.core.model.ScanResult
import com.composea11yscanner.core.rule.A11yRule
@@ -14,11 +13,9 @@ import java.util.UUID
*
* @param rules The rules to run. Build from [com.composea11yscanner.rules.ScannerRules.buildRules]
* or supply a custom list.
- * @param density Compose [Density] used by [A11yNodeExtractor] for pixel↔dp conversion.
*/
class A11yScanner(
private val rules: List,
- private val density: Density,
) {
/**
* Scans from a semantics node, typically in tests or manual tree walks.
@@ -27,7 +24,7 @@ class A11yScanner(
* @return Completed scan result.
*/
fun scan(rootNode: SemanticsNode): ScanResult =
- buildResult(A11yNodeExtractor(density).extract(rootNode))
+ buildResult(A11yNodeExtractor().extract(rootNode))
/**
* Scans the live Compose semantics tree via [SemanticsOwner].
@@ -37,7 +34,7 @@ class A11yScanner(
*/
@OptIn(InternalComposeUiApi::class)
fun scan(owner: SemanticsOwner): ScanResult =
- buildResult(A11yNodeExtractor(density).extract(owner))
+ buildResult(A11yNodeExtractor().extract(owner))
private fun buildResult(nodes: List): ScanResult {
val issues = rules
diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScannerController.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScannerController.kt
index 4f74b7f..6444c5e 100644
--- a/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScannerController.kt
+++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScannerController.kt
@@ -24,7 +24,7 @@ import kotlinx.coroutines.withContext
* Typical usage:
* ```
* val controller = A11yScannerController(
- * nodeProvider = { A11yNodeExtractor(density).extract(semanticsOwner) },
+ * nodeProvider = { A11yNodeExtractor().extract(semanticsOwner) },
* screenDensity = resources.displayMetrics.density,
* )
* .configure(ScannerConfig(enabledRules = ScannerRules.allRuleIds().toSet()))
diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScannerScaffold.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScannerScaffold.kt
index 6544080..0c49a90 100644
--- a/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScannerScaffold.kt
+++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScannerScaffold.kt
@@ -28,7 +28,6 @@ import androidx.compose.ui.unit.dp
import com.composea11yscanner.core.model.A11yIssue
import com.composea11yscanner.core.model.A11yNode
import com.composea11yscanner.core.model.A11ySeverity
-import com.composea11yscanner.core.model.DpSize
import com.composea11yscanner.core.model.Rect
import com.composea11yscanner.core.model.ScanResult
import com.composea11yscanner.core.model.ScannerConfig
@@ -194,7 +193,6 @@ private fun previewScanResult() = ScanResult(
nodeId = "node-1", composableName = "Button",
bounds = Rect(0, 0, 300, 120),
contentDescription = null, isTouchTarget = true,
- touchTargetSize = DpSize(100f, 40f),
textColor = null, backgroundColors = emptyList(),
isFocusable = true, isMergedDescendant = false, depth = 1,
),
diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueDetailPanel.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueDetailPanel.kt
index 78d54ad..daf76ea 100644
--- a/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueDetailPanel.kt
+++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueDetailPanel.kt
@@ -48,7 +48,6 @@ import androidx.compose.ui.unit.dp
import com.composea11yscanner.core.model.A11yIssue
import com.composea11yscanner.core.model.A11yNode
import com.composea11yscanner.core.model.A11ySeverity
-import com.composea11yscanner.core.model.DpSize
import com.composea11yscanner.core.model.Rect
import java.util.Locale
@@ -396,13 +395,6 @@ private fun IssueDetailPanelMultipleIssuesPreview() {
howToFix = "Apply Modifier.clickable(role = Role.Button) for action controls.",
wcagReference = "WCAG 4.1.2 Name, Role, Value (Level A)",
),
- previewIssue(
- severity = A11ySeverity.Error,
- ruleName = "Touch Target Size",
- message = "Touch target is 28x28dp. Minimum required is 48x48dp.",
- howToFix = "Apply Modifier.minimumInteractiveComponentSize() or add padding.",
- wcagReference = "WCAG 2.5.5 Target Size (Level AA)",
- ),
previewIssue(
severity = A11ySeverity.Warning,
ruleName = "Missing Content Description",
@@ -433,7 +425,6 @@ private fun previewIssue(
bounds = Rect(0, 0, 300, 120),
contentDescription = null,
isTouchTarget = true,
- touchTargetSize = DpSize(100f, 40f),
textColor = null,
backgroundColors = emptyList(),
isFocusable = true,
diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueHighlightBox.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueHighlightBox.kt
index 10c03c6..8d9225d 100644
--- a/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueHighlightBox.kt
+++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueHighlightBox.kt
@@ -17,7 +17,6 @@ import androidx.compose.ui.unit.dp
import com.composea11yscanner.core.model.A11yIssue
import com.composea11yscanner.core.model.A11yNode
import com.composea11yscanner.core.model.A11ySeverity
-import com.composea11yscanner.core.model.DpSize
import com.composea11yscanner.core.model.Rect
private val ErrorBorderColor = Color(0xFFD32F2F)
@@ -148,7 +147,6 @@ private fun previewIssue(severity: A11ySeverity) = A11yIssue(
bounds = Rect(0, 0, 300, 150),
contentDescription = null,
isTouchTarget = true,
- touchTargetSize = DpSize(100f, 50f),
textColor = null,
backgroundColors = emptyList(),
isFocusable = true,
diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanReportSheet.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanReportSheet.kt
index a7c6664..2d3b47a 100644
--- a/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanReportSheet.kt
+++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanReportSheet.kt
@@ -46,7 +46,6 @@ import androidx.compose.ui.unit.dp
import com.composea11yscanner.core.model.A11yIssue
import com.composea11yscanner.core.model.A11yNode
import com.composea11yscanner.core.model.A11ySeverity
-import com.composea11yscanner.core.model.DpSize
import com.composea11yscanner.core.model.Rect
import com.composea11yscanner.core.model.ScanResult
import com.composea11yscanner.export.ScanResultExporter
@@ -443,7 +442,7 @@ private fun previewResult(errors: Int, warnings: Int, info: Int): ScanResult {
val node = A11yNode(
nodeId = "node-1", composableName = "Button",
bounds = Rect(0, 0, 300, 120), contentDescription = null,
- isTouchTarget = true, touchTargetSize = DpSize(100f, 40f),
+ isTouchTarget = true,
textColor = null, backgroundColors = emptyList(),
isFocusable = true, isMergedDescendant = false, depth = 1,
)
diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanSummaryBar.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanSummaryBar.kt
index 0f58fd6..3b5c964 100644
--- a/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanSummaryBar.kt
+++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanSummaryBar.kt
@@ -40,7 +40,6 @@ import androidx.compose.ui.unit.dp
import com.composea11yscanner.core.model.A11yIssue
import com.composea11yscanner.core.model.A11yNode
import com.composea11yscanner.core.model.A11ySeverity
-import com.composea11yscanner.core.model.DpSize
import com.composea11yscanner.core.model.Rect
import com.composea11yscanner.core.model.ScanResult
import com.composea11yscanner.core.model.ScannerState
@@ -339,7 +338,7 @@ private fun previewResult(
val node = A11yNode(
nodeId = "node-1", composableName = "Button",
bounds = Rect(0, 0, 300, 120), contentDescription = null,
- isTouchTarget = true, touchTargetSize = DpSize(100f, 40f),
+ isTouchTarget = true,
textColor = null, backgroundColors = emptyList(),
isFocusable = true, isMergedDescendant = false, depth = 1,
)
diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/ScannerUi.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/ScannerUi.kt
index df02bd7..0092f5b 100644
--- a/scanner-ui/src/main/java/com/composea11yscanner/ui/ScannerUi.kt
+++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/ScannerUi.kt
@@ -14,7 +14,6 @@ import androidx.compose.ui.tooling.preview.Preview
import com.composea11yscanner.core.model.A11yIssue
import com.composea11yscanner.core.model.A11yNode
import com.composea11yscanner.core.model.A11ySeverity
-import com.composea11yscanner.core.model.DpSize
import com.composea11yscanner.core.model.Rect
import com.composea11yscanner.core.model.ScanResult
@@ -83,7 +82,6 @@ private fun A11yIssueOverlayPreview() {
bounds = Rect(0, 0, 120, 48),
contentDescription = null,
isTouchTarget = true,
- touchTargetSize = DpSize(120f, 48f),
textColor = null,
backgroundColors = emptyList(),
isFocusable = true,
diff --git a/scanner-ui/src/test/java/com/composea11yscanner/ui/IssueDetailPanelSnapshotTest.kt b/scanner-ui/src/test/java/com/composea11yscanner/ui/IssueDetailPanelSnapshotTest.kt
index 7b8e2f3..6223aab 100644
--- a/scanner-ui/src/test/java/com/composea11yscanner/ui/IssueDetailPanelSnapshotTest.kt
+++ b/scanner-ui/src/test/java/com/composea11yscanner/ui/IssueDetailPanelSnapshotTest.kt
@@ -82,14 +82,6 @@ class IssueDetailPanelSnapshotTest {
howToFix = "Apply Modifier.clickable(role = Role.Button) for action controls.",
wcagReference = "WCAG 4.1.2 Name, Role, Value (Level A)",
),
- issueFixture(
- severity = A11ySeverity.Error,
- issueId = "touch-target-size",
- ruleName = "Touch Target Size",
- message = "Touch target is 28x28dp. Minimum required is 48x48dp.",
- howToFix = "Apply Modifier.minimumInteractiveComponentSize() or add padding.",
- wcagReference = "WCAG 2.5.5 Target Size (Level AA)",
- ),
issueFixture(
severity = A11ySeverity.Warning,
issueId = "missing-content-description",
diff --git a/scanner-ui/src/test/java/com/composea11yscanner/ui/SnapshotFixtures.kt b/scanner-ui/src/test/java/com/composea11yscanner/ui/SnapshotFixtures.kt
index 5218ac8..18719c1 100644
--- a/scanner-ui/src/test/java/com/composea11yscanner/ui/SnapshotFixtures.kt
+++ b/scanner-ui/src/test/java/com/composea11yscanner/ui/SnapshotFixtures.kt
@@ -3,7 +3,6 @@ package com.composea11yscanner.ui
import com.composea11yscanner.core.model.A11yIssue
import com.composea11yscanner.core.model.A11yNode
import com.composea11yscanner.core.model.A11ySeverity
-import com.composea11yscanner.core.model.DpSize
import com.composea11yscanner.core.model.Rect
import com.composea11yscanner.core.model.ScanResult
@@ -31,7 +30,6 @@ internal fun nodeFixture() = A11yNode(
bounds = Rect(left = 0, top = 0, right = 300, bottom = 120),
contentDescription = null,
isTouchTarget = true,
- touchTargetSize = DpSize(width = 100f, height = 40f),
textColor = null,
backgroundColors = emptyList(),
isFocusable = true,