diff --git a/API_Reference.md b/API_Reference.md index 8a00ed5..a87838a 100644 --- a/API_Reference.md +++ b/API_Reference.md @@ -203,10 +203,48 @@ Dismisses the intro view and continues with the tour. | `TourCompleted` | `EventArgs` | Raised when the user completes all steps. | | `TourSkipped` | `EventArgs` | Raised when the user skips the tour. | | `TourEnded` | `EventArgs` | Raised when the tour ends (any reason). | +| `StepEntering` | `StepActionEventArgs` | Raised before entering a step. Async. Can set `Skip = true`. | +| `StepEntered` | `StepActionEventArgs` | Raised after step is fully visible (after animations). | +| `StepLeaving` | `StepActionEventArgs` | Raised before leaving the current step. Async. | | `StepChanged` | `OnboardingStepEventArgs` | Raised when moving to a new step. | | `IntroShown` | `EventArgs` | Raised when the intro view is shown. | | `IntroDismissed` | `EventArgs` | Raised when the intro view is dismissed. | +### Step Action Registration + +```csharp +void RegisterStepEnteringAction(string stepKey, Func action) +``` +Registers an action to execute when entering a specific step (by StepKey). The action runs before the spotlight animates to the step. + +--- + +```csharp +void RegisterStepLeavingAction(string stepKey, Func action) +``` +Registers an action to execute when leaving a specific step (by StepKey). The action runs before navigating away. + +--- + +```csharp +bool UnregisterStepEnteringAction(string stepKey) +``` +Removes a registered entering action for a step. Returns true if the action was removed. + +--- + +```csharp +bool UnregisterStepLeavingAction(string stepKey) +``` +Removes a registered leaving action for a step. Returns true if the action was removed. + +--- + +```csharp +void ClearStepActions() +``` +Clears all registered step actions (both entering and leaving). + ### Usage Example ```xml @@ -268,6 +306,13 @@ Static class providing attached properties for tagging UI elements as onboarding | `Placement` | `CalloutPlacement` | `Auto` | Where to place the callout relative to this element. | | `TapBehavior` | `SpotlightTapBehavior` | `None` | Behavior when user taps the spotlight. | +#### Step Actions + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `OnEntering` | `Func?` | `null` | Action to execute when entering this step. Runs before spotlight animation. | +| `OnLeaving` | `Func?` | `null` | Action to execute when leaving this step. Runs before navigating away. | + ### Static Methods ```csharp @@ -312,6 +357,19 @@ Onboarding.SetSpotlightShape(myButton, SpotlightShape.Circle); // Read properties var key = Onboarding.GetStepKey(myButton); var title = Onboarding.GetTitle(myButton); + +// Set step actions (async actions that run when entering/leaving this step) +Onboarding.SetOnEntering(myButton, async (step, cancellationToken) => +{ + // Prepare UI before spotlight appears (e.g., switch tabs, open drawer) + MainTabView.SelectedIndex = 2; + await Task.Delay(100); +}); + +Onboarding.SetOnLeaving(myButton, async (step, cancellationToken) => +{ + // Cleanup when leaving this step +}); ``` --- @@ -336,6 +394,8 @@ Represents a single step in an onboarding tour. Created automatically from tagge | `SpotlightPadding` | `Thickness` | Padding around target. Default: `8`. | | `SpotlightCornerRadius` | `double` | Corner radius for rounded rectangle. Default: `8.0`. | | `TapBehavior` | `SpotlightTapBehavior` | Behavior when tapping spotlight. Default: `None`. | +| `OnEntering` | `Func?` | Action to execute when entering this step. | +| `OnLeaving` | `Func?` | Action to execute when leaving this step. | ### Static Methods @@ -559,6 +619,64 @@ TourHost.StepChanged += (sender, e) => --- +### StepActionEventArgs + +Event arguments for step action events (`StepEntering`, `StepEntered`, `StepLeaving`). + +| Property | Type | Description | +|----------|------|-------------| +| `Step` | `OnboardingStep` | The step being entered or left. | +| `StepIndex` | `int` | Zero-based index of the step. | +| `TotalSteps` | `int` | Total number of steps in the tour. | +| `PreviousStepIndex` | `int` | Index of the previous step (-1 if first step). | +| `IsForward` | `bool` | Whether navigation is moving forward (true) or backward (false). | +| `Skip` | `bool` | Set to `true` to skip this step (only applies to `StepEntering`). | +| `CancellationToken` | `CancellationToken` | Cancellation token for the current operation. | + +### Step Action Usage Example + +```csharp +// Register action by StepKey +TourHost.RegisterStepEnteringAction("settings-tab", async (step, token) => +{ + // Switch to settings tab before spotlight appears + SettingsTabView.SelectedIndex = 1; + await Task.Delay(150); // Wait for tab transition +}); + +// Use event for conditional skipping +TourHost.StepEntering += async (sender, e) => +{ + // Skip premium-only steps for free users + if (e.Step.StepKey?.StartsWith("premium-") == true && !User.IsPremium) + { + e.Skip = true; + } +}; + +// Track when steps are viewed +TourHost.StepEntered += (sender, e) => +{ + Analytics.Track("tour_step_viewed", new { + step_key = e.Step.StepKey, + step_index = e.StepIndex, + is_forward = e.IsForward + }); +}; + +// Cleanup when leaving steps +TourHost.StepLeaving += async (sender, e) => +{ + if (e.Step.StepKey == "drawer-step") + { + Shell.Current.FlyoutIsPresented = false; + await Task.Delay(200); + } +}; +``` + +--- + ## Complete Integration Example ### XAML (MainPage.xaml) diff --git a/MarketAlly.MASpotlightTour/MASpotlightTour.Maui.csproj b/MarketAlly.MASpotlightTour/MASpotlightTour.Maui.csproj index bc12a75..e57217e 100644 --- a/MarketAlly.MASpotlightTour/MASpotlightTour.Maui.csproj +++ b/MarketAlly.MASpotlightTour/MASpotlightTour.Maui.csproj @@ -17,7 +17,7 @@ MarketAlly.SpotlightTour.Maui MASpotlightTour - Feature Tour & Onboarding for .NET MAUI - 1.0.0 + 1.1.0 David H Friedel Jr MarketAlly A powerful, declarative spotlight tour and onboarding library for .NET MAUI applications. Create beautiful feature tours with spotlight overlays, callout cards, and step-by-step guidance using simple XAML attached properties. Features include: multiple display modes (spotlight with callout, callout-only, inline labels), flexible callout positioning (following, fixed corner, auto-corner), customizable spotlight shapes (rectangle, rounded rectangle, circle), corner navigator with step indicators, intro/welcome views, auto-advance timers, tour looping, light/dark theme support, smooth animations, and awaitable tour completion for action chaining. Perfect for user onboarding, feature discovery, and interactive tutorials. Supports iOS, Android, Windows, and macOS. @@ -30,6 +30,15 @@ MIT README.md +Version 1.1.0 - Step Actions: +- NEW: Step Actions - Execute custom async code when entering/leaving tour steps +- NEW: OnEntering/OnLeaving attached properties for per-element actions +- NEW: RegisterStepEnteringAction/RegisterStepLeavingAction for centralized registration by StepKey +- NEW: StepEntering event with Skip property for conditional step skipping +- NEW: StepEntered event fired after step animations complete +- NEW: StepLeaving event for cleanup before navigation +- Perfect for UI preparation: switching tabs, opening drawers, triggering animations + Version 1.0.0 - Initial Release: - Declarative tour steps via XAML attached properties - Three display modes: SpotlightWithCallout, CalloutOnly, SpotlightWithInlineLabel diff --git a/MarketAlly.MASpotlightTour/Onboarding.cs b/MarketAlly.MASpotlightTour/Onboarding.cs index 1f01058..44dfb81 100644 --- a/MarketAlly.MASpotlightTour/Onboarding.cs +++ b/MarketAlly.MASpotlightTour/Onboarding.cs @@ -225,6 +225,49 @@ public static class Onboarding #endregion + #region OnEntering + + /// + /// 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. + /// + public static readonly BindableProperty OnEnteringProperty = + BindableProperty.CreateAttached( + "OnEntering", + typeof(Func), + typeof(Onboarding), + defaultValue: null); + + public static Func? GetOnEntering(BindableObject view) => + (Func?)view.GetValue(OnEnteringProperty); + + public static void SetOnEntering(BindableObject view, Func? value) => + view.SetValue(OnEnteringProperty, value); + + #endregion + + #region OnLeaving + + /// + /// Action to execute when the tour leaves this step. + /// The action runs before navigating to the next/previous step. + /// + public static readonly BindableProperty OnLeavingProperty = + BindableProperty.CreateAttached( + "OnLeaving", + typeof(Func), + typeof(Onboarding), + defaultValue: null); + + public static Func? GetOnLeaving(BindableObject view) => + (Func?)view.GetValue(OnLeavingProperty); + + public static void SetOnLeaving(BindableObject view, Func? value) => + view.SetValue(OnLeavingProperty, value); + + #endregion + /// /// Helper to get the effective step key, falling back to AutomationId. /// diff --git a/MarketAlly.MASpotlightTour/OnboardingHost.cs b/MarketAlly.MASpotlightTour/OnboardingHost.cs index ad24096..0f2a453 100644 --- a/MarketAlly.MASpotlightTour/OnboardingHost.cs +++ b/MarketAlly.MASpotlightTour/OnboardingHost.cs @@ -27,6 +27,8 @@ public class OnboardingHost : Grid, IDisposable private CalloutCorner? _currentCalloutCorner; private readonly SemaphoreSlim _tourLock = new(1, 1); private bool _isDisposed; + private readonly Dictionary> _enteringActions = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> _leavingActions = new(StringComparer.OrdinalIgnoreCase); #region Bindable Properties @@ -351,6 +353,24 @@ public class OnboardingHost : Grid, IDisposable /// public event EventHandler? IntroDismissed; + /// + /// Raised before entering a step, allowing async preparation or skipping. + /// Actions execute before the spotlight animates to the new position. + /// Set e.Skip = true to skip this step. + /// + public event Func? StepEntering; + + /// + /// Raised after a step is fully visible (after animations complete). + /// + public event EventHandler? StepEntered; + + /// + /// Raised before leaving the current step. + /// Actions execute before navigating to the next/previous step. + /// + public event Func? StepLeaving; + #endregion #region Read-only Properties @@ -384,6 +404,65 @@ public class OnboardingHost : Grid, IDisposable #endregion + #region Step Action Registration + + /// + /// Registers an action to execute when entering a specific step (by StepKey). + /// The action runs before the spotlight animates to the step. + /// + /// The StepKey of the target step. + /// The async action to execute. Receives the step and cancellation token. + public void RegisterStepEnteringAction(string stepKey, Func action) + { + ArgumentNullException.ThrowIfNull(stepKey); + ArgumentNullException.ThrowIfNull(action); + _enteringActions[stepKey] = action; + } + + /// + /// Registers an action to execute when leaving a specific step (by StepKey). + /// The action runs before navigating away from the step. + /// + /// The StepKey of the target step. + /// The async action to execute. Receives the step and cancellation token. + public void RegisterStepLeavingAction(string stepKey, Func action) + { + ArgumentNullException.ThrowIfNull(stepKey); + ArgumentNullException.ThrowIfNull(action); + _leavingActions[stepKey] = action; + } + + /// + /// Removes a registered entering action for a step. + /// + /// The StepKey of the target step. + /// True if the action was removed, false if no action was registered. + public bool UnregisterStepEnteringAction(string stepKey) + { + return _enteringActions.Remove(stepKey); + } + + /// + /// Removes a registered leaving action for a step. + /// + /// The StepKey of the target step. + /// True if the action was removed, false if no action was registered. + public bool UnregisterStepLeavingAction(string stepKey) + { + return _leavingActions.Remove(stepKey); + } + + /// + /// Clears all registered step actions. + /// + public void ClearStepActions() + { + _enteringActions.Clear(); + _leavingActions.Clear(); + } + + #endregion + public OnboardingHost() { IsVisible = false; @@ -798,6 +877,131 @@ public class OnboardingHost : Grid, IDisposable return await _tourCompletionSource.Task; } + /// + /// Executes all entering actions for a step (event, attached property, registered action). + /// Returns a StepActionEventArgs that may have Skip set to true. + /// + private async Task ExecuteStepEnteringActionsAsync( + OnboardingStep step, + int stepIndex, + int previousIndex, + CancellationToken cancellationToken = default) + { + var isForward = previousIndex < stepIndex || previousIndex < 0; + var eventArgs = new StepActionEventArgs( + step, stepIndex, _steps.Count, previousIndex, isForward, cancellationToken); + + // 1. Fire StepEntering event (allows Skip) + if (StepEntering != null) + { + try + { + await StepEntering.Invoke(this, eventArgs).ConfigureAwait(false); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[OnboardingHost] StepEntering event handler failed: {ex.Message}"); + } + } + + // If skipped via event, don't run other actions + if (eventArgs.Skip) + return eventArgs; + + // 2. Execute attached property action (OnEntering on the element) + if (step.OnEntering != null) + { + try + { + await step.OnEntering(step, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[OnboardingHost] OnEntering attached action failed: {ex.Message}"); + } + } + + // 3. Execute registered action (by StepKey) + if (!string.IsNullOrEmpty(step.StepKey) && _enteringActions.TryGetValue(step.StepKey, out var registeredAction)) + { + try + { + await registeredAction(step, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[OnboardingHost] Registered entering action failed: {ex.Message}"); + } + } + + return eventArgs; + } + + /// + /// Executes all leaving actions for a step (event, attached property, registered action). + /// + private async Task ExecuteStepLeavingActionsAsync( + OnboardingStep step, + int stepIndex, + int nextIndex, + CancellationToken cancellationToken = default) + { + var isForward = nextIndex > stepIndex; + var eventArgs = new StepActionEventArgs( + step, stepIndex, _steps.Count, stepIndex, isForward, cancellationToken); + + // 1. Fire StepLeaving event + if (StepLeaving != null) + { + try + { + await StepLeaving.Invoke(this, eventArgs).ConfigureAwait(false); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[OnboardingHost] StepLeaving event handler failed: {ex.Message}"); + } + } + + // 2. Execute attached property action (OnLeaving on the element) + if (step.OnLeaving != null) + { + try + { + await step.OnLeaving(step, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[OnboardingHost] OnLeaving attached action failed: {ex.Message}"); + } + } + + // 3. Execute registered action (by StepKey) + if (!string.IsNullOrEmpty(step.StepKey) && _leavingActions.TryGetValue(step.StepKey, out var registeredAction)) + { + try + { + await registeredAction(step, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[OnboardingHost] Registered leaving action failed: {ex.Message}"); + } + } + } + + /// + /// Fires the StepEntered event after animations complete. + /// + private void RaiseStepEntered(OnboardingStep step, int stepIndex, int previousIndex) + { + var isForward = previousIndex < stepIndex || previousIndex < 0; + var eventArgs = new StepActionEventArgs( + step, stepIndex, _steps.Count, previousIndex, isForward); + + StepEntered?.Invoke(this, eventArgs); + } + /// /// Navigates to a specific step by index. /// @@ -812,9 +1016,40 @@ public class OnboardingHost : Grid, IDisposable } var previousIndex = _currentIndex; + var step = _steps[index]; + + // Execute leaving actions for current step (if any) + if (previousIndex >= 0 && previousIndex < _steps.Count) + { + var previousStep = _steps[previousIndex]; + await ExecuteStepLeavingActionsAsync(previousStep, previousIndex, index).ConfigureAwait(false); + } + + // Execute entering actions for new step (before UI updates) + var enteringArgs = await ExecuteStepEnteringActionsAsync(step, index, previousIndex).ConfigureAwait(false); + + // Handle skip - move to next or previous step + if (enteringArgs.Skip) + { + System.Diagnostics.Debug.WriteLine($"[OnboardingHost] Step {index} skipped via StepEntering event"); + var isForward = previousIndex < index || previousIndex < 0; + var nextIndex = isForward ? index + 1 : index - 1; + + if (nextIndex >= 0 && nextIndex < _steps.Count) + { + await GoToStepAsync(nextIndex).ConfigureAwait(false); + } + else if (isForward) + { + // No more steps forward, complete tour + await CompleteTourAsync().ConfigureAwait(false); + } + // If going backward with no more steps, stay at current + return; + } + _currentIndex = index; System.Diagnostics.Debug.WriteLine($"[OnboardingHost] GoToStepAsync: previousIndex={previousIndex}, new _currentIndex={_currentIndex}"); - var step = _steps[index]; // Ensure element is visible (scroll if needed) - with timeout to prevent hanging try @@ -899,7 +1134,10 @@ public class OnboardingHost : Grid, IDisposable } _cornerNavigator.UpdateAutoPlacement(bounds, new Size(Width, Height), calloutBounds, _currentCalloutCorner); - // Raise event + // Raise StepEntered event (after animations complete) + RaiseStepEntered(step, index, previousIndex); + + // Raise legacy StepChanged event StepChanged?.Invoke(this, new OnboardingStepEventArgs(step, index, _steps.Count)); // Start auto-advance timer if enabled @@ -1358,9 +1596,39 @@ public class OnboardingHost : Grid, IDisposable var config = TourConfiguration.Default; var previousIndex = _currentIndex; - _currentIndex = index; var step = _steps[index]; + // Execute leaving actions for current step (if any) + if (previousIndex >= 0 && previousIndex < _steps.Count) + { + var previousStep = _steps[previousIndex]; + await ExecuteStepLeavingActionsAsync(previousStep, previousIndex, index, cancellationToken).ConfigureAwait(false); + } + + // Execute entering actions for new step (before UI updates) + var enteringArgs = await ExecuteStepEnteringActionsAsync(step, index, previousIndex, cancellationToken).ConfigureAwait(false); + + // Handle skip - move to next or previous step + if (enteringArgs.Skip) + { + System.Diagnostics.Debug.WriteLine($"[OnboardingHost] Step {index} skipped via StepEntering event (internal)"); + var isForward = previousIndex < index || previousIndex < 0; + var nextIndex = isForward ? index + 1 : index - 1; + + if (nextIndex >= 0 && nextIndex < _steps.Count) + { + await GoToStepInternalAsync(nextIndex, cancellationToken).ConfigureAwait(false); + } + else if (isForward) + { + // No more steps forward, complete tour + await CompleteTourAsync().ConfigureAwait(false); + } + return; + } + + _currentIndex = index; + // Ensure element is visible (scroll if needed) - with timeout to prevent hanging try { @@ -1448,7 +1716,10 @@ public class OnboardingHost : Grid, IDisposable } _cornerNavigator.UpdateAutoPlacement(bounds, new Size(Width, Height), calloutBounds, _currentCalloutCorner); - // Raise event + // Raise StepEntered event (after animations complete) + RaiseStepEntered(step, index, previousIndex); + + // Raise legacy StepChanged event StepChanged?.Invoke(this, new OnboardingStepEventArgs(step, index, _steps.Count)); // Start auto-advance timer if enabled @@ -1843,3 +2114,62 @@ public class OnboardingStepEventArgs : EventArgs TotalSteps = totalSteps; } } + +/// +/// Event args for step action events (StepEntering, StepLeaving). +/// Allows actions to skip steps or perform async preparation. +/// +public class StepActionEventArgs : EventArgs +{ + /// + /// The step being entered or left. + /// + public OnboardingStep Step { get; } + + /// + /// The index of the step (0-based). + /// + public int StepIndex { get; } + + /// + /// Total number of steps in the tour. + /// + public int TotalSteps { get; } + + /// + /// The index of the previous step (-1 if this is the first step). + /// + public int PreviousStepIndex { get; } + + /// + /// Whether navigation is moving forward (true) or backward (false). + /// + public bool IsForward { get; } + + /// + /// Set to true to skip this step and move to the next one. + /// Only applies to StepEntering event. + /// + public bool Skip { get; set; } + + /// + /// Cancellation token for the current operation. + /// + public CancellationToken CancellationToken { get; } + + public StepActionEventArgs( + OnboardingStep step, + int stepIndex, + int totalSteps, + int previousStepIndex, + bool isForward, + CancellationToken cancellationToken = default) + { + Step = step; + StepIndex = stepIndex; + TotalSteps = totalSteps; + PreviousStepIndex = previousStepIndex; + IsForward = isForward; + CancellationToken = cancellationToken; + } +} diff --git a/MarketAlly.MASpotlightTour/OnboardingStep.cs b/MarketAlly.MASpotlightTour/OnboardingStep.cs index 8bbebdc..c533112 100644 --- a/MarketAlly.MASpotlightTour/OnboardingStep.cs +++ b/MarketAlly.MASpotlightTour/OnboardingStep.cs @@ -65,6 +65,18 @@ public class OnboardingStep /// public SpotlightTapBehavior TapBehavior { get; init; } = SpotlightTapBehavior.None; + /// + /// Action to execute when entering this step. + /// Runs before the spotlight animates to allow UI preparation. + /// + public Func? OnEntering { get; init; } + + /// + /// Action to execute when leaving this step. + /// Runs before navigating to the next/previous step. + /// + public Func? OnLeaving { get; init; } + /// /// Creates an OnboardingStep from a tagged VisualElement. /// @@ -83,7 +95,9 @@ public class OnboardingStep SpotlightShape = Onboarding.GetSpotlightShape(element), SpotlightPadding = Onboarding.GetSpotlightPadding(element), SpotlightCornerRadius = Onboarding.GetSpotlightCornerRadius(element), - TapBehavior = Onboarding.GetTapBehavior(element) + TapBehavior = Onboarding.GetTapBehavior(element), + OnEntering = Onboarding.GetOnEntering(element), + OnLeaving = Onboarding.GetOnLeaving(element) }; } } diff --git a/README.md b/README.md index f938fad..d19b61b 100644 --- a/README.md +++ b/README.md @@ -332,17 +332,109 @@ TourHost.SkipTour(); TourHost.StopTour(); ``` +## Step Actions + +Execute custom code when entering or leaving tour steps. Perfect for UI preparation like switching tabs, opening drawers, or triggering animations before the spotlight appears. + +### Attached Property (Per-Element) + +```csharp +// In code-behind, attach action directly to the element +Onboarding.SetOnEntering(SettingsTab, async (step, cancellationToken) => +{ + // Switch to the settings tab before spotlight appears + MainTabView.SelectedIndex = 2; + await Task.Delay(100); // Wait for tab transition +}); + +Onboarding.SetOnLeaving(SettingsTab, async (step, cancellationToken) => +{ + // Cleanup when leaving this step + MainTabView.SelectedIndex = 0; +}); +``` + +### Centralized Registration (By StepKey) + +```csharp +// Register actions by step key - keeps tour logic organized +TourHost.RegisterStepEnteringAction("drawer-step", async (step, token) => +{ + Shell.Current.FlyoutIsPresented = true; + await Task.Delay(250); // Wait for drawer animation +}); + +TourHost.RegisterStepEnteringAction("settings-tab", async (step, token) => +{ + SettingsTabButton.IsSelected = true; +}); + +// Remove registered actions when no longer needed +TourHost.UnregisterStepEnteringAction("drawer-step"); +TourHost.ClearStepActions(); // Clear all +``` + +### Step Action Events + +```csharp +// Global event - fires for every step (useful for analytics, logging, conditional skipping) +TourHost.StepEntering += async (sender, e) => +{ + // Skip premium features for non-premium users + if (e.Step.StepKey == "premium-feature" && !currentUser.IsPremium) + { + e.Skip = true; // Skip this step, move to next + return; + } + + // Log analytics + Analytics.Track("tour_step_entering", e.Step.StepKey); +}; + +// Fires after step is fully visible (animations complete) +TourHost.StepEntered += (sender, e) => +{ + Console.WriteLine($"Now viewing step: {e.Step.Title}"); +}; + +// Fires before leaving current step +TourHost.StepLeaving += async (sender, e) => +{ + // Perform cleanup before navigating away + await SaveStepProgressAsync(e.StepIndex); +}; +``` + +### Execution Order + +Actions execute in this order when navigating to a step: + +1. `StepLeaving` event + actions (previous step) +2. `StepEntering` event (can set `Skip = true`) +3. `OnEntering` attached property action +4. Registered entering action (by StepKey) +5. Scroll target into view & animate spotlight +6. `StepEntered` event + ## Events ```csharp +// Tour lifecycle events TourHost.TourStarted += (s, e) => { /* Tour began */ }; TourHost.TourCompleted += (s, e) => { /* User finished all steps */ }; TourHost.TourSkipped += (s, e) => { /* User skipped */ }; TourHost.TourEnded += (s, e) => { /* Tour ended (any reason) */ }; + +// Step navigation events +TourHost.StepEntering += async (s, e) => { /* Before entering step - can set e.Skip = true */ }; +TourHost.StepEntered += (s, e) => { /* After step is visible */ }; +TourHost.StepLeaving += async (s, e) => { /* Before leaving step */ }; TourHost.StepChanged += (s, e) => { Console.WriteLine($"Step {e.StepIndex + 1}/{e.TotalSteps}: {e.Step.Title}"); }; + +// Intro events TourHost.IntroShown += (s, e) => { /* Intro view displayed */ }; TourHost.IntroDismissed += (s, e) => { /* Intro view dismissed */ }; ``` diff --git a/Test.SpotlightTour/AppShell.xaml b/Test.SpotlightTour/AppShell.xaml index fdd78fc..94e5bb9 100644 --- a/Test.SpotlightTour/AppShell.xaml +++ b/Test.SpotlightTour/AppShell.xaml @@ -77,4 +77,11 @@ Route="AnimationsPage" /> + + + + diff --git a/Test.SpotlightTour/MainPage.xaml b/Test.SpotlightTour/MainPage.xaml index d32758d..ba47122 100644 --- a/Test.SpotlightTour/MainPage.xaml +++ b/Test.SpotlightTour/MainPage.xaml @@ -150,6 +150,16 @@ + + + + + + + +