diff --git a/components/Behaviors/OpenSolution.bat b/components/Behaviors/OpenSolution.bat
new file mode 100644
index 00000000..814a56d4
--- /dev/null
+++ b/components/Behaviors/OpenSolution.bat
@@ -0,0 +1,3 @@
+@ECHO OFF
+
+powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %*
\ No newline at end of file
diff --git a/components/Behaviors/samples/AutoSelectBehaviorSample.xaml b/components/Behaviors/samples/AutoSelectBehaviorSample.xaml
new file mode 100644
index 00000000..f681e837
--- /dev/null
+++ b/components/Behaviors/samples/AutoSelectBehaviorSample.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Behaviors/samples/AutoSelectBehaviorSample.xaml.cs b/components/Behaviors/samples/AutoSelectBehaviorSample.xaml.cs
new file mode 100644
index 00000000..8fad7705
--- /dev/null
+++ b/components/Behaviors/samples/AutoSelectBehaviorSample.xaml.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI.Behaviors;
+
+namespace BehaviorsExperiment.Samples;
+
+[ToolkitSample(id: nameof(AutoSelectBehaviorSample), nameof(AutoSelectBehavior), description: $"A sample for showing how to use the AutoSelectBehavior.")]
+public sealed partial class AutoSelectBehaviorSample : Page
+{
+ public AutoSelectBehaviorSample()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/Behaviors/samples/Behaviors.Samples.csproj b/components/Behaviors/samples/Behaviors.Samples.csproj
new file mode 100644
index 00000000..95f546a6
--- /dev/null
+++ b/components/Behaviors/samples/Behaviors.Samples.csproj
@@ -0,0 +1,8 @@
+
+
+ Behaviors
+
+
+
+
+
diff --git a/components/Behaviors/samples/Behaviors.md b/components/Behaviors/samples/Behaviors.md
new file mode 100644
index 00000000..917d00fe
--- /dev/null
+++ b/components/Behaviors/samples/Behaviors.md
@@ -0,0 +1,47 @@
+---
+title: Behaviors
+author: Arlodotexe
+description:
+keywords: Behaviors
+dev_langs:
+ - csharp
+category: Xaml
+subcategory: Behaviors
+discussion-id: 0
+issue-id: 0
+---
+
+# Behaviors
+
+A behavior is a class that attaches to a XAML control and invokes an Action when triggered.
+
+The `Microsoft.Xaml.Behaviors.*` packages contains several useful triggers and actions, and the Windows Community Toolkit provides even more.
+
+See also [XamlBehaviors Wiki](https://github.com/Microsoft/XamlBehaviors/wiki)
+
+## KeyDownTriggerBehavior
+
+A behavior that listens to a key press event on the associated UIElement and triggers the set of actions.
+
+> [!Sample KeyDownTriggerBehaviorSample]
+
+## AutoSelectBehavior
+
+The AutoSelectBehavior automatically selects the entire content of its associated TextBox when it is loaded.
+
+> [!Sample AutoSelectBehaviorSample]
+
+## ViewportBehavior
+This behavior allows you to listen an element enter or exit the ScrollViewer viewport.
+
+> [!Sample ViewportBehaviorSample]
+
+## FocusBehavior
+
+Of the given targets, this behavior sets the focus on the first control which accepts it.
+
+A control only receives focus if it is enabled and loaded into the visual tree:
+> [!Sample FocusBehaviorButtonSample]
+
+Empty lists do not receive focus:
+> [!Sample FocusBehaviorListSample]
diff --git a/components/Behaviors/samples/Dependencies.props b/components/Behaviors/samples/Dependencies.props
new file mode 100644
index 00000000..e622e1df
--- /dev/null
+++ b/components/Behaviors/samples/Dependencies.props
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Behaviors/samples/FocusBehaviorButtonSample.xaml b/components/Behaviors/samples/FocusBehaviorButtonSample.xaml
new file mode 100644
index 00000000..1e57af3e
--- /dev/null
+++ b/components/Behaviors/samples/FocusBehaviorButtonSample.xaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Behaviors/samples/FocusBehaviorButtonSample.xaml.cs b/components/Behaviors/samples/FocusBehaviorButtonSample.xaml.cs
new file mode 100644
index 00000000..2fc697f9
--- /dev/null
+++ b/components/Behaviors/samples/FocusBehaviorButtonSample.xaml.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI.Behaviors;
+
+namespace BehaviorsExperiment.Samples;
+
+[ToolkitSampleBoolOption("IsButtonEnabled", true, Title = "Enable button")]
+[ToolkitSampleBoolOption("ControlLoaded", true, Title = "Toggle x:Bind")]
+[ToolkitSample(id: nameof(FocusBehaviorButtonSample), $"{nameof(FocusBehavior)}: Disabled / Unloaded items", description: $"A sample demonstrating how {nameof(FocusBehavior)} affects disabled or unloaded controls.")]
+public sealed partial class FocusBehaviorButtonSample : Page
+{
+ public FocusBehaviorButtonSample()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/Behaviors/samples/FocusBehaviorListSample.xaml b/components/Behaviors/samples/FocusBehaviorListSample.xaml
new file mode 100644
index 00000000..2a3f1645
--- /dev/null
+++ b/components/Behaviors/samples/FocusBehaviorListSample.xaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Item 1
+ Item 2
+
+
+
+
+
diff --git a/components/Behaviors/samples/FocusBehaviorListSample.xaml.cs b/components/Behaviors/samples/FocusBehaviorListSample.xaml.cs
new file mode 100644
index 00000000..e32e4797
--- /dev/null
+++ b/components/Behaviors/samples/FocusBehaviorListSample.xaml.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI.Behaviors;
+
+namespace BehaviorsExperiment.Samples;
+
+[ToolkitSample(id: nameof(FocusBehaviorListSample), $"{nameof(FocusBehavior)}: Lists", description: $"A sample demonstrating how {nameof(FocusBehavior)} affects lists.")]
+public sealed partial class FocusBehaviorListSample : Page
+{
+ public FocusBehaviorListSample()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/Behaviors/samples/KeyDownTriggerBehaviorSample.xaml b/components/Behaviors/samples/KeyDownTriggerBehaviorSample.xaml
new file mode 100644
index 00000000..57a7fa6c
--- /dev/null
+++ b/components/Behaviors/samples/KeyDownTriggerBehaviorSample.xaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Behaviors/samples/KeyDownTriggerBehaviorSample.xaml.cs b/components/Behaviors/samples/KeyDownTriggerBehaviorSample.xaml.cs
new file mode 100644
index 00000000..445664f0
--- /dev/null
+++ b/components/Behaviors/samples/KeyDownTriggerBehaviorSample.xaml.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI.Behaviors;
+
+namespace BehaviorsExperiment.Samples;
+
+[ToolkitSample(id: nameof(KeyDownTriggerBehaviorSample), nameof(KeyDownTriggerBehavior), description: $"A sample for showing how to use the {nameof(KeyDownTriggerBehavior)}.")]
+public sealed partial class KeyDownTriggerBehaviorSample : Page, INotifyPropertyChanged
+{
+ public KeyDownTriggerBehaviorSample()
+ {
+ this.InitializeComponent();
+ }
+
+ public int Count { get; set; }
+
+ public void IncrementCount()
+ {
+ Count++;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+}
diff --git a/components/Behaviors/samples/ViewportBehaviorSample.xaml b/components/Behaviors/samples/ViewportBehaviorSample.xaml
new file mode 100644
index 00000000..47967def
--- /dev/null
+++ b/components/Behaviors/samples/ViewportBehaviorSample.xaml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Behaviors/samples/ViewportBehaviorSample.xaml.cs b/components/Behaviors/samples/ViewportBehaviorSample.xaml.cs
new file mode 100644
index 00000000..eae9d808
--- /dev/null
+++ b/components/Behaviors/samples/ViewportBehaviorSample.xaml.cs
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI.Behaviors;
+
+namespace BehaviorsExperiment.Samples;
+
+[ToolkitSampleBoolOption("IsAlwaysOn", true, Title = "IsAlwaysOn")]
+[ToolkitSample(id: nameof(ViewportBehaviorSample), nameof(ViewportBehavior), description: $"A sample for showing how to use the {nameof(ViewportBehavior)}.")]
+public sealed partial class ViewportBehaviorSample : Page
+{
+ public ViewportBehaviorSample()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/Behaviors/src/AdditionalAssemblyInfo.cs b/components/Behaviors/src/AdditionalAssemblyInfo.cs
new file mode 100644
index 00000000..a909d34f
--- /dev/null
+++ b/components/Behaviors/src/AdditionalAssemblyInfo.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.CompilerServices;
+
+// These `InternalsVisibleTo` calls are intended to make it easier for
+// for any internal code to be testable in all the different test projects
+// used with the Labs infrastructure.
+[assembly: InternalsVisibleTo("Behaviors.Tests.Uwp")]
+[assembly: InternalsVisibleTo("Behaviors.Tests.WinAppSdk")]
+[assembly: InternalsVisibleTo("CommunityToolkit.Tests.Uwp")]
+[assembly: InternalsVisibleTo("CommunityToolkit.Tests.WinAppSdk")]
diff --git a/components/Behaviors/src/ApiInformationHelper.cs b/components/Behaviors/src/ApiInformationHelper.cs
new file mode 100644
index 00000000..15fed176
--- /dev/null
+++ b/components/Behaviors/src/ApiInformationHelper.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Windows.Foundation.Metadata;
+
+namespace CommunityToolkit.WinUI.Behaviors;
+
+internal class ApiInformationHelper
+{
+ // 1903 - 18362
+ public static bool IsXamlRootAvailable { get; } = ApiInformation.IsPropertyPresent("Windows.UI.Xaml.UIElement", "XamlRoot");
+}
diff --git a/components/Behaviors/src/BehaviorBase.cs b/components/Behaviors/src/BehaviorBase.cs
new file mode 100644
index 00000000..157ab217
--- /dev/null
+++ b/components/Behaviors/src/BehaviorBase.cs
@@ -0,0 +1,166 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Xaml.Interactivity;
+
+namespace CommunityToolkit.WinUI.Behaviors;
+
+///
+/// Base class for behaviors that solves 2 problems:
+/// 1. Prevent duplicate initialization that can happen (prevent multiple OnAttached calls);
+/// 2. Whenever initially fails, this method will subscribe to to allow lazy initialization.
+///
+/// The type of the associated object.
+///
+///
+/// For more info, see https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/1008.
+///
+public abstract class BehaviorBase : Behavior
+ where T : UIElement
+{
+ private bool _isAttaching;
+ private bool _isAttached;
+
+ ///
+ /// Gets a value indicating whether this behavior is attached.
+ ///
+ ///
+ /// true if this behavior is attached; otherwise, false.
+ ///
+ protected bool IsAttached
+ {
+ get { return _isAttached; }
+ }
+
+ ///
+ /// Called after the behavior is attached to the .
+ ///
+ ///
+ /// Override this to hook up functionality to the
+ ///
+ protected override void OnAttached()
+ {
+ base.OnAttached();
+
+ HandleAttach();
+
+ var frameworkElement = AssociatedObject as FrameworkElement;
+ if (frameworkElement != null)
+ {
+ frameworkElement.Loaded += OnAssociatedObjectLoaded;
+ frameworkElement.Unloaded += OnAssociatedObjectUnloaded;
+ frameworkElement.SizeChanged += OnAssociatedObjectSizeChanged;
+ }
+ }
+
+ ///
+ /// Called when the behavior is being detached from its .
+ ///
+ ///
+ /// Override this to unhook functionality from the
+ ///
+ protected override void OnDetaching()
+ {
+ base.OnDetaching();
+
+ var frameworkElement = AssociatedObject as FrameworkElement;
+ if (frameworkElement != null)
+ {
+ frameworkElement.Loaded -= OnAssociatedObjectLoaded;
+ frameworkElement.Unloaded -= OnAssociatedObjectUnloaded;
+ frameworkElement.SizeChanged -= OnAssociatedObjectSizeChanged;
+ }
+
+ HandleDetach();
+ }
+
+ ///
+ /// Called when the associated object has been loaded.
+ ///
+ protected virtual void OnAssociatedObjectLoaded()
+ {
+ }
+
+ ///
+ /// Called when the associated object has been unloaded.
+ ///
+ protected virtual void OnAssociatedObjectUnloaded()
+ {
+ }
+
+ ///
+ /// Initializes the behavior to the associated object.
+ ///
+ /// true if the initialization succeeded; otherwise false.
+ protected virtual bool Initialize()
+ {
+ return true;
+ }
+
+ ///
+ /// Uninitializes the behavior from the associated object.
+ ///
+ /// true if uninitialization succeeded; otherwise false.
+ protected virtual bool Uninitialize()
+ {
+ return true;
+ }
+
+ private void OnAssociatedObjectLoaded(object sender, RoutedEventArgs e)
+ {
+ if (!_isAttached)
+ {
+ HandleAttach();
+ }
+
+ OnAssociatedObjectLoaded();
+ }
+
+ private void OnAssociatedObjectUnloaded(object sender, RoutedEventArgs e)
+ {
+ OnAssociatedObjectUnloaded();
+
+ // Note: don't detach here, we'll let the behavior implementation take care of that
+ }
+
+ private void OnAssociatedObjectSizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ if (!_isAttached)
+ {
+ HandleAttach();
+ }
+ }
+
+ private void HandleAttach()
+ {
+ if (_isAttaching || _isAttached)
+ {
+ return;
+ }
+
+ _isAttaching = true;
+
+ var attached = Initialize();
+ if (attached)
+ {
+ _isAttached = true;
+ }
+
+ _isAttaching = false;
+ }
+
+ private void HandleDetach()
+ {
+ if (!_isAttached)
+ {
+ return;
+ }
+
+ var detached = Uninitialize();
+ if (detached)
+ {
+ _isAttached = false;
+ }
+ }
+}
diff --git a/components/Behaviors/src/CommunityToolkit.WinUI.Behaviors.csproj b/components/Behaviors/src/CommunityToolkit.WinUI.Behaviors.csproj
new file mode 100644
index 00000000..c5d35777
--- /dev/null
+++ b/components/Behaviors/src/CommunityToolkit.WinUI.Behaviors.csproj
@@ -0,0 +1,17 @@
+
+
+ Behaviors
+ This package contains Behaviors.
+ 8.0.0-beta.1
+
+
+ CommunityToolkit.WinUI.BehaviorsRns
+
+
+
+
+
+
+
+
+
diff --git a/components/Behaviors/src/Dependencies.props b/components/Behaviors/src/Dependencies.props
new file mode 100644
index 00000000..3f06d427
--- /dev/null
+++ b/components/Behaviors/src/Dependencies.props
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Behaviors/src/FocusBehavior.cs b/components/Behaviors/src/FocusBehavior.cs
new file mode 100644
index 00000000..e983e067
--- /dev/null
+++ b/components/Behaviors/src/FocusBehavior.cs
@@ -0,0 +1,200 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#if WINDOWS_UWP || HAS_UNO && WINUI2
+using Windows.System;
+#elif WINDOWS_WINAPPSDK || HAS_UNO && WINUI3
+using Microsoft.UI.Dispatching;
+#endif
+
+namespace CommunityToolkit.WinUI.Behaviors;
+
+#pragma warning disable SA1402 // File may only contain a single type
+///
+/// This behavior sets the focus on the first control of which accepts it.
+/// The focus will be set following the order. The first control being ready
+/// and accepting the focus will receive it.
+/// The focus can be set to another control with a higher priority if it loads before .
+/// The focus can be set to another control if some controls will be loaded/unloaded later.
+///
+[ContentProperty(Name = nameof(Targets))]
+public sealed class FocusBehavior : BehaviorBase
+{
+ ///
+ /// The DP to store the property value.
+ ///
+ public static readonly DependencyProperty TargetsProperty = DependencyProperty.Register(
+ nameof(Targets),
+ typeof(FocusTargetList),
+ typeof(FocusBehavior),
+ new PropertyMetadata(null, OnTargetsPropertyChanged));
+
+ ///
+ /// The DP to store the property value.
+ ///
+ public static readonly DependencyProperty FocusEngagementTimeoutProperty = DependencyProperty.Register(
+ nameof(FocusEngagementTimeout),
+ typeof(TimeSpan),
+ typeof(FocusBehavior),
+ new PropertyMetadata(TimeSpan.FromMilliseconds(100)));
+
+ private DispatcherQueueTimer _timer;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FocusBehavior()
+ {
+ _timer = DispatcherQueue.GetForCurrentThread().CreateTimer();
+ _timer.Tick += OnEngagementTimerTick;
+
+ Targets = new FocusTargetList();
+ }
+
+ ///
+ /// Gets or sets the ordered list of controls which should receive the focus when the associated object is loaded.
+ ///
+ public FocusTargetList Targets
+ {
+ get => (FocusTargetList)GetValue(TargetsProperty);
+ set => SetValue(TargetsProperty, value);
+ }
+
+ ///
+ /// Gets or sets the timeout before the stops trying to set the focus to a control with
+ /// a higher priority.
+ ///
+ public TimeSpan FocusEngagementTimeout
+ {
+ get => (TimeSpan)GetValue(FocusEngagementTimeoutProperty);
+ set => SetValue(FocusEngagementTimeoutProperty, value);
+ }
+
+ ///
+ protected override void OnAssociatedObjectLoaded()
+ {
+ foreach (var target in Targets)
+ {
+ target.ControlChanged += OnTargetControlChanged;
+ }
+
+ ApplyFocus();
+ }
+
+ ///
+ protected override bool Uninitialize()
+ {
+ foreach (var target in Targets)
+ {
+ target.ControlChanged -= OnTargetControlChanged;
+ }
+
+ Stop();
+ return true;
+ }
+
+ private static void OnTargetsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var behavior = (FocusBehavior)d;
+
+ if (e.OldValue is FocusTargetList oldTargets)
+ {
+ behavior.Stop(oldTargets);
+ }
+
+ behavior.ApplyFocus();
+ }
+
+ private void ApplyFocus()
+ {
+ if (Targets.Count == 0)
+ {
+ return;
+ }
+
+ var focusedControlIndex = -1;
+ var hasListViewBaseControl = false;
+ for (var i = 0; i < Targets.Count; i++)
+ {
+ var control = Targets[i].Control;
+ if (control is null)
+ {
+ continue;
+ }
+
+ if (control.IsLoaded)
+ {
+ if (control.Focus(FocusState.Programmatic))
+ {
+ focusedControlIndex = i;
+ break;
+ }
+
+ if (control is ListViewBase listViewBase)
+ {
+ listViewBase.ContainerContentChanging -= OnContainerContentChanging;
+ listViewBase.ContainerContentChanging += OnContainerContentChanging;
+ hasListViewBaseControl = true;
+ }
+ }
+ else
+ {
+ control.Loaded -= OnControlLoaded;
+ control.Loaded += OnControlLoaded;
+ }
+ }
+
+ if (focusedControlIndex == 0 || (!hasListViewBaseControl && Targets.All(t => t.Control?.IsLoaded == true)))
+ {
+ Stop();
+ }
+ else if (focusedControlIndex > 0)
+ {
+ // We have been able to set the focus on one control.
+ // We start the timer to detect if we can focus another control with an higher priority.
+ // This allows us to handle the case where the controls are not loaded in the order we expect.
+ _timer.Interval = FocusEngagementTimeout;
+ _timer.Start();
+ }
+ }
+
+ private void OnEngagementTimerTick(object? sender, object e)
+ {
+ ApplyFocus();
+ Stop();
+ }
+
+ private void OnControlLoaded(object? sender, RoutedEventArgs e) => ApplyFocus();
+
+ private void OnTargetControlChanged(object? sender, EventArgs e) => ApplyFocus();
+
+ private void OnContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
+ {
+ sender.ContainerContentChanging -= OnContainerContentChanging;
+ ApplyFocus();
+ }
+
+ private void Stop(FocusTargetList? targets = null)
+ {
+ if (_timer.IsRunning)
+ {
+ _timer.Stop();
+ }
+
+ foreach (var target in targets ?? Targets)
+ {
+ if (target.Control is null)
+ {
+ continue;
+ }
+
+ target.Control.Loaded -= OnControlLoaded;
+
+ if (target.Control is ListViewBase listViewBase)
+ {
+ listViewBase.ContainerContentChanging -= OnContainerContentChanging;
+ }
+ }
+ }
+}
diff --git a/components/Behaviors/src/FocusTarget.cs b/components/Behaviors/src/FocusTarget.cs
new file mode 100644
index 00000000..85249361
--- /dev/null
+++ b/components/Behaviors/src/FocusTarget.cs
@@ -0,0 +1,42 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace CommunityToolkit.WinUI.Behaviors;
+
+///
+/// A target for the .
+///
+public sealed partial class FocusTarget : DependencyObject
+{
+ ///
+ /// The DP to store the property value.
+ ///
+ public static readonly DependencyProperty ControlProperty = DependencyProperty.Register(
+ nameof(Control),
+ typeof(Control),
+ typeof(FocusTarget),
+ new PropertyMetadata(null, OnControlChanged));
+
+ ///
+ /// Raised when property changed.
+ /// It can change if we use x:Load on the control.
+ ///
+ public event EventHandler? ControlChanged;
+
+ ///
+ /// Gets or sets the control that will receive the focus.
+ ///
+ public Control Control
+ {
+ get => (Control)GetValue(ControlProperty);
+ set => SetValue(ControlProperty, value);
+ }
+
+ private static void OnControlChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var target = (FocusTarget)d;
+ target.ControlChanged?.Invoke(target, EventArgs.Empty);
+ }
+}
+#pragma warning restore SA1402 // File may only contain a single type
diff --git a/components/Behaviors/src/FocusTargetList.cs b/components/Behaviors/src/FocusTargetList.cs
new file mode 100644
index 00000000..65ae0778
--- /dev/null
+++ b/components/Behaviors/src/FocusTargetList.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace CommunityToolkit.WinUI.Behaviors;
+
+///
+/// A collection of .
+///
+public sealed class FocusTargetList : List
+{
+}
diff --git a/components/Behaviors/src/Keyboard/KeyDownTriggerBehavior.cs b/components/Behaviors/src/Keyboard/KeyDownTriggerBehavior.cs
new file mode 100644
index 00000000..2eddac0c
--- /dev/null
+++ b/components/Behaviors/src/Keyboard/KeyDownTriggerBehavior.cs
@@ -0,0 +1,60 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Xaml.Interactivity;
+using Windows.System;
+
+namespace CommunityToolkit.WinUI.Behaviors;
+
+///
+/// This behavior listens to a key down event on the associated when it is loaded and executes an action.
+///
+[TypeConstraint(typeof(FrameworkElement))]
+public class KeyDownTriggerBehavior : Trigger
+{
+ ///
+ /// Identifies the property.
+ ///
+ public static readonly DependencyProperty KeyProperty = DependencyProperty.Register(
+ nameof(Key),
+ typeof(VirtualKey),
+ typeof(KeyDownTriggerBehavior),
+ new PropertyMetadata(null));
+
+ ///
+ /// Gets or sets the key to listen when the associated object is loaded.
+ ///
+ public VirtualKey Key
+ {
+ get => (VirtualKey)GetValue(KeyProperty);
+ set => SetValue(KeyProperty, value);
+ }
+
+ ///
+ protected override void OnAttached()
+ {
+ AssociatedObject.KeyDown += OnAssociatedObjectKeyDown;
+ }
+
+ ///
+ protected override void OnDetaching()
+ {
+ AssociatedObject.KeyDown -= OnAssociatedObjectKeyDown;
+ }
+
+ ///
+ /// Invokes the current actions when the is pressed.
+ ///
+ /// The source instance.
+ /// The arguments for the event (unused).
+ private void OnAssociatedObjectKeyDown(object sender, KeyRoutedEventArgs keyRoutedEventArgs)
+ {
+ if (keyRoutedEventArgs.Key == Key)
+ {
+ keyRoutedEventArgs.Handled = true;
+ Interaction.ExecuteActions(sender, Actions, keyRoutedEventArgs);
+ }
+ }
+}
+
diff --git a/components/Behaviors/src/MultiTarget.props b/components/Behaviors/src/MultiTarget.props
new file mode 100644
index 00000000..b11c1942
--- /dev/null
+++ b/components/Behaviors/src/MultiTarget.props
@@ -0,0 +1,9 @@
+
+
+
+ uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android;
+
+
\ No newline at end of file
diff --git a/components/Behaviors/src/Select/AutoSelectBehavior.cs b/components/Behaviors/src/Select/AutoSelectBehavior.cs
new file mode 100644
index 00000000..136f39de
--- /dev/null
+++ b/components/Behaviors/src/Select/AutoSelectBehavior.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace CommunityToolkit.WinUI.Behaviors;
+
+///
+/// This behavior automatically selects the entire content of the associated when it is loaded.
+///
+public sealed class AutoSelectBehavior : BehaviorBase
+{
+ ///
+ protected override void OnAssociatedObjectLoaded() => AssociatedObject.SelectAll();
+}
+
diff --git a/components/Behaviors/src/Viewport/ViewportBehavior.Properties.cs b/components/Behaviors/src/Viewport/ViewportBehavior.Properties.cs
new file mode 100644
index 00000000..b134b240
--- /dev/null
+++ b/components/Behaviors/src/Viewport/ViewportBehavior.Properties.cs
@@ -0,0 +1,103 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Xaml.Interactivity;
+
+namespace CommunityToolkit.WinUI.Behaviors;
+
+///
+/// A class for listening to an element enter or exit the ScrollViewer viewport
+///
+public partial class ViewportBehavior
+{
+ ///
+ /// The IsFullyInViewport value of the associated element
+ ///
+ public static readonly DependencyProperty IsFullyInViewportProperty =
+ DependencyProperty.Register(nameof(IsFullyInViewport), typeof(bool), typeof(ViewportBehavior), new PropertyMetadata(default(bool), OnIsFullyInViewportChanged));
+
+ ///
+ /// The IsInViewport value of the associated element
+ ///
+ public static readonly DependencyProperty IsInViewportProperty =
+ DependencyProperty.Register(nameof(IsInViewport), typeof(bool), typeof(ViewportBehavior), new PropertyMetadata(default(bool), OnIsInViewportChanged));
+
+ ///
+ /// The IsAlwaysOn value of the associated element
+ ///
+ public static readonly DependencyProperty IsAlwaysOnProperty =
+ DependencyProperty.Register(nameof(IsAlwaysOn), typeof(bool), typeof(ViewportBehavior), new PropertyMetadata(default(bool)));
+
+ ///
+ /// Gets or sets a value indicating whether this behavior will remain attached after the associated element enters the viewport. When false, the behavior will remove itself after entering.
+ ///
+ public bool IsAlwaysOn
+ {
+ get { return (bool)GetValue(IsAlwaysOnProperty); }
+ set { SetValue(IsAlwaysOnProperty, value); }
+ }
+
+ ///
+ /// Gets a value indicating whether associated element is fully in the ScrollViewer viewport
+ ///
+ public bool IsFullyInViewport
+ {
+ get { return (bool)GetValue(IsFullyInViewportProperty); }
+ private set { SetValue(IsFullyInViewportProperty, value); }
+ }
+
+ ///
+ /// Gets a value indicating whether associated element is in the ScrollViewer viewport
+ ///
+ public bool IsInViewport
+ {
+ get { return (bool)GetValue(IsInViewportProperty); }
+ private set { SetValue(IsInViewportProperty, value); }
+ }
+
+ ///
+ /// Event tracking when the object is fully within the viewport or not
+ ///
+ /// DependencyObject
+ /// EventArgs
+ private static void OnIsFullyInViewportChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var obj = (ViewportBehavior)d;
+ var value = (bool)e.NewValue;
+
+ if (value)
+ {
+ obj.EnteredViewport?.Invoke(obj.AssociatedObject, EventArgs.Empty);
+
+ if (!obj.IsAlwaysOn)
+ {
+ Interaction.GetBehaviors(obj.AssociatedObject).Remove(obj);
+ }
+ }
+ else
+ {
+ obj.ExitingViewport?.Invoke(obj.AssociatedObject, EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Event tracking the state of the object as it moves into and out of the viewport
+ ///
+ /// DependencyObject
+ /// EventArgs
+ private static void OnIsInViewportChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var obj = (ViewportBehavior)d;
+ var value = (bool)e.NewValue;
+
+ if (value)
+ {
+ obj.EnteringViewport?.Invoke(obj.AssociatedObject, EventArgs.Empty);
+ }
+ else
+ {
+ obj.ExitedViewport?.Invoke(obj.AssociatedObject, EventArgs.Empty);
+ }
+ }
+}
diff --git a/components/Behaviors/src/Viewport/ViewportBehavior.cs b/components/Behaviors/src/Viewport/ViewportBehavior.cs
new file mode 100644
index 00000000..e071094b
--- /dev/null
+++ b/components/Behaviors/src/Viewport/ViewportBehavior.cs
@@ -0,0 +1,125 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace CommunityToolkit.WinUI.Behaviors;
+
+///
+/// A class for listening to an element enter or exit the ScrollViewer viewport
+///
+public partial class ViewportBehavior : BehaviorBase
+{
+ ///
+ /// The ScrollViewer hosting this element.
+ ///
+ private ScrollViewer? _hostScrollViewer;
+
+ ///
+ /// Associated element fully enter the ScrollViewer viewport event
+ ///
+ public event EventHandler? EnteredViewport;
+
+ ///
+ /// Associated element fully exit the ScrollViewer viewport event
+ ///
+ public event EventHandler? ExitedViewport;
+
+ ///
+ /// Associated element enter the ScrollViewer viewport event
+ ///
+ public event EventHandler? EnteringViewport;
+
+ ///
+ /// Associated element exit the ScrollViewer viewport event
+ ///
+ public event EventHandler? ExitingViewport;
+
+ ///
+ /// Called after the behavior is attached to the .
+ ///
+ ///
+ /// Override this to hook up functionality to the
+ ///
+ protected override void OnAttached()
+ {
+ base.OnAttached();
+
+ var parent = VisualTreeHelper.GetParent(AssociatedObject);
+ if (parent == null)
+ {
+ AssociatedObject.Loaded += AssociatedObject_Loaded;
+ return;
+ }
+
+ Init();
+ }
+
+ ///
+ /// Called when the behavior is being detached from its .
+ ///
+ ///
+ /// Override this to unhook functionality from the
+ ///
+ protected override void OnDetaching()
+ {
+ base.OnDetaching();
+
+ if (_hostScrollViewer is not null)
+ {
+ _hostScrollViewer.ViewChanged -= ParentScrollViewer_ViewChanged;
+ _hostScrollViewer = null;
+ }
+ }
+
+ private void ParentScrollViewer_ViewChanged(object? sender, ScrollViewerViewChangedEventArgs e)
+ {
+ var hostScrollViewer = (ScrollViewer)sender!;
+
+ var associatedElementRect = AssociatedObject
+ .TransformToVisual(_hostScrollViewer)
+ .TransformBounds(new Rect(0, 0, AssociatedObject.ActualWidth, AssociatedObject.ActualHeight));
+
+ var hostScrollViewerRect = new Rect(0, 0, hostScrollViewer.ActualWidth, hostScrollViewer.ActualHeight);
+
+ if (hostScrollViewerRect.Contains(new Point(associatedElementRect.Left, associatedElementRect.Top)) ||
+ hostScrollViewerRect.Contains(new Point(associatedElementRect.Right, associatedElementRect.Top)) ||
+ hostScrollViewerRect.Contains(new Point(associatedElementRect.Right, associatedElementRect.Bottom)) ||
+ hostScrollViewerRect.Contains(new Point(associatedElementRect.Left, associatedElementRect.Bottom)))
+ {
+ IsInViewport = true;
+
+ if (hostScrollViewerRect.Contains(new Point(associatedElementRect.Left, associatedElementRect.Top)) &&
+ hostScrollViewerRect.Contains(new Point(associatedElementRect.Right, associatedElementRect.Top)) &&
+ hostScrollViewerRect.Contains(new Point(associatedElementRect.Right, associatedElementRect.Bottom)) &&
+ hostScrollViewerRect.Contains(new Point(associatedElementRect.Left, associatedElementRect.Bottom)))
+ {
+ IsFullyInViewport = true;
+ }
+ else
+ {
+ IsFullyInViewport = false;
+ }
+ }
+ else
+ {
+ IsInViewport = false;
+ }
+ }
+
+ private void Init()
+ {
+ _hostScrollViewer = AssociatedObject.FindAscendant();
+ if (_hostScrollViewer == null)
+ {
+ throw new InvalidOperationException("This behavior can only be attached to an element which has a ScrollViewer as a parent.");
+ }
+
+ _hostScrollViewer.ViewChanged += ParentScrollViewer_ViewChanged;
+ }
+
+ private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
+ {
+ AssociatedObject.Loaded -= AssociatedObject_Loaded;
+ Init();
+ }
+}
diff --git a/components/Behaviors/tests/Behaviors.Tests.projitems b/components/Behaviors/tests/Behaviors.Tests.projitems
new file mode 100644
index 00000000..75ff7d8c
--- /dev/null
+++ b/components/Behaviors/tests/Behaviors.Tests.projitems
@@ -0,0 +1,23 @@
+
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+ true
+ C5924901-9000-4A6D-936D-BD69944E8C8F
+
+
+ BehaviorsExperiment.Tests
+
+
+
+
+ ExampleBehaviorsTestPage.xaml
+
+
+
+
+ Designer
+ MSBuild:Compile
+
+
+
\ No newline at end of file
diff --git a/components/Behaviors/tests/Behaviors.Tests.shproj b/components/Behaviors/tests/Behaviors.Tests.shproj
new file mode 100644
index 00000000..1a5e6e46
--- /dev/null
+++ b/components/Behaviors/tests/Behaviors.Tests.shproj
@@ -0,0 +1,13 @@
+
+
+
+ C5924901-9000-4A6D-936D-BD69944E8C8F
+ 14.0
+
+
+
+
+
+
+
+
diff --git a/components/Behaviors/tests/ExampleBehaviorsTestClass.cs b/components/Behaviors/tests/ExampleBehaviorsTestClass.cs
new file mode 100644
index 00000000..c22c3397
--- /dev/null
+++ b/components/Behaviors/tests/ExampleBehaviorsTestClass.cs
@@ -0,0 +1,41 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Tests;
+using CommunityToolkit.Tests.Internal;
+using CommunityToolkit.Tooling.TestGen;
+
+namespace BehaviorsExperiment.Tests;
+
+[TestClass]
+public partial class ExampleBehaviorsTestClass : VisualUITestBase
+{
+ // The UIThreadTestMethod can also easily grab a XAML Page for us by passing its type as a parameter.
+ // This lets us actually test a control as it would behave within an actual application.
+ // The page will already be loaded by the time your test is called.
+ [UIThreadTestMethod]
+ public void SimpleUIExamplePageTest(ExampleBehaviorsTestPage page)
+ {
+ // You can use the Toolkit Visual Tree helpers here to find the component by type or name:
+ /*var component = page.FindDescendant();
+
+ Assert.IsNotNull(component);
+
+ var componentByName = page.FindDescendant("BehaviorsControl");
+
+ Assert.IsNotNull(componentByName);*/
+ }
+
+ // You can still do async work with a UIThreadTestMethod as well.
+ [UIThreadTestMethod]
+ public async Task SimpleAsyncUIExamplePageTest(ExampleBehaviorsTestPage page)
+ {
+ // This helper can be used to wait for a rendering pass to complete.
+ await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { });
+
+ /*var component = page.FindDescendant();
+
+ Assert.IsNotNull(component);*/
+ }
+}
diff --git a/components/Behaviors/tests/ExampleBehaviorsTestPage.xaml b/components/Behaviors/tests/ExampleBehaviorsTestPage.xaml
new file mode 100644
index 00000000..b577b8ec
--- /dev/null
+++ b/components/Behaviors/tests/ExampleBehaviorsTestPage.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/components/Behaviors/tests/ExampleBehaviorsTestPage.xaml.cs b/components/Behaviors/tests/ExampleBehaviorsTestPage.xaml.cs
new file mode 100644
index 00000000..b9bface8
--- /dev/null
+++ b/components/Behaviors/tests/ExampleBehaviorsTestPage.xaml.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace BehaviorsExperiment.Tests;
+
+///
+/// An empty page that can be used on its own or navigated to within a Frame.
+///
+public sealed partial class ExampleBehaviorsTestPage : Page
+{
+ public ExampleBehaviorsTestPage()
+ {
+ this.InitializeComponent();
+ }
+}