diff --git a/change/react-native-windows-2020-04-16-11-09-41-jahiggin-backHandler.json b/change/react-native-windows-2020-04-16-11-09-41-jahiggin-backHandler.json new file mode 100644 index 00000000000..b7b3e0c8d40 --- /dev/null +++ b/change/react-native-windows-2020-04-16-11-09-41-jahiggin-backHandler.json @@ -0,0 +1,8 @@ +{ + "type": "prerelease", + "comment": "Add support for React Native BackHandler API", + "packageName": "react-native-windows", + "email": "jahiggin@microsoft.com", + "dependentChangeType": "patch", + "date": "2020-04-16T18:09:41.547Z" +} \ No newline at end of file diff --git a/packages/override-tools/src/Manifest.ts b/packages/override-tools/src/Manifest.ts index 78e872cf3d3..3c362767642 100644 --- a/packages/override-tools/src/Manifest.ts +++ b/packages/override-tools/src/Manifest.ts @@ -242,7 +242,8 @@ export default class Manifest { */ static hashContent(str: string) { const hasher = crypto.createHash('sha1'); - hasher.update(str); + const normalizedStr = str.replace(/(? { + console.log('hardwareBackPress'); + return true; + }; } const styles = StyleSheet.create({ diff --git a/vnext/Microsoft.ReactNative/Views/ReactRootControl.cpp b/vnext/Microsoft.ReactNative/Views/ReactRootControl.cpp index 0574a0762d3..bd5a340f130 100644 --- a/vnext/Microsoft.ReactNative/Views/ReactRootControl.cpp +++ b/vnext/Microsoft.ReactNative/Views/ReactRootControl.cpp @@ -184,6 +184,7 @@ void ReactRootControl::InitRootView( m_SIPEventHandler->AttachView(xamlRootView, /*fireKeyboradEvents:*/ true); UpdateRootViewInternal(); + AttachBackHandlers(xamlRootView); m_isInitialized = true; } @@ -232,6 +233,8 @@ void ReactRootControl::UninitRootView() noexcept { m_previewKeyboardEventHandlerOnRoot->unhook(); } + RemoveBackHandlers(); + // If the redbox error UI is shown we need to remove it, otherwise let the // natural teardown process do this if (m_redBoxGrid) { @@ -607,6 +610,73 @@ void ReactRootControl::ReloadViewHost() noexcept { } } +void ReactRootControl::AttachBackHandlers(XamlView const &rootView) noexcept { + auto weakThis = weak_from_this(); + m_backRequestedRevoker = winrt::Windows::UI::Core::SystemNavigationManager::GetForCurrentView().BackRequested( + winrt::auto_revoke, + [weakThis](winrt::IInspectable const & /*sender*/, winrt::BackRequestedEventArgs const &args) { + if (auto self = weakThis.lock()) { + args.Handled(self->OnBackRequested()); + } + }); + + // In addition to handling the BackRequested event, UWP suggests that we listen for other user inputs that should + // trigger back navigation that don't fire that event: + // https://docs.microsoft.com/en-us/windows/uwp/design/basics/navigation-history-and-backwards-navigation + auto rootElement(rootView.try_as()); + if (rootElement == nullptr) { + assert(false); + return; + } + + // Handle keyboard "back" button press + winrt::KeyboardAccelerator goBack{}; + goBack.Key(winrt::VirtualKey::GoBack); + goBack.Invoked( + [weakThis]( + winrt::KeyboardAccelerator const & /*sender*/, winrt::KeyboardAcceleratorInvokedEventArgs const &args) { + if (auto self = weakThis.lock()) { + args.Handled(self->OnBackRequested()); + } + }); + rootElement.KeyboardAccelerators().Append(goBack); + + // Handle Alt+Left keyboard shortcut + winrt::KeyboardAccelerator altLeft{}; + altLeft.Key(winrt::VirtualKey::Left); + altLeft.Invoked( + [weakThis]( + winrt::KeyboardAccelerator const & /*sender*/, winrt::KeyboardAcceleratorInvokedEventArgs const &args) { + if (auto self = weakThis.lock()) { + args.Handled(self->OnBackRequested()); + } + }); + rootElement.KeyboardAccelerators().Append(altLeft); + altLeft.Modifiers(winrt::VirtualKeyModifiers::Menu); + + // Hide keyboard accelerator tooltips + rootElement.KeyboardAcceleratorPlacementMode(winrt::KeyboardAcceleratorPlacementMode::Hidden); +} + +void ReactRootControl::RemoveBackHandlers() noexcept { + m_backRequestedRevoker.revoke(); + if (auto rootView = m_weakRootView.get()) { + if (auto element = rootView.try_as()) { + element.KeyboardAccelerators().Clear(); + } + } +} + +bool ReactRootControl::OnBackRequested() noexcept { + if (auto reactInstance = m_weakReactInstance.GetStrongPtr()) { + query_cast(*reactInstance) + .CallJsFunction("RCTDeviceEventEmitter", "emit", folly::dynamic::array("hardwareBackPress")); + return true; + } + + return false; +} + Mso::React::IReactViewHost *ReactRootControl::ReactViewHost() noexcept { return m_reactViewHost.Get(); } diff --git a/vnext/Microsoft.ReactNative/Views/ReactRootControl.h b/vnext/Microsoft.ReactNative/Views/ReactRootControl.h index 3166195164b..26bb5948cc2 100644 --- a/vnext/Microsoft.ReactNative/Views/ReactRootControl.h +++ b/vnext/Microsoft.ReactNative/Views/ReactRootControl.h @@ -15,6 +15,7 @@ namespace winrt { using namespace Windows::UI; +using namespace Windows::UI::Core; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Input; @@ -83,6 +84,10 @@ struct ReactRootControl final : std::enable_shared_from_this, void ReloadHost() noexcept; void ReloadViewHost() noexcept; + void AttachBackHandlers(XamlView const &rootView) noexcept; + void RemoveBackHandlers() noexcept; + bool OnBackRequested() noexcept; + private: int64_t m_rootTag{-1}; @@ -130,6 +135,7 @@ struct ReactRootControl final : std::enable_shared_from_this, winrt::Button::Click_revoker m_directDebuggingRevoker{}; winrt::Button::Click_revoker m_breakOnNextLineRevoker{}; winrt::CoreDispatcher::AcceleratorKeyActivated_revoker m_coreDispatcherAKARevoker{}; + winrt::SystemNavigationManager::BackRequested_revoker m_backRequestedRevoker{}; }; //! This class ensures that we access ReactRootView from UI thread. diff --git a/vnext/ReactUWP/Views/TouchEventHandler.cpp b/vnext/ReactUWP/Views/TouchEventHandler.cpp index cdcd9de40a8..d08300864ca 100644 --- a/vnext/ReactUWP/Views/TouchEventHandler.cpp +++ b/vnext/ReactUWP/Views/TouchEventHandler.cpp @@ -78,6 +78,13 @@ void TouchEventHandler::OnPointerPressed( if (!TagFromOriginalSource(args, &tag, &sourceElement)) return; + // If this was caused by the user pressing the "back" hardware button, fire that event instead + if (args.GetCurrentPoint(sourceElement).Properties().PointerUpdateKind() == + winrt::Windows::UI::Input::PointerUpdateKind::XButton1Pressed) { + args.Handled(DispatchBackEvent()); + return; + } + if (m_xamlView.as().CapturePointer(args.Pointer())) { // Pointer pressing updates the enter/leave state UpdatePointersInViews(args, tag, sourceElement); @@ -341,6 +348,16 @@ void TouchEventHandler::DispatchTouchEvent(TouchEventType eventType, size_t poin instance->CallJsFunction("RCTEventEmitter", "receiveTouches", std::move(params)); } +bool TouchEventHandler::DispatchBackEvent() { + auto instance = m_wkReactInstance.lock(); + if (instance != nullptr && !instance->IsInError()) { + instance->CallJsFunction("RCTDeviceEventEmitter", "emit", folly::dynamic::array("hardwareBackPress")); + return true; + } + + return false; +} + const char *TouchEventHandler::GetPointerDeviceTypeName( winrt::Windows::Devices::Input::PointerDeviceType deviceType) noexcept { const char *deviceTypeName = "unknown"; diff --git a/vnext/ReactUWP/Views/TouchEventHandler.h b/vnext/ReactUWP/Views/TouchEventHandler.h index 62488bc7e4f..df26eaf2c19 100644 --- a/vnext/ReactUWP/Views/TouchEventHandler.h +++ b/vnext/ReactUWP/Views/TouchEventHandler.h @@ -78,6 +78,7 @@ class TouchEventHandler { enum class TouchEventType { Start = 0, End, Move, Cancel, PointerEntered, PointerExited, PointerMove }; void OnPointerConcluded(TouchEventType eventType, const winrt::PointerRoutedEventArgs &args); void DispatchTouchEvent(TouchEventType eventType, size_t pointerIndex); + bool DispatchBackEvent(); const char *GetPointerDeviceTypeName(winrt::Windows::Devices::Input::PointerDeviceType deviceType) noexcept; const char *GetTouchEventTypeName(TouchEventType eventType) noexcept; diff --git a/vnext/src/Libraries/Utilities/BackHandler.windows.js b/vnext/src/Libraries/Utilities/BackHandler.windows.js index 8a57351da63..e6e3d035295 100644 --- a/vnext/src/Libraries/Utilities/BackHandler.windows.js +++ b/vnext/src/Libraries/Utilities/BackHandler.windows.js @@ -4,16 +4,30 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @format * @flow + * @format */ -// On Apple TV, this implements back navigation using the TV remote's menu button. -// On iOS, this just implements a stub. - 'use strict'; -// TODO Hx: Implement. +import NativeDeviceEventManager from '../../Libraries/NativeModules/specs/NativeDeviceEventManager'; +import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter'; + +const DEVICE_BACK_EVENT = 'hardwareBackPress'; + +type BackPressEventName = 'backPress' | 'hardwareBackPress'; + +const _backPressSubscriptions = []; + +RCTDeviceEventEmitter.addListener(DEVICE_BACK_EVENT, function() { + for (let i = _backPressSubscriptions.length - 1; i >= 0; i--) { + if (_backPressSubscriptions[i]()) { + return; + } + } + + BackHandler.exitApp(); +}); /** * Detect hardware button presses for back navigation. @@ -45,9 +59,6 @@ * }); * ``` */ - -type BackPressEventName = 'backPress' | 'hardwareBackPress'; - type TBackHandler = {| +exitApp: () => void, +addEventListener: ( @@ -59,13 +70,47 @@ type TBackHandler = {| handler: Function, ) => void, |}; - const BackHandler: TBackHandler = { - exitApp: () => {}, - addEventListener: (eventName: BackPressEventName, handler: Function) => { - return {remove: () => {}}; + exitApp: function(): void { + if (!NativeDeviceEventManager) { + return; + } + + NativeDeviceEventManager.invokeDefaultBackPressHandler(); + }, + + /** + * Adds an event handler. Supported events: + * + * - `hardwareBackPress`: Fires when the Android hardware back button is pressed or when the + * tvOS menu button is pressed. + */ + addEventListener: function( + eventName: BackPressEventName, + handler: Function, + ): {remove: () => void, ...} { + if (_backPressSubscriptions.indexOf(handler) === -1) { + _backPressSubscriptions.push(handler); + } + return { + remove: (): void => BackHandler.removeEventListener(eventName, handler), + }; + }, + + /** + * Removes the event handler. + */ + removeEventListener: function( + eventName: BackPressEventName, + handler: Function, + ): void { + if (_backPressSubscriptions.indexOf(handler) !== -1) { + _backPressSubscriptions.splice( + _backPressSubscriptions.indexOf(handler), + 1, + ); + } }, - removeEventListener: (eventName: BackPressEventName, handler: Function) => {}, }; module.exports = BackHandler; diff --git a/vnext/src/overrides.json b/vnext/src/overrides.json index 5a9e051c70d..a8a843948a5 100644 --- a/vnext/src/overrides.json +++ b/vnext/src/overrides.json @@ -679,10 +679,10 @@ { "type": "derived", "file": "Libraries\\Utilities\\BackHandler.windows.js", - "baseFile": "Libraries\\Utilities\\BackHandler.ios.js", - "baseVersion": "0.62.0-rc.3", - "baseHash": "aa2d482b80ae66104d632c75c24646fdcbe916af", - "issue": "LEGACY_FIXME" + "baseFile": "Libraries\\Utilities\\BackHandler.android.js", + "baseVersion": "0.62.2", + "baseHash": "b1633b6bb5e3dc842011b09f778626c39bf6b654", + "issue": 4629 }, { "type": "derived",