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.
Files changed (61) hide show
  1. package/README.md +25 -152
  2. package/android/build.gradle +0 -18
  3. package/android/src/main/java/com/a11yorder/services/focus/A11yFocusDelegate.java +10 -32
  4. package/android/src/main/java/com/a11yorder/services/order/A11yOrderService.java +70 -65
  5. package/android/src/main/java/com/a11yorder/services/order/linking/A11yLinkingQueue.java +37 -63
  6. package/android/src/main/java/com/a11yorder/services/order/linking/A11yOrderLinking.java +11 -14
  7. package/android/src/main/java/com/a11yorder/services/order/linking/WeakTreeMap.java +53 -31
  8. package/android/src/main/java/com/a11yorder/utils/A11yHelper.java +39 -59
  9. package/android/src/main/java/com/a11yorder/utils/ChoreographerUtils.java +7 -12
  10. package/android/src/main/java/com/a11yorder/utils/FragmentUtils.java +8 -48
  11. package/android/src/main/java/com/a11yorder/views/A11yIndexView/A11yIndexView.java +1 -1
  12. package/android/src/main/java/com/a11yorder/views/A11yLockView/A11yLockViewManager.java +5 -0
  13. package/android/src/main/java/com/a11yorder/views/A11yView/A11yView.java +1 -1
  14. package/android/src/oldarch/A11yLockViewManagerSpec.java +2 -0
  15. package/ios/delegates/RNAOViewItemDelegate/RNAOViewItemDelegate.mm +1 -1
  16. package/ios/extensions/RCTModalHostViewComponentView+RNAOA11yOrder.mm +21 -21
  17. package/ios/extensions/UIView+RNAOA11yOrder.mm +17 -20
  18. package/ios/extensions/UIViewController+RNAOA11yOrder.mm +8 -8
  19. package/ios/helpers/RNAOSwizzleInstall.h +30 -0
  20. package/ios/services/RNAOA11yItemDelegate/RNAOA11yItemDelegate.h +4 -6
  21. package/ios/services/RNAOA11yItemDelegate/RNAOA11yItemDelegate.mm +98 -87
  22. package/ios/services/RNAOA11yOrderLinking/RNAOA11yOrderLinking.h +5 -3
  23. package/ios/services/RNAOA11yOrderLinking/RNAOA11yOrderLinking.mm +49 -49
  24. package/ios/services/RNAOA11yRelationship/RNAOA11yRelationship.h +1 -0
  25. package/ios/services/RNAOA11yRelationship/RNAOA11yRelationship.mm +42 -36
  26. package/ios/services/RNAOSortedMap/RNAOSortedMap.h +2 -1
  27. package/ios/services/RNAOSortedMap/RNAOSortedMap.mm +48 -47
  28. package/ios/views/RNAOA11yLockView/RNAOA11yLockView.h +4 -2
  29. package/ios/views/RNAOA11yLockView/RNAOA11yLockView.mm +89 -3
  30. package/ios/views/RNAOA11yLockView/RNAOA11yLockViewManager.mm +3 -0
  31. package/lib/commonjs/components/A11yLock/A11yBaseLock/A11yBaseLock.js +18 -2
  32. package/lib/commonjs/components/A11yLock/A11yBaseLock/A11yBaseLock.js.map +1 -1
  33. package/lib/commonjs/components/A11yLock/A11yFocusTrap/A11yFocusTrap.js +23 -7
  34. package/lib/commonjs/components/A11yLock/A11yFocusTrap/A11yFocusTrap.js.map +1 -1
  35. package/lib/commonjs/nativeSpecs/A11yLockNativeComponent.ts +1 -0
  36. package/lib/module/components/A11yLock/A11yBaseLock/A11yBaseLock.js +17 -2
  37. package/lib/module/components/A11yLock/A11yBaseLock/A11yBaseLock.js.map +1 -1
  38. package/lib/module/components/A11yLock/A11yFocusTrap/A11yFocusTrap.js +23 -7
  39. package/lib/module/components/A11yLock/A11yFocusTrap/A11yFocusTrap.js.map +1 -1
  40. package/lib/module/nativeSpecs/A11yLockNativeComponent.ts +1 -0
  41. package/lib/typescript/src/components/A11yLock/A11yBaseLock/A11yBaseLock.d.ts +2 -1
  42. package/lib/typescript/src/components/A11yLock/A11yBaseLock/A11yBaseLock.d.ts.map +1 -1
  43. package/lib/typescript/src/components/A11yLock/A11yFocusTrap/A11yFocusTrap.d.ts +1 -1
  44. package/lib/typescript/src/components/A11yLock/A11yFocusTrap/A11yFocusTrap.d.ts.map +1 -1
  45. package/lib/typescript/src/index.d.ts +1 -1
  46. package/lib/typescript/src/nativeSpecs/A11yLockNativeComponent.d.ts +1 -0
  47. package/lib/typescript/src/nativeSpecs/A11yLockNativeComponent.d.ts.map +1 -1
  48. package/lib/typescript/src/types/A11yLock.types.d.ts +1 -0
  49. package/lib/typescript/src/types/A11yLock.types.d.ts.map +1 -1
  50. package/package.json +3 -2
  51. package/src/components/A11yLock/A11yBaseLock/A11yBaseLock.tsx +20 -3
  52. package/src/components/A11yLock/A11yFocusTrap/A11yFocusTrap.tsx +24 -5
  53. package/src/nativeSpecs/A11yLockNativeComponent.ts +1 -0
  54. package/src/types/A11yLock.types.ts +1 -0
  55. package/lib/commonjs/components/A11yLock/A11yBaseLock/A11yBaseLock.android.js +0 -23
  56. package/lib/commonjs/components/A11yLock/A11yBaseLock/A11yBaseLock.android.js.map +0 -1
  57. package/lib/module/components/A11yLock/A11yBaseLock/A11yBaseLock.android.js +0 -18
  58. package/lib/module/components/A11yLock/A11yBaseLock/A11yBaseLock.android.js.map +0 -1
  59. package/lib/typescript/src/components/A11yLock/A11yBaseLock/A11yBaseLock.android.d.ts +0 -4
  60. package/lib/typescript/src/components/A11yLock/A11yBaseLock/A11yBaseLock.android.d.ts.map +0 -1
  61. 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/ios-reader.gif" height="500" /> | <img src="/.github/images/android-reader.gif" height="500" /> |
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
@@ -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, DEFAULT_DELAY, DEFAULT_RETRIES);
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 onAccessibilityEvent(View child, AccessibilityEvent event) {
38
- if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
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((View) delegate);
36
+ Fragment currentFragment = FragmentUtils.findFragmentSafely((ViewGroup) delegate);
55
37
 
56
- if (currentFragment != null && currentFragment.isResumed()) {
57
- this.simpleFocus();
38
+ if (currentFragment == null) {
39
+ focus();
58
40
  return;
59
41
  }
60
42
 
61
- if(currentFragment != null) {
62
- Activity activity = getCurrentActivityFromContext();
63
- FragmentUtils.waitForFragment(activity, currentFragment, this::focus);
43
+ if (currentFragment.isResumed()) {
44
+ simpleFocus();
64
45
  return;
65
46
  }
66
47
 
67
- this.focus();
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
- public String orderKey;
18
+ private String orderKey;
17
19
  private Integer index;
18
- private Integer focusType = ORDER_FOCUS_TYPE_DEFAULT;
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
- if (focusType == ORDER_FOCUS_TYPE_LEGACY) {
32
- if (orderViewRef != null && orderViewRef.get() != null) {
33
- return orderViewRef.get();
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
- return delegate.getChildAt(0);
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
- public void setIndex(int index) {
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 (hasBeenChanged) {
55
- this.refresh();
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 prevFocusType = this.focusType;
69
+ int prev = this.focusType;
61
70
  this.focusType = focusType;
62
-
63
- if (isLinked && prevFocusType != focusType) {
64
- this.refresh();
71
+ if (isLinked && prev != focusType) {
72
+ refresh();
65
73
  }
66
74
  }
67
75
 
68
- public boolean getIsReady() {
76
+ // ─── Registration ─────────────────────────────────────────────────────────
77
+
78
+ private boolean isReady() {
69
79
  return orderKey != null && index != null;
70
80
  }
71
81
 
72
- public void link(View view) {
73
- if (orderViewRef == null || orderViewRef.get() == null) {
74
- orderViewRef = new WeakReference<>(view);
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
- if (!this.getIsReady()) return;
91
+ private void unregister() {
92
+ if (orderKey != null && index != null) {
93
+ A11yOrderLinking.getInstance().removeRelationship(orderKey, index);
94
+ }
95
+ isLinked = false;
96
+ }
77
97
 
78
- View target = getFocusView();
79
- if (target != null) {
80
- A11yOrderLinking.getInstance().addViewRelationship(target, orderKey, index);
81
- isLinked = true;
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
- public void refresh() {
87
- if (!getIsReady()) return;
106
+ // ─── Lifecycle ────────────────────────────────────────────────────────────
88
107
 
89
- View view = getFocusView();
90
- if (view != null) {
91
- A11yOrderLinking.getInstance().refreshIndexes(view, orderKey, index);
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 view) {
96
- if (orderViewRef != null && orderViewRef.get() == view) {
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
- public WeakTreeMap viewMap = new WeakTreeMap();
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 != null && next != null) {
17
- prev.setNextFocusForwardId(next.getId());
18
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
19
- prev.setAccessibilityTraversalBefore(next.getId());
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 addWithLinking(int position, View currentView) {
25
- viewMap.put(position, currentView);
26
- View nextView = viewMap.getNext(position);
27
- View prevView = viewMap.getPrev(position);
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
- View lastView = viewMap.last();
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
- private void reLinkWithRemove(int position) {
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 (this.viewMap.get(position) == view) return;
66
- this.addWithLinking(position, view);
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 (!this.viewMap.containsKey(position)) return;
71
- this.reLinkWithRemove(position);
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
- this.viewMap.put(position, view);
76
-
77
- for (Map.Entry<Integer, WeakReference<View>> positionEntry : this.viewMap.entrySet()) {
78
- View currentView = WeakTreeMap.unwrapViewRef(positionEntry);
79
- if (currentView != null) {
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 this.viewMap.isEmpty();
65
+ public boolean isEmpty() {
66
+ return viewMap.isEmpty();
93
67
  }
94
68
  }