Skip to content
Merged
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": "minor",
"comment": "Add trait collection support for multiwindow",
"packageName": "@fluentui-react-native/apple-theme",
"email": "78454019+lyzhan7@users.noreply.github.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
#import "FRNAppearanceAdditions.h"
#import "RCTUIManager.h"

#import <React/RCTBridgeModule.h>
#import <React/RCTConstants.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
#import <React/RCTRootView.h>
#import <React/RCTBridge+Private.h>

NSString *const FRNAppearanceSizeClassCompact = @"compact";
NSString *const FRNAppearanceSizeClassRegular = @"regular";
Expand Down Expand Up @@ -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;
}

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

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

// Early return on eventEmitter will either always or never return within a single instance
Expand Down