react-native-a11y-order 0.8.2 → 0.9.1-rc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -152
- package/android/build.gradle +0 -18
- package/android/src/main/java/com/a11yorder/services/focus/A11yFocusDelegate.java +10 -32
- package/android/src/main/java/com/a11yorder/services/order/A11yOrderService.java +70 -65
- package/android/src/main/java/com/a11yorder/services/order/linking/A11yLinkingQueue.java +37 -63
- package/android/src/main/java/com/a11yorder/services/order/linking/A11yOrderLinking.java +11 -14
- package/android/src/main/java/com/a11yorder/services/order/linking/WeakTreeMap.java +53 -31
- package/android/src/main/java/com/a11yorder/utils/A11yHelper.java +39 -59
- package/android/src/main/java/com/a11yorder/utils/ChoreographerUtils.java +7 -12
- package/android/src/main/java/com/a11yorder/utils/FragmentUtils.java +8 -48
- package/android/src/main/java/com/a11yorder/views/A11yIndexView/A11yIndexView.java +1 -1
- package/android/src/main/java/com/a11yorder/views/A11yLockView/A11yLockViewManager.java +5 -0
- package/android/src/main/java/com/a11yorder/views/A11yView/A11yView.java +1 -1
- package/android/src/oldarch/A11yLockViewManagerSpec.java +2 -0
- package/ios/delegates/RNAOViewItemDelegate/RNAOViewItemDelegate.mm +1 -1
- package/ios/extensions/RCTModalHostViewComponentView+RNAOA11yOrder.mm +21 -21
- package/ios/extensions/UIView+RNAOA11yOrder.mm +17 -20
- package/ios/extensions/UIViewController+RNAOA11yOrder.mm +8 -8
- package/ios/helpers/RNAOSwizzleInstall.h +30 -0
- package/ios/services/RNAOA11yItemDelegate/RNAOA11yItemDelegate.h +4 -6
- package/ios/services/RNAOA11yItemDelegate/RNAOA11yItemDelegate.mm +98 -87
- package/ios/services/RNAOA11yOrderLinking/RNAOA11yOrderLinking.h +5 -3
- package/ios/services/RNAOA11yOrderLinking/RNAOA11yOrderLinking.mm +49 -49
- package/ios/services/RNAOA11yRelationship/RNAOA11yRelationship.h +1 -0
- package/ios/services/RNAOA11yRelationship/RNAOA11yRelationship.mm +42 -36
- package/ios/services/RNAOSortedMap/RNAOSortedMap.h +2 -1
- package/ios/services/RNAOSortedMap/RNAOSortedMap.mm +48 -47
- package/ios/views/RNAOA11yLockView/RNAOA11yLockView.h +4 -2
- package/ios/views/RNAOA11yLockView/RNAOA11yLockView.mm +89 -3
- package/ios/views/RNAOA11yLockView/RNAOA11yLockViewManager.mm +3 -0
- package/lib/commonjs/components/A11yLock/A11yBaseLock/A11yBaseLock.js +18 -2
- package/lib/commonjs/components/A11yLock/A11yBaseLock/A11yBaseLock.js.map +1 -1
- package/lib/commonjs/components/A11yLock/A11yFocusTrap/A11yFocusTrap.js +23 -7
- package/lib/commonjs/components/A11yLock/A11yFocusTrap/A11yFocusTrap.js.map +1 -1
- package/lib/commonjs/nativeSpecs/A11yLockNativeComponent.ts +1 -0
- package/lib/module/components/A11yLock/A11yBaseLock/A11yBaseLock.js +17 -2
- package/lib/module/components/A11yLock/A11yBaseLock/A11yBaseLock.js.map +1 -1
- package/lib/module/components/A11yLock/A11yFocusTrap/A11yFocusTrap.js +23 -7
- package/lib/module/components/A11yLock/A11yFocusTrap/A11yFocusTrap.js.map +1 -1
- package/lib/module/nativeSpecs/A11yLockNativeComponent.ts +1 -0
- package/lib/typescript/src/components/A11yLock/A11yBaseLock/A11yBaseLock.d.ts +2 -1
- package/lib/typescript/src/components/A11yLock/A11yBaseLock/A11yBaseLock.d.ts.map +1 -1
- package/lib/typescript/src/components/A11yLock/A11yFocusTrap/A11yFocusTrap.d.ts +1 -1
- package/lib/typescript/src/components/A11yLock/A11yFocusTrap/A11yFocusTrap.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/nativeSpecs/A11yLockNativeComponent.d.ts +1 -0
- package/lib/typescript/src/nativeSpecs/A11yLockNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/types/A11yLock.types.d.ts +1 -0
- package/lib/typescript/src/types/A11yLock.types.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/components/A11yLock/A11yBaseLock/A11yBaseLock.tsx +20 -3
- package/src/components/A11yLock/A11yFocusTrap/A11yFocusTrap.tsx +24 -5
- package/src/nativeSpecs/A11yLockNativeComponent.ts +1 -0
- package/src/types/A11yLock.types.ts +1 -0
- package/lib/commonjs/components/A11yLock/A11yBaseLock/A11yBaseLock.android.js +0 -23
- package/lib/commonjs/components/A11yLock/A11yBaseLock/A11yBaseLock.android.js.map +0 -1
- package/lib/module/components/A11yLock/A11yBaseLock/A11yBaseLock.android.js +0 -18
- package/lib/module/components/A11yLock/A11yBaseLock/A11yBaseLock.android.js.map +0 -1
- package/lib/typescript/src/components/A11yLock/A11yBaseLock/A11yBaseLock.android.d.ts +0 -4
- package/lib/typescript/src/components/A11yLock/A11yBaseLock/A11yBaseLock.android.d.ts.map +0 -1
- package/src/components/A11yLock/A11yBaseLock/A11yBaseLock.android.tsx +0 -16
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Managing screen reader focus order can be challenging, especially in complex or
|
|
|
6
6
|
|
|
7
7
|
| iOS reader | Android reader |
|
|
8
8
|
| --------------------------------------------------------- | ------------------------------------------------------------- |
|
|
9
|
-
| <img src="/.github/images/
|
|
9
|
+
| <img src="/.github/images/ios_example.gif" height="500" /> | <img src="/.github/images/android_example.gif" height="500" /> |
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
- Bridgeless
|
|
@@ -36,157 +36,6 @@ npm install react-native-a11y-order
|
|
|
36
36
|
yarn add react-native-a11y-order
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
## Recent Updates
|
|
40
|
-
|
|
41
|
-
#### Screen Reader Focus Events
|
|
42
|
-
|
|
43
|
-
| iOS | Android |
|
|
44
|
-
| :-- | :-- |
|
|
45
|
-
| <img src="/.github/images/screen-reader-focus-ios.gif" height="500" /> | <img src="/.github/images/screen-reader-focus-android.gif" height="500" /> |
|
|
46
|
-
|
|
47
|
-
> To enhance accessibility and provide better focus management, screen reader focus handlers have been added. These handlers allow you to capture and respond to screen reader focus events effectively, enabling features like managing animations, timers, and other interactions based on focus changes.
|
|
48
|
-
|
|
49
|
-
<details>
|
|
50
|
-
<summary>More Information</summary>
|
|
51
|
-
|
|
52
|
-
A11y.View Props:
|
|
53
|
-
| Prop | Description |
|
|
54
|
-
| :-- | :-- |
|
|
55
|
-
| onScreenReaderFocused | Triggered when the view gets focus from the screen reader. |
|
|
56
|
-
| onScreenReaderSubViewFocused | Triggered when a subview within the component is focused by the screen reader. |
|
|
57
|
-
| onScreenReaderSubViewBlurred | Triggered when the screen reader focus moves away or is blurred from a subview. |
|
|
58
|
-
| onScreenReaderSubViewFocusChange | Triggered when the focus status of a subview changes (either focused or blurred). |
|
|
59
|
-
| onScreenReaderDescendantFocusChanged | Triggered when any descendant subview is focused by the screen reader. Provides an object containing the focus status and the nativeId of the focused subview, if applicable. Example: < { status: string, nativeId?: string } >. |
|
|
60
|
-
|
|
61
|
-
```tsx
|
|
62
|
-
<A11y.View
|
|
63
|
-
onScreenReaderDescendantFocusChanged={(e) => console.log(e)}
|
|
64
|
-
onScreenReaderSubViewFocused={() => console.log('List has been focused')}
|
|
65
|
-
onScreenReaderSubViewBlurred={() => console.log('List has been blurred')}
|
|
66
|
-
onScreenReaderFocused={() => console.log('Focused')}
|
|
67
|
-
>
|
|
68
|
-
...
|
|
69
|
-
</A11y.View>
|
|
70
|
-
```
|
|
71
|
-
</details>
|
|
72
|
-
|
|
73
|
-
#### Focus Lock Functionality
|
|
74
|
-
|
|
75
|
-
| iOS | Android |
|
|
76
|
-
| :-- | :-- |
|
|
77
|
-
| <img src="/.github/images/focus-lock-ios.gif" height="500" /> | <img src="/.github/images/focus-lock-android.gif" height="500" /> |
|
|
78
|
-
|
|
79
|
-
> The focus lock functionality has been introduced with two new components: `A11y.FocusFrame` and `A11y.FocusTrap`. These components enable more robust accessibility by managing and restricting focus within specific areas of the screen.
|
|
80
|
-
|
|
81
|
-
<details>
|
|
82
|
-
<summary>More Information</summary>
|
|
83
|
-
|
|
84
|
-
- On iOS, `A11y.FocusTrap` uses the native `accessibilityViewIsModal` property to keep the focus within a defined area.
|
|
85
|
-
- On Android, where no equivalent to `accessibilityViewIsModal` exists, custom logic has been implemented as a workaround. By default, Android uses a custom Activity or Modal to limit focus. While using a Modal is considered the best practice for focus locking on Android, some scenarios—such as issues with React Native's Modal or library-specific constraints—may require alternative implementations.
|
|
86
|
-
|
|
87
|
-
#### How It Works
|
|
88
|
-
|
|
89
|
-
The focus lock functionality should be used as a pair:
|
|
90
|
-
|
|
91
|
-
- `A11y.FocusFrame`: This component is used at the root level of a "screen" to detect focus leaks and ensure that focus remains contained.
|
|
92
|
-
- `A11y.FocusTrap`: This component wraps the content area where focus should be explicitly locked.
|
|
93
|
-
|
|
94
|
-
| Prop | Description |
|
|
95
|
-
| :-- | :-- |
|
|
96
|
-
| ViewProps | Includes all standard React Native View properties, such as style, testID, etc. |
|
|
97
|
-
|
|
98
|
-
```tsx
|
|
99
|
-
<A11y.FocusFrame>
|
|
100
|
-
...
|
|
101
|
-
<A11y.FocusTrap>
|
|
102
|
-
<Text accessibilityRole="header">Locked Area</Text>
|
|
103
|
-
<Button
|
|
104
|
-
title="Confirm"
|
|
105
|
-
accessibilityLabel="Confirm action"
|
|
106
|
-
/>
|
|
107
|
-
</A11y.FocusTrap>
|
|
108
|
-
...
|
|
109
|
-
</A11y.FocusFrame>
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
</details>
|
|
113
|
-
|
|
114
|
-
#### A11y.PaneTitle and A11y.ScreenChange
|
|
115
|
-
|
|
116
|
-
| iOS | Android |
|
|
117
|
-
| :-- | :-- |
|
|
118
|
-
| <img src="/.github/images/announce-ios.gif" height="500" /> | <img src="/.github/images/announce-android.gif" height="500" /> |
|
|
119
|
-
|
|
120
|
-
> The components `A11y.PaneTitle` and `A11y.ScreenChange` have been introduced to enhance accessibility by providing robust support for announcing screen changes and their states.
|
|
121
|
-
|
|
122
|
-
<details>
|
|
123
|
-
<summary>More Information</summary>
|
|
124
|
-
|
|
125
|
-
Platform-Specific Behavior
|
|
126
|
-
-On Android, `A11y.PaneTitle` and `A11y.ScreenChange` utilize native properties, specifically: `activity.setTitle` and `setAccessibilityPaneTitle`.
|
|
127
|
-
- On iOS, due to the lack of equivalent native functionality, `A11yModule.announce` is used as a workaround to announce screen changes (see the `A11yModule.announce` section for details).
|
|
128
|
-
|
|
129
|
-
##### When to Use:
|
|
130
|
-
|
|
131
|
-
Currently, React Native doesn't provide APIs for announcing modal or screen transitions. To address this and improve accessibility, you can use `A11y.PaneTitle` or `A11y.ScreenChange` to announce:
|
|
132
|
-
- Screen transitions, such as navigating to a new screen (e.g., "Login Screen").
|
|
133
|
-
- Modal presentations, such as when a modal appears (e.g., "Confirm Modal").
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
A11y.PaneTitle Props
|
|
137
|
-
| Prop | Description |
|
|
138
|
-
| :-- | :-- |
|
|
139
|
-
| title | The title message to be announced for the screen or modal. |
|
|
140
|
-
| detachMessage | The message to be announced when this component is detached (e.g., when leaving the screen). |
|
|
141
|
-
| type | The type of announcement for Android. Options: activity, pane, or announce. |
|
|
142
|
-
| displayed | A trigger for screen focus changes, used to properly update the Android Activity title when switching screens. |
|
|
143
|
-
| withFocusRestore | Ensures that the screen reader focus is preserved and restored appropriately after a screen change. (iOS-specific) |
|
|
144
|
-
|
|
145
|
-
The A11y.ScreenChange component is a specialized implementation of A11y.PaneTitle. It is preconfigured with `type="activity"` for screen change announcements on Android and works identically to `A11y.PaneTitle`.
|
|
146
|
-
|
|
147
|
-
Example:
|
|
148
|
-
```tsx
|
|
149
|
-
export const LoginScreen = ({ navigation }) => {
|
|
150
|
-
const isFocused = useIsFocused();
|
|
151
|
-
return (
|
|
152
|
-
<View>
|
|
153
|
-
<A11y.ScreenChange
|
|
154
|
-
title="Login Screen"
|
|
155
|
-
displayed={isFocused}
|
|
156
|
-
/>
|
|
157
|
-
<View style={styles.container}>
|
|
158
|
-
<Text>Welcome to the Login Screen</Text>
|
|
159
|
-
<Button title="Continue" onPress={() => navigation.navigate('Home')} />
|
|
160
|
-
</View>
|
|
161
|
-
</View>
|
|
162
|
-
);
|
|
163
|
-
};
|
|
164
|
-
```
|
|
165
|
-
</details>
|
|
166
|
-
|
|
167
|
-
#### A11yModule.announce - Alternative Announcement Function
|
|
168
|
-
|
|
169
|
-
> The `A11yModule.announce` function has been introduced to improve accessibility announcement behavior on iOS.
|
|
170
|
-
|
|
171
|
-
<details>
|
|
172
|
-
<summary>More Information</summary>
|
|
173
|
-
Why Use `A11yModule.announce`?
|
|
174
|
-
|
|
175
|
-
On iOS, the default `AccessibilityInfo.announceForAccessibility` function can be interrupted by focus changes. This means that if you attempt to announce a message, the announcement could be prematurely cut off due to various events, such as screen navigation or the display of a modal.
|
|
176
|
-
|
|
177
|
-
To address this limitation, `A11yModule.announce` uses a custom solution built on native events to ensure that announcements are made reliably and are less likely to be interrupted.
|
|
178
|
-
|
|
179
|
-
A11yModule API:
|
|
180
|
-
| Function | Description |
|
|
181
|
-
| :-- | :-- |
|
|
182
|
-
| announce(message: string): void | Posts a string to be announced by the screen reader, ensuring improved reliability on iOS. |
|
|
183
|
-
|
|
184
|
-
```tsx
|
|
185
|
-
A11yModule.announce('This is a custom announcement, now more reliable on iOS!');
|
|
186
|
-
```
|
|
187
|
-
</details>
|
|
188
|
-
|
|
189
|
-
|
|
190
39
|
## Usage
|
|
191
40
|
|
|
192
41
|
#### A11y.Order, A11y.Index
|
|
@@ -322,9 +171,17 @@ These components enhance accessibility by providing better control over focus ma
|
|
|
322
171
|
- `A11y.FocusFrame`: Used at the root level of a "screen" to detect and prevent focus leaks, ensuring focus remains contained.
|
|
323
172
|
- `A11y.FocusTrap`: Wraps the content area to explicitly enforce focus confinement within a defined region.
|
|
324
173
|
|
|
174
|
+
On iOS, `A11y.FocusTrap` uses `accessibilityViewIsModal` to keep focus within the defined area. When `forceLock` is enabled, it additionally uses active enforcement — redirecting VoiceOver back into the trap whenever focus escapes and blocking focus from leaving at the system level.
|
|
175
|
+
|
|
176
|
+
On Android, `A11y.FocusTrap` uses a custom Activity or Modal to limit focus.
|
|
177
|
+
|
|
178
|
+
`A11y.FocusTrap` Props:
|
|
179
|
+
|
|
325
180
|
| Prop | Description |
|
|
326
181
|
| :-- | :-- |
|
|
327
182
|
| ViewProps | Includes all standard React Native View properties, such as style, testID, etc. |
|
|
183
|
+
| lockDisabled? | Disables the focus lock when `true`. |
|
|
184
|
+
| forceLock? | (iOS only) Enables active focus enforcement — VoiceOver is redirected back into the trap whenever focus escapes. Use when `accessibilityViewIsModal` alone is not sufficient. |
|
|
328
185
|
|
|
329
186
|
```tsx
|
|
330
187
|
<A11y.FocusFrame>
|
|
@@ -340,6 +197,22 @@ These components enhance accessibility by providing better control over focus ma
|
|
|
340
197
|
</A11y.FocusFrame>
|
|
341
198
|
```
|
|
342
199
|
|
|
200
|
+
Use `forceLock` when the standard lock is not enough to keep VoiceOver inside the trap:
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
<A11y.FocusFrame>
|
|
204
|
+
...
|
|
205
|
+
<A11y.FocusTrap forceLock>
|
|
206
|
+
<Text accessibilityRole="header">Locked Area</Text>
|
|
207
|
+
<Button
|
|
208
|
+
title="Confirm"
|
|
209
|
+
accessibilityLabel="Confirm action"
|
|
210
|
+
/>
|
|
211
|
+
</A11y.FocusTrap>
|
|
212
|
+
...
|
|
213
|
+
</A11y.FocusFrame>
|
|
214
|
+
```
|
|
215
|
+
|
|
343
216
|
## A11y.PaneTitle, A11y.ScreenChange
|
|
344
217
|
|
|
345
218
|
Components for screen change announcements
|
package/android/build.gradle
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
buildscript {
|
|
2
|
-
// Buildscript is evaluated before everything else so we can't use getExtOrDefault
|
|
3
|
-
def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["ExternalKeyboard_kotlinVersion"]
|
|
4
|
-
|
|
5
|
-
repositories {
|
|
6
|
-
google()
|
|
7
|
-
mavenCentral()
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
dependencies {
|
|
11
|
-
classpath "com.android.tools.build:gradle:7.2.1"
|
|
12
|
-
// noinspection DifferentKotlinGradleVersion
|
|
13
|
-
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
1
|
def reactNativeArchitectures() {
|
|
18
2
|
def value = rootProject.getProperties().get("reactNativeArchitectures")
|
|
19
3
|
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
|
@@ -24,7 +8,6 @@ def isNewArchitectureEnabled() {
|
|
|
24
8
|
}
|
|
25
9
|
|
|
26
10
|
apply plugin: "com.android.library"
|
|
27
|
-
apply plugin: "kotlin-android"
|
|
28
11
|
|
|
29
12
|
|
|
30
13
|
def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') }
|
|
@@ -115,7 +98,6 @@ dependencies {
|
|
|
115
98
|
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
|
|
116
99
|
//noinspection GradleDynamicVersion
|
|
117
100
|
implementation "com.facebook.react:react-native:+"
|
|
118
|
-
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
119
101
|
}
|
|
120
102
|
|
|
121
103
|
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
package com.a11yorder.services.focus;
|
|
2
2
|
|
|
3
|
-
import android.app.Activity;
|
|
4
3
|
import android.content.Context;
|
|
5
|
-
import android.util.Log;
|
|
6
|
-
import android.view.View;
|
|
7
4
|
import android.view.ViewGroup;
|
|
8
|
-
import android.view.accessibility.AccessibilityEvent;
|
|
9
5
|
|
|
10
|
-
import androidx.annotation.Nullable;
|
|
11
6
|
import androidx.fragment.app.Fragment;
|
|
12
7
|
|
|
13
8
|
import com.a11yorder.utils.A11yHelper;
|
|
@@ -15,9 +10,6 @@ import com.a11yorder.utils.FragmentUtils;
|
|
|
15
10
|
import com.facebook.react.bridge.ReactContext;
|
|
16
11
|
|
|
17
12
|
public class A11yFocusDelegate {
|
|
18
|
-
private static final int DEFAULT_DELAY = 300;
|
|
19
|
-
private static final int DEFAULT_RETRIES = 3;
|
|
20
|
-
|
|
21
13
|
private final A11yFocusProtocol delegate;
|
|
22
14
|
private final Context context;
|
|
23
15
|
|
|
@@ -27,46 +19,32 @@ public class A11yFocusDelegate {
|
|
|
27
19
|
}
|
|
28
20
|
|
|
29
21
|
private void focus() {
|
|
30
|
-
A11yFocusService.getInstance().requestFocus((ViewGroup) delegate
|
|
22
|
+
A11yFocusService.getInstance().requestFocus((ViewGroup) delegate);
|
|
31
23
|
}
|
|
32
24
|
|
|
33
25
|
private void simpleFocus() {
|
|
34
26
|
A11yFocusService.getInstance().simpleFocus((ViewGroup) delegate);
|
|
35
27
|
}
|
|
36
28
|
|
|
37
|
-
public void
|
|
38
|
-
|
|
39
|
-
A11yFocusService.getInstance().onFocused((View) delegate);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
@Nullable
|
|
44
|
-
private Activity getCurrentActivityFromContext() {
|
|
45
|
-
if (context instanceof ReactContext) {
|
|
46
|
-
return ((ReactContext) context).getCurrentActivity();
|
|
47
|
-
}
|
|
48
|
-
return null;
|
|
29
|
+
public void onFocused() {
|
|
30
|
+
A11yFocusService.getInstance().onFocused((ViewGroup) delegate);
|
|
49
31
|
}
|
|
50
32
|
|
|
51
33
|
public void requestFocus() {
|
|
52
|
-
if(!A11yHelper.isA11yServiceEnabled(context)) return;
|
|
34
|
+
if (!A11yHelper.isA11yServiceEnabled(context)) return;
|
|
53
35
|
|
|
54
|
-
Fragment currentFragment = FragmentUtils.findFragmentSafely((
|
|
36
|
+
Fragment currentFragment = FragmentUtils.findFragmentSafely((ViewGroup) delegate);
|
|
55
37
|
|
|
56
|
-
if (currentFragment
|
|
57
|
-
|
|
38
|
+
if (currentFragment == null) {
|
|
39
|
+
focus();
|
|
58
40
|
return;
|
|
59
41
|
}
|
|
60
42
|
|
|
61
|
-
if(currentFragment
|
|
62
|
-
|
|
63
|
-
FragmentUtils.waitForFragment(activity, currentFragment, this::focus);
|
|
43
|
+
if (currentFragment.isResumed()) {
|
|
44
|
+
simpleFocus();
|
|
64
45
|
return;
|
|
65
46
|
}
|
|
66
47
|
|
|
67
|
-
this
|
|
48
|
+
FragmentUtils.waitForFragmentResume(currentFragment, this::focus);
|
|
68
49
|
}
|
|
69
50
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
@@ -7,15 +7,17 @@ import com.a11yorder.services.order.linking.A11yOrderLinking;
|
|
|
7
7
|
import com.a11yorder.utils.A11yHelper;
|
|
8
8
|
|
|
9
9
|
import java.lang.ref.WeakReference;
|
|
10
|
+
import java.util.Objects;
|
|
10
11
|
|
|
11
12
|
public class A11yOrderService {
|
|
12
13
|
public static final int ORDER_FOCUS_TYPE_DEFAULT = 0;
|
|
13
14
|
public static final int ORDER_FOCUS_TYPE_CHILD = 1;
|
|
14
15
|
public static final int ORDER_FOCUS_TYPE_LEGACY = 2;
|
|
16
|
+
|
|
15
17
|
private final ViewGroup delegate;
|
|
16
|
-
|
|
18
|
+
private String orderKey;
|
|
17
19
|
private Integer index;
|
|
18
|
-
private
|
|
20
|
+
private int focusType = ORDER_FOCUS_TYPE_DEFAULT;
|
|
19
21
|
private WeakReference<View> orderViewRef;
|
|
20
22
|
private boolean isLinked = false;
|
|
21
23
|
|
|
@@ -23,82 +25,104 @@ public class A11yOrderService {
|
|
|
23
25
|
this.delegate = delegate;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
// ─── Accessors ────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
26
30
|
public View getStoredView() {
|
|
27
31
|
return orderViewRef != null ? orderViewRef.get() : null;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
public View getFocusView() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
switch (focusType) {
|
|
36
|
+
case ORDER_FOCUS_TYPE_LEGACY: {
|
|
37
|
+
View stored = getStoredView();
|
|
38
|
+
return stored != null ? stored : delegate.getChildAt(0);
|
|
34
39
|
}
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
case ORDER_FOCUS_TYPE_CHILD:
|
|
41
|
+
return A11yHelper.findFirstAccessible(delegate, true);
|
|
42
|
+
default:
|
|
43
|
+
return delegate;
|
|
37
44
|
}
|
|
38
|
-
|
|
39
|
-
if (focusType == ORDER_FOCUS_TYPE_DEFAULT) {
|
|
40
|
-
return delegate;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (focusType == ORDER_FOCUS_TYPE_CHILD) {
|
|
44
|
-
return A11yHelper.findFirstAccessible(delegate, true);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return null;
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
boolean hasBeenChanged = this.index != null;
|
|
47
|
+
// ─── Prop setters ─────────────────────────────────────────────────────────
|
|
52
48
|
|
|
49
|
+
public void setIndex(int index) {
|
|
50
|
+
if (this.index != null && this.index.equals(index)) return;
|
|
51
|
+
boolean isFirstSet = this.index == null;
|
|
53
52
|
this.index = index;
|
|
54
|
-
if (
|
|
55
|
-
|
|
53
|
+
if (isFirstSet) {
|
|
54
|
+
// Both props may now be ready for the first time — attempt registration.
|
|
55
|
+
register();
|
|
56
|
+
} else if (isLinked) {
|
|
57
|
+
refresh();
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
public void setOrderKey(String newKey) {
|
|
62
|
+
if (Objects.equals(this.orderKey, newKey)) return;
|
|
63
|
+
if (isLinked) unregister();
|
|
64
|
+
this.orderKey = newKey;
|
|
65
|
+
register();
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
public void setFocusType(int focusType) {
|
|
60
|
-
int
|
|
69
|
+
int prev = this.focusType;
|
|
61
70
|
this.focusType = focusType;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.refresh();
|
|
71
|
+
if (isLinked && prev != focusType) {
|
|
72
|
+
refresh();
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
|
|
68
|
-
|
|
76
|
+
// ─── Registration ─────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
private boolean isReady() {
|
|
69
79
|
return orderKey != null && index != null;
|
|
70
80
|
}
|
|
71
81
|
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
82
|
+
private void register() {
|
|
83
|
+
if (!isReady() || isLinked) return;
|
|
84
|
+
View target = getFocusView();
|
|
85
|
+
if (target != null) {
|
|
86
|
+
A11yOrderLinking.getInstance().addViewRelationship(target, orderKey, index);
|
|
87
|
+
isLinked = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
75
90
|
|
|
76
|
-
|
|
91
|
+
private void unregister() {
|
|
92
|
+
if (orderKey != null && index != null) {
|
|
93
|
+
A11yOrderLinking.getInstance().removeRelationship(orderKey, index);
|
|
94
|
+
}
|
|
95
|
+
isLinked = false;
|
|
96
|
+
}
|
|
77
97
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
98
|
+
private void refresh() {
|
|
99
|
+
if (!isReady()) return;
|
|
100
|
+
View target = getFocusView();
|
|
101
|
+
if (target != null) {
|
|
102
|
+
A11yOrderLinking.getInstance().refreshIndexes(target, orderKey, index);
|
|
83
103
|
}
|
|
84
104
|
}
|
|
85
105
|
|
|
86
|
-
|
|
87
|
-
if (!getIsReady()) return;
|
|
106
|
+
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
88
107
|
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
108
|
+
public void link(View child) {
|
|
109
|
+
if (orderViewRef != null && orderViewRef.get() != null) return;
|
|
110
|
+
orderViewRef = new WeakReference<>(child);
|
|
111
|
+
register();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public void attach() {
|
|
115
|
+
if (!isReady() || isLinked) return;
|
|
116
|
+
View child = delegate.getChildAt(0);
|
|
117
|
+
if (child != null) link(child);
|
|
93
118
|
}
|
|
94
119
|
|
|
95
|
-
public void clear(View
|
|
96
|
-
if (orderViewRef != null && orderViewRef.get() ==
|
|
120
|
+
public void clear(View child) {
|
|
121
|
+
if (orderViewRef != null && orderViewRef.get() == child) {
|
|
97
122
|
orderViewRef.clear();
|
|
98
123
|
orderViewRef = null;
|
|
99
124
|
}
|
|
100
|
-
|
|
101
|
-
this.remove();
|
|
125
|
+
unregister();
|
|
102
126
|
}
|
|
103
127
|
|
|
104
128
|
public void detach() {
|
|
@@ -106,25 +130,6 @@ public class A11yOrderService {
|
|
|
106
130
|
orderViewRef.clear();
|
|
107
131
|
orderViewRef = null;
|
|
108
132
|
}
|
|
109
|
-
|
|
110
|
-
this.remove();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private void remove() {
|
|
114
|
-
if (orderKey != null && index != null) {
|
|
115
|
-
A11yOrderLinking.getInstance().removeRelationship(orderKey, index);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
isLinked = false;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
public void attach() {
|
|
123
|
-
if (!getIsReady() || isLinked) return;
|
|
124
|
-
|
|
125
|
-
View child = delegate.getChildAt(0);
|
|
126
|
-
if (child == null) return;
|
|
127
|
-
|
|
128
|
-
this.link(child);
|
|
133
|
+
unregister();
|
|
129
134
|
}
|
|
130
135
|
}
|
|
@@ -3,92 +3,66 @@ package com.a11yorder.services.order.linking;
|
|
|
3
3
|
import android.os.Build;
|
|
4
4
|
import android.view.View;
|
|
5
5
|
|
|
6
|
-
import java.lang.ref.WeakReference;
|
|
7
|
-
import java.util.Map;
|
|
8
|
-
import java.util.NavigableSet;
|
|
9
|
-
import java.util.TreeSet;
|
|
10
|
-
|
|
11
6
|
public class A11yLinkingQueue {
|
|
12
|
-
|
|
7
|
+
private final WeakTreeMap viewMap = new WeakTreeMap();
|
|
13
8
|
|
|
9
|
+
// ─── Link helpers ──────────────────────────────────────────────────────────
|
|
14
10
|
|
|
15
11
|
private void linkPosition(View prev, View next) {
|
|
16
|
-
if (prev
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
12
|
+
if (prev == null || next == null) return;
|
|
13
|
+
prev.setNextFocusForwardId(next.getId());
|
|
14
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
|
15
|
+
prev.setAccessibilityTraversalBefore(next.getId());
|
|
21
16
|
}
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
private void
|
|
25
|
-
|
|
26
|
-
View
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (prevView != null) {
|
|
30
|
-
this.linkPosition(prevView, currentView);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (nextView != null) {
|
|
34
|
-
this.linkPosition(currentView, nextView);
|
|
19
|
+
private void clearForwardLink(View view) {
|
|
20
|
+
if (view == null) return;
|
|
21
|
+
view.setNextFocusForwardId(View.NO_ID);
|
|
22
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
|
23
|
+
view.setAccessibilityTraversalBefore(View.NO_ID);
|
|
35
24
|
}
|
|
36
25
|
}
|
|
37
26
|
|
|
38
27
|
private void unlinkLast() {
|
|
39
|
-
|
|
40
|
-
if (lastView != null) {
|
|
41
|
-
lastView.setNextFocusForwardId(View.NO_ID);
|
|
42
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
|
43
|
-
lastView.setAccessibilityTraversalBefore(View.NO_ID);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
28
|
+
clearForwardLink(viewMap.last());
|
|
46
29
|
}
|
|
47
30
|
|
|
48
|
-
|
|
49
|
-
View nextView = viewMap.getNext(position);
|
|
50
|
-
View prevView = viewMap.getPrev(position);
|
|
51
|
-
|
|
52
|
-
if (prevView != null && nextView != null) {
|
|
53
|
-
this.linkPosition(prevView, nextView);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
boolean shouldUnlinkLast = nextView == null;
|
|
57
|
-
this.viewMap.remove(position);
|
|
58
|
-
|
|
59
|
-
if (shouldUnlinkLast) {
|
|
60
|
-
this.unlinkLast();
|
|
61
|
-
}
|
|
62
|
-
}
|
|
31
|
+
// ─── Public API ────────────────────────────────────────────────────────────
|
|
63
32
|
|
|
64
33
|
public void addPosition(View view, int position) {
|
|
65
|
-
if (
|
|
66
|
-
|
|
34
|
+
if (viewMap.get(position) == view) return;
|
|
35
|
+
viewMap.put(position, view);
|
|
36
|
+
View prev = viewMap.getPrev(position);
|
|
37
|
+
View next = viewMap.getNext(position);
|
|
38
|
+
if (prev != null) linkPosition(prev, view);
|
|
39
|
+
if (next != null) linkPosition(view, next);
|
|
67
40
|
}
|
|
68
41
|
|
|
69
42
|
public void removeFromOrder(int position) {
|
|
70
|
-
if (!
|
|
71
|
-
|
|
43
|
+
if (!viewMap.containsKey(position)) return;
|
|
44
|
+
View prev = viewMap.getPrev(position);
|
|
45
|
+
View next = viewMap.getNext(position);
|
|
46
|
+
boolean wasLast = next == null;
|
|
47
|
+
viewMap.remove(position);
|
|
48
|
+
if (prev != null && next != null) {
|
|
49
|
+
linkPosition(prev, next);
|
|
50
|
+
} else if (wasLast) {
|
|
51
|
+
// Clear the forward link on the new last element.
|
|
52
|
+
unlinkLast();
|
|
53
|
+
}
|
|
72
54
|
}
|
|
73
55
|
|
|
74
56
|
public void refreshIndexes(View view, int position) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
View nextEntry = this.viewMap.getNext(positionEntry.getKey());
|
|
81
|
-
|
|
82
|
-
if (nextEntry != null) {
|
|
83
|
-
linkPosition(currentView, nextEntry);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
57
|
+
viewMap.put(position, view);
|
|
58
|
+
viewMap.forEachLive((pos, current) -> {
|
|
59
|
+
View next = viewMap.getNext(pos);
|
|
60
|
+
if (next != null) linkPosition(current, next);
|
|
61
|
+
});
|
|
88
62
|
unlinkLast();
|
|
89
63
|
}
|
|
90
64
|
|
|
91
|
-
public boolean isEmpty
|
|
92
|
-
return
|
|
65
|
+
public boolean isEmpty() {
|
|
66
|
+
return viewMap.isEmpty();
|
|
93
67
|
}
|
|
94
68
|
}
|