public/controls/display/New-UiExpander.ps1
|
function New-UiExpander { <# .SYNOPSIS Creates a collapsible expander section with a header and content area. .DESCRIPTION Creates a theme-aware collapsible section. Click the header to toggle visibility of the content. Built from primitives for proper dark theme support. .PARAMETER Header The text displayed in the expander header. .PARAMETER Content Scriptblock containing the UI elements to show when expanded. .PARAMETER IsExpanded Start with the expander open. Default is collapsed. .PARAMETER Variable Variable name for accessing this control in button actions. .PARAMETER EnabledWhen Conditional enabling based on another control's state. Accepts either: - A control proxy (e.g., $toggleControl) - enables when that control is truthy - A scriptblock (e.g., { $toggle -and $userName }) - enables when expression is true Truthy values: CheckBox=checked, TextBox=non-empty, ComboBox=has selection. .PARAMETER ClearIfDisabled Accepted alongside -EnabledWhen for consistency, but an expander has nothing to clear: disabling leaves it exactly as it was. .PARAMETER WPFProperties Hashtable of additional WPF properties to set on the control. .EXAMPLE New-UiExpander -Header 'Advanced Options' -Content { New-UiToggle -Label 'Enable logging' -Variable 'enableLog' New-UiToggle -Label 'Verbose mode' -Variable 'verbose' } .EXAMPLE New-UiExpander -Header 'Details' -IsExpanded -Content { New-UiLabel -Text 'This section starts open' } #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Header, [Parameter(Mandatory)] [scriptblock]$Content, [switch]$IsExpanded, [Parameter()] [string]$Variable, [Parameter()] [object]$EnabledWhen, [switch]$ClearIfDisabled, [Parameter()] [hashtable]$WPFProperties ) # Grab session and theme context $session = Assert-UiSession -CallerName 'New-UiExpander' $colors = Get-ThemeColors $parent = $session.CurrentParent # Build the outer container with themed border $container = [System.Windows.Controls.Border]@{ BorderThickness = [System.Windows.Thickness]::new(1) BorderBrush = ConvertTo-UiBrush $colors.Border Background = ConvertTo-UiBrush $colors.ControlBg CornerRadius = [System.Windows.CornerRadius]::new(4) Margin = [System.Windows.Thickness]::new(4) Tag = 'ControlBgBrush' } # Stack panel holds header row and collapsible content $outerStack = [System.Windows.Controls.StackPanel]::new() $container.Child = $outerStack # Chevron glyph rotates 90deg when content is visible $chevron = [System.Windows.Controls.TextBlock]@{ Text = [PsUi.ModuleContext]::GetIcon('ChevronRight') FontFamily = [PsUi.ModuleContext]::ActiveIconFontFamily FontSize = 12 Foreground = ConvertTo-UiBrush $colors.SecondaryText VerticalAlignment = 'Center' Margin = [System.Windows.Thickness]::new(0, 0, 8, 0) RenderTransformOrigin = '0.5,0.5' } $chevron.RenderTransform = [System.Windows.Media.RotateTransform]::new(0) # Header label with semibold weight $headerText = [System.Windows.Controls.TextBlock]@{ Text = $Header FontFamily = [System.Windows.Media.FontFamily]::new('Segoe UI Variable, Segoe UI') FontSize = 13 FontWeight = 'SemiBold' Foreground = ConvertTo-UiBrush $colors.ControlFg VerticalAlignment = 'Center' Tag = 'ControlFgBrush' } # Clickable header row with chevron and label $headerPanel = [System.Windows.Controls.StackPanel]@{ Orientation = 'Horizontal' Cursor = 'Hand' Background = [System.Windows.Media.Brushes]::Transparent Margin = [System.Windows.Thickness]::new(10, 8, 10, 8) } [void]$headerPanel.Children.Add($chevron) [void]$headerPanel.Children.Add($headerText) [void]$outerStack.Children.Add($headerPanel) # Content area $contentPanel = [System.Windows.Controls.StackPanel]@{ Margin = [System.Windows.Thickness]::new(12, 0, 12, 10) Visibility = if ($IsExpanded) { 'Visible' } else { 'Collapsed' } } [void]$outerStack.Children.Add($contentPanel) if ($IsExpanded) { $chevron.RenderTransform.Angle = 90 } # Toggle visibility and rotate chevron on click $headerPanel.Add_MouseLeftButtonUp({ param($sender, $eventArgs) $outerStack = $sender.Parent $contentPanel = $outerStack.Children[1] $chevronGlyph = $sender.Children[0] if ($contentPanel.Visibility -eq 'Collapsed') { $contentPanel.Visibility = 'Visible' $chevronGlyph.RenderTransform.Angle = 90 } else { $contentPanel.Visibility = 'Collapsed' $chevronGlyph.RenderTransform.Angle = 0 } }) # Hover feedback on header row $headerPanel.Add_MouseEnter({ param($sender, $eventArgs) $sender.Opacity = 0.7 }) $headerPanel.Add_MouseLeave({ param($sender, $eventArgs) $sender.Opacity = 1.0 }) # Register control for variable hydration if name provided if ($Variable) { $session.AddControlSafe($Variable, $container) } # Register themed elements for dynamic switching [PsUi.ThemeEngine]::RegisterElement($container) [PsUi.ThemeEngine]::RegisterElement($headerText) Add-UiControlToParent -Control $container -Parent $parent # New-UiStatusBar reads the depth to keep a bar in here $previousParent = $session.CurrentParent $session.CurrentParent = $contentPanel $script:TabOrExpanderDepth = [int]$script:TabOrExpanderDepth + 1 # 5.1 NREs on a finally from a -NoAsync click or a row details template. The do loop keeps a break or continue in -Content from skipping the restores. try { do { & $Content } while ($false) } catch { $session.CurrentParent = $previousParent $script:TabOrExpanderDepth-- throw } $session.CurrentParent = $previousParent $script:TabOrExpanderDepth-- if ($WPFProperties) { Set-UiProperties -Control $container -Properties $WPFProperties } if ($EnabledWhen) { Register-UiCondition -TargetControl $container -Condition $EnabledWhen -ClearIfDisabled:$ClearIfDisabled } } |