@react-native-ama/core 0.0.1 → 0.0.2
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/package.json +2 -2
- package/src/components/AMAProvider.integration.test.tsx +145 -0
- package/src/components/AMAProvider.test.tsx +199 -0
- package/src/components/AMAProvider.tsx +231 -0
- package/src/components/AutofocusContainer.test.tsx +69 -0
- package/src/components/AutofocusContainer.tsx +43 -0
- package/src/components/HideChildrenFromAccessibilityTree.test.tsx +128 -0
- package/src/components/HideChildrenFromAccessibilityTree.tsx +55 -0
- package/src/hooks/useButtonChecks.ts +73 -0
- package/src/hooks/useChecks.test.ts +214 -0
- package/src/hooks/useChecks.ts +207 -0
- package/src/hooks/useFocus.test.ts +98 -0
- package/src/hooks/useFocus.ts +42 -0
- package/src/hooks/useTimedAction.android.test.ts +39 -0
- package/src/hooks/useTimedAction.ios.test.ts +46 -0
- package/src/hooks/useTimedAction.ts +40 -0
- package/src/index.ts +16 -0
- package/src/utils/numerify.test.ts +11 -0
- package/src/utils/numerify.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-native-ama/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Accessible Mobile App Library for React Native",
|
|
5
5
|
"private": false,
|
|
6
6
|
"react-native": "src/index",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"build": "rm -rf dist && tsc -p ./tsconfig.build.json",
|
|
24
24
|
"typecheck": "tsc --noEmit"
|
|
25
25
|
},
|
|
26
|
-
"
|
|
26
|
+
"dependencies": {
|
|
27
27
|
"@react-native-ama/internal": "*"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { act, render } from '@testing-library/react-native';
|
|
2
|
+
import { flushMicroTasks } from '@testing-library/react-native/build/flush-micro-tasks';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
import { AMAContextValue, AMAProvider, useAMAContext } from './AMAProvider';
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
jest.clearAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('AMAProvider', () => {
|
|
12
|
+
describe('When __DEV__ is true', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
// @ts-ignore
|
|
15
|
+
global.__DEV__ = true;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('trackError', () => {
|
|
19
|
+
it('exports trackError', async () => {
|
|
20
|
+
await renderAMAProvider();
|
|
21
|
+
|
|
22
|
+
expect(amaContextValuesRef.trackError).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('shows the AMA error banner when called', async () => {
|
|
26
|
+
const renderAPI = await renderAMAProvider();
|
|
27
|
+
|
|
28
|
+
expect(renderAPI.queryByTestId('amaError')).toBeNull();
|
|
29
|
+
|
|
30
|
+
act(() => {
|
|
31
|
+
amaContextValuesRef.trackError('123');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(renderAPI.getByTestId('amaError')).toBeDefined();
|
|
35
|
+
expect(
|
|
36
|
+
renderAPI.getByTestId('amaError').props.accessibilityLabel,
|
|
37
|
+
).toEqual("1 component(s) didn't pass the accessibility check(s)");
|
|
38
|
+
|
|
39
|
+
act(() => {
|
|
40
|
+
amaContextValuesRef.trackError('123');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(
|
|
44
|
+
renderAPI.getByTestId('amaError').props.accessibilityLabel,
|
|
45
|
+
).toEqual("1 component(s) didn't pass the accessibility check(s)");
|
|
46
|
+
|
|
47
|
+
expect(renderAPI.getByTestId('amaError')).toBeDefined();
|
|
48
|
+
|
|
49
|
+
act(() => {
|
|
50
|
+
amaContextValuesRef.trackError('another');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(
|
|
54
|
+
renderAPI.getByTestId('amaError').props.accessibilityLabel,
|
|
55
|
+
).toEqual("2 component(s) didn't pass the accessibility check(s)");
|
|
56
|
+
|
|
57
|
+
expect(renderAPI.getByTestId('amaError')).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('removeError', () => {
|
|
62
|
+
it('exports removeError', async () => {
|
|
63
|
+
await renderAMAProvider();
|
|
64
|
+
|
|
65
|
+
expect(amaContextValuesRef.removeError).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('shows the AMA error banner when called', async () => {
|
|
69
|
+
const renderAPI = await renderAMAProvider();
|
|
70
|
+
|
|
71
|
+
expect(renderAPI.queryByTestId('amaError')).toBeNull();
|
|
72
|
+
|
|
73
|
+
act(() => {
|
|
74
|
+
amaContextValuesRef.trackError('123');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(
|
|
78
|
+
renderAPI.getByTestId('amaError').props.accessibilityLabel,
|
|
79
|
+
).toEqual("1 component(s) didn't pass the accessibility check(s)");
|
|
80
|
+
expect(renderAPI.getByTestId('amaError')).toBeDefined();
|
|
81
|
+
|
|
82
|
+
act(() => {
|
|
83
|
+
amaContextValuesRef.removeError('invalidID');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(
|
|
87
|
+
renderAPI.getByTestId('amaError').props.accessibilityLabel,
|
|
88
|
+
).toEqual("1 component(s) didn't pass the accessibility check(s)");
|
|
89
|
+
|
|
90
|
+
expect(renderAPI.getByTestId('amaError')).toBeDefined();
|
|
91
|
+
|
|
92
|
+
act(() => {
|
|
93
|
+
amaContextValuesRef.removeError('123');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(renderAPI.queryByTestId('amaError')).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('When __DEV__ is false', () => {
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
// @ts-ignore
|
|
104
|
+
global.__DEV__ = false;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('does not export trackError', async () => {
|
|
108
|
+
await renderAMAProvider();
|
|
109
|
+
|
|
110
|
+
expect(amaContextValuesRef.trackError).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('does not export removeError', async () => {
|
|
114
|
+
await renderAMAProvider();
|
|
115
|
+
|
|
116
|
+
expect(amaContextValuesRef.removeError).toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const renderAMAProvider = async () => {
|
|
122
|
+
const result = render(
|
|
123
|
+
<AMAProvider>
|
|
124
|
+
<DummyComponent />
|
|
125
|
+
</AMAProvider>,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
await act(async () => {
|
|
129
|
+
await flushMicroTasks();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return result;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
let amaContextValuesRef: Extract<
|
|
136
|
+
AMAContextValue,
|
|
137
|
+
{ trackError: Function; removeError: Function }
|
|
138
|
+
>;
|
|
139
|
+
|
|
140
|
+
const DummyComponent = () => {
|
|
141
|
+
// @ts-ignore
|
|
142
|
+
amaContextValuesRef = useAMAContext();
|
|
143
|
+
|
|
144
|
+
return null;
|
|
145
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { act, render } from '@testing-library/react-native';
|
|
2
|
+
import { flushMicroTasks } from '@testing-library/react-native/build/flush-micro-tasks';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { AccessibilityInfo } from 'react-native';
|
|
5
|
+
|
|
6
|
+
import { AMAContextValue, AMAProvider } from './AMAProvider';
|
|
7
|
+
|
|
8
|
+
var mockProvider: jest.Mock;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('AMAProvider', () => {
|
|
15
|
+
it('adds event listener for "reduceMotionChanged"', async () => {
|
|
16
|
+
const spy = jest
|
|
17
|
+
.spyOn(AccessibilityInfo, 'addEventListener')
|
|
18
|
+
.mockReturnValue({ remove: jest.fn() } as any);
|
|
19
|
+
|
|
20
|
+
await renderAMAProvider();
|
|
21
|
+
|
|
22
|
+
expect(spy).toHaveBeenCalledWith(
|
|
23
|
+
'reduceMotionChanged',
|
|
24
|
+
expect.any(Function),
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('adds event listener for "screenReaderChanged"', async () => {
|
|
29
|
+
const spy = jest
|
|
30
|
+
.spyOn(AccessibilityInfo, 'addEventListener')
|
|
31
|
+
.mockReturnValue({ remove: jest.fn() } as any);
|
|
32
|
+
|
|
33
|
+
await renderAMAProvider();
|
|
34
|
+
|
|
35
|
+
expect(spy).toHaveBeenCalledWith(
|
|
36
|
+
'screenReaderChanged',
|
|
37
|
+
expect.any(Function),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('setups the initial value for all the states', async () => {
|
|
42
|
+
const events = [
|
|
43
|
+
'isReduceMotionEnabled',
|
|
44
|
+
'isScreenReaderEnabled',
|
|
45
|
+
'isReduceTransparencyEnabled',
|
|
46
|
+
'isGrayscaleEnabled',
|
|
47
|
+
'isBoldTextEnabled',
|
|
48
|
+
'isInvertColorsEnabled',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const states: any = {};
|
|
52
|
+
|
|
53
|
+
events.forEach(event => {
|
|
54
|
+
// @ts-ignore
|
|
55
|
+
jest.spyOn(AccessibilityInfo, event).mockResolvedValue('yay');
|
|
56
|
+
states[event] = 'yay';
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await renderAMAProvider();
|
|
60
|
+
|
|
61
|
+
expect(mockProvider.mock.calls[1][0]).toMatchObject({
|
|
62
|
+
value: states,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it.each`
|
|
67
|
+
eventName | valueName
|
|
68
|
+
${'reduceMotionChanged'} | ${'isReduceMotionEnabled'}
|
|
69
|
+
${'screenReaderChanged'} | ${'isScreenReaderEnabled'}
|
|
70
|
+
${'reduceTransparencyChanged'} | ${'isReduceTransparencyEnabled'}
|
|
71
|
+
${'grayscaleChanged'} | ${'isGrayscaleEnabled'}
|
|
72
|
+
${'boldTextChanged'} | ${'isBoldTextEnabled'}
|
|
73
|
+
${'invertColorsChanged'} | ${'isInvertColorsEnabled'}
|
|
74
|
+
`(
|
|
75
|
+
'reacts to the events change $eventName',
|
|
76
|
+
async ({
|
|
77
|
+
eventName,
|
|
78
|
+
valueName,
|
|
79
|
+
}: {
|
|
80
|
+
eventName: string;
|
|
81
|
+
valueName: keyof AMAContextValue;
|
|
82
|
+
}) => {
|
|
83
|
+
let handler: Function;
|
|
84
|
+
jest
|
|
85
|
+
.spyOn(AccessibilityInfo, 'addEventListener')
|
|
86
|
+
// @ts-ignore
|
|
87
|
+
.mockImplementation((name, callback) => {
|
|
88
|
+
if (name === eventName) {
|
|
89
|
+
handler = callback;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
remove: jest.fn(),
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await renderAMAProvider();
|
|
98
|
+
|
|
99
|
+
const state: any = {};
|
|
100
|
+
state[valueName] = false;
|
|
101
|
+
|
|
102
|
+
expect(mockProvider).toHaveBeenCalledWith(
|
|
103
|
+
expect.objectContaining({
|
|
104
|
+
value: expect.objectContaining(state),
|
|
105
|
+
}),
|
|
106
|
+
{},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
jest.clearAllMocks();
|
|
110
|
+
|
|
111
|
+
act(() => {
|
|
112
|
+
handler!(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
state[valueName] = true;
|
|
116
|
+
expect(mockProvider).toHaveBeenCalledWith(
|
|
117
|
+
expect.objectContaining({
|
|
118
|
+
value: expect.objectContaining(state),
|
|
119
|
+
}),
|
|
120
|
+
{},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
jest.clearAllMocks();
|
|
124
|
+
|
|
125
|
+
act(() => {
|
|
126
|
+
handler!(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
state[valueName] = false;
|
|
130
|
+
expect(mockProvider).toHaveBeenCalledWith(
|
|
131
|
+
expect.objectContaining({
|
|
132
|
+
value: expect.objectContaining(state),
|
|
133
|
+
}),
|
|
134
|
+
{},
|
|
135
|
+
);
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
it.each`
|
|
140
|
+
eventName
|
|
141
|
+
${'reduceMotionChanged'}
|
|
142
|
+
${'screenReaderChanged'}
|
|
143
|
+
${'reduceTransparencyChanged'}
|
|
144
|
+
${'grayscaleChanged'}
|
|
145
|
+
${'boldTextChanged'}
|
|
146
|
+
${'invertColorsChanged'}
|
|
147
|
+
`(
|
|
148
|
+
'remove the "$eventName" subscription when the provider is unmounted',
|
|
149
|
+
async ({ eventName }) => {
|
|
150
|
+
const removeMock = jest.fn();
|
|
151
|
+
jest
|
|
152
|
+
.spyOn(AccessibilityInfo, 'addEventListener')
|
|
153
|
+
// @ts-ignore
|
|
154
|
+
.mockImplementation((name, _callback) => {
|
|
155
|
+
return { remove: name === eventName ? removeMock : jest.fn() };
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const renderAPI = await renderAMAProvider();
|
|
159
|
+
renderAPI.unmount();
|
|
160
|
+
|
|
161
|
+
expect(removeMock).toHaveBeenCalledWith();
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const renderAMAProvider = async () => {
|
|
167
|
+
const result = render(
|
|
168
|
+
<AMAProvider>
|
|
169
|
+
<></>
|
|
170
|
+
</AMAProvider>,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
await act(async () => {
|
|
174
|
+
await flushMicroTasks();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return result;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
function mockCreateContext() {
|
|
181
|
+
const original = jest.requireActual('react');
|
|
182
|
+
|
|
183
|
+
mockProvider = jest.fn().mockReturnValue(null);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
...original,
|
|
187
|
+
createContext: () => {
|
|
188
|
+
return {
|
|
189
|
+
Provider: mockProvider,
|
|
190
|
+
Consumer: () => {},
|
|
191
|
+
displayName: '',
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
jest.mock('react', () => {
|
|
198
|
+
return mockCreateContext();
|
|
199
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/* eslint-disable react-native/no-inline-styles */
|
|
2
|
+
|
|
3
|
+
import { RED } from '@react-native-ama/internal';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import {
|
|
6
|
+
AccessibilityChangeEventName,
|
|
7
|
+
AccessibilityInfo,
|
|
8
|
+
NativeEventSubscription,
|
|
9
|
+
Text,
|
|
10
|
+
View,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
|
|
13
|
+
type AMAProviderProps = {
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type SharedContextValue = {
|
|
18
|
+
isBoldTextEnabled: boolean;
|
|
19
|
+
isScreenReaderEnabled: boolean;
|
|
20
|
+
isGrayscaleEnabled: boolean;
|
|
21
|
+
isInvertColorsEnabled: boolean;
|
|
22
|
+
isReduceMotionEnabled: boolean;
|
|
23
|
+
isReduceTransparencyEnabled: boolean;
|
|
24
|
+
reactNavigationScreenOptions: {
|
|
25
|
+
animationEnabled: boolean;
|
|
26
|
+
animation: 'default' | 'fade';
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export type AMADevContextValue = SharedContextValue & {
|
|
30
|
+
trackError: (id: string) => void;
|
|
31
|
+
removeError: (id: string) => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type AMAProdContextValue = SharedContextValue & {};
|
|
35
|
+
|
|
36
|
+
export type AMAContextValue = AMADevContextValue | AMAProdContextValue;
|
|
37
|
+
|
|
38
|
+
type AccessibilityEvents = Exclude<AccessibilityChangeEventName, 'change'>;
|
|
39
|
+
|
|
40
|
+
type AccessibilityInfoKey = Exclude<
|
|
41
|
+
keyof AMAContextValue,
|
|
42
|
+
'reactNavigationScreenOptions' | 'trackError' | 'removeError'
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
type Extractor<S extends AccessibilityEvents> = S extends `${infer R}Changed`
|
|
46
|
+
? R
|
|
47
|
+
: never;
|
|
48
|
+
|
|
49
|
+
type AccessibilityInfoEvents = {
|
|
50
|
+
[key in AccessibilityEvents]: `is${Capitalize<Extractor<key>>}Enabled`;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const eventsMapping: AccessibilityInfoEvents = {
|
|
54
|
+
reduceTransparencyChanged: 'isReduceTransparencyEnabled',
|
|
55
|
+
reduceMotionChanged: 'isReduceMotionEnabled',
|
|
56
|
+
grayscaleChanged: 'isGrayscaleEnabled',
|
|
57
|
+
boldTextChanged: 'isBoldTextEnabled',
|
|
58
|
+
invertColorsChanged: 'isInvertColorsEnabled',
|
|
59
|
+
screenReaderChanged: 'isScreenReaderEnabled',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const isDev = __DEV__;
|
|
63
|
+
|
|
64
|
+
export const isDevContextValue = (
|
|
65
|
+
value: AMAContextValue,
|
|
66
|
+
): value is AMADevContextValue =>
|
|
67
|
+
(value as AMADevContextValue).trackError !== undefined;
|
|
68
|
+
|
|
69
|
+
const DEFAULT_VALUES = {
|
|
70
|
+
isReduceTransparencyEnabled: false,
|
|
71
|
+
isBoldTextEnabled: false,
|
|
72
|
+
isGrayscaleEnabled: false,
|
|
73
|
+
isInvertColorsEnabled: false,
|
|
74
|
+
isReduceMotionEnabled: false,
|
|
75
|
+
isScreenReaderEnabled: false,
|
|
76
|
+
reactNavigationScreenOptions: {
|
|
77
|
+
animationEnabled: true,
|
|
78
|
+
animation: 'default',
|
|
79
|
+
},
|
|
80
|
+
} satisfies AMAContextValue;
|
|
81
|
+
|
|
82
|
+
const AMAContext = React.createContext<AMAContextValue | null>(null);
|
|
83
|
+
|
|
84
|
+
export const AMAProvider: React.FC<AMAProviderProps> = ({ children }) => {
|
|
85
|
+
const [values, setValues] = React.useState<AMAContextValue>(DEFAULT_VALUES);
|
|
86
|
+
|
|
87
|
+
const handleAccessibilityInfoChanged = (key: AccessibilityInfoKey) => {
|
|
88
|
+
return (newValue: boolean) => {
|
|
89
|
+
setValues(oldValues => {
|
|
90
|
+
const newValues = { ...oldValues };
|
|
91
|
+
newValues[key] = newValue;
|
|
92
|
+
|
|
93
|
+
if (key === 'isReduceMotionEnabled') {
|
|
94
|
+
newValues.reactNavigationScreenOptions.animationEnabled = !newValue;
|
|
95
|
+
newValues.reactNavigationScreenOptions.animation = newValue
|
|
96
|
+
? 'fade'
|
|
97
|
+
: 'default';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
...oldValues,
|
|
102
|
+
...newValues,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
React.useEffect(() => {
|
|
109
|
+
const allInitPromises: Promise<boolean>[] = [];
|
|
110
|
+
|
|
111
|
+
const subscriptions: NativeEventSubscription[] = Object.entries(
|
|
112
|
+
eventsMapping,
|
|
113
|
+
).map(([eventName, contextKey]) => {
|
|
114
|
+
allInitPromises.push(AccessibilityInfo[contextKey]());
|
|
115
|
+
|
|
116
|
+
return AccessibilityInfo.addEventListener(
|
|
117
|
+
eventName as AccessibilityEvents,
|
|
118
|
+
handleAccessibilityInfoChanged(contextKey),
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
Promise.all(allInitPromises).then(promisesValues => {
|
|
123
|
+
const newValues = Object.values(eventsMapping).reduce(
|
|
124
|
+
(list, key, index) => {
|
|
125
|
+
list[key] = promisesValues[index] as boolean;
|
|
126
|
+
|
|
127
|
+
return list;
|
|
128
|
+
},
|
|
129
|
+
{} as AMAContextValue,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
setValues(oldValues => {
|
|
133
|
+
return {
|
|
134
|
+
...oldValues,
|
|
135
|
+
...newValues,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return () => {
|
|
141
|
+
subscriptions.forEach(subscription => subscription?.remove());
|
|
142
|
+
};
|
|
143
|
+
}, []);
|
|
144
|
+
|
|
145
|
+
const [failedItems, setFailedItems] = React.useState<string[]>([]);
|
|
146
|
+
|
|
147
|
+
if (isDev) {
|
|
148
|
+
const trackError = (id: string) => {
|
|
149
|
+
if (failedItems.includes(id)) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
setFailedItems(items => [...items, id]);
|
|
154
|
+
|
|
155
|
+
AccessibilityInfo.announceForAccessibility(
|
|
156
|
+
"One or more component didn't pass the accessibility check, please check the console for more info",
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const removeError = (id: string) => {
|
|
161
|
+
setFailedItems(items => {
|
|
162
|
+
const index = items.indexOf(id);
|
|
163
|
+
|
|
164
|
+
if (index >= 0) {
|
|
165
|
+
items.splice(index);
|
|
166
|
+
|
|
167
|
+
return [...items];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return items;
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<AMAContext.Provider
|
|
176
|
+
value={{
|
|
177
|
+
...values,
|
|
178
|
+
trackError,
|
|
179
|
+
removeError,
|
|
180
|
+
}}>
|
|
181
|
+
<View style={{ flex: 1 }}>
|
|
182
|
+
<>
|
|
183
|
+
{children}
|
|
184
|
+
{failedItems.length > 0 ? (
|
|
185
|
+
<AMAError count={failedItems.length} />
|
|
186
|
+
) : null}
|
|
187
|
+
</>
|
|
188
|
+
</View>
|
|
189
|
+
</AMAContext.Provider>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return <AMAContext.Provider value={values}>{children}</AMAContext.Provider>;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const AMAError = ({ count }: { count: number }) => {
|
|
197
|
+
const error = `${count} component(s) didn't pass the accessibility check(s)`;
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<View
|
|
201
|
+
accessibilityLabel={error}
|
|
202
|
+
accessibilityHint="Please check the console for more info..."
|
|
203
|
+
style={{
|
|
204
|
+
paddingHorizontal: 24,
|
|
205
|
+
paddingTop: 24,
|
|
206
|
+
paddingBottom: 48,
|
|
207
|
+
backgroundColor: RED,
|
|
208
|
+
}}
|
|
209
|
+
testID="amaError">
|
|
210
|
+
<View accessible={true}>
|
|
211
|
+
<Text
|
|
212
|
+
style={{ color: 'white', fontSize: 16, lineHeight: 26 }}
|
|
213
|
+
testID="amaError.message">
|
|
214
|
+
{error}
|
|
215
|
+
</Text>
|
|
216
|
+
<Text style={{ color: 'white', fontSize: 16, lineHeight: 24 }}>
|
|
217
|
+
Please check the console for more info...
|
|
218
|
+
</Text>
|
|
219
|
+
</View>
|
|
220
|
+
</View>
|
|
221
|
+
);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const useAMAContext = () => {
|
|
225
|
+
const context = React.useContext(AMAContext);
|
|
226
|
+
if (!context) {
|
|
227
|
+
throw new Error('Please wrap your app with <AMAProvider />');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return context;
|
|
231
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { render, waitFor } from '@testing-library/react-native';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
import * as UseFocus from '../hooks/useFocus';
|
|
5
|
+
import { AutofocusContainer } from './AutofocusContainer';
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
jest.clearAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('AutofocusContainer', () => {
|
|
12
|
+
it('it call setFocus when gets rendered', async () => {
|
|
13
|
+
const setFocus = jest.fn();
|
|
14
|
+
jest.spyOn(UseFocus, 'useFocus').mockReturnValue({
|
|
15
|
+
setFocus,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
render(
|
|
19
|
+
<AutofocusContainer>
|
|
20
|
+
<></>
|
|
21
|
+
</AutofocusContainer>,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
await waitFor(() => expect(setFocus).toBeCalled());
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it.each([undefined, true])(
|
|
28
|
+
'wraps the children with an accessible View when wrapChildrenInAccessibleView is %s',
|
|
29
|
+
async () => {
|
|
30
|
+
const setFocus = jest.fn();
|
|
31
|
+
jest.spyOn(UseFocus, 'useFocus').mockReturnValue({
|
|
32
|
+
setFocus,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const renderAPI = render(
|
|
36
|
+
<AutofocusContainer wrapChildrenInAccessibleView={true}>
|
|
37
|
+
<></>
|
|
38
|
+
</AutofocusContainer>,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
await waitFor(() => expect(setFocus).toBeCalled());
|
|
42
|
+
|
|
43
|
+
expect(
|
|
44
|
+
renderAPI.getByTestId('autofocusContainer.accessibleView'),
|
|
45
|
+
).toBeDefined();
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
it('does not wrap the children with an accessible View when wrapChildrenInAccessibleView is false', async () => {
|
|
50
|
+
const setFocus = jest.fn();
|
|
51
|
+
jest.spyOn(UseFocus, 'useFocus').mockReturnValue({
|
|
52
|
+
setFocus,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const renderAPI = render(
|
|
56
|
+
<AutofocusContainer wrapChildrenInAccessibleView={false}>
|
|
57
|
+
<></>
|
|
58
|
+
</AutofocusContainer>,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
await waitFor(() => expect(setFocus).toBeCalled());
|
|
62
|
+
|
|
63
|
+
expect(
|
|
64
|
+
renderAPI.queryByTestId('autofocusContainer.accessibleView'),
|
|
65
|
+
).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
jest.mock('../hooks/useFocus');
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { TouchableWithoutFeedback, View, ViewProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { useFocus } from '../hooks/useFocus';
|
|
5
|
+
|
|
6
|
+
type AutofocusContainerProps = React.PropsWithChildren<
|
|
7
|
+
| ({
|
|
8
|
+
wrapChildrenInAccessibleView?: true;
|
|
9
|
+
} & ViewProps)
|
|
10
|
+
| {
|
|
11
|
+
wrapChildrenInAccessibleView: false;
|
|
12
|
+
}
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
export const AutofocusContainer = ({
|
|
16
|
+
children,
|
|
17
|
+
wrapChildrenInAccessibleView = true,
|
|
18
|
+
...viewProps
|
|
19
|
+
}: AutofocusContainerProps) => {
|
|
20
|
+
const containerRef = React.useRef(null);
|
|
21
|
+
const { setFocus } = useFocus();
|
|
22
|
+
|
|
23
|
+
React.useEffect(() => {
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
setFocus(containerRef.current);
|
|
26
|
+
}, 0);
|
|
27
|
+
}, [setFocus]);
|
|
28
|
+
|
|
29
|
+
return wrapChildrenInAccessibleView ? (
|
|
30
|
+
<TouchableWithoutFeedback ref={containerRef}>
|
|
31
|
+
<View
|
|
32
|
+
accessible={true}
|
|
33
|
+
testID="autofocusContainer.accessibleView"
|
|
34
|
+
{...viewProps}>
|
|
35
|
+
{children}
|
|
36
|
+
</View>
|
|
37
|
+
</TouchableWithoutFeedback>
|
|
38
|
+
) : (
|
|
39
|
+
<TouchableWithoutFeedback ref={containerRef}>
|
|
40
|
+
{children}
|
|
41
|
+
</TouchableWithoutFeedback>
|
|
42
|
+
);
|
|
43
|
+
};
|