Skip to content

Commit ceb3ebd

Browse files
Refactor PopupBox placement to respect configurable elevation (#3136)
* Add failing UI test * Fix Card clipping issue Fixes issue where Card.ContentClip was not correctly updated based on changes to the UniformCornerRadius. * Refactor PopupBox custom popup placement Custom placement (and Popup offsets) now adjusts according to the required space for the drop shadow * Add dedicated demo app page for PopupBox Useful for testing various scenarios/alignments and ensuring things work as expected. * Small cleanup done during review on stream * Change implementation to dedicated DP Also makes the demo app page a little more intuitive regarding content of the popup. * Fix PopupTest to use new dedicated DP * Moving converters inside of the tempalate --------- Co-authored-by: Kevin Bost <[email protected]>
1 parent c6540d6 commit ceb3ebd

File tree

9 files changed

+460
-41
lines changed

9 files changed

+460
-41
lines changed

MainDemo.Wpf/Domain/MainWindowViewModel.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,15 @@ private static IEnumerable<DemoItem> GenerateDemoItems(ISnackbarMessageQueue sna
428428
DocumentationLink.DemoPageLink<SmartHint>(),
429429
DocumentationLink.StyleLink("SmartHint"),
430430
});
431+
432+
yield return new DemoItem(
433+
"PopupBox",
434+
typeof(PopupBox),
435+
new[]
436+
{
437+
DocumentationLink.DemoPageLink<PopupBox>(),
438+
DocumentationLink.StyleLink("PopupBox"),
439+
});
431440
}
432441

433442
private bool DemoItemsFilter(object obj)

MainDemo.Wpf/PopupBox.xaml

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
<UserControl x:Class="MaterialDesignDemo.PopupBox"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
5+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
6+
xmlns:local="clr-namespace:MaterialDesignDemo"
7+
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
8+
xmlns:system="clr-namespace:System;assembly=System.Runtime"
9+
mc:Ignorable="d"
10+
d:DesignHeight="450" d:DesignWidth="800">
11+
<UserControl.Resources>
12+
<ResourceDictionary>
13+
<!-- This is needed to avoid runtime exceptions?! Seems like this might be a bug? -->
14+
<ResourceDictionary.MergedDictionaries>
15+
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.PopupBox.xaml" />
16+
</ResourceDictionary.MergedDictionaries>
17+
18+
<DataTemplate x:Key="ContentTemplateGrid">
19+
<Grid Width="200" Height="100" TextElement.Foreground="{DynamicResource MaterialDesignLightForeground}">
20+
<TextBlock Text="Popup content in grid" HorizontalAlignment="Center" VerticalAlignment="Center" />
21+
</Grid>
22+
</DataTemplate>
23+
24+
<DataTemplate x:Key="ContentTemplateGridWithBackground">
25+
<Grid Width="200" Height="100" Background="Fuchsia">
26+
<TextBlock Text="Popup content in colored grid" HorizontalAlignment="Center" VerticalAlignment="Center" />
27+
</Grid>
28+
</DataTemplate>
29+
30+
<DataTemplate x:Key="ContentTemplateButtonStack">
31+
<!-- Margin of 10 here is to "make room" for the elevation drop shadows of the buttons. Must be compensated for in some (left/right) alignment scenarios using PopupBox.PopupHorizontalOffset -->
32+
<StackPanel Margin="10">
33+
<Button Content="1"
34+
Opacity="0.5"
35+
ToolTip="One with custom opacity" />
36+
<Button Content="2" ToolTip="Two" />
37+
<Button Content="3" ToolTip="Three" />
38+
</StackPanel>
39+
</DataTemplate>
40+
41+
<DataTemplate x:Key="ContentTemplateButtonStackWithBackground">
42+
<Grid Background="Fuchsia">
43+
<StackPanel Margin="10">
44+
<Button Content="1"
45+
Opacity="0.5"
46+
ToolTip="One with custom opacity" />
47+
<Button Content="2" ToolTip="Two" />
48+
<Button Content="3" ToolTip="Three" />
49+
</StackPanel>
50+
</Grid>
51+
</DataTemplate>
52+
53+
<x:Array x:Key="{x:Static local:PopupBox.DefaultStyleContentKey}" Type="ComboBoxItem">
54+
<ComboBoxItem Tag="{StaticResource ContentTemplateGrid}" materialDesign:HintAssist.HelperText="Selected content works best when used with the MaterialDesignPopupBox style">Grid</ComboBoxItem>
55+
<ComboBoxItem Tag="{StaticResource ContentTemplateGridWithBackground}">Colored grid</ComboBoxItem>
56+
</x:Array>
57+
58+
<x:Array x:Key="{x:Static local:PopupBox.MultiFloatingActionStyleContentKey}" Type="ComboBoxItem">
59+
<ComboBoxItem Tag="{StaticResource ContentTemplateButtonStack}" materialDesign:HintAssist.HelperText="Margin in the selected content (stack of buttons) needs to be compensated for in certain alignments (left/right)">Stack of buttons</ComboBoxItem>
60+
<ComboBoxItem Tag="{StaticResource ContentTemplateButtonStackWithBackground}">Stack of buttons in colored grid</ComboBoxItem>
61+
</x:Array>
62+
63+
<local:ComboBoxItemToDataTemplateConverter x:Key="ComboBoxItemToDataTemplateConverter" />
64+
<local:ComboBoxItemToStyleConverter x:Key="ComboBoxItemToStyleConverter" />
65+
<local:ComboBoxItemToHelperTextConverter x:Key="ComboBoxItemToHelperTextConverter" />
66+
67+
<Thickness x:Key="Spacer">0,30,0,0</Thickness>
68+
</ResourceDictionary>
69+
</UserControl.Resources>
70+
<Grid>
71+
<Grid.ColumnDefinitions>
72+
<ColumnDefinition Width="*" />
73+
<ColumnDefinition Width="250" />
74+
</Grid.ColumnDefinitions>
75+
76+
<Grid Grid.Column="0">
77+
<Grid.RowDefinitions>
78+
<RowDefinition Height="Auto" />
79+
<RowDefinition Height="*" />
80+
</Grid.RowDefinitions>
81+
82+
<TextBlock Grid.Row="0" HorizontalAlignment="Center" Style="{StaticResource MaterialDesignSubtitle2TextBlock}"
83+
Text="{Binding ElementName=ContentComboBox, Path=SelectedItem, Converter={StaticResource ComboBoxItemToHelperTextConverter}}" />
84+
85+
<materialDesign:PopupBox Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Center" SnapsToDevicePixels="True" Padding="0"
86+
Style="{Binding ElementName=StyleComboBox, Path=SelectedItem, Converter={StaticResource ComboBoxItemToStyleConverter}}"
87+
PopupElevation="{Binding ElementName=ElevationComboBox, Path=SelectedItem}"
88+
PopupUniformCornerRadius="{Binding ElementName=UniformCornerRadiusComboBox, Path=SelectedItem}"
89+
PopupHorizontalOffset="{Binding ElementName=HorizontalOffsetComboBox, Path=SelectedItem}"
90+
PopupVerticalOffset="{Binding ElementName=VerticalOffsetComboBox, Path=SelectedItem}"
91+
PlacementMode="{Binding ElementName=PopupBoxPlacementModeComboBox, Path=SelectedItem}">
92+
<ContentControl ContentTemplate="{Binding ElementName=ContentComboBox, Path=SelectedItem, Converter={StaticResource ComboBoxItemToDataTemplateConverter}}" />
93+
</materialDesign:PopupBox>
94+
</Grid>
95+
96+
<StackPanel Grid.Column="1" Orientation="Vertical">
97+
<GroupBox Header="Properties" Padding="10">
98+
<StackPanel Orientation="Vertical">
99+
<TextBlock Text="Style:" Style="{StaticResource MaterialDesignSubtitle2TextBlock}" />
100+
<ComboBox x:Name="StyleComboBox" SelectionChanged="StyleComboBox_OnSelectionChanged">
101+
<ComboBoxItem Tag="{StaticResource MaterialDesignPopupBox}">MaterialDesignPopupBox</ComboBoxItem>
102+
<ComboBoxItem Tag="{StaticResource MaterialDesignMultiFloatingActionPopupBox}">MaterialDesignMultiFloatingActionPopupBox</ComboBoxItem>
103+
</ComboBox>
104+
105+
<TextBlock Text="Elevation:" Margin="{StaticResource Spacer}" Style="{StaticResource MaterialDesignSubtitle2TextBlock}" />
106+
<ComboBox x:Name="ElevationComboBox" SelectedIndex="6">
107+
<materialDesign:Elevation>Dp0</materialDesign:Elevation>
108+
<materialDesign:Elevation>Dp1</materialDesign:Elevation>
109+
<materialDesign:Elevation>Dp2</materialDesign:Elevation>
110+
<materialDesign:Elevation>Dp3</materialDesign:Elevation>
111+
<materialDesign:Elevation>Dp4</materialDesign:Elevation>
112+
<materialDesign:Elevation>Dp5</materialDesign:Elevation>
113+
<materialDesign:Elevation>Dp6</materialDesign:Elevation>
114+
<materialDesign:Elevation>Dp7</materialDesign:Elevation>
115+
<materialDesign:Elevation>Dp8</materialDesign:Elevation>
116+
<materialDesign:Elevation>Dp12</materialDesign:Elevation>
117+
<materialDesign:Elevation>Dp16</materialDesign:Elevation>
118+
<materialDesign:Elevation>Dp24</materialDesign:Elevation>
119+
</ComboBox>
120+
121+
<TextBlock Text="PopupUniformCornerRadius:" Margin="{StaticResource Spacer}" Style="{StaticResource MaterialDesignSubtitle2TextBlock}" />
122+
<ComboBox x:Name="UniformCornerRadiusComboBox" SelectedIndex="2">
123+
<system:Double>0</system:Double>
124+
<system:Double>2</system:Double>
125+
<system:Double>4</system:Double>
126+
<system:Double>6</system:Double>
127+
<system:Double>8</system:Double>
128+
<system:Double>10</system:Double>
129+
<system:Double>20</system:Double>
130+
</ComboBox>
131+
132+
<TextBlock Text="PopupHorizontalOffset:" Margin="{StaticResource Spacer}" Style="{StaticResource MaterialDesignSubtitle2TextBlock}" />
133+
<ComboBox x:Name="HorizontalOffsetComboBox" SelectedIndex="10">
134+
<system:Double>-100</system:Double>
135+
<system:Double>-50</system:Double>
136+
<system:Double>-20</system:Double>
137+
<system:Double>-15</system:Double>
138+
<system:Double>-10</system:Double>
139+
<system:Double>-5</system:Double>
140+
<system:Double>-4</system:Double>
141+
<system:Double>-3</system:Double>
142+
<system:Double>-2</system:Double>
143+
<system:Double>-1</system:Double>
144+
<system:Double>0</system:Double>
145+
<system:Double>1</system:Double>
146+
<system:Double>2</system:Double>
147+
<system:Double>3</system:Double>
148+
<system:Double>4</system:Double>
149+
<system:Double>5</system:Double>
150+
<system:Double>10</system:Double>
151+
<system:Double>15</system:Double>
152+
<system:Double>20</system:Double>
153+
<system:Double>50</system:Double>
154+
<system:Double>100</system:Double>
155+
</ComboBox>
156+
157+
<TextBlock Text="PopupVerticalOffset:" Margin="{StaticResource Spacer}" Style="{StaticResource MaterialDesignSubtitle2TextBlock}" />
158+
<ComboBox x:Name="VerticalOffsetComboBox" SelectedIndex="10">
159+
<system:Double>-100</system:Double>
160+
<system:Double>-50</system:Double>
161+
<system:Double>-20</system:Double>
162+
<system:Double>-15</system:Double>
163+
<system:Double>-10</system:Double>
164+
<system:Double>-5</system:Double>
165+
<system:Double>-4</system:Double>
166+
<system:Double>-3</system:Double>
167+
<system:Double>-2</system:Double>
168+
<system:Double>-1</system:Double>
169+
<system:Double>0</system:Double>
170+
<system:Double>1</system:Double>
171+
<system:Double>2</system:Double>
172+
<system:Double>3</system:Double>
173+
<system:Double>4</system:Double>
174+
<system:Double>5</system:Double>
175+
<system:Double>10</system:Double>
176+
<system:Double>15</system:Double>
177+
<system:Double>20</system:Double>
178+
<system:Double>50</system:Double>
179+
<system:Double>100</system:Double>
180+
</ComboBox>
181+
182+
<TextBlock Text="PopupBoxPlacementMode:" Margin="{StaticResource Spacer}" Style="{StaticResource MaterialDesignSubtitle2TextBlock}" />
183+
<ComboBox x:Name="PopupBoxPlacementModeComboBox" SelectedIndex="1">
184+
<materialDesign:PopupBoxPlacementMode>BottomAndAlignCentres</materialDesign:PopupBoxPlacementMode>
185+
<materialDesign:PopupBoxPlacementMode>BottomAndAlignLeftEdges</materialDesign:PopupBoxPlacementMode>
186+
<materialDesign:PopupBoxPlacementMode>BottomAndAlignRightEdges</materialDesign:PopupBoxPlacementMode>
187+
<materialDesign:PopupBoxPlacementMode>LeftAndAlignBottomEdges</materialDesign:PopupBoxPlacementMode>
188+
<materialDesign:PopupBoxPlacementMode>LeftAndAlignMiddles</materialDesign:PopupBoxPlacementMode>
189+
<materialDesign:PopupBoxPlacementMode>LeftAndAlignTopEdges</materialDesign:PopupBoxPlacementMode>
190+
<materialDesign:PopupBoxPlacementMode>RightAndAlignBottomEdges</materialDesign:PopupBoxPlacementMode>
191+
<materialDesign:PopupBoxPlacementMode>RightAndAlignMiddles</materialDesign:PopupBoxPlacementMode>
192+
<materialDesign:PopupBoxPlacementMode>RightAndAlignTopEdges</materialDesign:PopupBoxPlacementMode>
193+
<materialDesign:PopupBoxPlacementMode>TopAndAlignCentres</materialDesign:PopupBoxPlacementMode>
194+
<materialDesign:PopupBoxPlacementMode>TopAndAlignLeftEdges</materialDesign:PopupBoxPlacementMode>
195+
<materialDesign:PopupBoxPlacementMode>TopAndAlignRightEdges</materialDesign:PopupBoxPlacementMode>
196+
</ComboBox>
197+
</StackPanel>
198+
</GroupBox>
199+
200+
<GroupBox Header="Popup" Margin="{StaticResource Spacer}" Padding="10">
201+
<StackPanel Orientation="Vertical">
202+
<TextBlock Text="Popup Content:" Style="{StaticResource MaterialDesignSubtitle2TextBlock}" />
203+
<ComboBox x:Name="ContentComboBox" />
204+
</StackPanel>
205+
</GroupBox>
206+
</StackPanel>
207+
</Grid>
208+
</UserControl>

MainDemo.Wpf/PopupBox.xaml.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System.Collections;
2+
using System.Globalization;
3+
using System.Windows.Data;
4+
using MaterialDesignThemes.Wpf;
5+
6+
namespace MaterialDesignDemo;
7+
8+
/// <summary>
9+
/// Interaction logic for PopupBox.xaml
10+
/// </summary>
11+
public partial class PopupBox : UserControl
12+
{
13+
public const string DefaultStyleContentKey = nameof(DefaultStyleContentKey);
14+
public const string MultiFloatingActionStyleContentKey = nameof(MultiFloatingActionStyleContentKey);
15+
16+
private readonly IEnumerable _defaultStyleContent;
17+
private readonly IEnumerable _multiFloatingActionStyleContentKey;
18+
19+
public PopupBox()
20+
{
21+
InitializeComponent();
22+
23+
_defaultStyleContent = (IEnumerable)FindResource(DefaultStyleContentKey);
24+
_multiFloatingActionStyleContentKey = (IEnumerable)FindResource(MultiFloatingActionStyleContentKey);
25+
26+
Loaded += (sender, args) => StyleComboBox.SelectedIndex = 0;
27+
}
28+
29+
private void StyleComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
30+
{
31+
ComboBoxItem selectedItem = (ComboBoxItem) StyleComboBox.SelectedItem;
32+
if (Equals(selectedItem.Content, "MaterialDesignPopupBox"))
33+
{
34+
ContentComboBox.ItemsSource = _defaultStyleContent;
35+
}
36+
else
37+
{
38+
ContentComboBox.ItemsSource = _multiFloatingActionStyleContentKey;
39+
}
40+
ContentComboBox.SelectedIndex = 0;
41+
}
42+
}
43+
44+
internal class ComboBoxItemToDataTemplateConverter : IValueConverter
45+
{
46+
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
47+
=> value is ComboBoxItem { Tag: DataTemplate template} ? template : null;
48+
49+
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
50+
=> throw new NotImplementedException();
51+
}
52+
53+
internal class ComboBoxItemToStyleConverter : IValueConverter
54+
{
55+
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
56+
=> value is ComboBoxItem { Tag: Style style } ? style : null;
57+
58+
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
59+
=> throw new NotImplementedException();
60+
}
61+
62+
internal class ComboBoxItemToHelperTextConverter : IValueConverter
63+
{
64+
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
65+
=> value is ComboBoxItem item ? HintAssist.GetHelperText(item) : null!;
66+
67+
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
68+
=> throw new NotImplementedException();
69+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.ComponentModel;
2+
3+
namespace MaterialDesignThemes.UITests.WPF.PopupBox;
4+
5+
public class PopupBoxTests : TestBase
6+
{
7+
public PopupBoxTests(ITestOutputHelper output)
8+
: base(output)
9+
{ }
10+
11+
[Theory]
12+
[InlineData(Elevation.Dp0)]
13+
[InlineData(Elevation.Dp16)]
14+
[InlineData(Elevation.Dp24)]
15+
[Description("Issue 3129")]
16+
public async Task PopupBox_WithElevation_AppliesElevationToNestedCard(Elevation elevation)
17+
{
18+
await using var recorder = new TestRecorder(App);
19+
20+
//Arrange
21+
IVisualElement<Wpf.PopupBox> popupBox = await LoadXaml<Wpf.PopupBox>($@"
22+
<materialDesign:PopupBox VerticalAlignment=""Top""
23+
PopupElevation=""{elevation}"">
24+
<StackPanel>
25+
<Button Content=""More"" />
26+
<Button Content=""Options"" />
27+
</StackPanel>
28+
</materialDesign:PopupBox>");
29+
30+
IVisualElement<Card> card = await popupBox.GetElement<Card>("/Card");
31+
32+
// Assert
33+
Assert.Equal(elevation, await card.GetProperty<Elevation?>(ElevationAssist.ElevationProperty));
34+
35+
recorder.Success();
36+
}
37+
}

MaterialDesignThemes.Wpf/Card.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ public double UniformCornerRadius
1717
}
1818
public static readonly DependencyProperty UniformCornerRadiusProperty
1919
= DependencyProperty.Register(nameof(UniformCornerRadius), typeof(double), typeof(Card),
20-
new FrameworkPropertyMetadata(DefaultUniformCornerRadius, FrameworkPropertyMetadataOptions.AffectsMeasure));
20+
new FrameworkPropertyMetadata(DefaultUniformCornerRadius, FrameworkPropertyMetadataOptions.AffectsMeasure, UniformCornerRadiusChangedCallback));
21+
22+
private static void UniformCornerRadiusChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
23+
{
24+
Card card = (Card) d;
25+
card.UpdateContentClip();
26+
}
27+
2128
#endregion
2229

2330
#region DependencyProperty : ContentClipProperty
@@ -59,7 +66,11 @@ public override void OnApplyTemplate()
5966
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
6067
{
6168
base.OnRenderSizeChanged(sizeInfo);
69+
UpdateContentClip();
70+
}
6271

72+
private void UpdateContentClip()
73+
{
6374
if (_clipBorder is null)
6475
{
6576
return;

MaterialDesignThemes.Wpf/DpiHelper.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ public static double TransformToDeviceY(Visual visual, double y)
3131
return TransformToDeviceY(y);
3232
}
3333

34+
public static double TransformFromDeviceY(Visual visual, double y)
35+
{
36+
var source = PresentationSource.FromVisual(visual);
37+
if (source?.CompositionTarget != null) return y / source.CompositionTarget.TransformToDevice.M22;
38+
39+
return TransformFromDeviceY(y);
40+
}
41+
3442
public static double TransformToDeviceX(Visual visual, double x)
3543
{
3644
var source = PresentationSource.FromVisual(visual);
@@ -39,8 +47,20 @@ public static double TransformToDeviceX(Visual visual, double x)
3947
return TransformToDeviceX(x);
4048
}
4149

50+
public static double TransformFromDeviceX(Visual visual, double x)
51+
{
52+
var source = PresentationSource.FromVisual(visual);
53+
if (source?.CompositionTarget != null) return x / source.CompositionTarget.TransformToDevice.M11;
54+
55+
return TransformFromDeviceX(x);
56+
}
57+
4258
public static double TransformToDeviceY(double y) => y * DpiY / StandardDpiY;
4359

60+
public static double TransformFromDeviceY(double y) => y / DpiY * StandardDpiY;
61+
4462
public static double TransformToDeviceX(double x) => x * DpiX / StandardDpiX;
63+
64+
public static double TransformFromDeviceX(double x) => x / DpiX * StandardDpiX;
4565
}
4666
}

0 commit comments

Comments
 (0)