diff --git a/change/react-native-windows-2019-11-22-14-27-58-treeDump.json b/change/react-native-windows-2019-11-22-14-27-58-treeDump.json new file mode 100644 index 0000000000..2c63e59f74 --- /dev/null +++ b/change/react-native-windows-2019-11-22-14-27-58-treeDump.json @@ -0,0 +1,8 @@ +{ + "type": "prerelease", + "comment": "Properly support ScrollView KeyboardDismissMode", + "packageName": "react-native-windows", + "email": "dida@ntdev.microsoft.com", + "commit": "6b4df0e13a371763a12ecee273011ad762bd9322", + "date": "2019-11-22T22:27:58.216Z" +} \ No newline at end of file diff --git a/packages/playground/Samples/scrollViewSnapSample.tsx b/packages/playground/Samples/scrollViewSnapSample.tsx index e8b1012b1a..8000cd51bc 100644 --- a/packages/playground/Samples/scrollViewSnapSample.tsx +++ b/packages/playground/Samples/scrollViewSnapSample.tsx @@ -30,6 +30,7 @@ export default class Bootstrap extends React.Component<{}, any> { zoomValue: false, alignToStartValue: true, refreshing: false, + keyboardDismiss: false, snapToOffsets: true, pagingEnabled: false, }; @@ -62,6 +63,10 @@ export default class Bootstrap extends React.Component<{}, any> { this.setState({pagingEnabled: value}); }; + toggleSwitch8 = (value: boolean) => { + this.setState({keyboardDismiss: value}); + }; + onRefresh = () => { this.setState({refreshing: true}); wait(2000).then(() => this.setState({refreshing: false})); @@ -190,16 +195,29 @@ export default class Bootstrap extends React.Component<{}, any> { justifyContent: 'center', padding: 20, }}> - - {this.state.pagingEnabled - ? 'pagingEnabled on' - : 'pagingEnabled off'} - + {'PagingEnabled'} + + + {'KeyboardDismiss: '.concat( + this.state.keyboardDismiss ? 'on-drag' : 'none', + )} + + + { onRefresh={this.onRefresh} /> } + keyboardDismissMode={ + this.state.keyboardDismiss ? 'on-drag' : 'none' + } snapToOffsets={ this.state.snapToOffsets ? [100.0, 500.0] : undefined } diff --git a/vnext/ReactUWP/Utils/Helpers.cpp b/vnext/ReactUWP/Utils/Helpers.cpp index c6919e0b69..cbcb19c448 100644 --- a/vnext/ReactUWP/Utils/Helpers.cpp +++ b/vnext/ReactUWP/Utils/Helpers.cpp @@ -58,12 +58,37 @@ bool IsAPIContractVxAvailable() { return isAPIContractVxAvailable; } +bool IsAPIContractV5Available() { + return IsAPIContractVxAvailable<5>(); +} + bool IsAPIContractV6Available() { return IsAPIContractVxAvailable<6>(); } +bool IsAPIContractV7Available() { + return IsAPIContractVxAvailable<7>(); +} + +bool IsAPIContractV8Available() { + return IsAPIContractVxAvailable<8>(); +} + +bool IsRS3OrHigher() { + return IsAPIContractV5Available(); +} + bool IsRS4OrHigher() { return IsAPIContractV6Available(); } + +bool IsRS5OrHigher() { + return IsAPIContractV7Available(); +} + +bool Is19H1OrHigher() { + return IsAPIContractV8Available(); +} + } // namespace uwp }; // namespace react diff --git a/vnext/ReactUWP/Utils/Helpers.h b/vnext/ReactUWP/Utils/Helpers.h index 50c46a234b..ad12e1a9ee 100644 --- a/vnext/ReactUWP/Utils/Helpers.h +++ b/vnext/ReactUWP/Utils/Helpers.h @@ -30,6 +30,9 @@ inline typename T asEnum(folly::dynamic const &obj) { ReactId getViewId(_In_ IReactInstance *instance, winrt::FrameworkElement const &fe); std::int32_t CountOpenPopups(); +bool IsRS3OrHigher(); bool IsRS4OrHigher(); +bool IsRS5OrHigher(); +bool Is19H1OrHigher(); } // namespace uwp } // namespace react diff --git a/vnext/ReactUWP/Views/KeyboardEventHandler.cpp b/vnext/ReactUWP/Views/KeyboardEventHandler.cpp index c385a01a55..0e9f8bcc5f 100644 --- a/vnext/ReactUWP/Views/KeyboardEventHandler.cpp +++ b/vnext/ReactUWP/Views/KeyboardEventHandler.cpp @@ -68,11 +68,13 @@ PreviewKeyboardEventHandler::PreviewKeyboardEventHandler(KeyboardEventCallback & void PreviewKeyboardEventHandler::hook(XamlView xamlView) { auto uiElement = xamlView.as(); - if (m_keyDownCallback) - m_previewKeyDownRevoker = uiElement.PreviewKeyDown(winrt::auto_revoke, m_keyDownCallback); + if (uiElement.try_as()) { + if (m_keyDownCallback) + m_previewKeyDownRevoker = uiElement.PreviewKeyDown(winrt::auto_revoke, m_keyDownCallback); - if (m_keyUpCallback) - m_previewKeyUpRevoker = uiElement.PreviewKeyUp(winrt::auto_revoke, m_keyUpCallback); + if (m_keyUpCallback) + m_previewKeyUpRevoker = uiElement.PreviewKeyUp(winrt::auto_revoke, m_keyUpCallback); + } } void PreviewKeyboardEventHandler::unhook() { diff --git a/vnext/ReactUWP/Views/ReactControl.cpp b/vnext/ReactUWP/Views/ReactControl.cpp index f950020c90..80754f3d4f 100644 --- a/vnext/ReactUWP/Views/ReactControl.cpp +++ b/vnext/ReactUWP/Views/ReactControl.cpp @@ -203,6 +203,7 @@ void ReactControl::AttachRoot() noexcept { m_touchEventHandler->AddTouchHandlers(m_xamlRootView); m_previewKeyboardEventHandlerOnRoot->hook(m_xamlRootView); + m_SIPEventHandler->AttachView(m_xamlRootView, true /*fireKeyboradEvents*/); auto initialProps = m_initialProps; m_reactInstance->AttachMeasuredRootView(m_pParent, std::move(initialProps)); diff --git a/vnext/ReactUWP/Views/SIPEventHandler.cpp b/vnext/ReactUWP/Views/SIPEventHandler.cpp index f3c7ec7c4b..48a81af30e 100644 --- a/vnext/ReactUWP/Views/SIPEventHandler.cpp +++ b/vnext/ReactUWP/Views/SIPEventHandler.cpp @@ -7,6 +7,7 @@ #include +#include #include #include @@ -15,43 +16,96 @@ using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::UI::ViewManagement::Core; using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Media; } // namespace winrt namespace react { namespace uwp { SIPEventHandler::SIPEventHandler(const std::weak_ptr &reactInstance) - : m_wkReactInstance(reactInstance) { - auto coreInputView = winrt::CoreInputView::GetForCurrentView(); - if (coreInputView) { - m_occlusionsChanged_revoker = coreInputView.OcclusionsChanged( - winrt::auto_revoke, [=](auto &&, const winrt::CoreInputViewOcclusionsChangedEventArgs &e) { - if (!e.Handled()) { - winrt::Rect finalRect = winrt::RectHelper::Empty(); - winrt::IVectorView occlusions = e.Occlusions(); - for (uint32_t i = 0; i < occlusions.Size(); i++) { - winrt::CoreInputViewOcclusion occlusion = occlusions.GetAt(i); - if (occlusion.OcclusionKind() == winrt::CoreInputViewOcclusionKind::Docked) { - finalRect = winrt::RectHelper::Union(finalRect, occlusion.OccludingRect()); - } - } - - if (winrt::RectHelper::GetIsEmpty(finalRect)) { - folly::dynamic params = folly::dynamic::object("screenY", 0)("screenX", 0)("width", 0)("height", 0); - SendEvent("keyboardDidHide", std::move(params)); - } else { - folly::dynamic params = folly::dynamic::object( - "endCoordinates", - folly::dynamic::object("screenY", finalRect.Y)("screenX", finalRect.X)("width", finalRect.Width)( - "height", finalRect.Height)); - SendEvent("keyboardDidShow", std::move(params)); - } - } - }); - } -} + : m_wkReactInstance(reactInstance), m_fireKeyboradEvents(false), m_finalRect(winrt::RectHelper::Empty()){}; SIPEventHandler::~SIPEventHandler() { m_occlusionsChanged_revoker = {}; + m_loadedRevoker = {}; +} +// keyboardDidHide and keyboardDidShow events works on >= RS3 +// TryShow and TryHide works on >= RS5 + +void SIPEventHandler::AttachView(XamlView xamlView, bool fireKeyboardEvents) { + m_fireKeyboradEvents = fireKeyboardEvents; + // hookup CoreInputView only after element is in the tree + m_view = winrt::make_weak(xamlView); + if (winrt::VisualTreeHelper::GetParent(xamlView)) { + InitializeCoreInputView(); + } else { + m_loadedRevoker = xamlView.as().Loaded( + winrt::auto_revoke, [this](const auto &sender, const auto &) { InitializeCoreInputView(); }); + } +} + +void SIPEventHandler::InitializeCoreInputView() { + if (const auto xamlView = m_view.get()) { + if (!IsRS3OrHigher()) { + return; // CoreInputView is only supported on >= RS3. + } + + if (Is19H1OrHigher()) { + // 19H1 and higher supports island scenarios + auto uiElement(xamlView.as()); + m_coreInputView = winrt::CoreInputView::GetForUIContext(uiElement.UIContext()); + } else { + m_coreInputView = winrt::CoreInputView::GetForCurrentView(); + } + + if (m_coreInputView) { + auto occlusions = m_coreInputView.GetCoreInputViewOcclusions(); + m_isShowing = !IsOcclusionsEmpty(occlusions); + m_occlusionsChanged_revoker = m_coreInputView.OcclusionsChanged( + winrt::auto_revoke, [this](auto &&, const winrt::CoreInputViewOcclusionsChangedEventArgs &e) { + if (!e.Handled()) { + bool wasShowing = m_isShowing; + m_isShowing = !IsOcclusionsEmpty(e.Occlusions()); + if (wasShowing != m_isShowing && m_fireKeyboradEvents) { + if (!m_isShowing) { + folly::dynamic params = folly::dynamic::object("screenY", 0)("screenX", 0)("width", 0)("height", 0); + SendEvent("keyboardDidHide", std::move(params)); + } else { + folly::dynamic params = folly::dynamic::object( + "endCoordinates", + folly::dynamic::object("screenY", m_finalRect.Y)("screenX", m_finalRect.X)( + "width", m_finalRect.Width)("height", m_finalRect.Height)); + SendEvent("keyboardDidShow", std::move(params)); + } + } + } + }); + } + } +} +/* +void SIPEventHandler::TryShow() { +if (IsRS5OrHigher() && m_coreInputView && !m_isShowing) { // CoreInputView.TryShow is only avaliable after RS5 + m_coreInputView.TryShow(); +} +} +*/ + +void SIPEventHandler::TryHide() { + if (IsRS5OrHigher() && m_coreInputView && m_isShowing) { // CoreInputView.TryHide is only avaliable after RS5 + m_coreInputView.TryHide(); + } +} + +bool SIPEventHandler::IsOcclusionsEmpty(winrt::IVectorView occlusions) { + m_finalRect = winrt::RectHelper::Empty(); + if (occlusions) { + for (const auto &occlusion : occlusions) { + if (occlusion.OcclusionKind() == winrt::CoreInputViewOcclusionKind::Docked) { + m_finalRect = winrt::RectHelper::Union(m_finalRect, occlusion.OccludingRect()); + } + } + } + return (winrt::RectHelper::GetIsEmpty(m_finalRect)); } void SIPEventHandler::SendEvent(std::string &&eventName, folly::dynamic &¶meters) { diff --git a/vnext/ReactUWP/Views/SIPEventHandler.h b/vnext/ReactUWP/Views/SIPEventHandler.h index 142bd258f5..67d9652e29 100644 --- a/vnext/ReactUWP/Views/SIPEventHandler.h +++ b/vnext/ReactUWP/Views/SIPEventHandler.h @@ -8,7 +8,7 @@ #include namespace winrt { -using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; using namespace Windows::UI::ViewManagement::Core; } // namespace winrt @@ -20,10 +20,27 @@ class SIPEventHandler { SIPEventHandler(const std::weak_ptr &reactInstance); virtual ~SIPEventHandler(); + bool IsSIPShowing() { + return m_isShowing; + } + + void AttachView(XamlView xamlView, bool fireKeyboardEvents); + // void TryShow(); + void TryHide(); + private: + bool IsOcclusionsEmpty(winrt::IVectorView occlusions); void SendEvent(std::string &&eventName, folly::dynamic &¶meters); std::weak_ptr m_wkReactInstance; winrt::CoreInputView::OcclusionsChanged_revoker m_occlusionsChanged_revoker; + winrt::Rect m_finalRect; + winrt::CoreInputView m_coreInputView{nullptr}; + winrt::weak_ref m_view{}; + winrt::Windows::UI::Xaml::FrameworkElement::Loaded_revoker m_loadedRevoker{}; + bool m_isShowing{false}; + bool m_fireKeyboradEvents; + + void InitializeCoreInputView(); }; } // namespace uwp diff --git a/vnext/ReactUWP/Views/ScrollViewManager.cpp b/vnext/ReactUWP/Views/ScrollViewManager.cpp index 25b245aac9..1a7aa8375c 100644 --- a/vnext/ReactUWP/Views/ScrollViewManager.cpp +++ b/vnext/ReactUWP/Views/ScrollViewManager.cpp @@ -3,6 +3,7 @@ #include "pch.h" +#include #include #include "Impl/ScrollViewUWPImplementation.h" #include "ScrollViewManager.h" @@ -20,6 +21,7 @@ class ScrollViewShadowNode : public ShadowNodeBase { public: ScrollViewShadowNode(); + ~ScrollViewShadowNode(); void dispatchCommand(int64_t commandId, const folly::dynamic &commandArgs) override; void createView() override; void updateProperties(const folly::dynamic &&props) override; @@ -44,6 +46,9 @@ class ScrollViewShadowNode : public ShadowNodeBase { bool m_isHorizontal = false; bool m_isScrollingEnabled = true; bool m_changeViewAfterLoaded = false; + bool m_dismissKeyboardOnDrag = false; + + std::shared_ptr m_SIPEventHandler; winrt::FrameworkElement::SizeChanged_revoker m_scrollViewerSizeChangedRevoker{}; winrt::FrameworkElement::SizeChanged_revoker m_contentSizeChangedRevoker{}; @@ -56,6 +61,10 @@ class ScrollViewShadowNode : public ShadowNodeBase { ScrollViewShadowNode::ScrollViewShadowNode() {} +ScrollViewShadowNode::~ScrollViewShadowNode() { + m_SIPEventHandler.reset(); +} + void ScrollViewShadowNode::dispatchCommand(int64_t commandId, const folly::dynamic &commandArgs) { const auto scrollViewer = GetView().as(); if (scrollViewer == nullptr) @@ -186,6 +195,16 @@ void ScrollViewShadowNode::updateProperties(const folly::dynamic &&reactDiffMap) if (valid) { ScrollViewUWPImplementation(scrollViewer).SnapToEnd(snapToEnd); } + } else if (propertyName == "keyboardDismissMode") { + m_dismissKeyboardOnDrag = false; + if (propertyValue.isString()) { + m_dismissKeyboardOnDrag = (propertyValue.getString() == "on-drag"); + if (m_dismissKeyboardOnDrag) { + auto wkinstance = GetViewManager()->GetReactInstance(); + m_SIPEventHandler = std::make_unique(wkinstance); + m_SIPEventHandler->AttachView(GetView(), false /*fireKeyboardEvents*/); + } + } } else if (propertyName == "snapToAlignment") { const auto [valid, snapToAlignment] = getPropertyAndValidity(propertyValue, winrt::SnapPointsAlignment::Near); if (valid) { @@ -241,6 +260,11 @@ void ScrollViewShadowNode::AddHandlers(const winrt::ScrollViewer &scrollViewer) m_scrollViewerDirectManipulationStartedRevoker = scrollViewer.DirectManipulationStarted(winrt::auto_revoke, [this](const auto &sender, const auto &) { m_isScrolling = true; + + if (m_dismissKeyboardOnDrag && m_SIPEventHandler) { + m_SIPEventHandler->TryHide(); + } + const auto scrollViewer = sender.as(); EmitScrollEvent( scrollViewer, @@ -402,7 +426,7 @@ folly::dynamic ScrollViewManager::GetNativeProps() const { "showsHorizontalScrollIndicator", "boolean")("showsVerticalScrollIndicator", "boolean")( "minimumZoomScale", "float")("maximumZoomScale", "float")("zoomScale", "float")("snapToInterval", "float")( "snapToOffsets", "array")("snapToAlignment", "number")("snapToStart", "boolean")("snapToEnd", "boolean")( - "pagingEnabled", "boolean")); + "pagingEnabled", "boolean")("keyboardDismissMode", "string")); return props; }