-
Notifications
You must be signed in to change notification settings - Fork 712
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
Conversation
Disable TabView scroll buttons based on scroll position
Store the return value of scrollViewer.ViewChanged in a revoker
dev/TabView/TabView.cpp
Outdated
auto decreaseButton = SharedHelpers::FindInVisualTreeByName(scrollViewer, L"ScrollDecreaseButton").as<winrt::RepeatButton>(); | ||
auto increaseButton = SharedHelpers::FindInVisualTreeByName(scrollViewer, L"ScrollIncreaseButton").as<winrt::RepeatButton>(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
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: microsoft-ui-xaml/dev/NumberBox/InteractionTests/NumberBoxTests.cs Lines 302 to 328 in f083cde
|
Setup UI for testing scroll viewer's increase/decrease button
Any pointer on how to run UI/Integration tests? I don't see any documentation in the README. |
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. |
Oh I see. Maybe you can try to find the Scrollviewer in the VisualTree using the VisualTreeHelper. |
dev/TabView/TabView.cpp
Outdated
auto decreaseButton = SharedHelpers::FindInVisualTreeByName(scrollViewer, L"ScrollDecreaseButton").as<winrt::RepeatButton>(); | ||
auto increaseButton = SharedHelpers::FindInVisualTreeByName(scrollViewer, L"ScrollIncreaseButton").as<winrt::RepeatButton>(); | ||
|
||
auto minThreshold = 0.1; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
dev/TabView/TabView.cpp
Outdated
|
||
auto minThreshold = 0.1; | ||
|
||
if (abs(scrollViewer.HorizontalOffset() - scrollViewer.ScrollableWidth()) < minThreshold) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
…ents Save decrease/increase button ref for future use and address few comments
Added UI tests
There was a problem hiding this 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.
// 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"); |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
Use Wait.ForMilliseconds instead of Thread.Sleep
Use Wait.ForIdle() instead
/azp run |
Azure Pipelines successfully started running 1 pipeline(s). |
Disable scrolling animation
/azp run |
Azure Pipelines successfully started running 1 pipeline(s). |
Remove unused namespace
/azp run |
Azure Pipelines successfully started running 1 pipeline(s). |
There was a problem hiding this 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!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolve comments
/azp run |
Azure Pipelines successfully started running 1 pipeline(s). |
@Jasonstein can you please merge with master which includes some stability fixes for the PR pipeline ? |
Done, can you help trigger another CI build? |
/azp run |
Azure Pipelines successfully started running 1 pipeline(s). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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):