Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding accessible context menus to TabView pages #1664

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
102 changes: 102 additions & 0 deletions WinUIGallery/Helpers/TabViewHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System.Collections;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;

namespace WinUIGallery.Helpers;

public static class TabViewHelper
{
public static void PopulateTabViewContextMenu(MenuFlyout contextMenu)
{
contextMenu.Items.Clear();

var item = (TabViewItem)contextMenu.Target;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check type first before typecasting?

ListView tabViewListView = null;
TabView tabView = null;

DependencyObject current = item;

while (current != null)
{
DependencyObject parent = VisualTreeHelper.GetParent(current);

if (parent is ListView parentTabViewListView)
{
tabViewListView = parentTabViewListView;
}
else if (parent is TabView parentTabView)
{
tabView = parentTabView;
}

if (tabViewListView != null && tabView != null)
{
break;
}

current = parent;
}

if (tabViewListView == null || tabView == null)
{
return;
}

// First, if there are tabs to the left or to the right of the tab on which this context menu is opening,
// then we'll include menu items to move this tab to the left or to the right.
//
// There are two possible cases for tab views: either they have explicitly set tab items, or they have a data item source set.
// To move a tab left or right with explicitly set tab items, we'll remove and replace the tab item itself.
// To move a tab left or right with a data item source set, we'll instead remove and replace the data item in the source list.
int index = tabViewListView.IndexFromContainer(item);

if (index > 0)
{
MenuFlyoutItem moveLeftItem = new() { Text = "Move tab left" };
moveLeftItem.Click += (s, args) =>
{
if (tabView.TabItemsSource is IList itemsSourceList)
{
var item = itemsSourceList[index];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will index get copied over to lambda as it will be cleaned up from this method before the Click event actually happen?

itemsSourceList.RemoveAt(index);
itemsSourceList.Insert(index - 1, item);
}
else
{
var item = tabView.TabItems[index];
tabView.TabItems.RemoveAt(index);
tabView.TabItems.Insert(index - 1, item);
}
};
contextMenu.Items.Add(moveLeftItem);
}

if (index < tabViewListView.Items.Count - 1)
{
MenuFlyoutItem moveRightItem = new() { Text = "Move tab right" };
moveRightItem.Click += (s, args) =>
{
if (tabView.TabItemsSource is IList itemsSourceList)
{
var item = itemsSourceList[index];
itemsSourceList.RemoveAt(index);
itemsSourceList.Insert(index + 1, item);
}
else
{
var item = tabView.TabItems[index];
tabView.TabItems.RemoveAt(index);
tabView.TabItems.Insert(index + 1, item);
}
};
contextMenu.Items.Add(moveRightItem);
}

// If the context menu ended up with no items at all, then we'll prevent it from being shown.
if (contextMenu.Items.Count == 0)
{
contextMenu.Hide();
}
}
}
17 changes: 17 additions & 0 deletions WinUIGallery/Helpers/UIHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,21 @@ static public void AnnounceActionForAccessibility(UIElement ue, string annouceme
peer.RaiseNotificationEvent(AutomationNotificationKind.ActionCompleted,
AutomationNotificationProcessing.ImportantMostRecent, annoucement, activityID);
}

public static T GetParent<T>(DependencyObject child) where T : DependencyObject
{
DependencyObject current = child;

while (current != null)
{
if (current is T parent)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's behavior of this function that we want (not current behavior) when current DO is of type T and it has a parent of type T?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is reasonable to return the first result, otherwise you would need to scan up to the root to find the latest T

{
return parent;
}

current = VisualTreeHelper.GetParent(current);
}

return null;
}
}
4 changes: 2 additions & 2 deletions WinUIGallery/Helpers/Win32WindowHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ namespace WinUIGallery.Helpers;

internal class Win32WindowHelper
{
private static WinProc newWndProc = null;
private static nint oldWndProc = nint.Zero;
private WinProc newWndProc = null;
private nint oldWndProc = nint.Zero;

private POINT? minWindowSize = null;
private POINT? maxWindowSize = null;
Expand Down
31 changes: 18 additions & 13 deletions WinUIGallery/Samples/ControlPages/TabViewPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
xmlns:samplepages="using:WinUIGallery.SamplePages"
mc:Ignorable="d">

<Page.Resources>
<MenuFlyout x:Name="TabViewContextMenu" Opening="TabViewContextMenu_Opening" />
</Page.Resources>

<StackPanel>
<controls:ControlExample CSharpSource="TabView\TabViewBasicSample_cs.txt" HeaderText="A TabView with support for adding, closing, and rearranging tabs">
<controls:ControlExample.Example>
Expand Down Expand Up @@ -37,19 +41,19 @@
SelectedIndex="0"
TabCloseRequested="TabView_TabCloseRequested">
<TabView.TabItems>
<TabViewItem Header="Document 0">
<TabViewItem Header="Document 0" ContextFlyout="{x:Bind TabViewContextMenu}">
<TabViewItem.IconSource>
<SymbolIconSource Symbol="Placeholder" />
</TabViewItem.IconSource>
<samplepages:SamplePage1 />
</TabViewItem>
<TabViewItem Header="Document 1">
<TabViewItem Header="Document 1" ContextFlyout="{x:Bind TabViewContextMenu}">
<TabViewItem.IconSource>
<SymbolIconSource Symbol="Placeholder" />
</TabViewItem.IconSource>
<samplepages:SamplePage2 />
</TabViewItem>
<TabViewItem Header="Document 2">
<TabViewItem Header="Document 2" ContextFlyout="{x:Bind TabViewContextMenu}">
<TabViewItem.IconSource>
<SymbolIconSource Symbol="Placeholder" />
</TabViewItem.IconSource>
Expand Down Expand Up @@ -101,7 +105,8 @@
<TabViewItem
Content="{x:Bind DataContent}"
Header="{x:Bind DataHeader}"
IconSource="{x:Bind DataIconSource}" />
IconSource="{x:Bind DataIconSource}"
ContextFlyout="{x:Bind TabViewContextMenu}" />
</DataTemplate>
</TabView.TabItemTemplate>
</TabView>
Expand Down Expand Up @@ -276,19 +281,19 @@
IsAddTabButtonVisible="False"
SelectedIndex="0">
<TabView.TabItems>
<TabViewItem Header="Home" IsClosable="False">
<TabViewItem Header="Home" IsClosable="False" ContextFlyout="{x:Bind TabViewContextMenu}">
<TabViewItem.IconSource>
<SymbolIconSource Symbol="Home" />
</TabViewItem.IconSource>
<samplepages:SamplePage1 />
</TabViewItem>
<TabViewItem Header="Tab 2 Has Longer Text" IsClosable="False">
<TabViewItem Header="Tab 2 Has Longer Text" IsClosable="False" ContextFlyout="{x:Bind TabViewContextMenu}">
<TabViewItem.IconSource>
<SymbolIconSource Symbol="MusicInfo" />
</TabViewItem.IconSource>
<samplepages:SamplePage2 />
</TabViewItem>
<TabViewItem Header="Third Tab" IsClosable="False">
<TabViewItem Header="Third Tab" IsClosable="False" ContextFlyout="{x:Bind TabViewContextMenu}">
<TabViewItem.IconSource>
<SymbolIconSource Symbol="Placeholder" />
</TabViewItem.IconSource>
Expand Down Expand Up @@ -327,19 +332,19 @@
IsAddTabButtonVisible="False"
SelectedIndex="0">
<TabView.TabItems>
<TabViewItem Header="Home">
<TabViewItem Header="Home" ContextFlyout="{x:Bind TabViewContextMenu}">
<TabViewItem.IconSource>
<SymbolIconSource Symbol="Home" />
</TabViewItem.IconSource>
<samplepages:SamplePage1 />
</TabViewItem>
<TabViewItem Header="Tab 2 Has Longer Text">
<TabViewItem Header="Tab 2 Has Longer Text" ContextFlyout="{x:Bind TabViewContextMenu}">
<TabViewItem.IconSource>
<SymbolIconSource Symbol="MusicInfo" />
</TabViewItem.IconSource>
<samplepages:SamplePage2 />
</TabViewItem>
<TabViewItem Header="Third Tab">
<TabViewItem Header="Third Tab" ContextFlyout="{x:Bind TabViewContextMenu}">
<TabViewItem.IconSource>
<SymbolIconSource Symbol="Placeholder" />
</TabViewItem.IconSource>
Expand Down Expand Up @@ -383,17 +388,17 @@
SelectedIndex="0"
TabWidthMode="SizeToContent">
<TabView.TabItems>
<TabViewItem Header="CMD Prompt" IsClosable="False">
<TabViewItem Header="CMD Prompt" IsClosable="False" ContextFlyout="{x:Bind TabViewContextMenu}">
<TabViewItem.IconSource>
<BitmapIconSource ShowAsMonochrome="False" UriSource="ms-appx:///Assets/SampleMedia/cmd.png" />
</TabViewItem.IconSource>
</TabViewItem>
<TabViewItem Header="PowerShell" IsClosable="False">
<TabViewItem Header="PowerShell" IsClosable="False" ContextFlyout="{x:Bind TabViewContextMenu}">
<TabViewItem.IconSource>
<BitmapIconSource ShowAsMonochrome="False" UriSource="ms-appx:///Assets/SampleMedia/powershell.png" />
</TabViewItem.IconSource>
</TabViewItem>
<TabViewItem Header="Windows Subsystem for Linux" IsClosable="False">
<TabViewItem Header="Windows Subsystem for Linux" IsClosable="False" ContextFlyout="{x:Bind TabViewContextMenu}">
<TabViewItem.IconSource>
<BitmapIconSource ShowAsMonochrome="False" UriSource="ms-appx:///Assets/SampleMedia/linux.png" />
</TabViewItem.IconSource>
Expand Down
8 changes: 7 additions & 1 deletion WinUIGallery/Samples/ControlPages/TabViewPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ private TabViewItem CreateNewTab(int index)
TabViewItem newItem = new TabViewItem
{
Header = $"Document {index}",
IconSource = new Microsoft.UI.Xaml.Controls.SymbolIconSource() { Symbol = Symbol.Document }
IconSource = new Microsoft.UI.Xaml.Controls.SymbolIconSource() { Symbol = Symbol.Document },
ContextFlyout = TabViewContextMenu
};

// The content of the tab is often a frame that contains a page, though it could be any UIElement.
Expand Down Expand Up @@ -251,4 +252,9 @@ private void TabViewWindowingButton_Click(object sender, Microsoft.UI.Xaml.Route

newWindow.Activate();
}

private void TabViewContextMenu_Opening(object sender, object e)
{
TabViewHelper.PopulateTabViewContextMenu((MenuFlyout)sender);
}
}
4 changes: 2 additions & 2 deletions WinUIGallery/Samples/SamplePages/TabContentSampleControl.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<TextBlock Text="{Binding}" Style="{ThemeResource TitleTextBlockStyle}" />
<TextBlock Text="Drag the Tab outside of the window to spawn a new window." Style="{ThemeResource SubtitleTextBlockStyle}" />
<TextBlock Text="Notice that the state of the Tab is maintained in the new window. For example, if you toggle the ToggleSwitch ON, it will remain ON in the new window." Style="{ThemeResource BodyTextBlockStyle}" />
<ToggleSwitch x:Name="ControlToggle" Header="Turn on ProgressRing" Margin="0,8" />
<ProgressRing IsActive="{x:Bind ControlToggle.IsOn, Mode=OneWay}" HorizontalAlignment="Left" />
<ToggleSwitch x:Name="ControlToggle" Header="Turn on ProgressRing" Margin="0,8" IsOn="{x:Bind IsInProgress, Mode=TwoWay}" />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am unclear on why this change is being made & net impact on UX. Can you explain or add screenshot in PR Description.

<ProgressRing IsActive="{x:Bind IsInProgress, Mode=OneWay}" HorizontalAlignment="Left" />
</StackPanel>
</UserControl>
10 changes: 10 additions & 0 deletions WinUIGallery/Samples/SamplePages/TabContentSampleControl.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;

namespace WinUIGallery.SamplePages;
Expand All @@ -8,4 +9,13 @@ public TabContentSampleControl()
{
this.InitializeComponent();
}
public bool IsInProgress
{
get { return (bool)GetValue(IsInProgressProperty); }
set { SetValue(IsInProgressProperty, value); }
}

public static readonly DependencyProperty IsInProgressProperty = DependencyProperty.Register("IsInProgress", typeof(bool), typeof(TabContentSampleControl), new PropertyMetadata(false));


}
34 changes: 24 additions & 10 deletions WinUIGallery/Samples/SamplePages/TabViewWindowingSamplePage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,36 @@
mc:Ignorable="d">

<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<TabView
x:Name="Tabs"
VerticalAlignment="Stretch"
AddTabButtonClick="Tabs_AddTabButtonClick"
CanTearOutTabs="True"
ExternalTornOutTabsDropped="Tabs_ExternalTornOutTabsDropped"
ExternalTornOutTabsDropping="Tabs_ExternalTornOutTabsDropping"
TabCloseRequested="Tabs_TabCloseRequested"
TabTearOutRequested="Tabs_TabTearOutRequested"
TabTearOutWindowRequested="Tabs_TabTearOutWindowRequested">
<TabView
x:Name="Tabs"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you fix indentation and why order of event subscription being changed for all the items (only diff seems addition of TabItemsSource)?

VerticalAlignment="Stretch"
AddTabButtonClick="Tabs_AddTabButtonClick"
TabCloseRequested="Tabs_TabCloseRequested"
CanTearOutTabs="True"
TabTearOutWindowRequested="Tabs_TabTearOutWindowRequested"
TabTearOutRequested="Tabs_TabTearOutRequested"
ExternalTornOutTabsDropping="Tabs_ExternalTornOutTabsDropping"
ExternalTornOutTabsDropped="Tabs_ExternalTornOutTabsDropped"
TabItemsSource="{x:Bind TabItemDataList}">
<TabView.TabStripHeader>
<Grid x:Name="ShellTitleBarInset" Background="Transparent" />
</TabView.TabStripHeader>
<TabView.TabStripFooter>
<Grid x:Name="CustomDragRegion" Background="Transparent" />
</TabView.TabStripFooter>
<TabView.TabItemTemplate>
<DataTemplate x:DataType="local:TabItemData">
<TabViewItem Header="{x:Bind Header}">
<TabViewItem.ContextFlyout>
<MenuFlyout Opening="TabViewContextMenu_Opening" />
</TabViewItem.ContextFlyout>
<TabViewItem.IconSource>
<SymbolIconSource Symbol="Placeholder" />
</TabViewItem.IconSource>
<local:TabContentSampleControl DataContext="{x:Bind Content}" IsInProgress="{x:Bind IsInProgress, Mode=TwoWay}" />
</TabViewItem>
</DataTemplate>
</TabView.TabItemTemplate>
</TabView>
</Grid>
</Page>
Loading