diff --git a/change/@fluentui-react-native-apple-theme-94f6d0fc-ed05-4ddf-9c07-7bb62ee0275c.json b/change/@fluentui-react-native-apple-theme-94f6d0fc-ed05-4ddf-9c07-7bb62ee0275c.json new file mode 100644 index 0000000000..f20509bf99 --- /dev/null +++ b/change/@fluentui-react-native-apple-theme-94f6d0fc-ed05-4ddf-9c07-7bb62ee0275c.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add trait collection support for multiwindow", + "packageName": "@fluentui-react-native/apple-theme", + "email": "78454019+lyzhan7@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-experimental-appearance-additions-36f88791-2978-4482-a1a0-c3af49f818cf.json b/change/@fluentui-react-native-experimental-appearance-additions-36f88791-2978-4482-a1a0-c3af49f818cf.json new file mode 100644 index 0000000000..2b2fb43c20 --- /dev/null +++ b/change/@fluentui-react-native-experimental-appearance-additions-36f88791-2978-4482-a1a0-c3af49f818cf.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add trait collection support for multiwindow", + "packageName": "@fluentui-react-native/experimental-appearance-additions", + "email": "78454019+lyzhan7@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/experimental/AppearanceAdditions/ios/FRNAppearanceAdditions.m b/packages/experimental/AppearanceAdditions/ios/FRNAppearanceAdditions.m index 1b785aac70..9adafc69d3 100644 --- a/packages/experimental/AppearanceAdditions/ios/FRNAppearanceAdditions.m +++ b/packages/experimental/AppearanceAdditions/ios/FRNAppearanceAdditions.m @@ -1,8 +1,12 @@ #import "FRNAppearanceAdditions.h" +#import "RCTUIManager.h" #import #import #import +#import +#import +#import NSString *const FRNAppearanceSizeClassCompact = @"compact"; NSString *const FRNAppearanceSizeClassRegular = @"regular"; @@ -76,18 +80,35 @@ + (BOOL)requiresMainQueueSetup { return YES; } -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(horizontalSizeClass) -{ +/** + * When initializing this native module, not all the information required to consistently get all correct initial traits is accessible. + * In order to ensure that the correct traits are always be returned the first time they are reuqested, a react tag for a view in that window can + * be provided to the native module. + * + * This initialization step may be needed when accessing traits that can be different in different windows, which include horizontal size class and user interface level. + * This initialization step is not necessary if the only traits that are accessed are system wide traits common to all windows, such as accessibility contrast. + */ +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(initializeTraitCollection:(id)reactTag) { + RCTUnsafeExecuteOnMainQueueSync(^{ + if ([reactTag isKindOfClass:[NSNumber class]]) { + UIView *view = [[[self bridge] uiManager] viewForReactTag:reactTag]; + self->_horizontalSizeClass = RCTHorizontalSizeClassPreference([view traitCollection]); + self->_userInterfaceLevel = RCTUserInterfaceLevelPreference([view traitCollection]); + self->_accessibilityContrastOption = RCTAccessibilityContrastPreference([view traitCollection]); + } + }); + return nil; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(horizontalSizeClass) { return _horizontalSizeClass; } -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(userInterfaceLevel) -{ +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(userInterfaceLevel) { return _userInterfaceLevel; } -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(accessibilityContrastOption) -{ +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(accessibilityContrastOption) { return _accessibilityContrastOption; } @@ -97,37 +118,45 @@ + (BOOL)requiresMainQueueSetup { return @[ @"appearanceChanged" ]; } -- (dispatch_queue_t)methodQueue -{ +- (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } - (void)startObserving { _hasListeners = YES; - // Note that [UITraitCollection currentTraitCollection] always returns the same default trait collection, - // presumably because FRNAppearanceAdditions isn't a view, so it never gets updated with the right traitCollection - // (which happens when a view gets added to the view hierachy). In order to get the right trait collection, - // we need to access a view that's been added to the view hierarchy - UIViewController *viewControllerWithInitialTraitCollection = RCTPresentedViewController(); - UITraitCollection *initialTraitCollection; - - if (viewControllerWithInitialTraitCollection != nil) { - initialTraitCollection = [viewControllerWithInitialTraitCollection traitCollection]; - } else { - initialTraitCollection = [UITraitCollection currentTraitCollection]; - } - - _horizontalSizeClass = RCTHorizontalSizeClassPreference(initialTraitCollection); - _userInterfaceLevel = RCTUserInterfaceLevelPreference(initialTraitCollection); - _accessibilityContrastOption = RCTAccessibilityContrastPreference(initialTraitCollection); - + UITraitCollection *attemptedInitialTraitCollection = [self attemptTraitCollectionInitialization]; + + _horizontalSizeClass = RCTHorizontalSizeClassPreference(attemptedInitialTraitCollection); + _userInterfaceLevel = RCTUserInterfaceLevelPreference(attemptedInitialTraitCollection); + _accessibilityContrastOption = RCTAccessibilityContrastPreference(attemptedInitialTraitCollection); + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appearanceChanged:) name:RCTUserInterfaceStyleDidChangeNotification object:nil]; } +/** + When this native module is first initialized, we don't have access to the information necessary to consistently retrieve all the right traits. + We should still attempt to initialize the traits with the right values with the information we do have access to. + + The traits retrieved from RCTPresentedViewController() should be correct for non-multiwindow scenarios. + + The traits retrieved from [UITraitCollection currentTraitCollection] will always be the same default trait collection, + presumably because FRNAppearanceAdditions isn't a view, so it never gets updated with the right traitCollection + (which happens when a view gets added to the view hierachy). + */ +- (UITraitCollection *)attemptTraitCollectionInitialization { + UIViewController *presentedViewControllerTraitCollection = RCTPresentedViewController(); + + if (presentedViewControllerTraitCollection != nil) { + return [presentedViewControllerTraitCollection traitCollection]; + } else { + return [UITraitCollection currentTraitCollection]; + } +} + - (void)stopObserving { _hasListeners = NO; [[NSNotificationCenter defaultCenter] removeObserver:self]; @@ -137,9 +166,20 @@ - (void)stopObserving { - (void)appearanceChanged:(NSNotification *)notification { if (_hasListeners) { + RCTBridge *notificationBridge = [[notification object] bridge]; + if (![notificationBridge isKindOfClass:[RCTBridge class]]) { + return; + } + + // Don't send the appearanceChanged event if the notification didn't originate from the same react native instance + RCTBridge *currentBridge = [[self bridge] parentBridge]; + if (![currentBridge isEqual:notificationBridge]) { + return; + } + UITraitCollection *traitCollection = [[notification userInfo] valueForKey:RCTUserInterfaceStyleDidChangeNotificationTraitCollectionKey]; if (![traitCollection isKindOfClass:[UITraitCollection class]]) { - traitCollection = nil; + return; } NSString *horizontalSizeClass = RCTHorizontalSizeClassPreference(traitCollection); diff --git a/packages/experimental/AppearanceAdditions/src/NativeAppearanceAdditions.ts b/packages/experimental/AppearanceAdditions/src/NativeAppearanceAdditions.ts index 53d844c4a8..6d9e3c8356 100644 --- a/packages/experimental/AppearanceAdditions/src/NativeAppearanceAdditions.ts +++ b/packages/experimental/AppearanceAdditions/src/NativeAppearanceAdditions.ts @@ -5,6 +5,9 @@ export const NativeAppearanceAdditions = { addListener: (_: string) => {}, // eslint-disable-next-line @typescript-eslint/no-empty-function removeListeners: (_: number) => {}, + initializeTraitCollection: (_: number) => { + console.warn('NativeAppearanceAdditions is only available on iOS'); + }, horizontalSizeClass: () => { console.warn('NativeAppearanceAdditions is only available on iOS'); return 'regular' as SizeClass; diff --git a/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ios.ts b/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ios.ts index 01a17633cc..aeffcba10d 100644 --- a/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ios.ts +++ b/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ios.ts @@ -23,22 +23,24 @@ class AppearanceAdditionsImpl implements AppearanceAdditions { return this._accessibilityContrastOption; } - constructor() { + constructor(reactTag: number = null) { + NativeAppearanceAdditions.initializeTraitCollection(reactTag); + this._horizontalSizeClass = NativeAppearanceAdditions.horizontalSizeClass(); this._userInterfaceLevel = NativeAppearanceAdditions.userInterfaceLevel(); this._accessibilityContrastOption = NativeAppearanceAdditions.accessibilityContrastOption(); const eventEmitter = new NativeEventEmitter(NativeAppearanceAdditions as any); eventEmitter.addListener('appearanceChanged', (newValue) => { - this._horizontalSizeClass = newValue[HorizontalSizeClassKey]; - this._userInterfaceLevel = newValue[UserInterfaceLevelKey]; - this._accessibilityContrastOption = newValue[AccessibilityContrastOptionKey]; + this._horizontalSizeClass = newValue[HorizontalSizeClassKey] ?? this._horizontalSizeClass; + this._userInterfaceLevel = newValue[UserInterfaceLevelKey] ?? this._userInterfaceLevel; + this._accessibilityContrastOption = newValue[AccessibilityContrastOptionKey] ?? this._accessibilityContrastOption; }); } } -function getAppearanceAdditionsWorker() { - return new AppearanceAdditionsImpl() as AppearanceAdditions; +function getAppearanceAdditionsWorker(reactTag: number) { + return new AppearanceAdditionsImpl(reactTag) as AppearanceAdditions; } export const appearanceAdditions = memoize(getAppearanceAdditionsWorker); diff --git a/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ts b/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ts index 1731b01fcb..ecf41c6acb 100644 --- a/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ts +++ b/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ts @@ -3,7 +3,7 @@ import { memoize } from '@fluentui-react-native/framework'; import type { AppearanceAdditions } from './NativeAppearanceAdditions.types'; // Default values for non-iOS clients. -function getAppearanceAdditionsWorker() { +function getAppearanceAdditionsWorker(_reactTag: number) { return { horizontalSizeClass: 'regular', userInterfaceLevel: 'base', diff --git a/packages/experimental/AppearanceAdditions/src/getSizeClass.ios.ts b/packages/experimental/AppearanceAdditions/src/getSizeClass.ios.ts index 13c7708ea2..967da9d916 100644 --- a/packages/experimental/AppearanceAdditions/src/getSizeClass.ios.ts +++ b/packages/experimental/AppearanceAdditions/src/getSizeClass.ios.ts @@ -1,5 +1,6 @@ +import React from 'react'; import { useMemo } from 'react'; -import { NativeEventEmitter } from 'react-native'; +import { NativeEventEmitter, RootTagContext } from 'react-native'; import { useSubscription } from 'use-subscription'; @@ -10,6 +11,7 @@ import type { SizeClass } from './NativeAppearanceAdditions.types'; const eventEmitter = NativeAppearanceAdditions ? new NativeEventEmitter(NativeAppearanceAdditions as any) : undefined; export function useHorizontalSizeClass(): SizeClass { + const rootTag = React.useContext(RootTagContext); if (!eventEmitter) { return 'regular'; } @@ -18,7 +20,7 @@ export function useHorizontalSizeClass(): SizeClass { // eslint-disable-next-line react-hooks/rules-of-hooks const subscription = useMemo( () => ({ - getCurrentValue: () => appearanceAdditions().horizontalSizeClass, + getCurrentValue: () => appearanceAdditions(rootTag).horizontalSizeClass, subscribe: (callback) => { const appearanceSubscription = eventEmitter.addListener('appearanceChanged', callback); return () => { @@ -26,7 +28,7 @@ export function useHorizontalSizeClass(): SizeClass { }; }, }), - [], + [rootTag], ); // Early return on eventEmitter will either always or never return within a single instance