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,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"
}
3 changes: 2 additions & 1 deletion packages/override-tools/src/Manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ export default class Manifest {
*/
static hashContent(str: string) {
const hasher = crypto.createHash('sha1');
hasher.update(str);
const normalizedStr = str.replace(/(?<!\r)\n/g, '\r\n');
hasher.update(normalizedStr);
return hasher.digest('hex');
}

Expand Down
9 changes: 9 additions & 0 deletions packages/playground/Samples/mouse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Text,
GestureResponderEvent,
TouchableHighlight,
BackHandler,
} from 'react-native';

export default class Bootstrap extends React.Component<
Expand All @@ -32,6 +33,10 @@ export default class Bootstrap extends React.Component<
};
}

componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.back);
}

render() {
return (
<View
Expand Down Expand Up @@ -115,6 +120,10 @@ export default class Bootstrap extends React.Component<
console.log('pressout');
console.log(event);
};
back = () => {
console.log('hardwareBackPress');
return true;
};
}

const styles = StyleSheet.create({
Expand Down
70 changes: 70 additions & 0 deletions vnext/Microsoft.ReactNative/Views/ReactRootControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ void ReactRootControl::InitRootView(
m_SIPEventHandler->AttachView(xamlRootView, /*fireKeyboradEvents:*/ true);

UpdateRootViewInternal();
AttachBackHandlers(xamlRootView);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AttachBackHandlers [](start = 2, length = 18)

We must also detach in the UninitRootView.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a function to remove the keyboard accelerators attached to the XAML root element and the BackRequested listener on uninitialization.


m_isInitialized = true;
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<winrt::UIElement>());
if (rootElement == nullptr) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as will already throw if we can't QI (i.e. we terminate since it hits the noexcept boundary).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The non-throwing version is the try_as.


In reply to: 409825498 [](ancestors = 409825498)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to try_as with a null check

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vmoroz was there a preference towards null-checking, or was pointing towards try_as more educational?

It's splitting hairs but I actually slightly preferred the terminating version.

assert(false);
return;
}

// Handle keyboard "back" button press
winrt::KeyboardAccelerator goBack{};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is the right way to listen for the back action. From what I can find, SystemNavigationManager has a BackRequested event to express this intent. Can we hook into that instead?

Copy link
Contributor Author

@DrewHiggins DrewHiggins Apr 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The BackRequested event does not fire in all of the scenarios where the user might be using input devices to go back. The linked UWP documentation in the PR description goes into some more detail on this, and outlines this in a table:
image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow, you're right. I completely missed that in the description and in code. Thanks for linking it again.

It's unfortunate official reccomendations stray from the system API, but this makes sense then. I wonder a bit whether we could let developers opt into non system back events, but could see arguments both ways.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider to add a comment with a link.


In reply to: 409855996 [](ancestors = 409855996)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment with a link to the docs

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Append [](start = 37, length = 6)

It also must be reversible upon unload.
The ReactRootControl could be initialized/uninitialized multiple times.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a function to remove the keyboard accelerators attached to the XAML root element on uninitialization.


// 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<winrt::UIElement>()) {
element.KeyboardAccelerators().Clear();
}
}
}

bool ReactRootControl::OnBackRequested() noexcept {
if (auto reactInstance = m_weakReactInstance.GetStrongPtr()) {
query_cast<Mso::React::ILegacyReactInstance &>(*reactInstance)
.CallJsFunction("RCTDeviceEventEmitter", "emit", folly::dynamic::array("hardwareBackPress"));
return true;
}

return false;
}

Mso::React::IReactViewHost *ReactRootControl::ReactViewHost() noexcept {
return m_reactViewHost.Get();
}
Expand Down
6 changes: 6 additions & 0 deletions vnext/Microsoft.ReactNative/Views/ReactRootControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -83,6 +84,10 @@ struct ReactRootControl final : std::enable_shared_from_this<ReactRootControl>,
void ReloadHost() noexcept;
void ReloadViewHost() noexcept;

void AttachBackHandlers(XamlView const &rootView) noexcept;
void RemoveBackHandlers() noexcept;
bool OnBackRequested() noexcept;

private:
int64_t m_rootTag{-1};

Expand Down Expand Up @@ -130,6 +135,7 @@ struct ReactRootControl final : std::enable_shared_from_this<ReactRootControl>,
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.
Expand Down
17 changes: 17 additions & 0 deletions vnext/ReactUWP/Views/TouchEventHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<winrt::FrameworkElement>().CapturePointer(args.Pointer())) {
// Pointer pressing updates the enter/leave state
UpdatePointersInViews(args, tag, sourceElement);
Expand Down Expand Up @@ -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";
Expand Down
1 change: 1 addition & 0 deletions vnext/ReactUWP/Views/TouchEventHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
71 changes: 58 additions & 13 deletions vnext/src/Libraries/Utilities/BackHandler.windows.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -45,9 +59,6 @@
* });
* ```
*/

type BackPressEventName = 'backPress' | 'hardwareBackPress';

type TBackHandler = {|
+exitApp: () => void,
+addEventListener: (
Expand All @@ -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;
8 changes: 4 additions & 4 deletions vnext/src/overrides.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down