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

Disable TabView scroll buttons based on scroll position #2162

Merged
merged 12 commits into from
Apr 3, 2020

Conversation

0x7c13
Copy link
Member

@0x7c13 0x7c13 commented Mar 25, 2020

Disable TabView scroll buttons based on scroll position for a better accessibility UX.

Description

If TabView cannot scroll to the left (horizontal offset = 0), disable DecreaseScroll button.
If TabView cannot scroll to the right (horizontal offset = scrollable width), disable IncreaseScroll button.

Motivation and Context

Fixes #2143

How Has This Been Tested?

Tested using MUXControlsTestApp with below tested scenarios:

  • When buttons are visible. Scroll all the way from left to right and right to left to verified their visual state.

  • When buttons are visible. Using increase/decrease button to scroll the strip and verified their visual state.

  • When buttons are hidden, decrease size of the app window to make buttons appear and verified their visual state

  • When buttons are visible, increase size of the app window to make buttons disappear and verified their visual state.

Screenshots (if appropriate):

image
image
image

Disable TabView scroll buttons based on scroll position
@msft-github-bot msft-github-bot added the needs-triage Issue needs to be triaged by the area owners label Mar 25, 2020
@ranjeshj ranjeshj requested a review from teaP March 25, 2020 12:22
@ranjeshj ranjeshj added area-TabView team-Controls Issue for the Controls team labels Mar 25, 2020
Store the return value of scrollViewer.ViewChanged in a revoker
Comment on lines 369 to 370
auto decreaseButton = SharedHelpers::FindInVisualTreeByName(scrollViewer, L"ScrollDecreaseButton").as<winrt::RepeatButton>();
auto increaseButton = SharedHelpers::FindInVisualTreeByName(scrollViewer, L"ScrollIncreaseButton").as<winrt::RepeatButton>();
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seem that those button are always the same and do not get unloaded and loaded, so it might be better to get a reference in OnApplyTemplate instead of searching for those buttons every time we have changed the scrolling position.

Copy link
Contributor

Choose a reason for hiding this comment

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

We can save the buttons as private members. I was wondering why we are doing FindInVisualTree instead of GetTemplateChild, but that might be because the button is in a re-templated scrollviewer inside the control template ?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see a problem of getting it in OnApplyTemplate. I will make the changes to see if it works as indented and push out changes if it does.

Copy link
Member Author

@0x7c13 0x7c13 Mar 25, 2020

Choose a reason for hiding this comment

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

@ranjeshj you are right, it is not possible to get from GetTemplateChild since it is in a re-templated scroll-viewer. And this is true for both ScrollViewer and it's repeated buttons.

Copy link
Contributor

Choose a reason for hiding this comment

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

performance wise, I don't think this proposal is viable, FindInVisualTreeByName is expensive and we can't call it every time the view changes, I hope we are able to find a way to store a pointer to the buttons in a tracker_ref

Copy link
Member Author

Choose a reason for hiding this comment

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

I will save them after ListView loaded so we only do it once.

@marcelwgn
Copy link
Collaborator

Can you please add an interaction test for this? :)

You can take a look at the following test, to see how you can test scrolling behavior:

public void ScrollTest()
{
using (var setup = new TestSetupHelper("NumberBox Tests"))
{
RangeValueSpinner numBox = FindElement.ByName<RangeValueSpinner>("TestNumberBox");
numBox.SetValue(0);
Wait.ForIdle();
Log.Comment("Verify that scroll doesn't work when the control doesn't have focus.");
FindElement.ByName("MaxCheckBox").SetFocus();
Wait.ForIdle();
InputHelper.RotateWheel(numBox, 1);
Wait.ForIdle();
Verify.AreEqual(0, numBox.Value);
FindTextBox(numBox).SetFocus();
InputHelper.RotateWheel(numBox, 1);
InputHelper.RotateWheel(numBox, 1);
Wait.ForIdle();
Verify.AreEqual(2, numBox.Value);
InputHelper.RotateWheel(numBox, -1);
InputHelper.RotateWheel(numBox, -1);
InputHelper.RotateWheel(numBox, -1);
Wait.ForIdle();
Verify.AreEqual(-1, numBox.Value);

Setup UI for testing scroll viewer's increase/decrease button
@0x7c13
Copy link
Member Author

0x7c13 commented Mar 25, 2020

Can you please add an interaction test for this? :)

You can take a look at the following test, to see how you can test scrolling behavior:

public void ScrollTest()
{
using (var setup = new TestSetupHelper("NumberBox Tests"))
{
RangeValueSpinner numBox = FindElement.ByName<RangeValueSpinner>("TestNumberBox");
numBox.SetValue(0);
Wait.ForIdle();
Log.Comment("Verify that scroll doesn't work when the control doesn't have focus.");
FindElement.ByName("MaxCheckBox").SetFocus();
Wait.ForIdle();
InputHelper.RotateWheel(numBox, 1);
Wait.ForIdle();
Verify.AreEqual(0, numBox.Value);
FindTextBox(numBox).SetFocus();
InputHelper.RotateWheel(numBox, 1);
InputHelper.RotateWheel(numBox, 1);
Wait.ForIdle();
Verify.AreEqual(2, numBox.Value);
InputHelper.RotateWheel(numBox, -1);
InputHelper.RotateWheel(numBox, -1);
InputHelper.RotateWheel(numBox, -1);
Wait.ForIdle();
Verify.AreEqual(-1, numBox.Value);

Any pointer on how to run UI/Integration tests? I don't see any documentation in the README.

@marcelwgn
Copy link
Collaborator

You can run them through the Visual Studio Test explorer.

@0x7c13
Copy link
Member Author

0x7c13 commented Mar 25, 2020

You can run them through the Visual Studio Test explorer.

Ok, I was able to run the tests. Previously the "run" button was grayed out because I did not build it after few changes.

Btw, the NumberBox scroller trick does not apply here since the scroll viewer in NumberBox UI Test is external and owned by the test page. Here we are testing the scroll viewer inside TabView itself. Unless we expose some APIs to scroll it, otherwise it is hard to manipulate the scrolling here. Clicking the increase/decrease button might be a way out but it is hard to know if we are reaching to the end.

@marcelwgn
Copy link
Collaborator

Btw, the NumberBox scroller trick does not apply here since the scroll viewer in NumberBox UI Test is external and owned by the test page. Here we are testing the scroll viewer inside TabView itself. Unless we expose some APIs to scroll it, otherwise it is hard to manipulate the scrolling here. Clicking the increase/decrease button might be a way out but it is hard to know if we are reaching to the end.

Oh I see. Maybe you can try to find the Scrollviewer in the VisualTree using the VisualTreeHelper.

@StephenLPeters StephenLPeters removed the needs-triage Issue needs to be triaged by the area owners label Mar 25, 2020
auto decreaseButton = SharedHelpers::FindInVisualTreeByName(scrollViewer, L"ScrollDecreaseButton").as<winrt::RepeatButton>();
auto increaseButton = SharedHelpers::FindInVisualTreeByName(scrollViewer, L"ScrollIncreaseButton").as<winrt::RepeatButton>();

auto minThreshold = 0.1;
Copy link
Contributor

Choose a reason for hiding this comment

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

auto minThreshold = 0.1; [](start = 8, length = 24)

constexpr

Copy link
Collaborator

Choose a reason for hiding this comment

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

Would it make sense to inline the variable into the if statements if its constant, that is, just write 0.1 instead of minThreshold?

if (abs(scrollViewer.HorizontalOffset() - scrollViewer.ScrollableWidth()) < minThreshold)
{
decreaseButton.IsEnabled(true);
increaseButton.IsEnabled(false);
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 better than setting the properties from code behind would be to add visual states to the template and calling go to state

Copy link
Member Author

Choose a reason for hiding this comment

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

I am not sure if using VisualState is the way to go as I am not seeing much benefits here. We still need to do some calculation here to disable or enable it on the fly based on min threshold since it is a floating value. (Or we will have this magic number lives inside xaml?) On the other hand we need to make sure it works in all cases like Window resizing. Maybe we should rewrite this whole scrolling buttons thingy in TabView vNext? @chingucoding

Copy link
Collaborator

Choose a reason for hiding this comment

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

I am not sure if with TabView vNext, a rewrite is a) a good idea and b) a rewrite would fix this at all. After all, we need to provide some sort of scrolling, and disabling those buttons is something we have to do. @stmoy FYI


auto minThreshold = 0.1;

if (abs(scrollViewer.HorizontalOffset() - scrollViewer.ScrollableWidth()) < minThreshold)
Copy link
Contributor

Choose a reason for hiding this comment

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

if (abs(scrollViewer.HorizontalOffset() - scrollViewer.ScrollableWidth()) < minThreshold) [](start = 8, length = 89)

Is there a situation where both scroll buttons should be disabled, when the content is smaller than the viewport for instance.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If the content is smaller than the viewport, the ScrollViewer does not render the buttons at all, making disabling them unnecessary.

@0x7c13 0x7c13 changed the title Disable TabView scroll buttons based on scroll position [WIP] Disable TabView scroll buttons based on scroll position Mar 25, 2020
0x7c13 added 2 commits March 25, 2020 16:12
…ents

Save decrease/increase button ref for future use and address few comments
Added UI tests
@0x7c13 0x7c13 changed the title [WIP] Disable TabView scroll buttons based on scroll position Disable TabView scroll buttons based on scroll position Mar 26, 2020
Copy link
Collaborator

@marcelwgn marcelwgn left a comment

Choose a reason for hiding this comment

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

Edit: This looks good to go. :shipit:

// Scroll all the way to the right and verify decrease/increase button visual state
FindElement.ByName<Button>("ScrollTabViewToTheRight").InvokeAndWait();
Verify.IsTrue(IsScrollDecreaseButtonEnabled(), "Scroll decrease button should be enabled");
Verify.IsFalse(IsScrollIncreaseButtonEnabled(), "Scroll increase button should be disabled");
Copy link
Collaborator

@marcelwgn marcelwgn Mar 26, 2020

Choose a reason for hiding this comment

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

This assert fails on my machine 100% of the time, however adding a small delay after line 154 (e.g. 300ms) seems to fix that issue. I am not sure if it's just my machine or if this is generally an issue. Did the test run perfectly on your machine @Jasonstein ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it runs fine on my machine but I have only tested once. Let me check again.

Copy link
Member Author

Choose a reason for hiding this comment

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

You are right, it does not work if there is no delay (not sure why it works on my machine previously). Because the view state change triggered after scroll viewer view changed event so it is not going to happen right away. I added a delay after each invoke. Please check latest iteration.

Copy link
Collaborator

@marcelwgn marcelwgn Mar 26, 2020

Choose a reason for hiding this comment

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

Test looks fine now. Thanks for looking into this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we do Wait.ForIdle instead ? Thread.Sleep /Wait for milliseconds should be avoided in general, they tend to make the test slower on fast machines and fail on really slow machines causing the test to be flaky. Wait.ForIdle is designed for this exact purpose - once the system is done with everything it will come out of the wait and that time will be less on faster machines.

Copy link
Collaborator

Choose a reason for hiding this comment

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

There is some event in the WaitHelper.cs file which technically should work, though the fact that this exact class is letting us down here is not to promising. The easies/"safest" way would be to wait for a certain amount of time and retry. This is also something the WaitHelper does in some cases (I think).

Copy link
Member Author

@0x7c13 0x7c13 Mar 26, 2020

Choose a reason for hiding this comment

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

Even with wait on ViewChanged, I am still smelling race condition here since "IsEnabled" is set after ViewChanged not as part of ViewChanged event. It is unlikely but in theory, it could happen. @ranjeshj Should we keep 300ms waiting here for now? Or I would suggest to move this test to a new method and name it flaky test or give it an attribute. At the end of the day, this is UI test (integration test), "flaky" is hard to avoid.

Copy link
Collaborator

Choose a reason for hiding this comment

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

There are some unreliable tests, however we try to keep avoiding unreliable tests mostly. If waiting 300ms or 500ms or 1000ms makes it stable, that would be a possible solution, though it may create problems in the future.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Jasonstein The tests are PR gates, so if tests are flaky it will start blocking future PRs. We should try as much as possible to avoid that. Please file a test issue to investigate this and to unblock this PR for now. If the test starts failing, then we can disable it and link it to the issue. Please also add a comment next to the delay pointing to the issue.

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried to implement ViewChanged and even with that, it is not going to work. Then I realized that I thought I disabled scrolling animation, in fact, I did not. Now I have disabled all scrolling animation and things started to work and test is now passing with Wait.ForIdle.

Adding a sleep after each scrolling to make sure view state get updated
0x7c13 added 2 commits March 26, 2020 05:09
Use Wait.ForMilliseconds instead of Thread.Sleep
Use Wait.ForIdle() instead
@ranjeshj
Copy link
Contributor

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Disable scrolling animation
@ranjeshj
Copy link
Contributor

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Remove unused namespace
@ranjeshj
Copy link
Contributor

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Collaborator

@marcelwgn marcelwgn left a comment

Choose a reason for hiding this comment

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

Looks good to go!

Copy link
Contributor

@StephenLPeters StephenLPeters left a comment

Choose a reason for hiding this comment

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

:shipit:

Resolve comments
@ranjeshj
Copy link
Contributor

ranjeshj commented Apr 1, 2020

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@ranjeshj
Copy link
Contributor

ranjeshj commented Apr 3, 2020

@Jasonstein can you please merge with master which includes some stability fixes for the PR pipeline ?

@0x7c13
Copy link
Member Author

0x7c13 commented Apr 3, 2020

@Jasonstein can you please merge with master which includes some stability fixes for the PR pipeline ?

Done, can you help trigger another CI build?

@ranjeshj
Copy link
Contributor

ranjeshj commented Apr 3, 2020

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Contributor

@StephenLPeters StephenLPeters left a comment

Choose a reason for hiding this comment

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

:shipit:

@StephenLPeters StephenLPeters merged commit 68abde4 into microsoft:master Apr 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-TabView team-Controls Issue for the Controls team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Tab strip scroll bumpers don't disable when scrolling to the edge
5 participants