diff --git a/change/@fluentui-react-native-experimental-appearance-additions-bb61210e-5d2b-4ab0-a20e-97ec5b013b59.json b/change/@fluentui-react-native-experimental-appearance-additions-bb61210e-5d2b-4ab0-a20e-97ec5b013b59.json new file mode 100644 index 0000000000..292b0e530d --- /dev/null +++ b/change/@fluentui-react-native-experimental-appearance-additions-bb61210e-5d2b-4ab0-a20e-97ec5b013b59.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Support multiple trait collections on js side", + "packageName": "@fluentui-react-native/experimental-appearance-additions", + "email": "78454019+lyzhan7@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-notification-ebe39912-c773-49de-8517-60113b84bffa.json b/change/@fluentui-react-native-notification-ebe39912-c773-49de-8517-60113b84bffa.json new file mode 100644 index 0000000000..6dfb358173 --- /dev/null +++ b/change/@fluentui-react-native-notification-ebe39912-c773-49de-8517-60113b84bffa.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Support multiple trait collections on js side", + "packageName": "@fluentui-react-native/notification", + "email": "78454019+lyzhan7@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-tester-4d43d5a4-42ae-4171-ae43-7b55188e67dc.json b/change/@fluentui-react-native-tester-4d43d5a4-42ae-4171-ae43-7b55188e67dc.json new file mode 100644 index 0000000000..4137cd28ef --- /dev/null +++ b/change/@fluentui-react-native-tester-4d43d5a4-42ae-4171-ae43-7b55188e67dc.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Support multiple trait collections on js side", + "packageName": "@fluentui-react-native/tester", + "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..9e57dcfd64 100644 --- a/packages/experimental/AppearanceAdditions/ios/FRNAppearanceAdditions.m +++ b/packages/experimental/AppearanceAdditions/ios/FRNAppearanceAdditions.m @@ -3,6 +3,8 @@ #import #import #import +#import +#import NSString *const FRNAppearanceSizeClassCompact = @"compact"; NSString *const FRNAppearanceSizeClassRegular = @"regular"; @@ -67,28 +69,26 @@ @implementation FRNAppearanceAdditions { BOOL _hasListeners; - NSString *_horizontalSizeClass; - NSString *_userInterfaceLevel; - NSString *_accessibilityContrastOption; + + NSMutableDictionary, NSString *> * _rootTagHorizontalSizeClassMap; + NSMutableDictionary, NSString *> * _rootTagUserInterfaceLevelMap; + NSMutableDictionary, NSString *> * _rootTagAccessibilityContrastMap; } + (BOOL)requiresMainQueueSetup { return YES; } -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(horizontalSizeClass) -{ - return _horizontalSizeClass; +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(rootTagHorizontalSizeClassMap) { + return _rootTagHorizontalSizeClassMap; } -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(userInterfaceLevel) -{ - return _userInterfaceLevel; +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(rootTagUserInterfaceLevelMap) { + return _rootTagUserInterfaceLevelMap; } -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(accessibilityContrastOption) -{ - return _accessibilityContrastOption; +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(rootTagAccessibilityContrastOptionMap) { + return _rootTagAccessibilityContrastMap; } #pragma mark - RCTEventEmitter @@ -97,31 +97,29 @@ + (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; + _rootTagHorizontalSizeClassMap = [NSMutableDictionary new]; + _rootTagUserInterfaceLevelMap = [NSMutableDictionary new]; + _rootTagAccessibilityContrastMap = [NSMutableDictionary new]; - if (viewControllerWithInitialTraitCollection != nil) { - initialTraitCollection = [viewControllerWithInitialTraitCollection traitCollection]; - } else { - initialTraitCollection = [UITraitCollection currentTraitCollection]; + for (UIWindow *window in RCTSharedApplication().windows) { + id rootTag = [[[window rootViewController] view] reactTag]; + + if (rootTag != nil) { + UITraitCollection *windowTraitCollection = [window traitCollection]; + + _rootTagHorizontalSizeClassMap[rootTag] = RCTHorizontalSizeClassPreference(windowTraitCollection); + _rootTagUserInterfaceLevelMap[rootTag] = RCTUserInterfaceLevelPreference(windowTraitCollection); + _rootTagAccessibilityContrastMap[rootTag] = RCTAccessibilityContrastPreference(windowTraitCollection); + } } - _horizontalSizeClass = RCTHorizontalSizeClassPreference(initialTraitCollection); - _userInterfaceLevel = RCTUserInterfaceLevelPreference(initialTraitCollection); - _accessibilityContrastOption = RCTAccessibilityContrastPreference(initialTraitCollection); - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appearanceChanged:) name:RCTUserInterfaceStyleDidChangeNotification @@ -139,24 +137,34 @@ - (void)appearanceChanged:(NSNotification *)notification { if (_hasListeners) { UITraitCollection *traitCollection = [[notification userInfo] valueForKey:RCTUserInterfaceStyleDidChangeNotificationTraitCollectionKey]; if (![traitCollection isKindOfClass:[UITraitCollection class]]) { - traitCollection = nil; + return; + } + + RCTRootView *rootView = [notification object]; + if (![rootView isKindOfClass:[RCTRootView class]]) { + return; } NSString *horizontalSizeClass = RCTHorizontalSizeClassPreference(traitCollection); NSString *userInterfaceLevel = RCTUserInterfaceLevelPreference(traitCollection); NSString *accessibilityContrastOption = RCTAccessibilityContrastPreference(traitCollection); - - if (![horizontalSizeClass isEqualToString:_horizontalSizeClass] || - ![userInterfaceLevel isEqualToString:_userInterfaceLevel] || - ![accessibilityContrastOption isEqualToString:_accessibilityContrastOption]) { - _horizontalSizeClass = horizontalSizeClass; - _userInterfaceLevel = userInterfaceLevel; - _accessibilityContrastOption = accessibilityContrastOption; + + id rootTag = [[rootView contentView] reactTag]; + + if (![horizontalSizeClass isEqualToString: _rootTagHorizontalSizeClassMap[rootTag]] || + ![userInterfaceLevel isEqualToString:_rootTagUserInterfaceLevelMap[rootTag]] || + ![accessibilityContrastOption isEqualToString:_rootTagAccessibilityContrastMap[rootTag]]) { + + _rootTagHorizontalSizeClassMap[rootTag] = horizontalSizeClass; + _rootTagUserInterfaceLevelMap[rootTag] = userInterfaceLevel; + _rootTagAccessibilityContrastMap[rootTag] = accessibilityContrastOption; + [self sendEventWithName:@"appearanceChanged" body:@{ - @"horizontalSizeClass": _horizontalSizeClass, - @"userInterfaceLevel": _userInterfaceLevel, - @"accessibilityContrastOption": _accessibilityContrastOption, + @"rootTag": rootTag, + @"horizontalSizeClass": horizontalSizeClass, + @"userInterfaceLevel": userInterfaceLevel, + @"accessibilityContrastOption": accessibilityContrastOption, }]; } } diff --git a/packages/experimental/AppearanceAdditions/src/NativeAppearanceAdditions.ts b/packages/experimental/AppearanceAdditions/src/NativeAppearanceAdditions.ts index 53d844c4a8..2f2ec4bd7c 100644 --- a/packages/experimental/AppearanceAdditions/src/NativeAppearanceAdditions.ts +++ b/packages/experimental/AppearanceAdditions/src/NativeAppearanceAdditions.ts @@ -1,21 +1,19 @@ -import type { AccessibilityContrastOption, SizeClass, UserInterfaceLevel } from './NativeAppearanceAdditions.types'; - export const NativeAppearanceAdditions = { // eslint-disable-next-line @typescript-eslint/no-empty-function addListener: (_: string) => {}, // eslint-disable-next-line @typescript-eslint/no-empty-function removeListeners: (_: number) => {}, - horizontalSizeClass: () => { + rootTagHorizontalSizeClassMap: () => { console.warn('NativeAppearanceAdditions is only available on iOS'); - return 'regular' as SizeClass; + return {}; }, - userInterfaceLevel: () => { + rootTagUserInterfaceLevelMap: () => { console.warn('NativeAppearanceAdditions is only available on iOS'); - return 'base' as UserInterfaceLevel; + return {}; }, - accessibilityContrastOption: () => { + rootTagAccessibilityContrastOptionMap: () => { console.warn('NativeAppearanceAdditions is only available on iOS'); - return 'normal' as AccessibilityContrastOption; + return {}; }, }; diff --git a/packages/experimental/AppearanceAdditions/src/NativeAppearanceAdditions.types.ts b/packages/experimental/AppearanceAdditions/src/NativeAppearanceAdditions.types.ts index db253c34a6..c7ba09468a 100644 --- a/packages/experimental/AppearanceAdditions/src/NativeAppearanceAdditions.types.ts +++ b/packages/experimental/AppearanceAdditions/src/NativeAppearanceAdditions.types.ts @@ -1,9 +1,13 @@ +import type { RootTag } from 'react-native'; + export interface AppearanceAdditions { - readonly horizontalSizeClass: SizeClass; - readonly userInterfaceLevel: UserInterfaceLevel; - readonly accessibilityContrastOption: AccessibilityContrastOption; + horizontalSizeClassForRootTag(rootTag: RootTag): SizeClass; + userInterfaceLevelForRootTag(rootTag: RootTag): UserInterfaceLevel; + accessibilityContrastOptionForRootTag(rootTag: RootTag): AccessibilityContrastOption; } +export const RootTagKey = 'rootTag'; + export const HorizontalSizeClassKey = 'horizontalSizeClass'; export type SizeClass = 'compact' | 'regular'; diff --git a/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ios.ts b/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ios.ts index 01a17633cc..2b01439b9c 100644 --- a/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ios.ts +++ b/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ios.ts @@ -1,40 +1,46 @@ import { NativeEventEmitter } from 'react-native'; +import type { RootTag } from 'react-native'; import { memoize } from '@fluentui-react-native/framework'; import NativeAppearanceAdditions from './NativeAppearanceAdditions'; import type { AppearanceAdditions, SizeClass, UserInterfaceLevel, AccessibilityContrastOption } from './NativeAppearanceAdditions.types'; -import { HorizontalSizeClassKey, UserInterfaceLevelKey, AccessibilityContrastOptionKey } from './NativeAppearanceAdditions.types'; +import { + HorizontalSizeClassKey, + UserInterfaceLevelKey, + AccessibilityContrastOptionKey, + RootTagKey, +} from './NativeAppearanceAdditions.types'; class AppearanceAdditionsImpl implements AppearanceAdditions { - _horizontalSizeClass: SizeClass; - _userInterfaceLevel: UserInterfaceLevel; - _accessibilityContrastOption: AccessibilityContrastOption; - - get horizontalSizeClass(): SizeClass { - return this._horizontalSizeClass; - } - - get userInterfaceLevel(): UserInterfaceLevel { - return this._userInterfaceLevel; - } - - get accessibilityContrastOption(): AccessibilityContrastOption { - return this._accessibilityContrastOption; - } + _rootTagHorizontalSizeClassMap: { [key: number]: SizeClass } = {}; + _rootTagUserInterfaceLevelMap: { [key: number]: UserInterfaceLevel } = {}; + _rootTagAccessibilityContrastOptionMap: { [key: number]: AccessibilityContrastOption } = {}; constructor() { - this._horizontalSizeClass = NativeAppearanceAdditions.horizontalSizeClass(); - this._userInterfaceLevel = NativeAppearanceAdditions.userInterfaceLevel(); - this._accessibilityContrastOption = NativeAppearanceAdditions.accessibilityContrastOption(); + this._rootTagHorizontalSizeClassMap = NativeAppearanceAdditions.rootTagHorizontalSizeClassMap(); + this._rootTagUserInterfaceLevelMap = NativeAppearanceAdditions.rootTagUserInterfaceLevelMap(); + this._rootTagAccessibilityContrastOptionMap = NativeAppearanceAdditions.rootTagAccessibilityContrastOptionMap(); const eventEmitter = new NativeEventEmitter(NativeAppearanceAdditions as any); eventEmitter.addListener('appearanceChanged', (newValue) => { - this._horizontalSizeClass = newValue[HorizontalSizeClassKey]; - this._userInterfaceLevel = newValue[UserInterfaceLevelKey]; - this._accessibilityContrastOption = newValue[AccessibilityContrastOptionKey]; + const rootTag = newValue[RootTagKey]; + + this._rootTagHorizontalSizeClassMap[rootTag] = newValue[HorizontalSizeClassKey]; + this._rootTagUserInterfaceLevelMap[rootTag] = newValue[UserInterfaceLevelKey]; + this._rootTagAccessibilityContrastOptionMap[rootTag] = newValue[AccessibilityContrastOptionKey]; }); } + + horizontalSizeClassForRootTag(rootTag: RootTag): SizeClass { + return this._rootTagHorizontalSizeClassMap[rootTag]; + } + userInterfaceLevelForRootTag(rootTag: RootTag): UserInterfaceLevel { + return this._rootTagUserInterfaceLevelMap[rootTag]; + } + accessibilityContrastOptionForRootTag(rootTag: RootTag): AccessibilityContrastOption { + return this._rootTagAccessibilityContrastOptionMap[rootTag]; + } } function getAppearanceAdditionsWorker() { diff --git a/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ts b/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ts index 1731b01fcb..40f6f924a5 100644 --- a/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ts +++ b/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ts @@ -4,11 +4,7 @@ import type { AppearanceAdditions } from './NativeAppearanceAdditions.types'; // Default values for non-iOS clients. function getAppearanceAdditionsWorker() { - return { - horizontalSizeClass: 'regular', - userInterfaceLevel: 'base', - accessibilityContrastOption: 'normal', - } as AppearanceAdditions; + return {} as AppearanceAdditions; } export const appearanceAdditions = memoize(getAppearanceAdditionsWorker); diff --git a/packages/experimental/AppearanceAdditions/src/getSizeClass.ios.ts b/packages/experimental/AppearanceAdditions/src/getSizeClass.ios.ts index 13c7708ea2..93f96b7997 100644 --- a/packages/experimental/AppearanceAdditions/src/getSizeClass.ios.ts +++ b/packages/experimental/AppearanceAdditions/src/getSizeClass.ios.ts @@ -1,5 +1,7 @@ +import React from 'react'; import { useMemo } from 'react'; -import { NativeEventEmitter } from 'react-native'; +import { NativeEventEmitter, RootTagContext } from 'react-native'; +import type { RootTag } from 'react-native'; import { useSubscription } from 'use-subscription'; @@ -9,7 +11,9 @@ import type { SizeClass } from './NativeAppearanceAdditions.types'; const eventEmitter = NativeAppearanceAdditions ? new NativeEventEmitter(NativeAppearanceAdditions as any) : undefined; -export function useHorizontalSizeClass(): SizeClass { +export function useHorizontalSizeClass(_rootTag: RootTag): SizeClass { + const rootTag = React.useContext(RootTagContext); + if (!eventEmitter) { return 'regular'; } @@ -18,7 +22,7 @@ export function useHorizontalSizeClass(): SizeClass { // eslint-disable-next-line react-hooks/rules-of-hooks const subscription = useMemo( () => ({ - getCurrentValue: () => appearanceAdditions().horizontalSizeClass, + getCurrentValue: () => appearanceAdditions().horizontalSizeClassForRootTag(rootTag), subscribe: (callback) => { const appearanceSubscription = eventEmitter.addListener('appearanceChanged', callback); return () => { @@ -26,7 +30,7 @@ export function useHorizontalSizeClass(): SizeClass { }; }, }), - [], + [rootTag], ); // Early return on eventEmitter will either always or never return within a single instance diff --git a/packages/experimental/AppearanceAdditions/src/getSizeClass.ts b/packages/experimental/AppearanceAdditions/src/getSizeClass.ts index b34074de2d..e7a0eb6309 100644 --- a/packages/experimental/AppearanceAdditions/src/getSizeClass.ts +++ b/packages/experimental/AppearanceAdditions/src/getSizeClass.ts @@ -1,6 +1,8 @@ +import type { RootTag } from 'react-native'; + import type { SizeClass } from './NativeAppearanceAdditions.types'; -export function useHorizontalSizeClass(): SizeClass { +export function useHorizontalSizeClass(_rootTag: RootTag): SizeClass { // Stubbed out for non-iOS platforms return 'regular'; }