Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion samples/SampleApp/DemoPages/CalendarDatePickerDemo.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
RowSpacing="30">
<Grid Grid.Column="0" Grid.Row="0"
ColumnDefinitions="Auto, 250"
RowDefinitions="Auto, Auto, Auto, Auto, Auto, Auto, Auto"
RowDefinitions="Auto, Auto, Auto, Auto, Auto, Auto, Auto, Auto"
ColumnSpacing="15"
RowSpacing="14">
<TextBlock Grid.Row="0" Grid.Column="0" Text="Placeholder" VerticalAlignment="Center" />
Expand All @@ -37,6 +37,7 @@
<TextBlock Grid.Row="4" Grid.Column="0" Text="Custom Format" VerticalAlignment="Center" />
<TextBlock Grid.Row="5" Grid.Column="0" Text="Reset" VerticalAlignment="Center" />
<TextBlock Grid.Row="6" Grid.Column="0" Text="Error" VerticalAlignment="Center" />
<TextBlock Grid.Row="7" Grid.Column="0" Text="Inner slots" VerticalAlignment="Center" />

<CalendarDatePicker Grid.Row="0" Grid.Column="1" HorizontalAlignment="Left" />
<CalendarDatePicker Grid.Row="1" Grid.Column="1" Name="DatePicker" HorizontalAlignment="Left" />
Expand All @@ -58,6 +59,20 @@
</system:Exception>
</DataValidationErrors.Error>
</CalendarDatePicker>
<StackPanel Grid.Row="7" Grid.Column="1" Orientation="Horizontal" Spacing="10">
Comment thread
xfortin-devolutions marked this conversation as resolved.
<CalendarDatePicker HorizontalAlignment="Left">
<CalendarDatePickerContent.InnerLeftContent>
<Button Command="{Binding GreenCommand}"><Border Width="8" Height="8" Background="Green" /></Button>
Comment thread
xfortin-devolutions marked this conversation as resolved.
</CalendarDatePickerContent.InnerLeftContent>
<CalendarDatePickerContent.InnerLeftOfCalendarButtonContent>
<Button Command="{Binding OrangeCommand}"><Border Width="8" Height="8" Background="Orange" /></Button>
</CalendarDatePickerContent.InnerLeftOfCalendarButtonContent>
<CalendarDatePickerContent.InnerRightContent>
<Button Command="{Binding RedCommand}"><Border Width="8" Height="8" Background="Red" /></Button>
</CalendarDatePickerContent.InnerRightContent>
</CalendarDatePicker>
<Border Width="12" Height="12" VerticalAlignment="Center" Background="{Binding SelectedBrush}" />
</StackPanel>
</Grid>

<StackPanel Grid.Row="0" Grid.Column="1"
Expand Down
13 changes: 13 additions & 0 deletions samples/SampleApp/ViewModels/CalendarDatePickerViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using System;
using System.Collections.ObjectModel;
using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

Expand All @@ -15,4 +16,16 @@ private void SetToToday()
{
this.SelectedDate = DateTime.Today;
}

[ObservableProperty]
private IBrush selectedBrush = Brushes.Transparent;

[RelayCommand]
private void Green() => this.SelectedBrush = Brushes.Green;

[RelayCommand]
private void Orange() => this.SelectedBrush = Brushes.Orange;

[RelayCommand]
private void Red() => this.SelectedBrush = Brushes.Red;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
namespace Devolutions.AvaloniaControls.AttachedProperties;

using System.Collections;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;

public static class CalendarDatePickerContent
{
public static readonly AttachedProperty<IEnumerable> InnerLeftContentProperty =
AvaloniaProperty.RegisterAttached<CalendarDatePicker, IEnumerable>("InnerLeftContent", typeof(CalendarDatePickerContent));

public static readonly AttachedProperty<IEnumerable> InnerLeftOfCalendarButtonContentProperty =
AvaloniaProperty.RegisterAttached<CalendarDatePicker, IEnumerable>("InnerLeftOfCalendarButtonContent", typeof(CalendarDatePickerContent));

public static readonly AttachedProperty<IEnumerable> InnerRightContentProperty =
AvaloniaProperty.RegisterAttached<CalendarDatePicker, IEnumerable>("InnerRightContent", typeof(CalendarDatePickerContent));

public static IEnumerable GetInnerLeftContent(CalendarDatePicker element) => GetOrCreate(element, InnerLeftContentProperty);

public static void SetInnerLeftContent(CalendarDatePicker element, IEnumerable value) => element.SetValue(InnerLeftContentProperty, value);

public static IEnumerable GetInnerLeftOfCalendarButtonContent(CalendarDatePicker element) =>
GetOrCreate(element, InnerLeftOfCalendarButtonContentProperty);

public static void SetInnerLeftOfCalendarButtonContent(CalendarDatePicker element, IEnumerable value) =>
element.SetValue(InnerLeftOfCalendarButtonContentProperty, value);

public static IEnumerable GetInnerRightContent(CalendarDatePicker element) => GetOrCreate(element, InnerRightContentProperty);

public static void SetInnerRightContent(CalendarDatePicker element, IEnumerable value) => element.SetValue(InnerRightContentProperty, value);

// Attached-property defaults are shared across all instances, which is not what we want for
// mutable collections. Lazily create a per-instance AvaloniaList<Control> on first read so
// XAML element-syntax (<CalendarDatePickerContent.InnerLeftContent>...</...>) can Add into it.
private static IEnumerable GetOrCreate(CalendarDatePicker element, AttachedProperty<IEnumerable> property)
{
if (element.GetValue(property) is { } existing)
{
return existing;
}

// ReSharper disable once CollectionNeverUpdated.Local
AvaloniaList<Control> list = [];
element.SetValue(property, list);
return list;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using Avalonia;
using Avalonia;

namespace Devolutions.AvaloniaControls.Behaviors;

using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Threading;
using Avalonia.VisualTree;

public static class CalendarDatePickerBehavior
Expand All @@ -15,30 +18,62 @@ static CalendarDatePickerBehavior()
{
OpenOnSelectedDateProperty.Changed.Subscribe(args =>
{
if (args.Sender is CalendarDatePicker datePicker)
if (args.Sender is not CalendarDatePicker datePicker) return;

if (args.NewValue.GetValueOrDefault<bool>())
{
datePicker.CalendarOpened += OnCalendarOpened;
datePicker.CalendarClosed += OnCalendarClosed;
}
else
{
if (args.NewValue.GetValueOrDefault<bool>())
{
datePicker.CalendarOpened += OnCalendarOpen;
}
else
{
datePicker.CalendarOpened -= OnCalendarOpen;
}
datePicker.CalendarOpened -= OnCalendarOpened;
datePicker.CalendarClosed -= OnCalendarClosed;
}
});
}

private static void OnCalendarOpen(object? sender, EventArgs e)
private static void OnCalendarOpened(object? sender, EventArgs e)
{
if (sender is CalendarDatePicker datePicker)
{
Popup? popup = datePicker.GetVisualDescendants().OfType<Popup>().FirstOrDefault();
Calendar? calendar = popup?.Child?.FindDescendantOfType<Calendar>() ?? popup?.Child as Calendar;
if (sender is not CalendarDatePicker datePicker) return;

if (calendar == null) return;
Popup? popup = datePicker.GetVisualDescendants().OfType<Popup>().FirstOrDefault();
Calendar? calendar = popup?.Child?.FindDescendantOfType<Calendar>() ?? popup?.Child as Calendar;

if (calendar == null) return;

calendar.DisplayDate = datePicker.SelectedDate ?? DateTime.Now;

// Move keyboard focus into the calendar when the popup opens. The calendar button now lives inside
// PART_TextBox (to host inner-content slots), so opening no longer takes focus off the text box on
// its own — focus would otherwise stay in the box and its segmented arrow-keys would look like
// calendar input. Focusing the popup also lets Avalonia's light-dismiss close it on Tab again.
// (PART_Calendar sets Focusable="True" in each theme so this Focus() takes effect.)
Dispatcher.UIThread.Post(() => calendar.Focus(), DispatcherPriority.Input);
}

calendar.DisplayDate = datePicker.SelectedDate ?? DateTime.Now;
private static void OnCalendarClosed(object? sender, EventArgs e)
{
if (sender is not CalendarDatePicker datePicker) return;

// Focus was moved into the popup calendar on open (see OnCalendarOpened). The calendar lives in the
// popup's own tree, outside the window's tab order, so once the popup closes we return focus to the
// picker's text box — restoring the original tab flow (Tab then continues from the picker).
Dispatcher.UIThread.Post(() => ReturnFocusToPicker(datePicker), DispatcherPriority.Background);
}

// Returns focus to the picker's primary control. Prefers the text box, but on themes where it is hidden
// and non-focusable (Yaru shows a button instead) falls back to the button.
private static void ReturnFocusToPicker(CalendarDatePicker picker)
{
TextBox? textBox = picker.GetVisualDescendants().OfType<TextBox>().FirstOrDefault(tb => tb.Name == "PART_TextBox");
if (textBox is { Focusable: true, IsVisible: true })
{
textBox.Focus();
return;
}

Button? button = picker.GetVisualDescendants().OfType<Button>().FirstOrDefault(b => b.Name == "PART_Button");
button?.Focus();
}
}
}
Loading