From 6246c9787a4885032bd4f18f1c175ca7c510f8c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ramos?= Date: Fri, 23 Aug 2019 11:46:30 -0700 Subject: [PATCH 1/5] Add Appearance native module Summary: Implements the Appearance native module as discussed in https://github.com/react-native-community/discussions-and-proposals/issues/126. The purpose of the Appearance native module is to expose the user's appearance preferences. Initial support includes exposing the user's preferred color scheme on iOS 13 devices, also known as Dark Mode. The name, "Appearance", was chosen purposefully to allow for future expansion to cover other appearance preferences such as reduced motion, reduced transparency, or high contrast modes. It should be noted that the current implementation exposes the main UIWindow's user interface style, while it is possible to have multiple RCTRootViews with distinct user interface styles (for example, one RCTRootView that uses the system user interface style, while a second RCTRootView has a parent that has been forced to always display with a dark appearance). This is intentional as the goal is to provide the user's preferred color scheme. The module is written in a way that allows for future expansion to expose individual RCTRootView traits. Changelog: [iOS] [Added] - The Appearance native module can be used to prepare your app for Dark Mode on iOS 13. Differential Revision: D16699954 fbshipit-source-id: d6d78d808828931bde400c99986e6913877b1cee --- .../FBReactNativeSpec-generated.mm | 64 +++++++++++ .../FBReactNativeSpec/FBReactNativeSpec.h | 48 ++++++++ Libraries/Utilities/Appearance.js | 79 +++++++++++++ Libraries/Utilities/NativeAppearance.js | 36 ++++++ .../react-native-implementation.js | 4 + React/Base/RCTRootView.m | 19 +++- React/CoreModules/BUCK | 4 + React/CoreModules/CoreModulesPlugins.h | 1 + React/CoreModules/CoreModulesPlugins.mm | 3 +- React/CoreModules/RCTAppearance.h | 16 +++ React/CoreModules/RCTAppearance.mm | 107 ++++++++++++++++++ 11 files changed, 379 insertions(+), 2 deletions(-) create mode 100644 Libraries/Utilities/Appearance.js create mode 100644 Libraries/Utilities/NativeAppearance.js create mode 100644 React/CoreModules/RCTAppearance.h create mode 100644 React/CoreModules/RCTAppearance.mm diff --git a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm index eb25fc7d24bcf3..e585b613c95447 100644 --- a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm +++ b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm @@ -12,6 +12,7 @@ */ #import "FBReactNativeSpec.h" +#import namespace facebook { @@ -452,6 +453,69 @@ + (RCTManagedPointer *)JS_NativeAppState_SpecGetCurrentAppStateSuccessAppState:( } // namespace react } // namespace facebook +namespace facebook { + namespace react { + + + static facebook::jsi::Value __hostFunction_NativeAppearanceSpecJSI_getColorScheme(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, StringKind, "getColorScheme", @selector(getColorScheme), args, count); + } + + static facebook::jsi::Value __hostFunction_NativeAppearanceSpecJSI_addListener(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, "addListener", @selector(addListener:), args, count); + } + + static facebook::jsi::Value __hostFunction_NativeAppearanceSpecJSI_removeListeners(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, "removeListeners", @selector(removeListeners:), args, count); + } + + + NativeAppearanceSpecJSI::NativeAppearanceSpecJSI(id instance, std::shared_ptr jsInvoker) + : ObjCTurboModule("Appearance", instance, jsInvoker) { + + methodMap_["getColorScheme"] = MethodMetadata {0, __hostFunction_NativeAppearanceSpecJSI_getColorScheme}; + + + methodMap_["addListener"] = MethodMetadata {1, __hostFunction_NativeAppearanceSpecJSI_addListener}; + + + methodMap_["removeListeners"] = MethodMetadata {1, __hostFunction_NativeAppearanceSpecJSI_removeListeners}; + + + + } + + } // namespace react +} // namespace facebook +folly::Optional NSStringToNativeAppearanceColorSchemeName(NSString *value) { + static NSDictionary *dict = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dict = @{ + @"light": @0, + @"dark": @1, + }; + }); + return value ? (NativeAppearanceColorSchemeName)[dict[value] integerValue] : folly::Optional{}; +} + +NSString *NativeAppearanceColorSchemeNameToNSString(folly::Optional value) { + static NSDictionary *dict = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dict = @{ + @0: @"light", + @1: @"dark", + }; + }); + return value.hasValue() ? dict[@(value.value())] : nil; +} +@implementation RCTCxxConvert (NativeAppearance_AppearancePreferences) ++ (RCTManagedPointer *)JS_NativeAppearance_AppearancePreferences:(id)json +{ + return facebook::react::managedPointer(json); +} +@end @implementation RCTCxxConvert (NativeAsyncStorage_SpecMultiGetCallbackErrorsElement) + (RCTManagedPointer *)JS_NativeAsyncStorage_SpecMultiGetCallbackErrorsElement:(id)json { diff --git a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h index eee2be7d6fcb37..c5be4c7179a422 100644 --- a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h +++ b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h @@ -432,6 +432,49 @@ namespace facebook { }; } // namespace react } // namespace facebook +@protocol NativeAppearanceSpec + +- (NSString *)getColorScheme; +- (void)addListener:(NSString *)eventName; +- (void)removeListeners:(double)count; + +@end +namespace facebook { + namespace react { + /** + * ObjC++ class for module 'Appearance' + */ + + class JSI_EXPORT NativeAppearanceSpecJSI : public ObjCTurboModule { + public: + NativeAppearanceSpecJSI(id instance, std::shared_ptr jsInvoker); + + }; + } // namespace react +} // namespace facebook +typedef NS_ENUM(NSInteger, NativeAppearanceColorSchemeName) { + NativeAppearanceColorSchemeNameLight = 0, + NativeAppearanceColorSchemeNameDark, +}; + +folly::Optional NSStringToNativeAppearanceColorSchemeName(NSString *value); +NSString *NativeAppearanceColorSchemeNameToNSString(folly::Optional value); + +namespace JS { + namespace NativeAppearance { + struct AppearancePreferences { + NSString *colorScheme() const; + + AppearancePreferences(NSDictionary *const v) : _v(v) {} + private: + NSDictionary *_v; + }; + } +} + +@interface RCTCxxConvert (NativeAppearance_AppearancePreferences) ++ (RCTManagedPointer *)JS_NativeAppearance_AppearancePreferences:(id)json; +@end namespace JS { namespace NativeAsyncStorage { @@ -2553,6 +2596,11 @@ inline JS::NativeAppState::Constants::Builder::Builder(const Input i) : _factory inline JS::NativeAppState::Constants::Builder::Builder(Constants i) : _factory(^{ return i.unsafeRawValue(); }) {} +inline NSString *JS::NativeAppearance::AppearancePreferences::colorScheme() const +{ + id const p = _v[@"colorScheme"]; + return RCTBridgingToString(p); +} inline NSString *JS::NativeAsyncStorage::SpecMultiGetCallbackErrorsElement::message() const { id const p = _v[@"message"]; diff --git a/Libraries/Utilities/Appearance.js b/Libraries/Utilities/Appearance.js new file mode 100644 index 00000000000000..60bd0c92d9b319 --- /dev/null +++ b/Libraries/Utilities/Appearance.js @@ -0,0 +1,79 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +import EventEmitter from '../vendor/emitter/EventEmitter'; +import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; +import NativeAppearance, { + type AppearancePreferences, + type ColorSchemeName, +} from './NativeAppearance'; +import invariant from 'invariant'; + +type AppearanceListener = (preferences: AppearancePreferences) => void; +const eventEmitter = new EventEmitter(); + +const nativeColorScheme: ?string = + NativeAppearance == null ? null : NativeAppearance.getColorScheme(); +invariant( + nativeColorScheme === 'dark' || + nativeColorScheme === 'light' || + nativeColorScheme == null, + "Unrecognized color scheme. Did you mean 'dark' or 'light'?", +); + +let currentColorScheme: ?ColorSchemeName = nativeColorScheme; + +if (NativeAppearance) { + const nativeEventEmitter = new NativeEventEmitter(NativeAppearance); + nativeEventEmitter.addListener( + 'appearanceChanged', + (newAppearance: AppearancePreferences) => { + const {colorScheme} = newAppearance; + invariant( + colorScheme === 'dark' || + colorScheme === 'light' || + colorScheme == null, + "Unrecognized color scheme. Did you mean 'dark' or 'light'?", + ); + currentColorScheme = colorScheme; + eventEmitter.emit('change', {colorScheme}); + }, + ); +} + +module.exports = { + /** + * Note: Although color scheme is available immediately, it may change at any + * time. Any rendering logic or styles that depend on this should try to call + * this function on every render, rather than caching the value (for example, + * using inline styles rather than setting a value in a `StyleSheet`). + * + * Example: `const colorScheme = Appearance.getColorScheme();` + * + * @returns {?ColorSchemeName} Value for the color scheme preference. + */ + getColorScheme(): ?ColorSchemeName { + return currentColorScheme; + }, + /** + * Add an event handler that is fired when appearance preferences change. + */ + addChangeListener(listener: AppearanceListener): void { + eventEmitter.addListener('change', listener); + }, + /** + * Remove an event handler. + */ + removeChangeListener(listener: AppearanceListener): void { + eventEmitter.removeListener('change', listener); + }, +}; diff --git a/Libraries/Utilities/NativeAppearance.js b/Libraries/Utilities/NativeAppearance.js new file mode 100644 index 00000000000000..5d37b8aded32a9 --- /dev/null +++ b/Libraries/Utilities/NativeAppearance.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; + +export type ColorSchemeName = 'light' | 'dark'; + +export type AppearancePreferences = {| + // TODO: (hramos) T52919652 Use ?ColorSchemeName once codegen supports union + // types. + /* 'light' | 'dark' */ + colorScheme?: ?string, +|}; + +export interface Spec extends TurboModule { + // TODO: (hramos) T52919652 Use ?ColorSchemeName once codegen supports union + // types. + /* 'light' | 'dark' */ + +getColorScheme: () => ?string; + + // RCTEventEmitter + +addListener: (eventName: string) => void; + +removeListeners: (count: number) => void; +} + +export default (TurboModuleRegistry.get('Appearance'): ?Spec); diff --git a/Libraries/react-native/react-native-implementation.js b/Libraries/react-native/react-native-implementation.js index 4b186729337dd0..56a8232080d244 100644 --- a/Libraries/react-native/react-native-implementation.js +++ b/Libraries/react-native/react-native-implementation.js @@ -49,6 +49,7 @@ import typeof VirtualizedSectionList from '../Lists/VirtualizedSectionList'; import typeof ActionSheetIOS from '../ActionSheetIOS/ActionSheetIOS'; import typeof Alert from '../Alert/Alert'; import typeof Animated from '../Animated/src/Animated'; +import typeof Appearance from '../Utilities/Appearance'; import typeof AppRegistry from '../ReactNative/AppRegistry'; import typeof AppState from '../AppState/AppState'; import typeof AsyncStorage from '../Storage/AsyncStorage'; @@ -252,6 +253,9 @@ module.exports = { get Animated(): Animated { return require('../Animated/src/Animated'); }, + get Appearance(): Appearance { + return require('../Utilities/Appearance'); + }, get AppRegistry(): AppRegistry { return require('../ReactNative/AppRegistry'); }, diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index f806a4974dc45c..4dd34e6f457704 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -33,6 +33,7 @@ #endif NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotification"; +NSString *const RCTUserInterfaceStyleDidChangeNotification = @"RCTUserInterfaceStyleDidChangeNotification"; @interface RCTUIManager (RCTRootView) @@ -347,7 +348,7 @@ - (void)setIntrinsicContentSize:(CGSize)intrinsicContentSize if (bothSizesHaveAZeroDimension || sizesAreEqual) { return; } - + [self invalidateIntrinsicContentSize]; [self.superview setNeedsLayout]; @@ -366,6 +367,22 @@ - (void)contentViewInvalidated [self showLoadingView]; } +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \ + __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 +- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection +{ + [super traitCollectionDidChange:previousTraitCollection]; + + if (@available(iOS 13.0, *)) { + if ([previousTraitCollection hasDifferentColorAppearanceComparedToTraitCollection:self.traitCollection]) { + [[NSNotificationCenter defaultCenter] postNotificationName:RCTUserInterfaceStyleDidChangeNotification + object:self + userInfo:@{@"traitCollection": self.traitCollection}]; + } + } +} +#endif + - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; diff --git a/React/CoreModules/BUCK b/React/CoreModules/BUCK index dfd93ea49631ea..dafa376272d3e0 100644 --- a/React/CoreModules/BUCK +++ b/React/CoreModules/BUCK @@ -38,6 +38,10 @@ rn_apple_library( ["-D PIC_MODIFIER=@PLT"], )], plugins = react_module_plugin_providers( + name = "AppearanceConstants", + native_class_func = "RCTAppearanceCls", + ) + + react_module_plugin_providers( name = "DeviceInfo", native_class_func = "RCTDeviceInfoCls", ) + diff --git a/React/CoreModules/CoreModulesPlugins.h b/React/CoreModules/CoreModulesPlugins.h index 8d49aaf456f183..a4cfb51ab8a3ad 100644 --- a/React/CoreModules/CoreModulesPlugins.h +++ b/React/CoreModules/CoreModulesPlugins.h @@ -29,6 +29,7 @@ extern "C" { Class RCTCoreModulesClassProvider(const char *name); // Lookup functions +Class RCTAppearanceCls(void); Class RCTDeviceInfoCls(void); Class RCTExceptionsManagerCls(void); Class RCTImageLoaderCls(void); diff --git a/React/CoreModules/CoreModulesPlugins.mm b/React/CoreModules/CoreModulesPlugins.mm index 4b7a90103c99c7..a9b7b2c8d9188e 100644 --- a/React/CoreModules/CoreModulesPlugins.mm +++ b/React/CoreModules/CoreModulesPlugins.mm @@ -17,7 +17,8 @@ #import static std::unordered_map sCoreModuleClassMap = { - {"DeviceInfo", RCTDeviceInfoCls}, + {"AppearanceConstants", RCTAppearanceCls}, +{"DeviceInfo", RCTDeviceInfoCls}, {"ExceptionsManager", RCTExceptionsManagerCls}, {"ImageLoader", RCTImageLoaderCls}, {"PlatformConstants", RCTPlatformCls}, diff --git a/React/CoreModules/RCTAppearance.h b/React/CoreModules/RCTAppearance.h new file mode 100644 index 00000000000000..689ef7e113ead9 --- /dev/null +++ b/React/CoreModules/RCTAppearance.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import + +NSString *const RCTUserInterfaceStyleDidChangeNotification = @"RCTUserInterfaceStyleDidChangeNotification"; + +@interface RCTAppearance : RCTEventEmitter +@end diff --git a/React/CoreModules/RCTAppearance.mm b/React/CoreModules/RCTAppearance.mm new file mode 100644 index 00000000000000..b58351990a8a4c --- /dev/null +++ b/React/CoreModules/RCTAppearance.mm @@ -0,0 +1,107 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTAppearance.h" + +#import +#import + +#import "CoreModulesPlugins.h" + +using namespace facebook::react; + +NSString *const RCTAppearanceColorSchemeLight = @"light"; +NSString *const RCTAppearanceColorSchemeDark = @"dark"; + +static NSString *RCTColorSchemePreference(UITraitCollection *traitCollection) +{ +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 + if (@available(iOS 13.0, *)) { + static NSDictionary *appearances; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + appearances = @{ + @(UIUserInterfaceStyleLight): RCTAppearanceColorSchemeLight, + @(UIUserInterfaceStyleDark): RCTAppearanceColorSchemeDark + }; + }); + + traitCollection = traitCollection ?: [UITraitCollection currentTraitCollection]; + return appearances[@(traitCollection.userInterfaceStyle)] ?: nil; + } +#endif + + return nil; +} + +@interface RCTAppearance () +@end + +@implementation RCTAppearance + +RCT_EXPORT_MODULE(AppearanceConstants) + ++ (BOOL)requiresMainQueueSetup +{ + return YES; +} + +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + +- (std::shared_ptr)getTurboModuleWithJsInvoker:(std::shared_ptr)jsInvoker +{ + return std::make_shared(self, jsInvoker); +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getColorScheme) +{ + return RCTColorSchemePreference(nil); +} + +- (void)appearanceChanged:(NSNotification *)notification +{ + NSDictionary *userInfo = [notification userInfo]; + UITraitCollection *traitCollection = nil; + if (userInfo) { + traitCollection = userInfo[@"traitCollection"]; + } + [self sendEventWithName:@"appearanceChanged" body:@{@"colorScheme": RCTColorSchemePreference(traitCollection)}]; +} + +#pragma mark - RCTEventEmitter + +- (NSArray *)supportedEvents +{ + return @[@"appearanceChanged"]; +} + +- (void)startObserving +{ + if (@available(iOS 13.0, *)) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appearanceChanged:) + name:RCTUserInterfaceStyleDidChangeNotification + object:nil]; + } +} + +- (void)stopObserving +{ + if (@available(iOS 13.0, *)) { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + } +} + +@end + +Class RCTAppearanceCls(void) { + return RCTAppearance.class; +} From bab28dc96b331ab853873ed6d1e3e492634a8570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ramos?= Date: Fri, 23 Aug 2019 11:46:30 -0700 Subject: [PATCH 2/5] Support light and dark themes in RNTester Summary: Initial conversion of RNTester to support light and dark themes. Theming is implemented by providing the desired color theme via context. Example: ``` const ThemedContainer = props => ( {theme => { return ( {props.children} ); }} ); ``` As RNTester's design follows the base iOS system appearance, I've chosen light and dark themes based on the actual iOS 13 semantic colors. The themes are RNTester-specific, however, and we'd expect individual apps to build their own color palettes. ## Examples The new Appearance Examples screen demonstrates how context can be used to force a theme. It also displays the list of colors in each RNTester theme. https://pxl.cl/HmzW (screenshot: Appearance Examples screen on RNTester with Dark Mode enabled. Displays useColorScheme hook, and context examples.) https://pxl.cl/HmB3 (screenshot: Same screen, with light and dark RNTester themes visible) Theming support in this diff mostly focused on the main screen and the Dark Mode examples screen. This required updating the components used by most of the examples, as you can see in this Image example: https://pxl.cl/H0Hv (screenshot: Image Examples screen in Dark Mode theme) Note that I have yet to go through every single example screen to update it. There's individual cases, such as the FlatList example screen, that are not fully converted to use a dark theme when appropriate. This can be taken care later as it's non-blocking. Reviewed By: zackargyle Differential Revision: D16681909 fbshipit-source-id: 1a5fff44b0dd00b8c3f439c81837d24771b28d94 --- RNTester/js/RNTesterApp.ios.js | 87 +++++--- RNTester/js/components/RNTesterBlock.js | 51 +++-- .../js/components/RNTesterExampleFilter.js | 52 +++-- RNTester/js/components/RNTesterExampleList.js | 146 +++++++++---- RNTester/js/components/RNTesterPage.js | 27 ++- RNTester/js/components/RNTesterTheme.js | 88 ++++++++ RNTester/js/components/RNTesterTitle.js | 24 ++- .../examples/Appearance/AppearanceExample.js | 201 ++++++++++++++++++ RNTester/js/examples/Button/ButtonExample.js | 77 ++++--- RNTester/js/utils/RNTesterActions.js | 20 +- RNTester/js/utils/RNTesterList.ios.js | 5 + .../js/utils/RNTesterNavigationReducer.js | 17 ++ 12 files changed, 650 insertions(+), 145 deletions(-) create mode 100644 RNTester/js/components/RNTesterTheme.js create mode 100644 RNTester/js/examples/Appearance/AppearanceExample.js diff --git a/RNTester/js/RNTesterApp.ios.js b/RNTester/js/RNTesterApp.ios.js index 646b33920c9e95..1a333589a39558 100644 --- a/RNTester/js/RNTesterApp.ios.js +++ b/RNTester/js/RNTesterApp.ios.js @@ -20,11 +20,13 @@ const SnapshotViewIOS = require('./examples/Snapshot/SnapshotViewIOS.ios'); const URIActionMap = require('./utils/URIActionMap'); const { + Appearance, AppRegistry, AsyncStorage, BackHandler, Button, Linking, + Platform, SafeAreaView, StyleSheet, Text, @@ -35,6 +37,7 @@ const { import type {RNTesterExample} from './types/RNTesterTypes'; import type {RNTesterAction} from './utils/RNTesterActions'; import type {RNTesterNavigationState} from './utils/RNTesterNavigationReducer'; +import {RNTesterThemeContext, themes} from './components/RNTesterTheme'; type Props = { exampleFromAppetizeParams?: ?string, @@ -47,18 +50,40 @@ YellowBox.ignoreWarnings([ const APP_STATE_KEY = 'RNTesterAppState.v2'; const Header = ({onBack, title}: {onBack?: () => mixed, title: string}) => ( - - - - {title} - - {onBack && ( - -