Skip to content

Commit 85ddc46

Browse files
authored
chore(macOS): Consolidate RCTAccessibilityManager between iOS and macOS (#2400)
## Summary: This PR attempts to solve two problems: 1. We have had a forked AccessibilityManager module for macOS for a while. In general, I prefer ifdefing rather than forking so that we get changes to the iOS code during merges. Let's combine the files. 2. I want to cleanup macOS's "highContrast" API in AccessibilityInfo so it can be upstreamed, so that RNW can follow it and we have a cross-platform way to get high contrast state. Luckily, the API we have for macOS can be ported to iOS, so I did that. Unluckily, someone already did that (see: facebook#46826 ) and used the iOS specific name "Darker system colors" instead of something like "increase contrast" or "high contrast". So I'll probably need to add my API upstream, deprecate that one, and then remove it in a future release.
1 parent 09fc3b7 commit 85ddc46

File tree

5 files changed

+167
-188
lines changed

5 files changed

+167
-188
lines changed

packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,12 @@ const EventNames: Map<
7171
['boldTextChanged', 'boldTextChanged'],
7272
['change', 'screenReaderChanged'],
7373
['grayscaleChanged', 'grayscaleChanged'],
74-
['highContrastChanged', 'highContrastChanged'], // [macOS]
7574
['invertColorsChanged', 'invertColorsChanged'],
7675
['reduceMotionChanged', 'reduceMotionChanged'],
7776
['reduceTransparencyChanged', 'reduceTransparencyChanged'],
7877
['screenReaderChanged', 'screenReaderChanged'],
7978
['darkerSystemColorsChanged', 'darkerSystemColorsChanged'],
79+
['highContrastChanged', 'highContrastChanged'], // [macOS]
8080
]);
8181

8282
/**
@@ -152,7 +152,7 @@ const AccessibilityInfo = {
152152
* macOS only
153153
*/
154154
isHighContrastEnabled: function (): Promise<boolean> {
155-
if (Platform.OS === 'macos') {
155+
if (Platform.OS === 'macos' || Platform.OS === 'ios') {
156156
return new Promise((resolve, reject) => {
157157
if (NativeAccessibilityManagerApple) {
158158
NativeAccessibilityManagerApple.getCurrentHighContrastState(

packages/react-native/React/CoreModules/RCTAccessibilityManager.mm

Lines changed: 152 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717

1818
#import "CoreModulesPlugins.h"
1919

20-
#if !TARGET_OS_OSX // [macOS]
21-
2220
NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification =
2321
@"RCTAccessibilityManagerDidUpdateMultiplierNotification";
2422

23+
static void *AccessibilityVoiceOverChangeContext = &AccessibilityVoiceOverChangeContext; // [macOS]
24+
2525
@interface RCTAccessibilityManager () <NativeAccessibilityManagerSpec>
2626

2727
@property (nonatomic, copy) NSString *contentSizeCategory;
@@ -47,6 +47,7 @@ - (instancetype)init
4747
if (self = [super init]) {
4848
_multiplier = 1.0;
4949

50+
#if !TARGET_OS_OSX // [macOS]
5051
// TODO: can this be moved out of the startup path?
5152
[[NSNotificationCenter defaultCenter] addObserver:self
5253
selector:@selector(didReceiveNewContentSizeCategory:)
@@ -92,7 +93,18 @@ - (instancetype)init
9293
selector:@selector(voiceVoiceOverStatusDidChange:)
9394
name:UIAccessibilityVoiceOverStatusDidChangeNotification
9495
object:nil];
96+
#else // [macOS
97+
[[NSWorkspace sharedWorkspace] addObserver:self
98+
forKeyPath:@"voiceOverEnabled"
99+
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
100+
context:AccessibilityVoiceOverChangeContext];
101+
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
102+
selector:@selector(accessibilityDisplayOptionsChange:)
103+
name:NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification
104+
object:nil];
105+
#endif // macOS]
95106

107+
#if !TARGET_OS_OSX // [macOS]
96108
self.contentSizeCategory = RCTSharedApplication().preferredContentSizeCategory;
97109
_isBoldTextEnabled = UIAccessibilityIsBoldTextEnabled();
98110
_isGrayscaleEnabled = UIAccessibilityIsGrayscaleEnabled();
@@ -101,10 +113,29 @@ - (instancetype)init
101113
_isDarkerSystemColorsEnabled = UIAccessibilityDarkerSystemColorsEnabled();
102114
_isReduceTransparencyEnabled = UIAccessibilityIsReduceTransparencyEnabled();
103115
_isVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning();
116+
_isHighContrastEnabled = UIAccessibilityDarkerSystemColorsEnabled(); // [macOS] Implement high Contrast for iOS
117+
#else // [macOS
118+
NSWorkspace *sharedWorkspace = [NSWorkspace sharedWorkspace];
119+
_isInvertColorsEnabled = [sharedWorkspace accessibilityDisplayShouldInvertColors];
120+
_isReduceMotionEnabled = [sharedWorkspace accessibilityDisplayShouldReduceMotion];
121+
_isReduceTransparencyEnabled = [sharedWorkspace accessibilityDisplayShouldReduceTransparency];
122+
_isVoiceOverEnabled = [sharedWorkspace isVoiceOverEnabled];
123+
_isHighContrastEnabled = [sharedWorkspace accessibilityDisplayShouldIncreaseContrast];
124+
#endif // macOS]
104125
}
105126
return self;
106127
}
107128

129+
#if TARGET_OS_OSX // [macOS
130+
- (void)dealloc
131+
{
132+
[[NSWorkspace sharedWorkspace] removeObserver:self
133+
forKeyPath:@"voiceOverEnabled"
134+
context:AccessibilityVoiceOverChangeContext];
135+
}
136+
#endif // macOS]
137+
138+
#if !TARGET_OS_OSX // [macOS]
108139
- (void)didReceiveNewContentSizeCategory:(NSNotification *)note
109140
{
110141
self.contentSizeCategory = note.userInfo[UIContentSizeCategoryNewValueKey];
@@ -186,6 +217,11 @@ - (void)darkerSystemColorsDidChange:(__unused NSNotification *)notification
186217
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
187218
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"darkerSystemColorsChanged"
188219
body:@(_isDarkerSystemColorsEnabled)];
220+
// [macOS Also fire the highContrastChanged event for iOS
221+
_isHighContrastEnabled = newDarkerSystemColorsEnabled;
222+
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"highContrastChanged"
223+
body:@(_isDarkerSystemColorsEnabled)];
224+
// macOS]
189225

190226
#pragma clang diagnostic pop
191227
}
@@ -209,6 +245,71 @@ - (void)voiceVoiceOverStatusDidChange:(__unused NSNotification *)notification
209245
BOOL isVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning();
210246
[self _setIsVoiceOverEnabled:isVoiceOverEnabled];
211247
}
248+
#else // [macOS
249+
- (void)accessibilityDisplayOptionsChange:(NSNotification *)notification
250+
{
251+
BOOL newInvertColorsEnabled = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldInvertColors];
252+
BOOL newReduceMotionEnabled = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceMotion];
253+
BOOL newReduceTransparencyEnabled = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceTransparency];
254+
BOOL newHighContrastEnabled = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldIncreaseContrast];
255+
256+
257+
if (_isHighContrastEnabled != newHighContrastEnabled) {
258+
_isHighContrastEnabled = newHighContrastEnabled;
259+
#pragma clang diagnostic push
260+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
261+
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"highContrastChanged"
262+
body:@(_isHighContrastEnabled)];
263+
#pragma clang diagnostic pop
264+
}
265+
if (_isInvertColorsEnabled != newInvertColorsEnabled) {
266+
_isInvertColorsEnabled = newInvertColorsEnabled;
267+
#pragma clang diagnostic push
268+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
269+
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"invertColorsChanged"
270+
body:@(_isInvertColorsEnabled)];
271+
#pragma clang diagnostic pop
272+
}
273+
if (_isReduceMotionEnabled != newReduceMotionEnabled) {
274+
_isReduceMotionEnabled = newReduceMotionEnabled;
275+
#pragma clang diagnostic push
276+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
277+
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"reduceMotionChanged"
278+
body:@(_isReduceMotionEnabled)];
279+
#pragma clang diagnostic pop
280+
}
281+
if (_isReduceTransparencyEnabled != newReduceTransparencyEnabled) {
282+
_isReduceTransparencyEnabled = newReduceTransparencyEnabled;
283+
#pragma clang diagnostic push
284+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
285+
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"reduceTransparencyChanged"
286+
body:@(_isReduceTransparencyEnabled)];
287+
#pragma clang diagnostic pop
288+
}
289+
}
290+
291+
- (void)observeValueForKeyPath:(NSString *)keyPath
292+
ofObject:(id)object
293+
change:(NSDictionary *)change
294+
context:(void *)context {
295+
if (context == AccessibilityVoiceOverChangeContext) {
296+
BOOL newIsVoiceOverEnabled = [[NSWorkspace sharedWorkspace] isVoiceOverEnabled];
297+
if (_isVoiceOverEnabled != newIsVoiceOverEnabled) {
298+
_isVoiceOverEnabled = newIsVoiceOverEnabled;
299+
#pragma clang diagnostic push
300+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
301+
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"screenReaderChanged"
302+
body:@(_isVoiceOverEnabled)];
303+
#pragma clang diagnostic pop
304+
}
305+
} else {
306+
[super observeValueForKeyPath:keyPath
307+
ofObject:object
308+
change:change
309+
context:context];
310+
}
311+
}
312+
#endif // macOS]
212313

213314
- (void)_setIsVoiceOverEnabled:(BOOL)isVoiceOverEnabled
214315
{
@@ -257,6 +358,7 @@ - (void)setMultipliers:(NSDictionary<NSString *, NSNumber *> *)multipliers
257358

258359
- (NSDictionary<NSString *, NSNumber *> *)multipliers
259360
{
361+
#if !TARGET_OS_OSX // [macOS]
260362
if (_multipliers == nil) {
261363
_multipliers = @{
262364
UIContentSizeCategoryExtraSmall : @0.823,
@@ -273,6 +375,7 @@ - (void)setMultipliers:(NSDictionary<NSString *, NSNumber *> *)multipliers
273375
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge : @3.571
274376
};
275377
}
378+
#endif // [macOS]
276379
return _multipliers;
277380
}
278381

@@ -281,6 +384,7 @@ - (void)setMultipliers:(NSDictionary<NSString *, NSNumber *> *)multipliers
281384
JSMultipliers)
282385
{
283386
NSMutableDictionary<NSString *, NSNumber *> *multipliers = [NSMutableDictionary new];
387+
#if !TARGET_OS_OSX // [macOS]
284388
setMultipliers(multipliers, UIContentSizeCategoryExtraSmall, JSMultipliers.extraSmall());
285389
setMultipliers(multipliers, UIContentSizeCategorySmall, JSMultipliers.small());
286390
setMultipliers(multipliers, UIContentSizeCategoryMedium, JSMultipliers.medium());
@@ -297,6 +401,7 @@ - (void)setMultipliers:(NSDictionary<NSString *, NSNumber *> *)multipliers
297401
multipliers,
298402
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge,
299403
JSMultipliers.accessibilityExtraExtraExtraLarge());
404+
#endif // [macOS]
300405
self.multipliers = multipliers;
301406
}
302407

@@ -313,20 +418,38 @@ static void setMultipliers(
313418
RCT_EXPORT_METHOD(setAccessibilityFocus : (double)reactTag)
314419
{
315420
dispatch_async(dispatch_get_main_queue(), ^{
316-
UIView *view = [self.viewRegistry_DEPRECATED viewForReactTag:@(reactTag)];
421+
RCTPlatformView *view = [self.viewRegistry_DEPRECATED viewForReactTag:@(reactTag)]; // [macOS]
422+
#if !TARGET_OS_OSX // [macOS]
317423
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, view);
424+
#else // [macOS
425+
[[view window] makeFirstResponder:view];
426+
NSAccessibilityPostNotification(view, NSAccessibilityLayoutChangedNotification);
427+
#endif // macOS]
318428
});
319429
}
320430

321431
RCT_EXPORT_METHOD(announceForAccessibility : (NSString *)announcement)
322432
{
433+
#if !TARGET_OS_OSX // [macOS]
323434
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
435+
#else // [macOS
436+
dispatch_async(dispatch_get_main_queue(), ^{
437+
NSAccessibilityPostNotificationWithUserInfo(
438+
NSApp,
439+
NSAccessibilityAnnouncementRequestedNotification,
440+
@{NSAccessibilityAnnouncementKey : announcement,
441+
NSAccessibilityPriorityKey : @(NSAccessibilityPriorityHigh)
442+
}
443+
);
444+
});
445+
#endif // macOS]
324446
}
325447

326448
RCT_EXPORT_METHOD(announceForAccessibilityWithOptions
327449
: (NSString *)announcement options
328450
: (JS::NativeAccessibilityManager::SpecAnnounceForAccessibilityWithOptionsOptions &)options)
329451
{
452+
#if !TARGET_OS_OSX // [macOS]
330453
NSMutableDictionary<NSString *, NSNumber *> *attrsDictionary = [NSMutableDictionary new];
331454
if (options.queue()) {
332455
attrsDictionary[UIAccessibilitySpeechAttributeQueueAnnouncement] = @(*(options.queue()) ? YES : NO);
@@ -339,6 +462,18 @@ static void setMultipliers(
339462
} else {
340463
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
341464
}
465+
#else // [macOS
466+
NSAccessibilityPriorityLevel announcementPriority = options.queue() ? NSAccessibilityPriorityLow : NSAccessibilityPriorityHigh;
467+
dispatch_async(dispatch_get_main_queue(), ^{
468+
NSAccessibilityPostNotificationWithUserInfo(
469+
NSApp,
470+
NSAccessibilityAnnouncementRequestedNotification,
471+
@{NSAccessibilityAnnouncementKey : announcement,
472+
NSAccessibilityPriorityKey : @(announcementPriority)
473+
}
474+
);
475+
});
476+
#endif // macOS]
342477
}
343478

344479
RCT_EXPORT_METHOD(getMultiplier : (RCTResponseSenderBlock)callback)
@@ -387,11 +522,15 @@ static void setMultipliers(
387522
: (RCTResponseSenderBlock)onSuccess onError
388523
: (__unused RCTResponseSenderBlock)onError)
389524
{
525+
#if !TARGET_OS_OSX // [macOS]
390526
if (@available(iOS 14.0, *)) {
391527
onSuccess(@[ @(UIAccessibilityPrefersCrossFadeTransitions()) ]);
392528
} else {
393529
onSuccess(@[ @(false) ]);
394530
}
531+
#else // [macOS
532+
onSuccess(@[ @(false) ]); // iOS only
533+
#endif // macOS]
395534
}
396535

397536
RCT_EXPORT_METHOD(getCurrentReduceTransparencyState
@@ -408,6 +547,16 @@ static void setMultipliers(
408547
onSuccess(@[ @(_isVoiceOverEnabled) ]);
409548
}
410549

550+
// [macOS Implement for both iOS and macOS
551+
RCT_EXPORT_METHOD(getCurrentHighContrastState
552+
: (RCTResponseSenderBlock)onSuccess onError
553+
: (__unused RCTResponseSenderBlock)onError)
554+
{
555+
onSuccess(@[ @(_isHighContrastEnabled) ]);
556+
}
557+
// macOS]
558+
559+
411560
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
412561
(const facebook::react::ObjCTurboModule::InitParams &)params
413562
{
@@ -447,4 +596,3 @@ Class RCTAccessibilityManagerCls(void)
447596
{
448597
return RCTAccessibilityManager.class;
449598
}
450-
#endif // [macOS]

0 commit comments

Comments
 (0)