Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#import <React/RCTBridgeModule.h>
#import <React/RCTConstants.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
#import <React/RCTRootView.h>

NSString *const FRNAppearanceSizeClassCompact = @"compact";
NSString *const FRNAppearanceSizeClassRegular = @"regular";
Expand Down Expand Up @@ -67,28 +69,26 @@

@implementation FRNAppearanceAdditions {
BOOL _hasListeners;
NSString *_horizontalSizeClass;
NSString *_userInterfaceLevel;
NSString *_accessibilityContrastOption;

NSMutableDictionary<id<NSCopying>, NSString *> * _rootTagHorizontalSizeClassMap;
NSMutableDictionary<id<NSCopying>, NSString *> * _rootTagUserInterfaceLevelMap;
NSMutableDictionary<id<NSCopying>, 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
Expand All @@ -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<NSCopying> 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
Expand All @@ -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<NSCopying> 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,
}];
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {};
},
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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';
}
Expand All @@ -18,15 +22,15 @@ 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 () => {
appearanceSubscription.remove();
};
},
}),
[],
[rootTag],
);

// Early return on eventEmitter will either always or never return within a single instance
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
}