diff --git a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs index ecd58db7cae4..72f56506df04 100644 --- a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs +++ b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs @@ -69,6 +69,18 @@ internal static IMauiHandlersCollection AddControlsHandlers(this IMauiHandlersCo #else handlersCollection.AddHandler(); handlersCollection.AddHandler(); +#endif +#if ANDROID + if (RuntimeFeature.IsMaterial3Enabled) + { + handlersCollection.AddHandler(); + } + else + { + handlersCollection.AddHandler(); + } +#else + handlersCollection.AddHandler(); #endif handlersCollection.AddHandler(); handlersCollection.AddHandler(); @@ -87,7 +99,6 @@ internal static IMauiHandlersCollection AddControlsHandlers(this IMauiHandlersCo handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); - handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); if (RuntimeFeature.IsHybridWebViewSupported) diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_InitialState_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_InitialState_VerifyVisualState.png new file mode 100644 index 000000000000..2a1523066c5f Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_InitialState_VerifyVisualState.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetCulture_arEG_VerifyTimeFormat.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetCulture_arEG_VerifyTimeFormat.png new file mode 100644 index 000000000000..4b7e61051927 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetCulture_arEG_VerifyTimeFormat.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetCulture_enUS_VerifyTimeFormat.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetCulture_enUS_VerifyTimeFormat.png new file mode 100644 index 000000000000..86c0d8d887c3 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetCulture_enUS_VerifyTimeFormat.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetCulture_jaJP_VerifyTimeFormat.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetCulture_jaJP_VerifyTimeFormat.png new file mode 100644 index 000000000000..1b7457a95f91 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetCulture_jaJP_VerifyTimeFormat.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontAttributesAndFontFamily_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontAttributesAndFontFamily_VerifyVisualState.png new file mode 100644 index 000000000000..45fece76f20d Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontAttributesAndFontFamily_VerifyVisualState.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontAttributesAndFontSize_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontAttributesAndFontSize_VerifyVisualState.png new file mode 100644 index 000000000000..7294c7caa88d Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontAttributesAndFontSize_VerifyVisualState.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontAttributesAndFormat_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontAttributesAndFormat_VerifyVisualState.png new file mode 100644 index 000000000000..18d317ae38e6 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontAttributesAndFormat_VerifyVisualState.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontFamilyAndFontSize_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontFamilyAndFontSize_VerifyVisualState.png new file mode 100644 index 000000000000..f290f4f5a940 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontFamilyAndFontSize_VerifyVisualState.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontFamilyAndFormat_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontFamilyAndFormat_VerifyVisualState.png new file mode 100644 index 000000000000..13e22f5fae71 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontFamilyAndFormat_VerifyVisualState.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontSizeAndFormat_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontSizeAndFormat_VerifyVisualState.png new file mode 100644 index 000000000000..a0148cc0aa1f Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFontSizeAndFormat_VerifyVisualState.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFormatTAndTime_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFormatTAndTime_VerifyVisualState.png new file mode 100644 index 000000000000..1d1c74255d05 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFormatTAndTime_VerifyVisualState.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFormat_T_WithFontAttributes_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFormat_T_WithFontAttributes_VerifyVisualState.png new file mode 100644 index 000000000000..dd5154dfb086 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFormat_T_WithFontAttributes_VerifyVisualState.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFormat_T_WithFontFamily_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFormat_T_WithFontFamily_VerifyVisualState.png new file mode 100644 index 000000000000..4dbde6f082b7 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFormat_T_WithFontFamily_VerifyVisualState.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFormat_T_WithFontSize_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFormat_T_WithFontSize_VerifyVisualState.png new file mode 100644 index 000000000000..352e1b05f725 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFormat_T_WithFontSize_VerifyVisualState.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFormat_t_AndTime_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFormat_t_AndTime_VerifyVisualState.png new file mode 100644 index 000000000000..9dc7ba9d73c6 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetFormat_t_AndTime_VerifyVisualState.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetShadow_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetShadow_VerifyVisualState.png new file mode 100644 index 000000000000..4dd423631a84 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetShadow_VerifyVisualState.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetTimeAndCharacterSpacing_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetTimeAndCharacterSpacing_VerifyVisualState.png new file mode 100644 index 000000000000..5c2dbdabf03c Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetTimeAndCharacterSpacing_VerifyVisualState.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetTimeAndIsEnabled_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetTimeAndIsEnabled_VerifyVisualState.png new file mode 100644 index 000000000000..c608057a789d Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetTimeAndIsEnabled_VerifyVisualState.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetTimeAndTextColor_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetTimeAndTextColor_VerifyVisualState.png new file mode 100644 index 000000000000..4f559cf7606c Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android-notch-36/Material3TimePicker_SetTimeAndTextColor_VerifyVisualState.png differ diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/Material3TimePickerFeatureTests.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/Material3TimePickerFeatureTests.cs new file mode 100644 index 000000000000..14fefcf2b4b5 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/Material3TimePickerFeatureTests.cs @@ -0,0 +1,400 @@ +// Material3 TimePicker tests reuse the existing TimePicker Feature Matrix HostApp page. +// The native Android TimePicker uses Material3 styling (MauiMaterialTimePicker) when Material3 is enabled, +// so these tests produce separate screenshot baselines under the Material3 category. +#if ANDROID +using System; +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests; + +public class Material3TimePickerFeatureTests : _GalleryUITest +{ + public override string GalleryPageName => "Time Picker Feature Matrix"; + + public Material3TimePickerFeatureTests(TestDevice device) + : base(device) + { + } + + [Test, Order(1)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_InitialState_VerifyVisualState() + { + App.WaitForElement("TimePickerControl"); + App.Tap("TimePickerControl"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(2)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetTimeAndCharacterSpacing_VerifyVisualState() + { + App.WaitForElement("OK"); + App.Tap("OK"); + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("CharacterSpacingEntry"); + App.ClearText("CharacterSpacingEntry"); + App.EnterText("CharacterSpacingEntry", "5"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + +#if TEST_FAILS_ON_ANDROID // Issue Link - https://github.com/dotnet/maui/issues/30192 + + [Test, Order(3)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetFlowDirectionAndTime_VerifyVisualState() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("FlowDirectionRTL"); + App.Tap("FlowDirectionRTL"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } +#endif + + [Test, Order(4)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetTimeAndTextColor_VerifyVisualState() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("TextColorGreenButton"); + App.Tap("TextColorGreenButton"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(5)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetFontAttributesAndFontFamily_VerifyVisualState() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("FontAttributesItalicButton"); + App.Tap("FontAttributesItalicButton"); + App.WaitForElement("FontFamilyDokdoButton"); + App.Tap("FontFamilyDokdoButton"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(6)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetFontAttributesAndFontSize_VerifyVisualState() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("FontAttributesItalicButton"); + App.Tap("FontAttributesItalicButton"); + App.WaitForElement("FontSizeEntry"); + App.ClearText("FontSizeEntry"); + App.EnterText("FontSizeEntry", "20"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(7)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetFontAttributesAndFormat_VerifyVisualState() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("FontAttributesItalicButton"); + App.Tap("FontAttributesItalicButton"); + App.WaitForElement("FormatEntry"); + App.ClearText("FormatEntry"); + App.EnterText("FormatEntry", "HH:mm"); + App.WaitForElement("SetFormatButton"); + App.Tap("SetFormatButton"); + App.WaitForElement("TimeEntry"); + App.ClearText("TimeEntry"); + App.EnterText("TimeEntry", "17:00"); + App.WaitForElement("SetTimeButton"); + App.Tap("SetTimeButton"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + App.WaitForElement("CultureFormatLabel"); + App.Tap("CultureFormatLabel"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(8)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetFontFamilyAndFontSize_VerifyVisualState() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("FontFamilyDokdoButton"); + App.Tap("FontFamilyDokdoButton"); + App.WaitForElement("FontSizeEntry"); + App.ClearText("FontSizeEntry"); + App.EnterText("FontSizeEntry", "20"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(9)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetFontFamilyAndFormat_VerifyVisualState() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("FontFamilyDokdoButton"); + App.Tap("FontFamilyDokdoButton"); + App.WaitForElement("FormatEntry"); + App.ClearText("FormatEntry"); + App.EnterText("FormatEntry", "HH:mm"); + App.WaitForElement("SetFormatButton"); + App.Tap("SetFormatButton"); + App.WaitForElement("TimeEntry"); + App.ClearText("TimeEntry"); + App.EnterText("TimeEntry", "17:00"); + App.WaitForElement("SetTimeButton"); + App.Tap("SetTimeButton"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + App.WaitForElement("CultureFormatLabel"); + App.Tap("CultureFormatLabel"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(10)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetFontSizeAndFormat_VerifyVisualState() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("FontSizeEntry"); + App.ClearText("FontSizeEntry"); + App.EnterText("FontSizeEntry", "20"); + App.WaitForElement("FormatEntry"); + App.ClearText("FormatEntry"); + App.EnterText("FormatEntry", "hh:mm"); + App.WaitForElement("SetFormatButton"); + App.Tap("SetFormatButton"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + App.WaitForElement("CultureFormatLabel"); + App.Tap("CultureFormatLabel"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(11)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetTimeAndIsEnabled_VerifyVisualState() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("IsEnabledFalseButton"); + App.Tap("IsEnabledFalseButton"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + App.Tap("TimePickerControl"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(12)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetTimeAndIsVisible_VerifyVisualState() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("IsVisibleFalseButton"); + App.Tap("IsVisibleFalseButton"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForNoElement("TimePickerControl"); + } + + [Test, Order(13)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetShadow_VerifyVisualState() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("ShadowTrueButton"); + App.Tap("ShadowTrueButton"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(14)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetFormat_t_AndTime_VerifyVisualState() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("FormatEntry"); + App.ClearText("FormatEntry"); + App.EnterText("FormatEntry", "t"); + App.WaitForElement("SetFormatButton"); + App.Tap("SetFormatButton"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(15)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetFormatTAndTime_VerifyVisualState() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("FormatEntry"); + App.ClearText("FormatEntry"); + App.EnterText("FormatEntry", "T"); + App.WaitForElement("SetFormatButton"); + App.Tap("SetFormatButton"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(16)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetFormat_T_WithFontAttributes_VerifyVisualState() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("FormatEntry"); + App.ClearText("FormatEntry"); + App.EnterText("FormatEntry", "T"); + App.WaitForElement("SetFormatButton"); + App.Tap("SetFormatButton"); + App.WaitForElement("FontAttributesItalicButton"); + App.Tap("FontAttributesItalicButton"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(17)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetFormat_T_WithFontFamily_VerifyVisualState() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("FormatEntry"); + App.ClearText("FormatEntry"); + App.EnterText("FormatEntry", "T"); + App.WaitForElement("SetFormatButton"); + App.Tap("SetFormatButton"); + App.WaitForElement("FontFamilyDokdoButton"); + App.Tap("FontFamilyDokdoButton"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(18)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetFormat_T_WithFontSize_VerifyVisualState() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("FormatEntry"); + App.ClearText("FormatEntry"); + App.EnterText("FormatEntry", "T"); + App.WaitForElement("SetFormatButton"); + App.Tap("SetFormatButton"); + App.WaitForElement("FontSizeEntry"); + App.ClearText("FontSizeEntry"); + App.EnterText("FontSizeEntry", "20"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + App.WaitForElement("CultureFormatLabel"); + App.Tap("CultureFormatLabel"); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(19)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetCulture_enUS_VerifyTimeFormat() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("CultureUSButton"); + App.Tap("CultureUSButton"); + App.WaitForElement("TimeEntry"); + App.ClearText("TimeEntry"); + App.EnterText("TimeEntry", "5:30"); + App.WaitForElement("SetTimeButton"); + App.Tap("SetTimeButton"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + var cultureFormatText = App.WaitForElement("CultureFormatLabel").GetText(); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(20)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetCulture_arEG_VerifyTimeFormat() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("CultureEGButton"); + App.Tap("CultureEGButton"); + App.WaitForElement("TimeEntry"); + App.ClearText("TimeEntry"); + App.EnterText("TimeEntry", "11:30"); + App.WaitForElement("SetTimeButton"); + App.Tap("SetTimeButton"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + var cultureFormatText = App.WaitForElement("CultureFormatLabel").GetText(); + Assert.That(cultureFormatText, Is.EqualTo("Culture: ar-EG, Time: 11:30 ص")); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } + + [Test, Order(21)] + [Category(UITestCategories.Material3)] + public void Material3TimePicker_SetCulture_jaJP_VerifyTimeFormat() + { + App.WaitForElement("Options"); + App.Tap("Options"); + App.WaitForElement("CultureJPButton"); + App.Tap("CultureJPButton"); + App.WaitForElement("TimeEntry"); + App.ClearText("TimeEntry"); + App.EnterText("TimeEntry", "17:30"); + App.WaitForElement("SetTimeButton"); + App.Tap("SetTimeButton"); + App.WaitForElement("Apply"); + App.Tap("Apply"); + App.WaitForElementTillPageNavigationSettled("TimePickerControl"); + var cultureFormatText = App.WaitForElement("CultureFormatLabel").GetText(); + Assert.That(cultureFormatText, Is.EqualTo("Culture: ja-JP, Time: 17:30")); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); + } +} +#endif diff --git a/src/Core/src/Handlers/TimePicker/TimePickerHandler2.Android.cs b/src/Core/src/Handlers/TimePicker/TimePickerHandler2.Android.cs new file mode 100644 index 000000000000..77182cf0a4da --- /dev/null +++ b/src/Core/src/Handlers/TimePicker/TimePickerHandler2.Android.cs @@ -0,0 +1,287 @@ +using System; +using Android.Content; +using Android.Text.Format; +using Android.Views; +using AndroidX.Fragment.App; +using Google.Android.Material.TimePicker; + +namespace Microsoft.Maui.Handlers; + +// TODO: Material3: Make it public in .NET 11 +internal partial class TimePickerHandler2 : ViewHandler +{ + internal MaterialTimePicker? _dialog; + internal bool _isUpdatingIsOpen; + internal MaterialTimePickerPositiveButtonClickListener? _positiveButtonClickListener; + internal MaterialTimePickerDismissListener? _dismissListener; + + public static PropertyMapper Mapper = + new(ViewMapper) + { + [nameof(ITimePicker.Background)] = MapBackground, + [nameof(ITimePicker.CharacterSpacing)] = MapCharacterSpacing, + [nameof(ITimePicker.Font)] = MapFont, + [nameof(ITimePicker.Format)] = MapFormat, + [nameof(ITimePicker.TextColor)] = MapTextColor, + [nameof(ITimePicker.Time)] = MapTime, + [nameof(ITimePicker.IsOpen)] = MapIsOpen, + }; + + public static CommandMapper CommandMapper = new(ViewCommandMapper) + { + }; + + public TimePickerHandler2() : base(Mapper, CommandMapper) + { + } + + protected override void ConnectHandler(MauiMaterialTimePicker platformView) + { + base.ConnectHandler(platformView); + + _positiveButtonClickListener = new MaterialTimePickerPositiveButtonClickListener(this); + _dismissListener = new MaterialTimePickerDismissListener(this); + + platformView.ShowPicker = ShowPickerDialog; + platformView.HidePicker = HidePickerDialog; + } + + protected override void DisconnectHandler(MauiMaterialTimePicker platformView) + { + if (_dialog is not null) + { + RemoveListeners(); + + if (_dialog.IsAdded) + { + _dialog.DismissAllowingStateLoss(); + } + + _dialog = null; + } + + _positiveButtonClickListener?.Dispose(); + _positiveButtonClickListener = null; + _dismissListener?.Dispose(); + _dismissListener = null; + + platformView.ShowPicker = null; + platformView.HidePicker = null; + + base.DisconnectHandler(platformView); + } + + void RemoveListeners() + { + if (_dialog is not null) + { + if (_dismissListener is not null) + { + _dialog.RemoveOnDismissListener(_dismissListener); + } + if (_positiveButtonClickListener is not null) + { + _dialog.RemoveOnPositiveButtonClickListener(_positiveButtonClickListener); + } + } + } + + + internal void HidePickerDialog() + { + if (_dialog is null) + { + UpdateIsOpenState(false); + return; + } + + RemoveListeners(); + + if (_dialog.IsAdded) + { + _dialog.DismissAllowingStateLoss(); + } + + _dialog = null; + UpdateIsOpenState(false); + } + + void ShowPickerDialog() + { + if (VirtualView is null) + { + return; + } + + ShowPickerDialog(VirtualView.Time); + } + + void ShowPickerDialog(TimeSpan? time) + { + // Get FragmentActivity - MaterialTimePicker requires AndroidX FragmentManager + if (Context?.GetActivity() is not FragmentActivity fragmentActivity || + fragmentActivity.IsDestroyed || + fragmentActivity.IsFinishing) + { + return; + } + + var fragmentManager = fragmentActivity.SupportFragmentManager; + if (fragmentManager is null) + { + return; + } + + // Prevent duplicate dialogs + if (_dialog is not null && (_dialog.IsVisible || _dialog.IsAdded)) + { + return; + } + + var hour = time?.Hours ?? 0; + var minute = time?.Minutes ?? 0; + + _dialog = CreateTimePickerDialog(hour, minute); + if (_dialog is null) + { + return; + } + + _dialog.Show(fragmentManager, "MaterialTimePicker"); + + UpdateIsOpenState(true); + } + + protected virtual MaterialTimePicker? CreateTimePickerDialog(int hour, int minute) + { + var dialog = new MaterialTimePicker.Builder() + .SetHour(hour) + .SetMinute(minute) + .SetTimeFormat(Use24HourView ? TimeFormat.Clock24h : TimeFormat.Clock12h) + .SetInputMode(MaterialTimePicker.InputModeClock) // Dial/Clock face mode + .Build(); + + if (_positiveButtonClickListener is not null && _dismissListener is not null) + { + dialog?.AddOnPositiveButtonClickListener(_positiveButtonClickListener); + dialog?.AddOnDismissListener(_dismissListener); + } + + return dialog; + } + + public static void MapBackground(TimePickerHandler2 handler, ITimePicker timePicker) + { + handler.PlatformView?.UpdateBackground(timePicker); + } + + public static void MapIsOpen(TimePickerHandler2 handler, ITimePicker picker) + { + if (handler.IsConnected() && !handler._isUpdatingIsOpen) + { + if (picker.IsOpen) + { + handler.ShowPickerDialog(); + } + else + { + handler.HidePickerDialog(); + } + } + } + + public static void MapTime(TimePickerHandler2 handler, ITimePicker picker) + { + handler.PlatformView?.UpdateTime(picker); + } + + public static void MapTextColor(TimePickerHandler2 handler, ITimePicker picker) + { + handler.PlatformView?.UpdateTextColor(picker); + } + + public static void MapFormat(TimePickerHandler2 handler, ITimePicker picker) + { + handler.PlatformView?.UpdateFormat(picker); + } + + public static void MapFont(TimePickerHandler2 handler, ITimePicker picker) + { + var fontManager = handler.GetRequiredService(); + + handler.PlatformView?.UpdateFont(picker, fontManager); + } + + public static void MapCharacterSpacing(TimePickerHandler2 handler, ITimePicker picker) + { + handler.PlatformView?.UpdateCharacterSpacing(picker); + } + + protected override MauiMaterialTimePicker CreatePlatformView() + { + return new MauiMaterialTimePicker(Context); + } + + internal void UpdateIsOpenState(bool isOpen) + { + if (VirtualView is null || _isUpdatingIsOpen) + { + return; + } + + _isUpdatingIsOpen = true; + VirtualView.IsOpen = isOpen; + _isUpdatingIsOpen = false; + } + + bool Use24HourView => VirtualView is not null && (DateFormat.Is24HourFormat(PlatformView?.Context) + && VirtualView.Format == "t" || VirtualView.Format == "HH:mm"); +} + +internal class MaterialTimePickerPositiveButtonClickListener : Java.Lang.Object, View.IOnClickListener +{ + readonly WeakReference _handler; + + public MaterialTimePickerPositiveButtonClickListener(TimePickerHandler2 handler) + { + _handler = new WeakReference(handler); + } + + public void OnClick(View? v) + { + if (!_handler.TryGetTarget(out var handler) || handler.VirtualView is null || handler._dialog is null) + { + return; + } + + handler.VirtualView.Time = new TimeSpan(handler._dialog.Hour, handler._dialog.Minute, 0); + handler.VirtualView.IsFocused = false; + + // HidePickerDialog removes all listeners and dismisses properly + handler.HidePickerDialog(); + } +} + +internal class MaterialTimePickerDismissListener : Java.Lang.Object, IDialogInterfaceOnDismissListener +{ + readonly WeakReference _handler; + + public MaterialTimePickerDismissListener(TimePickerHandler2 handler) + { + _handler = new WeakReference(handler); + } + + public void OnDismiss(IDialogInterface? dialog) + { + if (!_handler.TryGetTarget(out var handler)) + { + return; + } + + // Dialog was dismissed (back button, outside tap, cancel button, etc.) + // Clean up without trying to dismiss again + handler._dialog = null; + + handler.UpdateIsOpenState(false); + } +} \ No newline at end of file diff --git a/src/Core/src/Platform/Android/MauiMaterialTimePicker.cs b/src/Core/src/Platform/Android/MauiMaterialTimePicker.cs new file mode 100644 index 000000000000..315a0b2a3833 --- /dev/null +++ b/src/Core/src/Platform/Android/MauiMaterialTimePicker.cs @@ -0,0 +1,58 @@ +using System; +using Android.Content; +using Android.Runtime; +using Android.Text.Method; +using Android.Util; +using Android.Views; +using AndroidX.Core.Graphics.Drawable; +using Google.Android.Material.TextField; +using static Android.Views.View; + +namespace Microsoft.Maui.Platform; + +// TODO: material3 - make it public in .net 11 +internal class MauiMaterialTimePicker : TextInputEditText, IOnClickListener +{ + public MauiMaterialTimePicker(Context context) : base(MauiMaterialContextThemeWrapper.Create(context)) + { + Initialize(); + } + + public MauiMaterialTimePicker(Context context, IAttributeSet? attrs) : base(MauiMaterialContextThemeWrapper.Create(context), attrs) + { + Initialize(); + } + + public MauiMaterialTimePicker(Context context, IAttributeSet? attrs, int defStyleAttr) : base(MauiMaterialContextThemeWrapper.Create(context), attrs, defStyleAttr) + { + Initialize(); + } + + protected MauiMaterialTimePicker(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + // MovementMethod handles cursor positioning, scrolling, and text selection (per Android docs). + // Since text is readonly, we disable it to avoid unnecessary cursor navigation during keyboard input. + protected override IMovementMethod? DefaultMovementMethod => null; + + public Action? ShowPicker { get; set; } + public Action? HidePicker { get; set; } + + public void OnClick(View? v) + { + ShowPicker?.Invoke(); + } + + void Initialize() + { + if (Background is not null) + { + DrawableCompat.Wrap(Background); + } + + PickerManager.Init(this); + + SetOnClickListener(this); + } +} diff --git a/src/Core/src/Platform/Android/TimePickerExtensions.cs b/src/Core/src/Platform/Android/TimePickerExtensions.cs index 096c16f125dd..e6917fe89280 100644 --- a/src/Core/src/Platform/Android/TimePickerExtensions.cs +++ b/src/Core/src/Platform/Android/TimePickerExtensions.cs @@ -1,29 +1,47 @@ using System; using Android.Content.Res; +using AndroidX.AppCompat.Widget; namespace Microsoft.Maui.Platform; public static class TimePickerExtensions { public static void UpdateFormat(this MauiTimePicker mauiTimePicker, ITimePicker timePicker) - { - mauiTimePicker.SetTime(timePicker); - } + => SetTimeImpl(mauiTimePicker, timePicker); + + // TODO: Material3: Make it public in .NET 11 + internal static void UpdateFormat(this MauiMaterialTimePicker mauiTimePicker, ITimePicker timePicker) + => SetTimeImpl(mauiTimePicker, timePicker); public static void UpdateTime(this MauiTimePicker mauiTimePicker, ITimePicker timePicker) - { - mauiTimePicker.SetTime(timePicker); - } + => SetTimeImpl(mauiTimePicker, timePicker); + + // TODO: Material3: Make it public in .NET 11 + internal static void UpdateTime(this MauiMaterialTimePicker mauiTimePicker, ITimePicker timePicker) + => SetTimeImpl(mauiTimePicker, timePicker); internal static void SetTime(this MauiTimePicker mauiTimePicker, ITimePicker timePicker) + => SetTimeImpl(mauiTimePicker, timePicker); + + internal static void SetTime(this MauiMaterialTimePicker mauiTimePicker, ITimePicker timePicker) + => SetTimeImpl(mauiTimePicker, timePicker); + + public static void UpdateTextColor(this MauiTimePicker platformTimePicker, ITimePicker timePicker) + => UpdateTextColorImpl(platformTimePicker, timePicker); + + // TODO: Material3: Make it public in .NET 11 + internal static void UpdateTextColor(this MauiMaterialTimePicker platformTimePicker, ITimePicker timePicker) + => UpdateTextColorImpl(platformTimePicker, timePicker); + + static void SetTimeImpl(AppCompatEditText editText, ITimePicker timePicker) { var time = timePicker.Time; var format = timePicker.Format; - mauiTimePicker.Text = time?.ToFormattedString(format); + editText.Text = time?.ToFormattedString(format); } - public static void UpdateTextColor(this MauiTimePicker platformTimePicker, ITimePicker timePicker) + static void UpdateTextColorImpl(AppCompatEditText platformTimePicker, ITimePicker timePicker) { var textColor = timePicker.TextColor;