Skip to content

Commit 4f75ba8

Browse files
authored
Merge pull request #11 from mohdaquib/feature/publishing
Feature/publishing
2 parents 8bcb55f + fe79c63 commit 4f75ba8

45 files changed

Lines changed: 975 additions & 234 deletions

Some content is hidden

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

.github/workflows/docs.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: API Docs
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
7+
permissions:
8+
contents: read
9+
pages: write
10+
id-token: write
11+
12+
concurrency:
13+
group: pages
14+
cancel-in-progress: false
15+
16+
jobs:
17+
publish-dokka:
18+
name: Publish Dokka HTML
19+
runs-on: ubuntu-latest
20+
environment:
21+
name: github-pages
22+
url: ${{ steps.deployment.outputs.page_url }}
23+
steps:
24+
- uses: actions/checkout@v4
25+
- name: Make gradlew executable
26+
run: chmod +x ./gradlew
27+
- uses: actions/setup-java@v4
28+
with:
29+
distribution: temurin
30+
java-version: 17
31+
cache: gradle
32+
- name: Generate Dokka HTML
33+
run: ./gradlew dokkaGenerate
34+
- uses: actions/configure-pages@v5
35+
- uses: actions/upload-pages-artifact@v3
36+
with:
37+
path: build/dokka/html
38+
- id: deployment
39+
uses: actions/deploy-pages@v4

README.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Compose A11y Scanner
2+
3+
Runtime accessibility scanner that overlays issues directly on your Compose UI
4+
5+
![Annotated GIF showing the Compose A11y Scanner issue summary, view highlights, and issue detail sheet in the sample app](docs/overlay-demo.gif)
6+
7+
## Architecture
8+
9+
```mermaid
10+
flowchart LR
11+
SemanticsTree["SemanticsTree"] --> Extractor[":scanner-ui<br/>A11yNodeExtractor"]
12+
Extractor --> ColorExtractor["ColorExtractor"]
13+
ColorExtractor --> Nodes["A11yNode list"]
14+
Extractor --> Nodes
15+
Nodes --> Core[":scanner-core<br/>A11yScanEngine"]
16+
Rules[":scanner-rules<br/>Built-in A11yRule implementations"] --> Core
17+
Core --> Result["ScanResult / ScannerState"]
18+
Result --> Overlay[":scanner-ui<br/>Overlay, summary, report sheet"]
19+
```
20+
21+
`:scanner-core` owns the scan engine, models, result state, and `A11yRule` contract. `:scanner-rules` contains the built-in rules. `:scanner-ui` reads the Compose semantics tree, samples colors through `ColorExtractor`, runs scans, and draws issue overlays on top of the host UI.
22+
23+
## Installation
24+
25+
```kotlin
26+
repositories { maven("https://jitpack.io") }
27+
dependencies { debugImplementation("io.github.mohdaquib:scanner-ui:<version>") }
28+
```
29+
30+
Use a debug-only dependency because the scanner is intended for development builds. `:scanner-ui` brings `:scanner-core` and `:scanner-rules` transitively.
31+
32+
## Integration
33+
34+
### A11yScannerScaffold
35+
36+
Wrap the screen you want to inspect with `A11yScannerScaffold`. Provide an `A11yScannerController` that can extract nodes from your current Compose semantics tree.
37+
38+
```kotlin
39+
@Composable
40+
fun DebugScannerScreen(
41+
scannerController: A11yScannerController,
42+
content: @Composable () -> Unit,
43+
) {
44+
val config = remember {
45+
ScannerConfig(
46+
enabledRules = ScannerRules.allRuleIds().toSet(),
47+
autoScan = false,
48+
)
49+
}
50+
51+
A11yScannerScaffold(
52+
scannerController = scannerController,
53+
config = config,
54+
modifier = Modifier.fillMaxSize(),
55+
) {
56+
content()
57+
}
58+
}
59+
```
60+
61+
The scaffold renders your content first, then draws issue highlight boxes, the scan summary bar, and the issue detail sheet above it.
62+
63+
### Shake To Scan
64+
65+
Call `scanOnShake()` from a composable that is active while the scanned screen is visible.
66+
67+
```kotlin
68+
scanOnShake(
69+
enabled = true,
70+
onScanRequested = {
71+
scannerController.startScan()
72+
},
73+
)
74+
```
75+
76+
### Manual Trigger
77+
78+
Wire a debug menu item, toolbar button, or test-only action directly to the controller.
79+
80+
```kotlin
81+
IconButton(onClick = { scannerController.startScan() }) {
82+
Icon(
83+
imageVector = Icons.Default.Search,
84+
contentDescription = "Scan accessibility",
85+
)
86+
}
87+
```
88+
89+
If you use the top-level activity overlay API, call `ComposeA11yScanner.triggerScan()` instead.
90+
91+
```kotlin
92+
ComposeA11yScanner.triggerScan()
93+
```
94+
95+
## Built-In Rules
96+
97+
See [RULES.md](RULES.md) for complete behavior, fixes, WCAG references, and examples.
98+
99+
| Rule ID | Name | Severity | Details |
100+
| --- | --- | --- | --- |
101+
| `touch-target-size` | Touch Target Size | Error | [RULES.md](RULES.md#touch-target-size---touch-target-size) |
102+
| `missing-content-description` | Missing Content Description | Error | [RULES.md](RULES.md#missing-content-description---missing-content-description) |
103+
| `duplicate-content-description` | Duplicate Content Description | Warning | [RULES.md](RULES.md#duplicate-content-description---duplicate-content-description) |
104+
| `focus-order` | Focus Order | Error | [RULES.md](RULES.md#focus-order---focus-order) |
105+
| `text-scaling` | Text Scaling | Warning | [RULES.md](RULES.md#text-scaling---text-scaling) |
106+
| `image-text-overlay` | Image With Text Overlay | Warning | [RULES.md](RULES.md#image-text-overlay---image-with-text-overlay) |
107+
| `clickable-role` | Clickable Role | Error | [RULES.md](RULES.md#clickable-role---clickable-role) |
108+
109+
## Custom Rules
110+
111+
Create a rule by implementing `A11yRule`. Use a stable `ruleId`, assign a severity, and return an `A11yIssue` only when the node fails your check.
112+
113+
```kotlin
114+
class MissingTestTagRule : A11yRule {
115+
override val ruleId = "missing-test-tag"
116+
override val ruleName = "Missing Test Tag"
117+
override val severity = A11ySeverity.Warning
118+
override val wcagReference: String? = null
119+
120+
override fun evaluate(node: A11yNode): A11yIssue? {
121+
if (!node.isTouchTarget || node.isMergedDescendant) return null
122+
if (node.composableName.contains("TestTag", ignoreCase = true)) return null
123+
124+
return A11yIssue(
125+
issueId = "${ruleId}_${node.nodeId}",
126+
severity = severity,
127+
ruleId = ruleId,
128+
ruleName = ruleName,
129+
affectedNode = node,
130+
message = "Interactive node does not expose a stable test tag.",
131+
howToFix = "Add Modifier.testTag() to make this control easier to identify in tests.",
132+
wcagReference = wcagReference,
133+
)
134+
}
135+
}
136+
```
137+
138+
Register custom rules on the controller:
139+
140+
```kotlin
141+
val scannerController = A11yScannerController(
142+
nodeProvider = { extractNodesFromCurrentSemanticsTree() },
143+
screenDensity = density,
144+
).withRules(MissingTestTagRule())
145+
```
146+
147+
Custom rule IDs are automatically enabled by `A11yScannerController.withRules(...)` before each scan.

0 commit comments

Comments
 (0)