Skip to content

Commit

Permalink
Basic implementation of RCTModalHostView (#1996)
Browse files Browse the repository at this point in the history
My first stab at creating a modal implementation #618
  • Loading branch information
howlettt authored and reseul committed Nov 6, 2018
1 parent 9b69a9a commit 9614b68
Show file tree
Hide file tree
Showing 11 changed files with 609 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ public DevSupportManager(
ReloadSettings();
}

public event Action BeforeShowDevOptionsDialog;

public IDeveloperSettings DevSettings
{
get
Expand Down Expand Up @@ -288,6 +290,8 @@ public void ShowDevOptionsDialog()
_dismissRedBoxDialog();
}

BeforeShowDevOptionsDialog?.Invoke();

#if WINDOWS_UWP
var asyncInfo = _devOptionsDialog.ShowAsync();
_dismissDevOptionsDialog = asyncInfo.Cancel;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ public string JavaScriptBundleUrlForRemoteDebugging
}
}

public event Action BeforeShowDevOptionsDialog;

public void HandleException(Exception exception)
{
RnLog.Fatal(ReactConstants.RNW, exception, $"Exception caught in top handler");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public interface IDevSupportManager
/// </summary>
string JavaScriptBundleUrlForRemoteDebugging { get; }

/// <summary>
/// Called before the dev options dialog is opened
/// </summary>
event Action BeforeShowDevOptionsDialog;

/// <summary>
/// Handle a native exception.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Portions derived from React Native:
// Copyright (c) 2015-present, Facebook, Inc.
// Licensed under the MIT License.
Expand Down Expand Up @@ -41,6 +41,21 @@ public override string Name
}
}

/// <summary>
/// Called before the dev options dialog is opened
/// </summary>
public event Action BeforeShowDevOptionsDialog
{
add
{
_devSupportManager.BeforeShowDevOptionsDialog += value;
}
remove
{
_devSupportManager.BeforeShowDevOptionsDialog -= value;
}
}

/// <summary>
/// Report a fatal exception from JavaScript.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,7 @@ public void MarkLayoutSeen()
/// </summary>
/// <param name="child">The child.</param>
/// <param name="index">The index.</param>
public void AddChildAt(ReactShadowNode child, int index)
public virtual void AddChildAt(ReactShadowNode child, int index)
{
if (child._parent != null)
{
Expand Down
3 changes: 3 additions & 0 deletions ReactWindows/ReactNative/ReactNative.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
<Compile Include="UIManager\UIViewOperationQueue.cs" />
<Compile Include="Views\Flip\ReactFlipViewManager.cs" />
<Compile Include="Views\Image\ReactImageManager.cs" />
<Compile Include="Views\Modal\ReactModalHostView.cs" />
<Compile Include="Views\Modal\ReactModalShadowNode.cs" />
<Compile Include="Views\Modal\ReactModalViewManager.cs" />
<Compile Include="Views\Progress\ReactProgressBarViewManager.cs" />
<Compile Include="Views\Progress\ProgressBarShadowNode.cs" />
<Compile Include="Views\Progress\ReactProgressRingManager.cs" />
Expand Down
2 changes: 2 additions & 0 deletions ReactWindows/ReactNative/Shell/MainReactPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using ReactNative.Views.ControlView;
using ReactNative.Views.Flip;
using ReactNative.Views.Image;
using ReactNative.Views.Modal;
using ReactNative.Views.Picker;
using ReactNative.Views.Progress;
using ReactNative.Views.Scroll;
Expand Down Expand Up @@ -86,6 +87,7 @@ public IReadOnlyList<IViewManager> CreateViewManagers(
new ReactSimpleTextViewManager(),
new ReactFlipViewManager(),
new ReactImageManager(),
new ReactModalViewManager(),
new ReactProgressBarViewManager(),
new ReactProgressRingViewManager(),
new ReactPickerManager(),
Expand Down
249 changes: 249 additions & 0 deletions ReactWindows/ReactNative/Views/Modal/ReactModalHostView.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using Windows.Graphics.Display;
using Windows.System;
using Windows.UI;
using Windows.UI.Core;
using Windows.UI.ViewManagement;
using ReactNative.Accessibility;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Automation.Peers;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using ReactNative.Modules.Core;
using ReactNative.Touch;
using ReactNative.UIManager;

namespace ReactNative.Views.Modal
{
/// <summary>
/// A native control with a single ContentDialog child.
/// </summary>
public class ReactModalHostView : FrameworkElement, IAccessible
{
private ContentDialog _contentDialog;

private bool _isLoaded;

private bool _isClosing;

private int _resizeCount;

private TouchHandler _touchHandler;

/// <inheritdoc />
public AccessibilityTrait[] AccessibilityTraits { get; set; }

/// <summary>
/// The current dialog content
/// </summary>
public DependencyObject Content
{
get
{
return _contentDialog.Content as DependencyObject;
}
set
{
if (value is FrameworkElement element)
{
if (value is BorderedCanvas canvas)
canvas.Background = new SolidColorBrush(Colors.Transparent);

_touchHandler = new TouchHandler(element);
}

_contentDialog.Content = value;
}
}

/// <summary>
/// If the modal will render over a transparent background
/// </summary>
public bool Transparent { get; set; }

/// <summary>
/// Called when the user taps the hardware back button
/// </summary>
public event Action<ReactModalHostView> OnRequestCloseListener;

/// <summary>
/// Called once the modal has been shown
/// </summary>
public event Action<ReactModalHostView> OnShowListener;

/// <summary>
/// Instantiates the <see cref="ReactModalHostView"/>.
/// </summary>
public ReactModalHostView()
{
Loaded += (sender, args) =>
{
_isLoaded = true;
Show();
};
}

/// <summary>
/// Shows the dialog if not visible otherwise updates its properties
/// </summary>
public void ShowOrUpdate()
{
if (this._contentDialog != null)
{
UpdateProperties();
return;
}

_contentDialog = new ContentDialog
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
Resources =
{
["ContentDialogBorderWidth"] = new Thickness(0),
["ContentDialogContentMargin"] = new Thickness(0),
["ContentDialogContentScrollViewerMargin"] = new Thickness(0),
["ContentDialogMaxHeight"] = double.NaN,
["ContentDialogMaxWidth"] = double.NaN,
["ContentDialogMinHeight"] = 0.0,
["ContentDialogMinWidth"] = 0.0,
["ContentDialogPadding"] = new Thickness(0)
},
};

_contentDialog.Opened += (sender, e) =>
{
OnShowListener?.Invoke(this);
};

_contentDialog.SizeChanged += (sender, args) =>
{
SetContentSize();
};

_contentDialog.Closing += (sender, e) =>
{
if (_isClosing)
{
_isClosing = false;
}
else
{
e.Cancel = true;
OnRequestCloseListener?.Invoke(this);
}
};

UpdateProperties();

if (_isLoaded)
{
Show();
}
}

/// <summary>
/// Closes the dialog
/// </summary>
public void Close()
{
_isClosing = true;
_contentDialog.Hide();
_touchHandler.Dispose();
Window.Current.CoreWindow.KeyDown -= CoreWindowOnKeyDown;
SystemNavigationManager.GetForCurrentView().BackRequested -= OnBackRequested;
DisplayInformation.GetForCurrentView().OrientationChanged -= OnOrientationChanged;
var inputPane = InputPane.GetForCurrentView();
inputPane.Hiding -= InputPaneOnHidingOrShowing;
inputPane.Showing -= InputPaneOnHidingOrShowing;
this.GetReactContext().GetNativeModule<ExceptionsManagerModule>().BeforeShowDevOptionsDialog -= Close;
}

/// <inheritdoc />
protected override AutomationPeer OnCreateAutomationPeer()
{
return new DynamicAutomationPeer<ReactModalHostView>(this);
}

private void Show()
{
_contentDialog.ShowAsync().GetResults();

Window.Current.CoreWindow.KeyDown += CoreWindowOnKeyDown;
SystemNavigationManager.GetForCurrentView().BackRequested += OnBackRequested;
DisplayInformation.GetForCurrentView().OrientationChanged += OnOrientationChanged;
var inputPane = InputPane.GetForCurrentView();
inputPane.Hiding += InputPaneOnHidingOrShowing;
inputPane.Showing += InputPaneOnHidingOrShowing;
this.GetReactContext().GetNativeModule<ExceptionsManagerModule>().BeforeShowDevOptionsDialog += Close;
}

private void CoreWindowOnKeyDown(CoreWindow sender, KeyEventArgs e)
{
if (e.VirtualKey == VirtualKey.Escape && !e.Handled)
{
_contentDialog.Hide();
}
}

private async void InputPaneOnHidingOrShowing(InputPane sender, InputPaneVisibilityEventArgs args)
{
// Delay until the input pane has finished hiding or showing
await Dispatcher.RunAsync(CoreDispatcherPriority.Low, () =>
{
SetContentSize();
});
}

private void OnBackRequested(object sender, BackRequestedEventArgs e)
{
e.Handled = true;
_contentDialog.Hide();
}

private void OnOrientationChanged(DisplayInformation sender, object args)
{
SetContentSize();
}

/// <summary>
/// Size the dialog content to fill the entire screen
/// </summary>
private void SetContentSize()
{
var uiManagerModule = this.GetReactContext().GetNativeModule<UIManagerModule>();
var contentTag = Content.GetTag();
var currentCount = ++_resizeCount;

var contentSize = ApplicationView.GetForCurrentView().VisibleBounds;
var inputPane = InputPane.GetForCurrentView();

// Windows phone has resize issues if you make the modal less then half
// the screen size and then rotate the phone
if (inputPane.OccludedRect.Height < contentSize.Height / 2)
contentSize.Height -= inputPane.OccludedRect.Height;

uiManagerModule.ActionQueue.Dispatch(() =>
{
// If multiple events have been dispatched, ignore all but the newest
if (currentCount == _resizeCount)
{
// The modal content isn't a root node, but behaves similar to one
// in that it fills the entire window
uiManagerModule.UIImplementation.UpdateRootNodeSize(contentTag, contentSize.Width, contentSize.Height);
}
});
}

private void UpdateProperties()
{
if (Transparent)
{
_contentDialog.Background = new SolidColorBrush(Colors.Transparent);
}
}
}
}
38 changes: 38 additions & 0 deletions ReactWindows/ReactNative/Views/Modal/ReactModalShadowNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using ReactNative.UIManager;

namespace ReactNative.Views.Modal
{
/// <summary>
/// The shadow node implementation for modal views.
/// </summary>
public class ReactModalShadowNode : LayoutShadowNode
{
/// <summary>
/// Instantiates the <see cref="ReactModalShadowNode"/>.
/// </summary>
public ReactModalShadowNode()
{
}

/// <summary>
/// Insert a child at the given index.
/// </summary>
/// <param name="child">The child.</param>
/// <param name="index">The index.</param>
public override void AddChildAt(ReactShadowNode child, int index)
{
base.AddChildAt(child, index);

// Fixes a issue on Windows phone where rotating from horizontal to
// vertical would leave a gap between the modal bottom and the screen edge.
// This is set to the proper screen height when loading the modal in
// ReactModalHostView.SetContentSize. The actual screen height cannot be
// set here due to it being run by the layout manager thread which has no
// active window and only mimics the main view bounds.
child.StyleHeight = int.MaxValue;
}
}
}
Loading

0 comments on commit 9614b68

Please sign in to comment.