Bug 203292 - Accessory bar next/previous buttons do not work on inputs in shadow roots
Summary: Accessory bar next/previous buttons do not work on inputs in shadow roots
Status: RESOLVED FIXED
Alias: None
Product: WebKit
Classification: Unclassified
Component: DOM (show other bugs)
Version: Safari 13
Hardware: iPhone / iPad iOS 13
: P2 Normal
Assignee: Nobody
URL:
Keywords: InRadar
Depends on:
Blocks: 148695
  Show dependency treegraph
 
Reported: 2019-10-23 07:45 PDT by Liam DeBeasi
Modified: 2021-02-02 02:09 PST (History)
11 users (show)

See Also:


Attachments
WIP (1.31 KB, patch)
2020-09-24 22:44 PDT, Ryosuke Niwa
no flags Details | Formatted Diff | Diff
Patch (17.77 KB, patch)
2020-10-08 11:56 PDT, Tetsuharu Ohzeki [UTC+9]
no flags Details | Formatted Diff | Diff
Patch (18.10 KB, patch)
2020-10-09 09:18 PDT, Tetsuharu Ohzeki [UTC+9]
no flags Details | Formatted Diff | Diff
Patch (20.93 KB, patch)
2020-10-09 11:16 PDT, Tetsuharu Ohzeki [UTC+9]
no flags Details | Formatted Diff | Diff
Patch (34.58 KB, patch)
2020-10-11 14:04 PDT, Tetsuharu Ohzeki [UTC+9]
no flags Details | Formatted Diff | Diff
Patch (32.08 KB, patch)
2020-10-12 09:00 PDT, Tetsuharu Ohzeki [UTC+9]
no flags Details | Formatted Diff | Diff
Patch (22.68 KB, patch)
2020-10-25 10:32 PDT, Tetsuharu Ohzeki [UTC+9]
no flags Details | Formatted Diff | Diff
Patch (22.51 KB, patch)
2020-10-26 11:14 PDT, Tetsuharu Ohzeki [UTC+9]
no flags Details | Formatted Diff | Diff
Patch (22.50 KB, patch)
2020-10-27 09:47 PDT, Tetsuharu Ohzeki [UTC+9]
no flags Details | Formatted Diff | Diff
Patch (22.50 KB, patch)
2020-10-27 10:30 PDT, Tetsuharu Ohzeki [UTC+9]
no flags Details | Formatted Diff | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Liam DeBeasi 2019-10-23 07:45:24 PDT
Inputs that are slotted in shadow roots are not accessible via the next/previous buttons in the iOS Safari Accessory Bar.

Link to CodePen reproduction: https://codepen.io/liamdebeasi/pen/bGGqMpO

Steps to reproduce:

1. Open CodePen link on an iOS device (tested on iOS 12 and iOS 13).
2. Tap "Light DOM Input 2". Notice that the next/previous buttons are active.
3. Tap "Shadow DOM Input 2". Notice that the next/previous buttons are disabled.
4. Tap "Shadow DOM Input 3". Notice that the next button is disabled, even though the next input is in the light DOM and the same form.


Expected Result:

I would expect to be able to use the next/previous buttons to jump between inputs regardless of whether or not the input is in the Shadow DOM.


Other Info:

Next/Previous buttons on inputs in the Shadow DOM behave as expected on Chrome for Android.
Comment 1 Radar WebKit Bug Importer 2019-10-23 10:45:58 PDT
<rdar://problem/56544829>
Comment 2 cyptus 2019-12-10 01:09:30 PST
can we get any attention here? this is quite a big issue.
Comment 3 Niklas Merz 2020-01-20 06:26:40 PST
Can we provide anything to get this resolved? This is a pretty annoying usability bug for Ionic apps which typically use shadow DOM for all inputs.
Comment 4 Ryosuke Niwa 2020-01-20 23:25:49 PST
I can reproduce the issue. It's just a matter of taking the time to fix the bug now.
Comment 5 Luan Freitas 2020-09-22 21:44:15 PDT
I know it sucks to be asking. But do we have any predictions? Even how far? Middle 2021, maybe?
Comment 6 Ryosuke Niwa 2020-09-22 22:02:06 PDT
If someone can point out a major website which gets affected by this bug, that would help us prioritize it.
Comment 7 cyptus 2020-09-22 22:31:34 PDT
The whole ionic framework (https://ionicframework.com/) and every app that is build for iOS or visited by safari is affected by this bug.

Here is an offical list of major ionic customers who build their apps with this framework:
https://ionicframework.com/customers
Comment 8 Ryosuke Niwa 2020-09-22 23:11:10 PDT
(In reply to cyptus from comment #7)
> The whole ionic framework (https://ionicframework.com/) and every app that
> is build for iOS or visited by safari is affected by this bug.
> 
> Here is an offical list of major ionic customers who build their apps with
> this framework:
> https://ionicframework.com/customers

Rather than saying every app is affected, saying X doesn't work in app Y would be useful. Apple's way of prioritizing things rely heavily on data & exact user impact.
Comment 9 Niklas Merz 2020-09-22 23:56:36 PDT
I can give an example how this affects our app. We are working on an enterprise app with many different forms with lots of inputs item. Imagine a for where you enter contact information like: name, company, street, city etc. Most of our forms have can have dozens intputs as a list. So the user enters the first input an enters information. No they click on the up and down arrows of the accessory bar of the keyboard and nothing happens. Users would expect the focus cursor to jump to the next input just below.

Apps that use Ionic just like ours all will have this problem for their forms, because Ionic uses shadow DOM for almost all their components. An Ionic input element is using shadow DOM and may be one of the most used components because it styles an HTML input just like a native input. That's why Ionic is popular for app devs and probably many enterprise CRUD apps that work like ours with many input fields in forms.

I hope that gives a perspective. Fixing this would really improve the UX of apps built with Ionic.
Comment 10 cyptus 2020-09-23 00:12:16 PDT
Referred to the ionic customers link i provided, please download the app "UNTAPPD" on any iOS device (https://apps.apple.com/de/app/untappd-discover-beer/id449141888).

- Open the app and click on the bottom link "No Account? Register".
- A form will be shown to register your new account.
- iOS keyboard does not provide any option to switch between the input fields, the user needs to click manually on them for each input.

Screenshot: https://abload.de/img/ios-shadowdom-forms-bl1kor.jpg

See the same behavior in the example initially provided by Liam.
Comment 11 Simanchal Sahu 2020-09-24 07:46:31 PDT
Amtrak is currently in the process of upgrading our mobile application that is built with the Ionic Framework. With the latest upgrade including support for the Shadow Dom, this has interfered with the functionality of the accessory bar buttons to navigate forms effectively on iOS. The Amtrak app powers our entire customer facing native experience and with users filling out forms constantly for ticket purchases, the disruption from a typical iOS app flow can cause a significant impact in terms of usability as well as customer satisfaction and retention.

This is a key issue for the ADA community which is one of the top priority for Amtrak in addition to millions of other users.

We would love to see this issue resolved quickly to reduce the impact that this bug has on our app, as well as improve the overall iOS experience for our customers.

We would highly appreciate if this can be resolved at the earliest.


Thanks,
Simanchal Sahu, Sr Principal Technologist
Customer, Architecture and Platform Services - CAPS
Comment 12 Ryosuke Niwa 2020-09-24 22:44:00 PDT
Created attachment 409657 [details]
WIP

I think this will do the trick but need to find a time to write a test & verify that it actually fixes the bug.
Comment 13 Ryosuke Niwa 2020-09-24 22:45:41 PDT
Wensons says:
On UIScriptController, there’s:
undefined keyboardAccessoryBarNext();
undefined keyboardAccessoryBarPrevious();
These call WKWebView SPI that directly call into -accessoryTab: (edited) 

So that's how I'd write a test.
Comment 14 Tetsuharu Ohzeki [UTC+9] 2020-10-05 09:32:20 PDT
I'll try to write a test for this.

Ryosuke, are there any others to need to work?
Comment 15 Tetsuharu Ohzeki [UTC+9] 2020-10-08 11:56:31 PDT
Created attachment 410867 [details]
Patch

I added tests on the top of Ryosuke's original fix and format the patch.
Comment 16 Ryosuke Niwa 2020-10-08 15:28:48 PDT
Comment on attachment 410867 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=410867&action=review

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-in-shadow-roots.html:61
> +    this
> +        .attachShadow({ mode: 'open' })
> +        .innerHTML = '<p><slot></slot></p>';  

Can we just fit this in a single line?

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-in-shadow-roots.html:72
> +    this
> +        .attachShadow({ mode: 'closed' })
> +        .innerHTML = '<p><slot></slot></p>';  

Ditto.

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-in-shadow-roots.html:78
> +function moveToNextElement() {
> +    return new Promise((resolve) => {

Hm... can we add this function to UIHelper in ui-helper.js?
Maybe call it moveToNextByKeyboardAccessoryBar.

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-in-shadow-roots.html:86
> +function moveToPrevElement() {

Ditto.

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-in-shadow-roots.html:96
> +if (window.testRunner) {
> +    description('Test that Accessory bar next/previous buttons work on inputs in shadow roots');

Please add an instruction on how to test this manually in a browser.

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-in-shadow-roots.html:98
> +    const runTest = async () => {

Can we define this outside if?

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-in-shadow-roots.html:112
> +                shouldBe(() => document.activeElement.getAttribute('placeholder'), () => element.getAttribute('placeholder'));

shouldBe doesn't take functions. They take strings to be eval'ed.

Can we mirror tests like negative-tabindex-on-shadow-host and just log the element which got focus?
This will allow easier manual testing.
Comment 17 Tetsuharu Ohzeki [UTC+9] 2020-10-09 09:18:14 PDT
Created attachment 410941 [details]
Patch

I moved testcases to under LayoutTests/platform/ios/ios/. I'll address points reviewed by Ryosuke in the next patch
Comment 18 Tetsuharu Ohzeki [UTC+9] 2020-10-09 11:16:32 PDT
Created attachment 410953 [details]
Patch

I addressed reviewed points
Comment 19 Tetsuharu Ohzeki [UTC+9] 2020-10-09 11:17:17 PDT
(In reply to Ryosuke Niwa from comment #16)
> > LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-in-shadow-roots.html:112
> > +                shouldBe(() => document.activeElement.getAttribute('placeholder'), () => element.getAttribute('placeholder'));
> 
> shouldBe doesn't take functions. They take strings to be eval'ed.

If we don't pass a function, `shouldBe()` will fail by `ReferrenceError`. It cannot capture a block scupe variables...
Comment 20 Ryosuke Niwa 2020-10-09 15:01:41 PDT
Comment on attachment 410953 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=410953&action=review

> Source/WebCore/ChangeLog:9
> +        Tests: platform/ios/ios/fast/shadow-dom/accessory-bar-work-on-input-in-shadow-roots.html
> +               platform/ios/ios/fast/shadow-dom/accessory-bar-work-on-input-in-shadow-tree.html

This isn't right. We need to have this test under fast/shadow-dom/ios as you did in your original patch.

platform directories are supposed to only contain -expected.txt files going forward.

> LayoutTests/platform/ios/ios/fast/shadow-dom/accessory-bar-work-on-input-in-shadow-roots.html:14
> +        <input type="text" onfocus="logOnFocus(this)" placeholder="0. Shadow DOM Input 0">

Just do: Array.from(document.querySelectorAll('input')).forEach((input) => input.addEventListener(logOnFocus)).

We also need to test input elements inside a shadow tree, input elements hosts not assigned to any slot,
as well as non-input elements with negative, positive, and 0 tabIndex.

This is why I was suggesting earlier to model it over other existing tabindex tests.

> LayoutTests/platform/ios/ios/fast/shadow-dom/accessory-bar-work-on-input-in-shadow-roots.html:59
> +<pre style="display: none" id="console-for-manually"></pre>

It's okay for this element to exist even inside WebKitTestRunner. Just forget about display: none.
"manually" is an adverb, we need a noun there instead. We usually call this just "log" so: id="log".

> LayoutTests/platform/ios/ios/fast/shadow-dom/accessory-bar-work-on-input-in-shadow-roots.html:130
> +    consoleForManually.style.display = '';

consoleForManually.style.display = null would be better.
Comment 21 Tetsuharu Ohzeki [UTC+9] 2020-10-11 14:04:06 PDT
Created attachment 411063 [details]
Patch
Comment 22 Darin Adler 2020-10-11 19:30:17 PDT
Comment on attachment 411063 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=411063&action=review

> LayoutTests/platform/gtk/TestExpectations:650
> +# These tests are for iOS' accessory bar. Not for other ports.

The preferred pattern here is to skip this directory in the top level TestExpectations file and then set expectations of [Pass] in the iOS TestExpectations file.

Also part of the pattern is to do the expectation for the whole directory, not a file at a time.
Comment 23 Tetsuharu Ohzeki [UTC+9] 2020-10-12 09:00:34 PDT
Created attachment 411122 [details]
Patch
Comment 24 Darin Adler 2020-10-12 10:37:52 PDT
Comment on attachment 411122 [details]
Patch

Waiting to see successful test results on iOS-wk2 EWS.
Comment 25 Ryosuke Niwa 2020-10-13 00:04:01 PDT
Comment on attachment 411122 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=411122&action=review

It's not great to split these tests into multiple files. Now not all the combinations are tested.
Also, it seems like once accessory-bar-work-on-input-with-tabindex-in-shadow-tree.html becomes complete,
all other test files are sort of redundant.

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-in-shadow-roots.html:130
> +} else {
> +    document.getElementById('console').style.display = 'none';
> +}

Nit: not curly bracket around a single line statement:
https://webkit.org/code-style-guidelines/#braces-one-line

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-in-shadow-tree.html:33
> +    this
> +        .attachShadow({ mode: 'open' })
> +        .innerHTML = '<p><input onfocus="logOnFocus(this)" placeholder="1. input hosted by input-holder"/></p>';

Can we place attachShadow & this in the same line?

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-in-shadow-tree.html:43
> +    this
> +        .attachShadow({ mode: 'open' })

Ditto.

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-in-shadow-tree.html:44
> +        .innerHTML = '<input-holder></input-holder>';

We should add another input before & after this inner shadow host.

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-with-tabindex-in-shadow-tree.html:22
> +    <input id="input-host-with-positive-tabindex" tabindex="0" onfocus="logOnFocus(this)" placeholder="1. Shadow host with a positive tabindex" />

Seems like we're missing setting tabIndex to positive & negative values in the slotted content.

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-with-tabindex-in-shadow-tree.html:45
> +    <slot></slot>
> +    <input id="inside-shadow-host-with-positive-tabindex" tabindex="0" onfocus="logOnFocus(this)" placeholder="2. Focusable element inside a shadow host with a positive tabindex"/>

It seems like we're missing a test case where set a positive & negative tabIndex values inside a shadow tree.
Comment 26 Tetsuharu Ohzeki [UTC+9] 2020-10-23 06:49:01 PDT
I'm sorry to no reaction for a while.
I'll reply in this weekend.
Comment 27 Tetsuharu Ohzeki [UTC+9] 2020-10-25 10:32:10 PDT
Created attachment 412272 [details]
Patch
Comment 28 Ryosuke Niwa 2020-10-25 17:24:07 PDT
Comment on attachment 412272 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=412272&action=review

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-with-tabindex-in-shadow-tree.html:130
> +class Testcase {
> +    constructor(documentOrShadowRoot, inputGetter) {

It seems that we can just have a helper function that creates a simple dictionary instead of a class with a getter.
For tests, we should strive to make things as simple as possible because we don't want to be debugging test code.

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-with-tabindex-in-shadow-tree.html:145
> +        new Testcase(document, (_) => document.getElementById('first')),

It seems this class is totally unnecessary.
Given we're not dynamically mutating the tree, we can simply compute the input element right here:
e.g. testCase(document, document.getElementById('first'))

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-with-tabindex-in-shadow-tree.html:181
> +    for (const [i, { documentOrShadowRoot, inputElement, }] of stack.entries()) {

Why not just start at i=1 instead?

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-with-tabindex-in-shadow-tree.html:196
> +    for (let i = stack.length - 1; -1 < i; --i) {
> +        if (stack.length - 1 === i) {

Why not just start i = stack.length - 2?

> LayoutTests/resources/ui-helper.js:1383
> +    static moveToNextByKeyboardAccessoryBar() {

Nit: { should be on the next line.

> LayoutTests/resources/ui-helper.js:1392
> +    static moveToPrevByKeyboardAccessoryBar() {

Ditto.
Comment 29 Tetsuharu Ohzeki [UTC+9] 2020-10-26 10:09:51 PDT
(In reply to Ryosuke Niwa from comment #28)
> Comment on attachment 412272 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=412272&action=review
> 
> > LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-with-tabindex-in-shadow-tree.html:130
> > +class Testcase {
> > +    constructor(documentOrShadowRoot, inputGetter) {
> 
> It seems that we can just have a helper function that creates a simple
> dictionary instead of a class with a getter.
> For tests, we should strive to make things as simple as possible because we
> don't want to be debugging test code.


As a viewpoint to simplify the tests, how do you think about change `runTest()` in this patch to following?:

```
async function runTest() {
    const NUMBER_OF_INPUT_ELEMENTS_SHOULD_BE_FOCUSED = 10;
    for (let i = 0; i < NUMBER_OF_INPUT_ELEMENTS_SHOULD_BE_FOCUED; ++i) {
        await UIHelper.moveToNextByKeyboardAccessoryBar();        
    }

    for (let i = NUMBER_OF_INPUT_ELEMENTS_SHOULD_BE_FOCUED; -1 < i; --i) {
        await UIHelper.moveToPrevByKeyboardAccessoryBar();
    }
}
```

I had thought this pattern is at first, but I also thought that 

1. this might be hard to debug failures in CI job.
2. this might be a oversimplified.


So I chose the current patch's approach.
What do you think about this?



> > LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-with-tabindex-in-shadow-tree.html:181
> > +    for (const [i, { documentOrShadowRoot, inputElement, }] of stack.entries()) {
> 
> Why not just start at i=1 instead?

This is because my refactoring was not enough....


> > LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-with-tabindex-in-shadow-tree.html:196
> > +    for (let i = stack.length - 1; -1 < i; --i) {
> > +        if (stack.length - 1 === i) {
> 


Ditto.
Comment 30 Tetsuharu Ohzeki [UTC+9] 2020-10-26 11:14:20 PDT
Created attachment 412339 [details]
Patch

I addressed the comment in the  comment #28
Comment 31 Tetsuharu Ohzeki [UTC+9] 2020-10-27 09:47:53 PDT
Created attachment 412433 [details]
Patch

I rebased the patch on bug 218162.
Comment 32 Darin Adler 2020-10-27 10:01:42 PDT
I presume Ryosuke wants to review the revised tests.
Comment 33 Tetsuharu Ohzeki [UTC+9] 2020-10-27 10:04:39 PDT
I also think so.
I'd like to hear Ryosuke's feedback about comment #29
Comment 34 Ryosuke Niwa 2020-10-27 10:18:47 PDT
Comment on attachment 412433 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=412433&action=review

New test looks good!

> LayoutTests/fast/shadow-dom/ios/accessory-bar-work-on-input-with-tabindex-in-shadow-tree.html:206
> +} else {
> +    document.getElementById('console').style.display = 'none';
> +}

Nit: no curly braces around a single line statement.
Comment 35 Tetsuharu Ohzeki [UTC+9] 2020-10-27 10:23:26 PDT
(In reply to Ryosuke Niwa from comment #34)
> Comment on attachment 412433 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=412433&action=review
> 
> New test looks good!


Thank you!
Is "New test" the attachment 412433 [details], or the proposal that I commented in comment #29?



> Nit: no curly braces around a single line statement.

Oops. I'll address...
Comment 36 Tetsuharu Ohzeki [UTC+9] 2020-10-27 10:30:19 PDT
Created attachment 412441 [details]
Patch
Comment 37 Ryosuke Niwa 2020-10-27 11:03:52 PDT
Did you also meant to set cq?
Comment 38 Tetsuharu Ohzeki [UTC+9] 2020-10-27 11:08:39 PDT
(In reply to Ryosuke Niwa from comment #37)
> Did you also meant to set cq?

Ah, not yet.
I thought your feedback about #comment 35 will come and I was going to set cq? after that.
Comment 39 Ryosuke Niwa 2020-10-27 11:33:14 PDT
Comment on attachment 412441 [details]
Patch

Great. Thanks for writing a test & refining it many times!
Comment 40 EWS 2020-10-27 11:51:43 PDT
Committed r269059: <https://trac.webkit.org/changeset/269059>

All reviewed patches have been landed. Closing bug and clearing flags on attachment 412441 [details].
Comment 41 Tetsuharu Ohzeki [UTC+9] 2020-10-27 22:42:24 PDT
(In reply to Ryosuke Niwa from comment #39)

> Great. Thanks for writing a test & refining it many times!

You're welcome!
I also say thank you for your review.
Comment 42 Niklas Merz 2021-02-02 02:09:36 PST
I just tested our app with iOS 14.5 Developer Beta and this issue is fixed.

Thank you all very much for this patch. This is a huge UX improvement for our users.