Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/jest-preset/jest/mocks/AccessibilityInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const AccessibilityInfo = {
$FlowFixMe,
$FlowFixMe,
>,
getEnabledAccessibilityServices: jest.fn(() =>
Promise.resolve([]),
) as JestMockFn<$FlowFixMe, $FlowFixMe>,
isAccessibilityServiceEnabled: jest.fn(() =>
Promise.resolve(false),
) as JestMockFn<$FlowFixMe, $FlowFixMe>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ export interface AccessibilityInfoStatic {
*/
isAccessibilityServiceEnabled(): Promise<boolean>;

/**
* Query the enabled accessibility services.
*
* Returns a promise which resolves to an array of enabled accessibility
* service IDs (from `AccessibilityServiceInfo.getId()`).
*
* @platform android
*/
getEnabledAccessibilityServices(
feedbackTypeFlags?: number,
): Promise<Array<string>>;

/**
* Add an event handler. Supported events:
* - announcementFinished: iOS-only event. Fires when the screen reader has finished making an announcement.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,40 @@ const AccessibilityInfo = {
});
},

/**
* Query the enabled accessibility services.
*
* Returns a promise which resolves to an array of enabled accessibility
* service IDs (from `AccessibilityServiceInfo.getId()`).
*
* @platform android
*/
getEnabledAccessibilityServices(
feedbackTypeFlags: number = -1,
): Promise<Array<string>> {
if (Platform.OS === 'android') {
return new Promise((resolve, reject) => {
if (
NativeAccessibilityInfoAndroid != null &&
NativeAccessibilityInfoAndroid.getEnabledAccessibilityServices != null
) {
NativeAccessibilityInfoAndroid.getEnabledAccessibilityServices(
feedbackTypeFlags,
resolve,
);
} else {
reject(
new Error(
'NativeAccessibilityInfoAndroid.getEnabledAccessibilityServices is not available',
),
);
}
});
} else {
return Promise.resolve([]);
}
},

/**
* Add an event handler. Supported events:
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ let mockNativeAccessibilityManagerIOS: {
};

const ANDROID_RECOMMENDED_TIMEOUT = 6000;
const ANDROID_ENABLED_SERVICES = [
'com.example.AccessibilityService1',
'com.example.AccessibilityService2',
];
const mockIsHighTextContrastEnabled = jest.fn(onSuccess => onSuccess(true));
const mockIsGrayscaleEnabled = jest.fn(onSuccess => onSuccess(true));
const mockIsInvertColorsEnabled = jest.fn(onSuccess => onSuccess(true));
Expand All @@ -113,6 +117,9 @@ const mockIsAccessibilityServiceEnabled = jest.fn(onSuccess => onSuccess(true));
const mockGetRecommendedTimeoutMillis = jest.fn((originalTimeout, onSuccess) =>
onSuccess(ANDROID_RECOMMENDED_TIMEOUT),
);
const mockGetEnabledAccessibilityServices = jest.fn(
(feedbackTypeFlags, onSuccess) => onSuccess(ANDROID_ENABLED_SERVICES),
);
let mockNativeAccessibilityInfoAndroid: {
isHighTextContrastEnabled: JestMockFn<
[onSuccess: (isHighTextContrastEnabled: boolean) => void],
Expand Down Expand Up @@ -142,6 +149,13 @@ let mockNativeAccessibilityInfoAndroid: {
[originalTimeout: number, onSuccess: (recommendedTimeout: number) => void],
void,
> | null,
getEnabledAccessibilityServices: JestMockFn<
[
feedbackTypeFlags: number,
onSuccess: (enabledServices: Array<string>) => void,
],
void,
> | null,
} = {
isHighTextContrastEnabled: mockIsHighTextContrastEnabled,
isGrayscaleEnabled: mockIsGrayscaleEnabled,
Expand All @@ -150,6 +164,7 @@ let mockNativeAccessibilityInfoAndroid: {
isTouchExplorationEnabled: mockIsTouchExplorationEnabled,
isAccessibilityServiceEnabled: mockIsAccessibilityServiceEnabled,
getRecommendedTimeoutMillis: mockGetRecommendedTimeoutMillis,
getEnabledAccessibilityServices: mockGetEnabledAccessibilityServices,
};

jest.mock('../NativeAccessibilityManager', () => ({
Expand Down Expand Up @@ -789,6 +804,69 @@ describe('AccessibilityInfo', () => {
});
});

describe('getEnabledAccessibilityServices', () => {
describe('Android', () => {
it('should return the enabled accessibility service IDs', async () => {
/* $FlowFixMe[incompatible-type] */
Platform.OS = 'android';

const enabledServices =
await AccessibilityInfo.getEnabledAccessibilityServices();

expect(mockGetEnabledAccessibilityServices).toHaveBeenCalledWith(
-1,
expect.any(Function),
);
expect(enabledServices).toEqual(ANDROID_ENABLED_SERVICES);
});

it('should pass the provided feedbackTypeFlags to native', async () => {
/* $FlowFixMe[incompatible-type] */
Platform.OS = 'android';

await AccessibilityInfo.getEnabledAccessibilityServices(1);

expect(mockGetEnabledAccessibilityServices).toHaveBeenCalledWith(
1,
expect.any(Function),
);
});

it('should throw error if getEnabledAccessibilityServices is not available', async () => {
/* $FlowFixMe[incompatible-type] */
Platform.OS = 'android';

mockNativeAccessibilityInfoAndroid.getEnabledAccessibilityServices =
null;

const result: unknown =
await AccessibilityInfo.getEnabledAccessibilityServices().catch(
e => e,
);

invariant(
result instanceof Error,
'Expected getEnabledAccessibilityServices to reject',
);
expect(result.message).toEqual(
'NativeAccessibilityInfoAndroid.getEnabledAccessibilityServices is not available',
);
});
});

describe('iOS', () => {
it('should return an empty array', async () => {
/* $FlowFixMe[incompatible-type] */
Platform.OS = 'ios';

const enabledServices =
await AccessibilityInfo.getEnabledAccessibilityServices();

expect(enabledServices).toEqual([]);
});
});
});

afterEach(() => {
mockNativeAccessibilityManagerIOS = {
getCurrentPrefersCrossFadeTransitionsState:
Expand All @@ -812,6 +890,7 @@ describe('AccessibilityInfo', () => {
isTouchExplorationEnabled: mockIsTouchExplorationEnabled,
isAccessibilityServiceEnabled: mockIsAccessibilityServiceEnabled,
getRecommendedTimeoutMillis: mockGetRecommendedTimeoutMillis,
getEnabledAccessibilityServices: mockGetEnabledAccessibilityServices,
};
jest.requireMock('../NativeAccessibilityInfo').default =
mockNativeAccessibilityInfoAndroid;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

package com.facebook.react.modules.accessibilityinfo

import android.accessibilityservice.AccessibilityServiceInfo
import android.content.ContentResolver
import android.content.Context
import android.database.ContentObserver
Expand All @@ -20,6 +21,7 @@ import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.bridge.WritableNativeArray
import com.facebook.react.module.annotations.ReactModule

/**
Expand Down Expand Up @@ -178,6 +180,20 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) :
successCallback.invoke(accessibilityServiceEnabled)
}

override fun getEnabledAccessibilityServices(
feedbackTypeFlags: Double,
successCallback: Callback,
) {
val enabledServices =
accessibilityManager?.getEnabledAccessibilityServiceList(feedbackTypeFlags.toInt())
?: emptyList<AccessibilityServiceInfo>()
val serviceIds = WritableNativeArray()
for (service in enabledServices) {
service.id?.let { serviceIds.pushString(it) }
}
successCallback.invoke(serviceIds)
}

private fun updateAndSendReduceMotionChangeEvent() {
val isReduceMotionEnabled = isReduceMotionEnabledValue
if (reduceMotionEnabled != isReduceMotionEnabled) {
Expand Down
7 changes: 5 additions & 2 deletions packages/react-native/ReactNativeApi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<53280bff7703ea0e9e912e22543fd568>>
* @generated SignedSource<<f083ed39cb1c620188903377fbf2d633>>
*
* This file was generated by scripts/js-api/build-types/index.js.
*/
Expand Down Expand Up @@ -102,6 +102,9 @@ declare const AccessibilityInfo_default: {
queue?: boolean
},
): void
getEnabledAccessibilityServices(
feedbackTypeFlags?: number,
): Promise<Array<string>>
getRecommendedTimeoutMillis(originalTimeout: number): Promise<number>
isAccessibilityServiceEnabled(): Promise<boolean>
isBoldTextEnabled(): Promise<boolean>
Expand Down Expand Up @@ -5705,7 +5708,7 @@ declare type WrapperComponentProvider = (
export {
AccessibilityActionEvent, // a0d4daa0
AccessibilityActionInfo, // db47a917
AccessibilityInfo, // 23a3aa9b
AccessibilityInfo, // ec383129
AccessibilityProps, // eda80653
AccessibilityRole, // f2f2e066
AccessibilityState, // b0c2b3f7
Expand Down
5 changes: 5 additions & 0 deletions packages/react-native/__typetests__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,11 @@ AccessibilityInfo.getRecommendedTimeoutMillis(5000).then(timeoutMiles =>
`AccessibilityInfo.getRecommendedTimeoutMillis => ${timeoutMiles}`,
),
);
AccessibilityInfo.getEnabledAccessibilityServices(1).then(services =>
console.log(
`AccessibilityInfo.getEnabledAccessibilityServices => ${services}`,
),
);

AccessibilityInfo.addEventListener('accessibilityServiceChanged', isEnabled =>
console.log(`AccessibilityInfo.accessibilityServiceChanged => ${isEnabled}`),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export interface Spec extends TurboModule {
mSec: number,
onSuccess: (recommendedTimeoutMillis: number) => void,
) => void;
readonly getEnabledAccessibilityServices?: (
feedbackTypeFlags: number,
onSuccess: (enabledServices: Array<string>) => void,
) => void;
readonly isGrayscaleEnabled?: (
onSuccess: (isGrayscaleEnabled: boolean) => void,
) => void;
Expand Down