Microsoft-ui-xaml: ItemsRepeater in ScrollViewer with no focusable elements ignores keyboard navigation

Created on 8 May 2020  路  17Comments  路  Source: microsoft/microsoft-ui-xaml

Describe the bug
If you're trying to use PageDown/PageUp or Up/Down to scroll a ScrollViewer with an ItemsRepeater in it, it will not work if the ItemsRepeater contains no focusable elements. It can only be scrolled with the mousewheel or using the scrollbar handle directly.

Discovered while adding ItemsRepeater layouts to the Toolkit here

Steps to reproduce the bug

  1. Copy XAML into XAML Studio:
<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:muxc="using:Microsoft.UI.Xaml.Controls" 
    mc:Ignorable="d">

    <Page.Resources>
        <converters:StringFormatConverter x:Key="StringFormatConverter"/>
    </Page.Resources>

    <ScrollViewer HorizontalScrollMode="Disabled"
                    VerticalScrollMode="Enabled"
                    HorizontalScrollBarVisibility="Disabled"
                    VerticalScrollBarVisibility="Auto" Margin="20">
        <muxc:ItemsRepeater 
            ItemsSource="{Binding}">
            <muxc:ItemsRepeater.Layout>
                <muxc:UniformGridLayout Orientation="Horizontal" 
                                        MinItemWidth="200" MinItemHeight="200"
                                        MinRowSpacing="16" MinColumnSpacing="8"
                                        ItemsStretch="Uniform"
                                        MaximumRowsOrColumns="4"
                                        />
            </muxc:ItemsRepeater.Layout>
            <muxc:ItemsRepeater.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Image Source="{Binding id, Converter={StaticResource StringFormatConverter}, ConverterParameter='https://picsum.photos/id/{0}0/200/200'}" Stretch="UniformToFill"/>
                        <Border Background="#99FFFFFF" VerticalAlignment="Bottom">
                            <TextBlock Text="{Binding title}" TextWrapping="WrapWholeWords"
                                    Foreground="Black" Margin="4"/>
                        </Border>
                    </Grid>
                </DataTemplate>
            </muxc:ItemsRepeater.ItemTemplate>
        </muxc:ItemsRepeater>
    </ScrollViewer>
</Page>


2. And use the following DataSource (click here)

[
  {
    "userId": 1,
    "id": 1,
    "title": "quidem molestiae enim"
  },
  {
    "userId": 1,
    "id": 2,
    "title": "sunt qui excepturi placeat culpa"
  },
  {
    "userId": 1,
    "id": 3,
    "title": "omnis laborum odio"
  },
  {
    "userId": 1,
    "id": 4,
    "title": "non esse culpa molestiae omnis sed optio"
  },
  {
    "userId": 1,
    "id": 5,
    "title": "eaque aut omnis a"
  },
  {
    "userId": 1,
    "id": 6,
    "title": "natus impedit quibusdam illo est"
  },
  {
    "userId": 1,
    "id": 7,
    "title": "quibusdam autem aliquid et et quia"
  },
  {
    "userId": 1,
    "id": 8,
    "title": "qui fuga est a eum"
  },
  {
    "userId": 1,
    "id": 9,
    "title": "saepe unde necessitatibus rem"
  },
  {
    "userId": 1,
    "id": 10,
    "title": "distinctio laborum qui"
  },
  {
    "userId": 2,
    "id": 11,
    "title": "quam nostrum impedit mollitia quod et dolor"
  },
  {
    "userId": 2,
    "id": 12,
    "title": "consequatur autem doloribus natus consectetur"
  },
  {
    "userId": 2,
    "id": 13,
    "title": "ab rerum non rerum consequatur ut ea unde"
  },
  {
    "userId": 2,
    "id": 14,
    "title": "ducimus molestias eos animi atque nihil"
  },
  {
    "userId": 2,
    "id": 15,
    "title": "ut pariatur rerum ipsum natus repellendus praesentium"
  },
  {
    "userId": 2,
    "id": 16,
    "title": "voluptatem aut maxime inventore autem magnam atque repellat"
  },
  {
    "userId": 2,
    "id": 17,
    "title": "aut minima voluptatem ut velit"
  },
  {
    "userId": 2,
    "id": 18,
    "title": "nesciunt quia et doloremque"
  },
  {
    "userId": 2,
    "id": 19,
    "title": "velit pariatur quaerat similique libero omnis quia"
  },
  {
    "userId": 2,
    "id": 20,
    "title": "voluptas rerum iure ut enim"
  },
  {
    "userId": 3,
    "id": 21,
    "title": "repudiandae voluptatem optio est consequatur rem in temporibus et"
  },
  {
    "userId": 3,
    "id": 22,
    "title": "et rem non provident vel ut"
  },
  {
    "userId": 3,
    "id": 23,
    "title": "incidunt quisquam hic adipisci sequi"
  },
  {
    "userId": 3,
    "id": 24,
    "title": "dolores ut et facere placeat"
  },
  {
    "userId": 3,
    "id": 25,
    "title": "vero maxime id possimus sunt neque et consequatur"
  },
  {
    "userId": 3,
    "id": 26,
    "title": "quibusdam saepe ipsa vel harum"
  },
  {
    "userId": 3,
    "id": 27,
    "title": "id non nostrum expedita"
  },
  {
    "userId": 3,
    "id": 28,
    "title": "omnis neque exercitationem sed dolor atque maxime aut cum"
  },
  {
    "userId": 3,
    "id": 29,
    "title": "inventore ut quasi magnam itaque est fugit"
  },
  {
    "userId": 3,
    "id": 30,
    "title": "tempora assumenda et similique odit distinctio error"
  },
  {
    "userId": 4,
    "id": 31,
    "title": "adipisci laborum fuga laboriosam"
  },
  {
    "userId": 4,
    "id": 32,
    "title": "reiciendis dolores a ut qui debitis non quo labore"
  },
  {
    "userId": 4,
    "id": 33,
    "title": "iste eos nostrum"
  },
  {
    "userId": 4,
    "id": 34,
    "title": "cumque voluptatibus rerum architecto blanditiis"
  },
  {
    "userId": 4,
    "id": 35,
    "title": "et impedit nisi quae magni necessitatibus sed aut pariatur"
  },
  {
    "userId": 4,
    "id": 36,
    "title": "nihil cupiditate voluptate neque"
  },
  {
    "userId": 4,
    "id": 37,
    "title": "est placeat dicta ut nisi rerum iste"
  },
  {
    "userId": 4,
    "id": 38,
    "title": "unde a sequi id"
  },
  {
    "userId": 4,
    "id": 39,
    "title": "ratione porro illum labore eum aperiam sed"
  },
  {
    "userId": 4,
    "id": 40,
    "title": "voluptas neque et sint aut quo odit"
  },
  {
    "userId": 5,
    "id": 41,
    "title": "ea voluptates maiores eos accusantium officiis tempore mollitia consequatur"
  },
  {
    "userId": 5,
    "id": 42,
    "title": "tenetur explicabo ea"
  },
  {
    "userId": 5,
    "id": 43,
    "title": "aperiam doloremque nihil"
  },
  {
    "userId": 5,
    "id": 44,
    "title": "sapiente cum numquam officia consequatur vel natus quos suscipit"
  },
  {
    "userId": 5,
    "id": 45,
    "title": "tenetur quos ea unde est enim corrupti qui"
  },
  {
    "userId": 5,
    "id": 46,
    "title": "molestiae voluptate non"
  },
  {
    "userId": 5,
    "id": 47,
    "title": "temporibus molestiae aut"
  },
  {
    "userId": 5,
    "id": 48,
    "title": "modi consequatur culpa aut quam soluta alias perspiciatis laudantium"
  },
  {
    "userId": 5,
    "id": 49,
    "title": "ut aut vero repudiandae voluptas ullam voluptas at consequatur"
  },
  {
    "userId": 5,
    "id": 50,
    "title": "sed qui sed quas sit ducimus dolor"
  },
  {
    "userId": 6,
    "id": 51,
    "title": "odit laboriosam sint quia cupiditate animi quis"
  },
  {
    "userId": 6,
    "id": 52,
    "title": "necessitatibus quas et sunt at voluptatem"
  },
  {
    "userId": 6,
    "id": 53,
    "title": "est vel sequi voluptatem nemo quam molestiae modi enim"
  },
  {
    "userId": 6,
    "id": 54,
    "title": "aut non illo amet perferendis"
  },
  {
    "userId": 6,
    "id": 55,
    "title": "qui culpa itaque omnis in nesciunt architecto error"
  },
  {
    "userId": 6,
    "id": 56,
    "title": "omnis qui maiores tempora officiis omnis rerum sed repellat"
  },
  {
    "userId": 6,
    "id": 57,
    "title": "libero excepturi voluptatem est architecto quae voluptatum officia tempora"
  },
  {
    "userId": 6,
    "id": 58,
    "title": "nulla illo consequatur aspernatur veritatis aut error delectus et"
  },
  {
    "userId": 6,
    "id": 59,
    "title": "eligendi similique provident nihil"
  },
  {
    "userId": 6,
    "id": 60,
    "title": "omnis mollitia sunt aliquid eum consequatur fugit minus laudantium"
  },
  {
    "userId": 7,
    "id": 61,
    "title": "delectus iusto et"
  },
  {
    "userId": 7,
    "id": 62,
    "title": "eos ea non recusandae iste ut quasi"
  },
  {
    "userId": 7,
    "id": 63,
    "title": "velit est quam"
  },
  {
    "userId": 7,
    "id": 64,
    "title": "autem voluptatem amet iure quae"
  },
  {
    "userId": 7,
    "id": 65,
    "title": "voluptates delectus iure iste qui"
  },
  {
    "userId": 7,
    "id": 66,
    "title": "velit sed quia dolor dolores delectus"
  },
  {
    "userId": 7,
    "id": 67,
    "title": "ad voluptas nostrum et nihil"
  },
  {
    "userId": 7,
    "id": 68,
    "title": "qui quasi nihil aut voluptatum sit dolore minima"
  },
  {
    "userId": 7,
    "id": 69,
    "title": "qui aut est"
  },
  {
    "userId": 7,
    "id": 70,
    "title": "et deleniti unde"
  },
  {
    "userId": 8,
    "id": 71,
    "title": "et vel corporis"
  },
  {
    "userId": 8,
    "id": 72,
    "title": "unde exercitationem ut"
  },
  {
    "userId": 8,
    "id": 73,
    "title": "quos omnis officia"
  },
  {
    "userId": 8,
    "id": 74,
    "title": "quia est eius vitae dolor"
  },
  {
    "userId": 8,
    "id": 75,
    "title": "aut quia expedita non"
  },
  {
    "userId": 8,
    "id": 76,
    "title": "dolorem magnam facere itaque ut reprehenderit tenetur corrupti"
  },
  {
    "userId": 8,
    "id": 77,
    "title": "cupiditate sapiente maiores iusto ducimus cum excepturi veritatis quia"
  },
  {
    "userId": 8,
    "id": 78,
    "title": "est minima eius possimus ea ratione velit et"
  },
  {
    "userId": 8,
    "id": 79,
    "title": "ipsa quae voluptas natus ut suscipit soluta quia quidem"
  },
  {
    "userId": 8,
    "id": 80,
    "title": "id nihil reprehenderit"
  },
  {
    "userId": 9,
    "id": 81,
    "title": "quibusdam sapiente et"
  },
  {
    "userId": 9,
    "id": 82,
    "title": "recusandae consequatur vel amet unde"
  },
  {
    "userId": 9,
    "id": 83,
    "title": "aperiam odio fugiat"
  },
  {
    "userId": 9,
    "id": 84,
    "title": "est et at eos expedita"
  },
  {
    "userId": 9,
    "id": 85,
    "title": "qui voluptatem consequatur aut ab quis temporibus praesentium"
  },
  {
    "userId": 9,
    "id": 86,
    "title": "eligendi mollitia alias aspernatur vel ut iusto"
  },
  {
    "userId": 9,
    "id": 87,
    "title": "aut aut architecto"
  },
  {
    "userId": 9,
    "id": 88,
    "title": "quas perspiciatis optio"
  },
  {
    "userId": 9,
    "id": 89,
    "title": "sit optio id voluptatem est eum et"
  },
  {
    "userId": 9,
    "id": 90,
    "title": "est vel dignissimos"
  },
  {
    "userId": 10,
    "id": 91,
    "title": "repellendus praesentium debitis officiis"
  },
  {
    "userId": 10,
    "id": 92,
    "title": "incidunt et et eligendi assumenda soluta quia recusandae"
  },
  {
    "userId": 10,
    "id": 93,
    "title": "nisi qui dolores perspiciatis"
  },
  {
    "userId": 10,
    "id": 94,
    "title": "quisquam a dolores et earum vitae"
  },
  {
    "userId": 10,
    "id": 95,
    "title": "consectetur vel rerum qui aperiam modi eos aspernatur ipsa"
  },
  {
    "userId": 10,
    "id": 96,
    "title": "unde et ut molestiae est molestias voluptatem sint"
  },
  {
    "userId": 10,
    "id": 97,
    "title": "est quod aut"
  },
  {
    "userId": 10,
    "id": 98,
    "title": "omnis quia possimus nesciunt deleniti assumenda sed autem"
  },
  {
    "userId": 10,
    "id": 99,
    "title": "consectetur ut id impedit dolores sit ad ex aut"
  },
  {
    "userId": 10,
    "id": 100,
    "title": "enim repellat iste"
  }
]

Expected behavior
Still able to scroll viewport with keyboard.

Screenshots
image

Version Info

NuGet package version: Microsoft.UI.Xaml 2.3.191129002


| Windows 10 version | Saw the problem? |
| :--------------------------------- | :-------------------- |
| Insider Build (19624) | Yes |
| November 2019 Update (18363) | |
| May 2019 Update (18362) | |
| October 2018 Update (17763) | |
| April 2018 Update (17134) | |
| Fall Creators Update (16299) | |
| Creators Update (15063) | |


| Device form factor | Saw the problem? |
| :-------------------- | :------------------- |
| Desktop | Yes |
| Mobile | |
| Xbox | |
| Surface Hub | |
| IoT | |

Additional context
If you wrap the Grid on line 29 in a Button, now you'll see that PageUp/PageDown and Up/Down work.

FYI @ranjeshj @anawishnoff

team-Controls

Most helpful comment

@StephenLPeters I think this is still a bug, it seems like the hosting ScrollViewer should still be getting the keyboard input to respond to the key events. It responds to Mouse Events (scroll wheel), so it's really odd this isn't keyboard accessible by default like the previous panel based models.

We shouldn't be putting extra onus on the developers using ItemsRepeater to have to do additional work to make their apps accessible, something like this should be just built-in.

All 17 comments

I've experienced something similar in New Edge. If I click the scrollbar and then try to press the up or down arrows, nothing happens.

Would this be any different from any panel, say StackPanel or Grid ? If nothing can take focus, then where would the key events bubble from ?

Where is focus in this case when you press the keys ?

You would need something focusable to have focus to get the events. It could be the ScrollViewer itself that has focus, in which case it should scroll.

@ranjeshj I think that's the key, if there's nothing to focus on in the ItemsRepeater, why isn't the focus on the ScrollViewer? Is there any tool in VS currently which shows you what has the current focus?

I care less about something in the collection having focus, then the ability to scroll the viewport with the keyboard like I can with the mouse wheel (just like I would a webpage or any other container). That should just be the default behavior, no? (Thus the bug.)

For example, if we used ItemsControl here instead, this works perfectly fine with the keyboard:

<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls" 
    mc:Ignorable="d">

    <Page.Resources>
        <converters:StringFormatConverter x:Key="StringFormatConverter"/>
    </Page.Resources>

    <ScrollViewer HorizontalScrollMode="Disabled"
                    VerticalScrollMode="Enabled"
                    HorizontalScrollBarVisibility="Disabled"
                    VerticalScrollBarVisibility="Auto" Margin="20">
        <ItemsControl 
            ItemsSource="{Binding}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <controls:UniformGrid Columns="4" 
                                          RowSpacing="16" 
                                          ColumnSpacing="8" />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Image Source="{Binding id, Converter={StaticResource StringFormatConverter}, ConverterParameter='https://picsum.photos/id/{0}0/200/200'}" Stretch="UniformToFill"/>
                        <Border Background="#99FFFFFF" VerticalAlignment="Bottom">
                            <TextBlock Text="{Binding title}" TextWrapping="WrapWholeWords"
                                    Foreground="Black" Margin="4"/>
                        </Border>
                    </Grid>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </ScrollViewer>
</Page>

ItemsControl probably works because focus is on the ItemsControl. As a workaround you can wrap the ItemsRepeater something that can take focus like a UserControl and ensure IsTabStop is set to true on it.

The best way to track focus issues is to add an OnGotFocus handler on the page and tracing it out to debug output.

Closing this as by design. If you think that's in error feel free to reopen with your thoughts.

@StephenLPeters I think this is still a bug, it seems like the hosting ScrollViewer should still be getting the keyboard input to respond to the key events. It responds to Mouse Events (scroll wheel), so it's really odd this isn't keyboard accessible by default like the previous panel based models.

We shouldn't be putting extra onus on the developers using ItemsRepeater to have to do additional work to make their apps accessible, something like this should be just built-in.

@StephenLPeters I think this is still a bug, it seems like the hosting ScrollViewer should still be getting the keyboard input to respond to the key events. It responds to Mouse Events (scroll wheel), so it's really odd this isn't keyboard accessible by default like the previous panel based models.

We shouldn't be putting extra onus on the developers using ItemsRepeater to have to do additional work to make their apps accessible, something like this should be just built-in.

It's definitely a bug. If I have a scroll bar I should have the ability to scroll. I stated above, I had the same thing happen in Chromium Edge where scroll had focus and nothing would happen. I am using a update version where it is fixed as it was a bug.

A user is going to expect to be able to scroll with the arrow keys.

Does the ScrollViewer have IsTabStop set to true and focus is on the ScrollViewer ? I would expect the same experience if i replaced ItemsRepeater with a StackPanel

@ranjeshj I can check, but in the two samples I provided the only difference to see the keyboard behavior change was swapping ItemsControl/UniformGrid for the 'equivalent' ItemsRepeater/UniformGridLayout.

The difference is that ItemsControl is a 'Control' that can take focus, have a template etc. but ItemsRepeater is not a control and does not take focus. ItemsRepeater is more like a Panel.

Thanks @ranjeshj, so the better comparison is to place a Grid of elements in a ScrollViewer and see that it doesn't scroll via keyboard. I did notice in that scenario even, if I click on the scrollbar and drag it and then try and use the keyboard, nothing happens still.

Yes. That is exactly correct. The workaround is to have some control take focus so that there is a place for key events to start bubbling up from. If the ScrollViewer itself has focus, I believe that should work.

This behavior is a product of how the keyboard system works. when you press a key the framework raises a key pressed event with the origin of the event being the element which has focus. This event bubbles up the visual tree until it finds an element which handles the event. In this case since there is no child of the scrollviewer which is focusable and the scrollviewer itself is not focusable, bubbling up the visual tree will start at some other element and never reach the scrollviewer where the arrow key would be handled. The two options you have to fix this are to handle the arrow keys yourself, maybe at the page level, or to make the scrollviewer, or something in the scrollviewer a tab stop so that the keyboard event originates in a location that can reach the scrollviewer.

Thanks @ranjeshj, so the better comparison is to place a Grid of elements in a ScrollViewer and see that it doesn't scroll via keyboard. I did notice in that scenario even, if I click on the scrollbar and drag it and then try and use the keyboard, nothing happens still.

And this was the behavior I experienced.

@michael-hawker Setting "IsTabStop" to true on the ScrollViewer resolves the issues, and allows to scroll using the keys.

I would also say that this behavior is by "design". After all ItemsRepeater is responsible for giving you the rendered items, while the ScrollViewer is responsible for the scrolling. The only "design solution" would be to enforce scrollviewer to always be focusable or make itemsrepeater focusable, which both are not good solutions. In a lot of cases, the ScrollViewer shouldn't take the focus (but rather the GridView inside for example), and if there is no focusable content inside an ItemsRepeater it doesn't make much sense that the ItemsRepeater is focusable, after all it is not something that you can interact with.

There isn't much the framework can do here. ItemsRepeater cannot take focus because it is equivalent to a Panel. ScrollViewer can but we cannot change the default there (that is a breaking change). The workarounds are
(1) make the ScrollViewer a tab stop (set IsTabStop=true)
(2) have items inside the repeater be tab stops that can take focus. In the case of ListView for example ListViewItems can take focus.
(3) make some element in the tree between the ScrollViewer and ItemsRepeater tab stoppable (a border for example)
I don't see any framework fixes here to improve this so this bug is currently not actionable.

I'm going to close this issue as by design with work arounds.

Was this page helpful?
0 / 5 - 0 ratings