-
Notifications
You must be signed in to change notification settings - Fork 495
Add SoftInputKeyboardPopup and integrate keyboard handling for popups #3117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
d8323fb
512d9c9
8beb2d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| <?xml version="1.0" encoding="utf-8" ?> | ||
| <mct:Popup xmlns="http://schemas.microsoft.com/dotnet/2021/maui" | ||
| xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | ||
| xmlns:mct="http://schemas.microsoft.com/dotnet/2022/maui/toolkit" | ||
| x:Class="CommunityToolkit.Maui.Sample.Views.Popups.SoftInputKeyboardPopup" | ||
| CanBeDismissedByTappingOutsideOfPopup="True"> | ||
|
|
||
| <VerticalStackLayout Spacing="12"> | ||
|
|
||
| <Label | ||
| Text="Keyboard for Entry" | ||
| FontSize="18" | ||
| HorizontalTextAlignment="Center" | ||
| VerticalTextAlignment="Center" | ||
| VerticalOptions="Center" | ||
| HorizontalOptions="Center" | ||
| FontAttributes="Bold" /> | ||
|
|
||
| <Entry | ||
| Placeholder="Click here to enter text..." | ||
| TextColor="Black" | ||
| HorizontalTextAlignment="Start" | ||
| VerticalTextAlignment="Center" | ||
| VerticalOptions="Center" | ||
| HorizontalOptions="Center" /> | ||
|
|
||
| <Label | ||
| x:Name="PickerForIOSLabel" | ||
| Text="Keyboard for Picker (iOS only)" | ||
| FontSize="18" | ||
| HorizontalTextAlignment="Center" | ||
| VerticalTextAlignment="Center" | ||
| VerticalOptions="Center" | ||
| HorizontalOptions="Center" | ||
| FontAttributes="Bold" | ||
| IsVisible="False" /> | ||
|
|
||
| <Picker | ||
| x:Name="PickerForIOS" | ||
| Title="Select a monkey" | ||
| HorizontalTextAlignment="Center" | ||
| VerticalTextAlignment="Center" | ||
| VerticalOptions="Center" | ||
| HorizontalOptions="Center" | ||
| IsVisible="False"> | ||
| <Picker.ItemsSource> | ||
| <x:Array Type="{x:Type x:String}"> | ||
| <x:String>Baboon</x:String> | ||
| <x:String>Capuchin Monkey</x:String> | ||
| <x:String>Blue Monkey</x:String> | ||
| <x:String>Squirrel Monkey</x:String> | ||
| <x:String>Golden Lion Tamarin</x:String> | ||
| <x:String>Howler Monkey</x:String> | ||
| <x:String>Japanese Macaque</x:String> | ||
| </x:Array> | ||
| </Picker.ItemsSource> | ||
| </Picker> | ||
|
|
||
| <Label | ||
| x:Name="DatePickerForIOSLabel" | ||
| Text="Keyboard for DatePicker (iOS only)" | ||
| FontSize="18" | ||
| HorizontalTextAlignment="Center" | ||
| VerticalTextAlignment="Center" | ||
| VerticalOptions="Center" | ||
| HorizontalOptions="Center" | ||
| FontAttributes="Bold" | ||
| IsVisible="False" /> | ||
|
|
||
| <DatePicker | ||
| x:Name="DatePickerForIOS" | ||
| MinimumDate="01/01/2020" | ||
| MaximumDate="12/31/2030" | ||
| Date="02/23/2026" | ||
| VerticalOptions="Center" | ||
| HorizontalOptions="Center" | ||
| IsVisible="False" /> | ||
|
|
||
| <Button | ||
| Text="Close" | ||
| HorizontalOptions="Center" | ||
| VerticalOptions="Center" | ||
| Clicked="ClosePopup" /> | ||
|
|
||
| </VerticalStackLayout> | ||
| </mct:Popup> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| using CommunityToolkit.Maui.Views; | ||
|
|
||
| namespace CommunityToolkit.Maui.Sample.Views.Popups; | ||
|
|
||
| public partial class SoftInputKeyboardPopup : Popup | ||
| { | ||
| public SoftInputKeyboardPopup() | ||
| { | ||
| InitializeComponent(); | ||
|
|
||
| #if IOS | ||
| PickerForIOSLabel.IsVisible = true; | ||
| PickerForIOS.IsVisible = true; | ||
| DatePickerForIOSLabel.IsVisible = true; | ||
| DatePickerForIOS.IsVisible = true; | ||
| #endif | ||
| } | ||
|
|
||
| async void ClosePopup(object? sender, EventArgs eventArgs) | ||
| { | ||
| await CloseAsync(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,11 @@ | ||
|
|
||
| using CommunityToolkit.Maui.Extensions; | ||
| #if ANDROID | ||
| using Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific; | ||
| #elif IOS && !NET10_0_OR_GREATER | ||
| using Foundation; | ||
| using UIKit; | ||
| #endif | ||
|
|
||
| namespace CommunityToolkit.Maui.Views; | ||
|
|
||
|
|
@@ -56,7 +62,7 @@ public Popup() | |
| /// </remarks> | ||
| [BindableProperty] | ||
| public partial bool CanBeDismissedByTappingOutsideOfPopup { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the padding between the <see cref="Popup"/> border and the <see cref="Popup"/> content. | ||
| /// </summary> | ||
|
|
@@ -71,14 +77,65 @@ public Popup() | |
| /// </summary> | ||
| public virtual Task CloseAsync(CancellationToken token = default) => GetPopupPage().CloseAsync(new PopupResult(false), token); | ||
|
|
||
| #if IOS && !NET10_0_OR_GREATER | ||
| /// <summary> | ||
| /// Stores the keyboard will show notification observer to manage keyboard lifecycle. | ||
| /// </summary> | ||
| NSObject? willShow; | ||
|
|
||
| /// <summary> | ||
| /// Stores the keyboard will hide notification observer to manage keyboard lifecycle. | ||
| /// </summary> | ||
| NSObject? willHide; | ||
|
|
||
| /// <summary> | ||
| /// Stores the native platform view to adjust safe area insets when keyboard appears. | ||
| /// </summary> | ||
| UIView? popupNativeView; | ||
|
|
||
| /// <summary> | ||
| /// Stores the view controller associated with the popup to adjust safe area insets. | ||
| /// </summary> | ||
| UIViewController? viewController; | ||
| #endif | ||
|
|
||
| internal void NotifyPopupIsOpened() | ||
| { | ||
| Opened?.Invoke(this, EventArgs.Empty); | ||
|
|
||
| #if ANDROID | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of using |
||
| // On Android, configure the window soft input mode to resize when the keyboard appears | ||
| Microsoft.Maui.Controls.Application.Current?.On<Microsoft.Maui.Controls.PlatformConfiguration.Android>().UseWindowSoftInputModeAdjust(WindowSoftInputModeAdjust.Resize); | ||
| #elif IOS && !NET10_0_OR_GREATER | ||
| // On iOS, store the native view and subscribe to keyboard events to adjust safe area insets | ||
| if (Handler?.PlatformView is UIView view) | ||
| { | ||
| popupNativeView = view; | ||
| } | ||
|
|
||
| willShow = UIKeyboard.Notifications.ObserveWillShow((_, args) => HandleKeyboard(args)); | ||
|
|
||
| willHide = UIKeyboard.Notifications.ObserveWillHide((_, args) => ResetSafeArea()); | ||
| #endif | ||
| } | ||
|
|
||
| internal void NotifyPopupIsClosed() | ||
| { | ||
| Closed?.Invoke(this, EventArgs.Empty); | ||
|
|
||
| #if ANDROID | ||
| // On Android, reset the window soft input mode to unspecified when the popup closes | ||
| Microsoft.Maui.Controls.Application.Current?.On<Microsoft.Maui.Controls.PlatformConfiguration.Android>().UseWindowSoftInputModeAdjust(WindowSoftInputModeAdjust.Unspecified); | ||
| #elif IOS && !NET10_0_OR_GREATER | ||
| // On iOS, dispose of keyboard event observers and clean up stored references | ||
| willShow?.Dispose(); | ||
| willHide?.Dispose(); | ||
|
|
||
| willShow = willHide = null; | ||
|
|
||
| popupNativeView = null; | ||
| viewController = null; | ||
| #endif | ||
| } | ||
|
|
||
| private protected PopupPage GetPopupPage() | ||
|
|
@@ -97,6 +154,42 @@ private protected PopupPage GetPopupPage() | |
|
|
||
| throw new PopupNotFoundException(); | ||
| } | ||
|
|
||
| #if IOS && !NET10_0_OR_GREATER | ||
| /// <summary> | ||
| /// Adjusts the safe area insets when the keyboard appears on iOS. | ||
| /// </summary> | ||
| /// <param name="args">The keyboard event arguments containing the keyboard frame.</param> | ||
| void HandleKeyboard(UIKeyboardEventArgs args) | ||
| { | ||
| if (popupNativeView is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Get the view controller associated with the popup's native view | ||
| viewController ??= popupNativeView.Window?.RootViewController?.PresentedViewController; | ||
|
|
||
| if (viewController is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Adjust the bottom safe area inset to account for keyboard height | ||
| viewController.AdditionalSafeAreaInsets = new UIEdgeInsets(0, 0, args.FrameEnd.Height, 0); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Resets the safe area insets when the keyboard is hidden on iOS. | ||
| /// </summary> | ||
| void ResetSafeArea() | ||
| { | ||
| if (viewController is not null) | ||
| { | ||
| viewController.AdditionalSafeAreaInsets = UIEdgeInsets.Zero; | ||
| } | ||
| } | ||
| #endif | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,6 +61,11 @@ public PopupPage(Popup popup, IPopupOptions? popupOptions) | |
| Shell.SetPresentationMode(this, PresentationMode.ModalNotAnimated); | ||
| On<iOS>().SetModalPresentationStyle(UIModalPresentationStyle.OverFullScreen); | ||
| NavigationPage.SetHasNavigationBar(this, false); | ||
|
|
||
| #if NET10_0_OR_GREATER | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can remove this |
||
| // On .NET 10.0 and greater, configure safe area edges to respect the keyboard and system UI while keeping the popup content within safe boundaries | ||
| this.SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container, SafeAreaRegions.Container, SafeAreaRegions.Container, SafeAreaRegions.SoftInput); | ||
| #endif | ||
| } | ||
|
|
||
| public event EventHandler<IPopupResult>? PopupClosed; | ||
|
|
@@ -221,7 +226,7 @@ public PopupPageLayout(in Popup popupContent, in IPopupOptions options, in Actio | |
| Content = popupContent | ||
| }; | ||
|
|
||
| // Bind `Popup` values through to Border using OneWay Bindings | ||
| // Bind `Popup` values through to Border using OneWay Bindings | ||
| PopupBorder.SetBinding(Border.MarginProperty, static (Popup popup) => popup.Margin, source: popupContent, mode: BindingMode.OneWay, converter: new MarginConverter()); | ||
| PopupBorder.SetBinding(Border.BackgroundProperty, static (Popup popup) => popup.Background, source: popupContent, mode: BindingMode.OneWay); | ||
| PopupBorder.SetBinding(Border.BackgroundColorProperty, static (Popup popup) => popup.BackgroundColor, source: popupContent, mode: BindingMode.OneWay, converter: new BackgroundColorConverter()); | ||
|
|
@@ -254,7 +259,7 @@ void HandleOverlayTapped(object? sender, TappedEventArgs e) | |
| return; | ||
| } | ||
|
|
||
| // Execute tapOutsideOfPopupCommand only if tap occurred outside the PopupBorder | ||
| // Execute tapOutsideOfPopupCommand only if tap occurred outside the PopupBorder | ||
| if (PopupBorder.Bounds.Contains(position.Value) is false) | ||
| { | ||
| tryExecuteTapOutsideOfPopupCommand(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can have back compatibility, this version will not install on NET 9 anyway. On toolkit we only support the current .NET version