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

Resize tab view items only once the pointer has left the TabViewItem strip #2569

Merged
merged 10 commits into from
Jun 5, 2020
103 changes: 102 additions & 1 deletion dev/TabView/InteractionTests/TabViewTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System;
Expand Down Expand Up @@ -647,6 +647,107 @@ public void VerifyTabViewItemHeaderForegroundResource()
}
}

[TestMethod]
public void VerifySizingBehaviorOnTabCloseComingFromScroll()
{
int pixelTolerance = 10;

using (var setup = new TestSetupHelper(new[] { "TabView Tests", "TabViewTabClosingBehaviorButton" }))
{

Log.Comment("Verifying sizing behavior when closing a tab");
CloseTabAndVerifyWidth("Tab 1", 500, "True;False;");

CloseTabAndVerifyWidth("Tab 2", 500, "True;False;");

CloseTabAndVerifyWidth("Tab 3", 500, "False;False;");

CloseTabAndVerifyWidth("Tab 5", 401, "False;False;");

CloseTabAndVerifyWidth("Tab 4", 401, "False;False;");

Log.Comment("Leaving the pointer exited area");
var readTabViewWidthButton = new Button(FindElement.ByName("GetActualWidthButton"));
readTabViewWidthButton.Click();
Wait.ForIdle();

readTabViewWidthButton.Click();
Wait.ForIdle();

Log.Comment("Verify correct TabView width");
Verify.IsTrue(Math.Abs(GetActualTabViewWidth() - 283) < pixelTolerance);
}

void CloseTabAndVerifyWidth(string tabName, int expectedValue, string expectedScrollbuttonStates)
{
Log.Comment("Closing tab:" + tabName);
FindCloseButton(FindElement.ByName(tabName)).Click();
Wait.ForIdle();
Log.Comment("Verifying TabView width");
Verify.IsTrue(Math.Abs(GetActualTabViewWidth() - expectedValue) < pixelTolerance);
Verify.AreEqual(expectedScrollbuttonStates, FindElement.ByName("ScrollButtonStatus").GetText());

}

double GetActualTabViewWidth()
{
var tabviewWidth = new TextBlock(FindElement.ByName("TabViewWidth"));

return Double.Parse(tabviewWidth.GetText());
}
}

[TestMethod]
public void VerifySizingBehaviorOnTabCloseComingFromNonScroll()
{
int pixelTolerance = 10;

using (var setup = new TestSetupHelper(new[] { "TabView Tests", "TabViewTabClosingBehaviorButton" }))
{

Log.Comment("Verifying sizing behavior when closing a tab");
CloseTabAndVerifyWidth("Tab 1", 500, "True;False;");

CloseTabAndVerifyWidth("Tab 2", 500, "True;False;");

CloseTabAndVerifyWidth("Tab 3", 500, "False;False;");

var readTabViewWidthButton = new Button(FindElement.ByName("GetActualWidthButton"));
readTabViewWidthButton.Click();
Wait.ForIdle();

CloseTabAndVerifyWidth("Tab 5", 500, "False;False;");

CloseTabAndVerifyWidth("Tab 4", 500, "False;False;");

Log.Comment("Leaving the pointer exited area");

readTabViewWidthButton.Click();
Wait.ForIdle();

Log.Comment("Verify correct TabView width");
Verify.IsTrue(Math.Abs(GetActualTabViewWidth() - 500) < pixelTolerance);
}

void CloseTabAndVerifyWidth(string tabName, int expectedValue, string expectedScrollbuttonStates)
{
Log.Comment("Closing tab:" + tabName);
FindCloseButton(FindElement.ByName(tabName)).Click();
Wait.ForIdle();
Log.Comment("Verifying TabView width");
Verify.IsTrue(Math.Abs(GetActualTabViewWidth() - expectedValue) < pixelTolerance);
Verify.AreEqual(expectedScrollbuttonStates, FindElement.ByName("ScrollButtonStatus").GetText());

}

double GetActualTabViewWidth()
{
var tabviewWidth = new TextBlock(FindElement.ByName("TabViewWidth"));

return Double.Parse(tabviewWidth.GetText());
}
}

public void PressButtonAndVerifyText(String buttonName, String textBlockName, String expectedText)
{
Button button = FindElement.ByName<Button>(buttonName);
Expand Down
164 changes: 121 additions & 43 deletions dev/TabView/TabView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,15 @@ void TabView::OnApplyTemplate()
m_addButtonColumn.set(GetTemplateChildT<winrt::ColumnDefinition>(L"AddButtonColumn", controlProtected));
m_rightContentColumn.set(GetTemplateChildT<winrt::ColumnDefinition>(L"RightContentColumn", controlProtected));

m_tabContainerGrid.set(GetTemplateChildT<winrt::Grid>(L"TabContainerGrid", controlProtected));
if (const auto& containerGrid = GetTemplateChildT<winrt::Grid>(L"TabContainerGrid", controlProtected))
{
m_tabContainerGrid.set(containerGrid);
Copy link
Contributor

Choose a reason for hiding this comment

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

m_tabContainerGrid [](start = 8, length = 18)

a downside to this approach is that you will never set m_tabContainerGrid to null, even if a second template is later applied which doesn't have a TabContainerGrid. Seems unlikely in this case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated the code to set it to null if we get a nullptr.

m_tabStripPointerExitedRevoker = containerGrid.PointerExited(winrt::auto_revoke, { this,&TabView::OnTabStripPointerExited });
}
else
{
m_tabContainerGrid.set(nullptr);
}

m_shadowReceiver.set(GetTemplateChildT<winrt::Grid>(L"ShadowReceiver", controlProtected));

Expand Down Expand Up @@ -341,6 +349,18 @@ void TabView::OnListViewLoaded(const winrt::IInspectable&, const winrt::RoutedEv
}
}

void TabView::OnTabStripPointerExited(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args)
{
if (updateTabWidthOnPointerLeave)
{
auto scopeGuard = gsl::finally([this]()
{
updateTabWidthOnPointerLeave = false;
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Scope guarded flag should be declared before UpdateTabWidths() is called. This is so that if UpdateTabWidths() throws but that exception is later handled we will still update this flag.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point, moved the UpdateTabWidths call behind the scopeguard.

UpdateTabWidths();
}
}

void TabView::OnScrollViewerLoaded(const winrt::IInspectable&, const winrt::RoutedEventArgs& args)
{
if (auto&& scrollViewer = m_scrollViewer.get())
Expand Down Expand Up @@ -423,8 +443,11 @@ void TabView::UpdateScrollViewerDecreaseAndIncreaseButtonsViewState()

void TabView::OnItemsPresenterSizeChanged(const winrt::IInspectable& sender, const winrt::SizeChangedEventArgs& args)
{
UpdateTabWidths();
UpdateScrollViewerDecreaseAndIncreaseButtonsViewState();
if (!updateTabWidthOnPointerLeave)
{
// Presenter size didn't change because of item being removed, so update manually
UpdateScrollViewerDecreaseAndIncreaseButtonsViewState();
}
}

void TabView::OnItemsChanged(winrt::IInspectable const& item)
Expand All @@ -434,42 +457,57 @@ void TabView::OnItemsChanged(winrt::IInspectable const& item)
m_tabItemsChangedEventSource(*this, args);

int numItems = static_cast<int>(TabItems().Size());
if (args.CollectionChange() == winrt::CollectionChange::ItemRemoved && numItems > 0)

if (args.CollectionChange() == winrt::CollectionChange::ItemRemoved)
{
// SelectedIndex might also already be -1
auto selectedIndex = SelectedIndex();
if (selectedIndex == -1 || selectedIndex == static_cast<int32_t>(args.Index()))
updateTabWidthOnPointerLeave = true;
if (numItems > 0)
{
// Find the closest tab to select instead.
int startIndex = static_cast<int>(args.Index());
if (startIndex >= numItems)
// SelectedIndex might also already be -1
auto selectedIndex = SelectedIndex();
if (selectedIndex == -1 || selectedIndex == static_cast<int32_t>(args.Index()))
{
startIndex = numItems - 1;
}
int index = startIndex;

do
{
auto nextItem = ContainerFromIndex(index).as<winrt::ListViewItem>();

if (nextItem && nextItem.IsEnabled() && nextItem.Visibility() == winrt::Visibility::Visible)
// Find the closest tab to select instead.
int startIndex = static_cast<int>(args.Index());
if (startIndex >= numItems)
{
SelectedItem(TabItems().GetAt(index));
break;
startIndex = numItems - 1;
}
int index = startIndex;

// try the next item
index++;
if (index >= numItems)
do
{
index = 0;
}
} while (index != startIndex);
auto nextItem = ContainerFromIndex(index).as<winrt::ListViewItem>();

if (nextItem && nextItem.IsEnabled() && nextItem.Visibility() == winrt::Visibility::Visible)
{
SelectedItem(TabItems().GetAt(index));
break;
}

// try the next item
index++;
if (index >= numItems)
{
index = 0;
}
} while (index != startIndex);
}

}
// Last item removed, update sizes
// The index of the last element is "Size() - 1", but in TabItems, it is already removed.
if (args.Index() == TabItems().Size())
{
UpdateTabWidths(true,false);
}
}
else
{
UpdateTabWidths();
}
}

UpdateTabWidths();
}

void TabView::OnListViewSelectionChanged(const winrt::IInspectable& sender, const winrt::SelectionChangedEventArgs& args)
Expand Down Expand Up @@ -620,6 +658,7 @@ void TabView::RequestCloseTab(winrt::TabViewItem const& container)
internalTabViewItem->RaiseRequestClose(*args);
}
}
UpdateTabWidths(false);
}

void TabView::OnScrollDecreaseClick(const winrt::IInspectable&, const winrt::RoutedEventArgs&)
Expand Down Expand Up @@ -649,7 +688,7 @@ winrt::Size TabView::MeasureOverride(winrt::Size const& availableSize)
return __super::MeasureOverride(availableSize);
}

void TabView::UpdateTabWidths()
void TabView::UpdateTabWidths(bool shouldUpdateWidths,bool fillAllAvailableSpace)
{
double tabWidth = std::numeric_limits<double>::quiet_NaN();

Expand Down Expand Up @@ -685,14 +724,41 @@ void TabView::UpdateTabWidths()
{
if (TabWidthMode() == winrt::TabViewWidthMode::Equal)
{

auto const minTabWidth = unbox_value<double>(SharedHelpers::FindInApplicationResources(c_tabViewItemMinWidthName, box_value(c_tabMinimumWidth)));
auto const maxTabWidth = unbox_value<double>(SharedHelpers::FindInApplicationResources(c_tabViewItemMaxWidthName, box_value(c_tabMaximumWidth)));

// Calculate the proportional width of each tab given the width of the ScrollViewer.
// If we should fill all of the available space, use scrollviewer dimensions
auto const padding = Padding();
auto const tabWidthForScroller = (availableWidth - (padding.Left + padding.Right)) / (double)(TabItems().Size());
if (fillAllAvailableSpace)
{
// Calculate the proportional width of each tab given the width of the ScrollViewer.
auto const tabWidthForScroller = (availableWidth - (padding.Left + padding.Right)) / (double)(TabItems().Size());
tabWidth = std::clamp(tabWidthForScroller, minTabWidth, maxTabWidth);
}
else
{
double availableTabViewSpace = (tabColumn.ActualWidth() - (padding.Left + padding.Right));
if (const auto increaseButton = m_scrollIncreaseButton.get())
{
if (increaseButton.Visibility() == winrt::Visibility::Visible)
{
availableTabViewSpace -= increaseButton.ActualWidth();
}
}

if (const auto decreaseButton = m_scrollDecreaseButton.get())
{
if (decreaseButton.Visibility() == winrt::Visibility::Visible)
{
availableTabViewSpace -= decreaseButton.ActualWidth();
}
}

// Use current size to update items to fill the currently occupied space
tabWidth = availableTabViewSpace / (double)(TabItems().Size());
}

tabWidth = std::clamp(tabWidthForScroller, minTabWidth, maxTabWidth);

// Size tab column to needed size
tabColumn.MaxWidth(availableWidth);
Expand All @@ -711,7 +777,15 @@ void TabView::UpdateTabWidths()
tabColumn.Width(winrt::GridLengthHelper::FromValueAndType(1.0, winrt::GridUnitType::Auto));
if (auto listview = m_listView.get())
{
winrt::FxScrollViewer::SetHorizontalScrollBarVisibility(listview, winrt::Windows::UI::Xaml::Controls::ScrollBarVisibility::Hidden);
if (shouldUpdateWidths && fillAllAvailableSpace)
{
winrt::FxScrollViewer::SetHorizontalScrollBarVisibility(listview, winrt::Windows::UI::Xaml::Controls::ScrollBarVisibility::Hidden);
}
else
{
m_scrollDecreaseButton.get().IsEnabled(false);
m_scrollIncreaseButton.get().IsEnabled(false);
}
}
}
}
Expand All @@ -731,7 +805,7 @@ void TabView::UpdateTabWidths()
winrt::FxScrollViewer::SetHorizontalScrollBarVisibility(listview, visible
? winrt::Windows::UI::Xaml::Controls::ScrollBarVisibility::Visible
: winrt::Windows::UI::Xaml::Controls::ScrollBarVisibility::Hidden);
if (visible)
if (visible && shouldUpdateWidths)
{
UpdateScrollViewerDecreaseAndIncreaseButtonsViewState();
}
Expand All @@ -742,18 +816,22 @@ void TabView::UpdateTabWidths()
}
}

for (auto item : TabItems())

if (shouldUpdateWidths)
{
// Set the calculated width on each tab.
auto tvi = item.try_as<winrt::TabViewItem>();
if (!tvi)
for (auto item : TabItems())
{
tvi = ContainerFromItem(item).as<winrt::TabViewItem>();
}
// Set the calculated width on each tab.
auto tvi = item.try_as<winrt::TabViewItem>();
if (!tvi)
{
tvi = ContainerFromItem(item).as<winrt::TabViewItem>();
}

if (tvi)
{
tvi.Width(tabWidth);
if (tvi)
{
tvi.Width(tabWidth);
}
}
}
}
Expand Down
Loading