Files
maspotlighttour/MarketAlly.MASpotlightTour/Onboarding.cs
2025-12-16 23:22:01 -05:00

297 lines
9.6 KiB
C#

namespace MarketAlly.SpotlightTour.Maui;
/// <summary>
/// Provides attached properties for tagging UI elements as onboarding/tour steps.
/// </summary>
public static class Onboarding
{
#region StepKey
/// <summary>
/// Identifies the step with a unique key. Falls back to AutomationId if not set.
/// </summary>
public static readonly BindableProperty StepKeyProperty =
BindableProperty.CreateAttached(
"StepKey",
typeof(string),
typeof(Onboarding),
defaultValue: null);
public static string? GetStepKey(BindableObject view) =>
(string?)view.GetValue(StepKeyProperty);
public static void SetStepKey(BindableObject view, string? value) =>
view.SetValue(StepKeyProperty, value);
#endregion
#region Title
/// <summary>
/// The title text displayed in the callout for this step.
/// </summary>
public static readonly BindableProperty TitleProperty =
BindableProperty.CreateAttached(
"Title",
typeof(string),
typeof(Onboarding),
defaultValue: null);
public static string? GetTitle(BindableObject view) =>
(string?)view.GetValue(TitleProperty);
public static void SetTitle(BindableObject view, string? value) =>
view.SetValue(TitleProperty, value);
#endregion
#region Description
/// <summary>
/// The description text displayed in the callout for this step.
/// </summary>
public static readonly BindableProperty DescriptionProperty =
BindableProperty.CreateAttached(
"Description",
typeof(string),
typeof(Onboarding),
defaultValue: null);
public static string? GetDescription(BindableObject view) =>
(string?)view.GetValue(DescriptionProperty);
public static void SetDescription(BindableObject view, string? value) =>
view.SetValue(DescriptionProperty, value);
#endregion
#region Order
/// <summary>
/// The order/sequence number for this step within its group.
/// </summary>
public static readonly BindableProperty OrderProperty =
BindableProperty.CreateAttached(
"Order",
typeof(int),
typeof(Onboarding),
defaultValue: 0);
public static int GetOrder(BindableObject view) =>
(int)view.GetValue(OrderProperty);
public static void SetOrder(BindableObject view, int value) =>
view.SetValue(OrderProperty, value);
#endregion
#region Group
/// <summary>
/// The group name for organizing multiple tours on the same page.
/// </summary>
public static readonly BindableProperty GroupProperty =
BindableProperty.CreateAttached(
"Group",
typeof(string),
typeof(Onboarding),
defaultValue: null);
public static string? GetGroup(BindableObject view) =>
(string?)view.GetValue(GroupProperty);
public static void SetGroup(BindableObject view, string? value) =>
view.SetValue(GroupProperty, value);
#endregion
#region SpotlightEnabled
/// <summary>
/// Whether this element participates in spotlight cutouts.
/// </summary>
public static readonly BindableProperty SpotlightEnabledProperty =
BindableProperty.CreateAttached(
"SpotlightEnabled",
typeof(bool),
typeof(Onboarding),
defaultValue: true);
public static bool GetSpotlightEnabled(BindableObject view) =>
(bool)view.GetValue(SpotlightEnabledProperty);
public static void SetSpotlightEnabled(BindableObject view, bool value) =>
view.SetValue(SpotlightEnabledProperty, value);
#endregion
#region Placement
/// <summary>
/// Where to place the callout card relative to the spotlight.
/// </summary>
public static readonly BindableProperty PlacementProperty =
BindableProperty.CreateAttached(
"Placement",
typeof(CalloutPlacement),
typeof(Onboarding),
defaultValue: CalloutPlacement.Auto);
public static CalloutPlacement GetPlacement(BindableObject view) =>
(CalloutPlacement)view.GetValue(PlacementProperty);
public static void SetPlacement(BindableObject view, CalloutPlacement value) =>
view.SetValue(PlacementProperty, value);
#endregion
#region SpotlightShape
/// <summary>
/// The shape of the spotlight cutout for this step.
/// </summary>
public static readonly BindableProperty SpotlightShapeProperty =
BindableProperty.CreateAttached(
"SpotlightShape",
typeof(SpotlightShape),
typeof(Onboarding),
defaultValue: SpotlightShape.RoundedRectangle);
public static SpotlightShape GetSpotlightShape(BindableObject view) =>
(SpotlightShape)view.GetValue(SpotlightShapeProperty);
public static void SetSpotlightShape(BindableObject view, SpotlightShape value) =>
view.SetValue(SpotlightShapeProperty, value);
#endregion
#region SpotlightPadding
/// <summary>
/// Padding around the target element in the spotlight cutout.
/// </summary>
public static readonly BindableProperty SpotlightPaddingProperty =
BindableProperty.CreateAttached(
"SpotlightPadding",
typeof(Thickness),
typeof(Onboarding),
defaultValue: new Thickness(8));
public static Thickness GetSpotlightPadding(BindableObject view) =>
(Thickness)view.GetValue(SpotlightPaddingProperty);
public static void SetSpotlightPadding(BindableObject view, Thickness value) =>
view.SetValue(SpotlightPaddingProperty, value);
#endregion
#region SpotlightCornerRadius
/// <summary>
/// Corner radius for RoundedRectangle spotlight shape.
/// </summary>
public static readonly BindableProperty SpotlightCornerRadiusProperty =
BindableProperty.CreateAttached(
"SpotlightCornerRadius",
typeof(double),
typeof(Onboarding),
defaultValue: 8.0);
public static double GetSpotlightCornerRadius(BindableObject view) =>
(double)view.GetValue(SpotlightCornerRadiusProperty);
public static void SetSpotlightCornerRadius(BindableObject view, double value) =>
view.SetValue(SpotlightCornerRadiusProperty, value);
#endregion
#region TapBehavior
/// <summary>
/// Behavior when the user taps on the spotlighted element.
/// </summary>
public static readonly BindableProperty TapBehaviorProperty =
BindableProperty.CreateAttached(
"TapBehavior",
typeof(SpotlightTapBehavior),
typeof(Onboarding),
defaultValue: SpotlightTapBehavior.None);
public static SpotlightTapBehavior GetTapBehavior(BindableObject view) =>
(SpotlightTapBehavior)view.GetValue(TapBehaviorProperty);
public static void SetTapBehavior(BindableObject view, SpotlightTapBehavior value) =>
view.SetValue(TapBehaviorProperty, value);
#endregion
#region OnEntering
/// <summary>
/// Action to execute when the tour enters this step.
/// The action runs before the spotlight animates to the element, allowing UI preparation
/// (e.g., switching tabs, opening drawers) to complete first.
/// </summary>
public static readonly BindableProperty OnEnteringProperty =
BindableProperty.CreateAttached(
"OnEntering",
typeof(Func<OnboardingStep, CancellationToken, Task>),
typeof(Onboarding),
defaultValue: null);
public static Func<OnboardingStep, CancellationToken, Task>? GetOnEntering(BindableObject view) =>
(Func<OnboardingStep, CancellationToken, Task>?)view.GetValue(OnEnteringProperty);
public static void SetOnEntering(BindableObject view, Func<OnboardingStep, CancellationToken, Task>? value) =>
view.SetValue(OnEnteringProperty, value);
#endregion
#region OnLeaving
/// <summary>
/// Action to execute when the tour leaves this step.
/// The action runs before navigating to the next/previous step.
/// </summary>
public static readonly BindableProperty OnLeavingProperty =
BindableProperty.CreateAttached(
"OnLeaving",
typeof(Func<OnboardingStep, CancellationToken, Task>),
typeof(Onboarding),
defaultValue: null);
public static Func<OnboardingStep, CancellationToken, Task>? GetOnLeaving(BindableObject view) =>
(Func<OnboardingStep, CancellationToken, Task>?)view.GetValue(OnLeavingProperty);
public static void SetOnLeaving(BindableObject view, Func<OnboardingStep, CancellationToken, Task>? value) =>
view.SetValue(OnLeavingProperty, value);
#endregion
/// <summary>
/// Helper to get the effective step key, falling back to AutomationId.
/// </summary>
public static string? GetEffectiveStepKey(BindableObject view)
{
var stepKey = GetStepKey(view);
if (!string.IsNullOrWhiteSpace(stepKey))
return stepKey;
if (view is VisualElement ve && !string.IsNullOrWhiteSpace(ve.AutomationId))
return ve.AutomationId;
return null;
}
/// <summary>
/// Determines if an element is tagged as an onboarding step.
/// </summary>
public static bool IsOnboardingStep(BindableObject view)
{
// An element is considered a step if it has a StepKey, Title, or Description set
return !string.IsNullOrWhiteSpace(GetStepKey(view)) ||
!string.IsNullOrWhiteSpace(GetTitle(view)) ||
!string.IsNullOrWhiteSpace(GetDescription(view));
}
}