-
Notifications
You must be signed in to change notification settings - Fork 6k
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
[web] Move text editing nodes outside of shadowDOM #39688
Conversation
final DomElement element = | ||
domDocument.createElement('flt-shadow-host-node'); |
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.
Do we need this extra element? What do you think of the following:
flt-glass-pane
-------------------------
/ | \
#shadow-root text-editing-host semantics-host
| |
text nodes semantics nodes
EDIT: After discussing with @htoor3 it looks like the extra element above the shadow root avoids some issues with event listeners. Please add this explanation in a comment for future reference.
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.
Yeah that's correct, having a direct sibling to the #shadow-root
caused some rendering issues. I noticed the input
listeners on the active element weren't being attached, and the inputs weren't functioning correctly.
This is likely due to the entire subtree within flt-glass-pane
getting replaced whenever attachShadow
gets called on it - which is standard shadowDOM behavior.
Two ways around that:
- Contain the
#shadow-root
within its own node, which is what I did above. WhenattachShadow
gets called on that node, only that node's children will get replaced, which allows the text-editing nodes to render correctly since they exist outside of the#shadow-root
's parent node. - We can use
<slot>
s within our#shadow-root
to render any of its siblings. However, it's unclear whether text inputs rendered via<slot>
s will face the same Chrome password autofill issues, since the<slot>
s are technically within the shadowDOM.
Edit: Option 2 actually would work and keep our tree slightly flatter if we deem that important. Quick jsfiddle shows that the items rendered via the <slot>
aren't affected by the Chrome autofill bug.
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.
Yeah, IIRC in the presence of a shadow root direct children of the node are treated quite differently from normal elements. There's an expectation that there will be a <slot>
inside the shadow root to project direct children into, because the elements are laid out based on constraints provided to the slot. Without one I'm not even sure how the element would be rendered. So moving text-editing-host into a separate branch is probably safer.
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.
nit: Would <flt-render-host>
be a better name? It seems we're moving all the non-rendering things out of it, and it's used purely for positioning, laying out, and painting, which are all rendering aspects. Also, the -node
suffix doesn't add useful information.
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 that makes sense. I pushed up the naming change and moved the semantics tree out of the text-editing host
DomElement? get textEditingHostNode => _textEditingHostNode; | ||
DomElement? _textEditingHostNode; |
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.
@ditman I had a discussion with @htoor3 about this and he mentioned that you were planning to remove host_node.dart
. I have a different opinion that I would like to share here and get your input on:
The benefit I see in HostNode
is to encapsulate the high-level DOM structure of the app (including the shadow root). This way, the rest of the engine doesn't have to worry about any of the following:
- Does the glass pane have a shadow root attached?
- Is the semantics tree rendered behind or in front of the scene host?
- Are text fields inside or outside the shadow root?
That said, you are more familiar with this area, maybe you have a different vision for this, especially with your work on element embedding? E.g. should HostNode
and embedding strategy be merged together? Or maybe all of the HostNode
encapsulation should be moved to FlutterViewEmbedder
?
One thing that may help here is to determine which classes are supposed to be global/singletons and which classes are supposed to be instantiated for each app or view. To clarify: in the future, we are going to support rendering multiple flutter apps on the same page (into different host elements). In that case, is each app going to have its own FlutterViewEmbedder
? Or is that supposed to be global across all apps?
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 was planning on removing the HostNode
because the original version was only to decide if we were going to render in an Element or the ShadowDOM of an element.
We can repurpose the name for any other thing we like.
My idea was to have a FlutterApplicationElement (call it HostNode) that knows how to create all the different elements for a Flutter Web app view (the glass-pane, the different element hosts, etc...), and that can be used later in place of the global "textEditingNode", "glassPaneElement", "semanticsHelper" and family (because it's likely that the global ViewEmbedder needs to go away in a multi-view world)
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.
Ok, I'm glad we are in agreement on the core idea.
I actually don't like the name HostNode
because it could easily be confused with the host element for embedding. So a new name is in order.
I also looked at flutter/flutter#116204 which indicates that there may be a need to keep the non-shadow implementation?
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 also looked at flutter/flutter#116204 which indicates that there may be a need to keep the non-shadow implementation?
Talked to Yegor about it, and there's no need to support the non-shadow implementation moving forward.
@@ -202,9 +207,10 @@ class FlutterViewEmbedder { | |||
// elements transparent. This way, if a platform view appears among other | |||
// interactive Flutter widgets, as long as those widgets do not intersect | |||
// with the platform view, the platform view will be reachable. | |||
semanticsHostElement, |
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.
@yjbanov would love to get your thoughts on if we can safely move the semantics tree outside of the shadowDOM and into its own node?
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.
With the current problematic implementation of semantics, this shouldn't break anything I know of. However, one issue with the current semantics tree is that it does not contain platform views. This creates some issues with traversal order. I was thinking in the future to slice the semantics tree and interleave it with the render tree such that platform views can be embedded into the semantics tree while preserving the paint order. To do that, the semantics tree would have to be inside the shadow root, otherwise the <slot>
elements won't work.
Having said that, I'm willing to cross that bridge when we get there. What's the motivation for moving it out?
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.
Primary motivation to move it out is to fix the password autofill issue for when semantics is enabled, since <input>
s will be rendered in the semantics tree in that case and they'll face the same shadowDOM issues. AFAIK, the 2 nodes that host text editing elements would be whatever host we specify in text_editing.dart
and the semantics tree.
How hard would it be to create a minimal repro case of this, and send it to Chrome for them to fix (if they don't have it already)? This looks like a big change for a browser workaround :/ |
@htoor3 already added some details and a jsfiddle repro to the existing chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=1378402#c8
Unfortunately, we don't know when Chrome will fix this bug, and this is a very popular issue in flutter web that has been open for a while. I would say let's bite the bullet and do the workaround. |
Note that since Safari Tech Preview 162, this seems like a Chrome-only bug. The only drawback I see to this fix is that with the custom element embedding, things outside of the shadow DOM can be very easily affected by the surrounding page's CSS. Luckily not many people are using that embedding mode yet, so I guess it's not as problematic as password autofills (yet). |
Are the issues mainly due to styling collisions? I wonder if generating unique classnames for nodes outside of the shadowDOM or using the |
The ones I can think off the top of my head: yes.
Specificity is good, maybe with a bunch of |
activeElement comes from the |
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.
Overall looks good to me with some minor comments.
// On top of a platform view | ||
if (event.target != actualTarget) { | ||
return _computeOffsetOnPlatformView(event, actualTarget); | ||
} | ||
// On a TalkBack event | ||
if (EngineSemanticsOwner.instance.semanticsEnabled && event.offsetX == 0 && event.offsetY == 0) { |
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.
@ditman is the conditional order ok to change here? Running into a TalkBack issue where the offsets aren't being calculated correctly for pointer events. The problem is arising because any event that crosses into the shadowDOM has a target
of whatever the shadowDOM's host is (glass pane). With the semantic tree moved outside of the shadowDOM, the target
is now the actual element being clicked, which was causing us to branch into the platform view offset conditional, since target
and actualTarget
won't both be the glass pane. If we move the TalkBack offset computation conditional first, it fixes the issue.
cc @yjbanov this change fixes the TalkBack issue you discovered in the Gallery app.
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 this change makes sense. I'm not sure I have any special criteria when I added the ordering in the first place, but in this case it seems we're going from "most special corner case" to "least special case", so I think this change makes a lot of sense (also, glad that the change is simple and not something involving changing the measurements/algos :P)
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.
Spoke with @htoor3 over a VC. Some notes:
- It seems the
event.target == actualTarget
is never true becauseactualTarget
is<flt-glass-pane>
, but the shadow root host is the new<flt-render-host>
. - Simply changing to
<flt-render-host>
may not be sufficient because the semantics tree now behaves like a platform view (it's outside the shadow root) and may require similar logic to_computeOffsetOnPlatformView
.
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.
Update on this:
- To your first point, this logic actually still does work because
<flt-render-host>
is invisible. So when we hover/click on anything in the app, theevent.target
is still<flt-glass-pane>
and the offset logic works correctly. - Since we're removing text and semantic nodes outside of the shadowDOM, those offsets now need to be calculated with the same logic as how platform view offsets were being calculated. This was already happening but it was a bit confusing as to why. The
event.target != actualTarget
expression is actually a good check for this because it doesn't seem like we need to differentiate between nodes that are platform views and other non-shadowDOM nodes - we just need to determine if any given target is outside the shadowDOM. If so, we need to recalculate the offsets. - To make this clearer, I renamed the boolean to check for this to
isTargetOutsideOfShadowDOM
and the function to a more generalized name_computeOffsetRelativeToActualTarget
since it's computing offsets for more than just platform views now. - Tested this in various corner cases and the offsets are working correctly for:
- Text nodes
- Platform views
- Semantic tree nodes
- When the page is scrolled
- When the app is embedded into a
<div>
- Using screen readers (both VoiceOver and Talkback)
@@ -705,7 +709,7 @@ Future<void> testMain() async { | |||
expect(spy.messages, hasLength(0)); | |||
await Future<void>.delayed(Duration.zero); | |||
// DOM element still keeps the focus. | |||
expect(defaultTextEditingRoot.activeElement, | |||
expect(defaultTextEditingRoot.ownerDocument?.activeElement, |
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.
@eyebrowsoffire this is the change I've made, with the firefox tests passing - however, this is dependent on a few other changes in this PR, mainly defaultTextEditingRoot
being changed, so it might not be super easy to drop these changes in.
Oddly enough, I tried running the same tests on the latest version of main
branch and they still pass with the --browser=firefox
flag
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.
The problem might be with the way Firefox reports activeElement
when the focus is inside the shadow dom. Now that text editing elements moved out of the shadow dom, the issue is fixed.
Reverts #39688 Looks like this is causing the roll to the framework to fail. See https://ci.chromium.org/ui/p/flutter/builders/try/Linux%20web_long_running_tests_4_5/34083/overview ``` 00:03 �[32m+1�[0m�[31m -1�[0m: Hello World App enable accessibility �[1m�[31m[E]�[0m�[0m JavaScriptException (500): javascript error: Cannot read properties of null (reading 'querySelector') (Session info: headless chrome=96.0.4664.0) package:webdriver/src/handler/w3c/utils.dart 57:9 parseW3cResponse package:webdriver/src/handler/w3c/core.dart 59:19 W3cCoreHandler.parseExecuteResponse package:webdriver/src/async/web_driver.dart 260:37 WebDriver.execute.<fn> package:webdriver/src/common/request_client.dart 96:32 AsyncRequestClient.send ===== asynchronous gap =========================== test_driver/smoke_web_engine_test.dart 41:40 main.<fn>.<fn> ===== asynchronous gap =========================== package:test_api/src/backend/declarer.dart 215:9 Declarer.test.<fn>.<fn> ===== asynchronous gap =========================== package:test_api/src/backend/declarer.dart 213:7 Declarer.test.<fn> ===== asynchronous gap =========================== package:test_api/src/backend/invoker.dart 258:9 Invoker._waitForOutstandingCallbacks.<fn> 00:03 �[32m+1�[0m�[31m -1�[0m: Hello World App (tearDownAll)�[0m ```
…sions) (#123924) Manual roll requested by [email protected] flutter/engine@b2855e0...a48eedb 2023-04-01 [email protected] Revert "[web] Move text editing nodes outside of shadowDOM" (flutter/engine#40847) 2023-04-01 [email protected] Roll Skia from 4d1e9cabf0c8 to 9973ef180f1f (2 revisions) (flutter/engine#40843) 2023-04-01 [email protected] Roll Dart SDK from 6ac8d3ad105f to 7e36e11608f3 (6 revisions) (flutter/engine#40842) 2023-04-01 [email protected] [Impeller] take advantage of native decal sampling, blend cleanups (flutter/engine#40839) 2023-04-01 [email protected] Revert "[web] use callConstructor for FinalizationRegistry due to bug… (flutter/engine#40841) 2023-04-01 [email protected] Revert "Add ui_web to embedder.yaml so that the analyzer knows about it." (flutter/engine#40840) 2023-04-01 [email protected] [Impeller] Migrate gaussian blur to half precision. (flutter/engine#40800) 2023-03-31 [email protected] Roll Skia from 33f80c07a09c to 4d1e9cabf0c8 (3 revisions) (flutter/engine#40836) 2023-03-31 [email protected] [web] Move text editing nodes outside of shadowDOM (flutter/engine#39688) 2023-03-31 [email protected] [web] Fix canvasKitVariant test (flutter/engine#40833) 2023-03-31 [email protected] [web] use callConstructor for FinalizationRegistry due to bug in dart2js (flutter/engine#40798) 2023-03-31 [email protected] Roll Fuchsia Mac SDK from kiAbXJ_MIn6CAC9-C... to f16HBH4MJdaKy7Hlf... (flutter/engine#40831) 2023-03-31 [email protected] Remove ios-release-nobitcode from engine v2 builders. (flutter/engine#40830) 2023-03-31 [email protected] Add ui_web to embedder.yaml so that the analyzer knows about it. (flutter/engine#40827) 2023-03-31 [email protected] Roll Skia from 2b86c6d364d0 to 33f80c07a09c (1 revision) (flutter/engine#40826) Also rolling transitive DEPS: fuchsia/sdk/core/mac-amd64 from kiAbXJ_MIn6C to f16HBH4MJdaK If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-engine-flutter-autoroll Please CC [email protected],[email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
…sions) (flutter#123924) Manual roll requested by [email protected] flutter/engine@b2855e0...a48eedb 2023-04-01 [email protected] Revert "[web] Move text editing nodes outside of shadowDOM" (flutter/engine#40847) 2023-04-01 [email protected] Roll Skia from 4d1e9cabf0c8 to 9973ef180f1f (2 revisions) (flutter/engine#40843) 2023-04-01 [email protected] Roll Dart SDK from 6ac8d3ad105f to 7e36e11608f3 (6 revisions) (flutter/engine#40842) 2023-04-01 [email protected] [Impeller] take advantage of native decal sampling, blend cleanups (flutter/engine#40839) 2023-04-01 [email protected] Revert "[web] use callConstructor for FinalizationRegistry due to bug… (flutter/engine#40841) 2023-04-01 [email protected] Revert "Add ui_web to embedder.yaml so that the analyzer knows about it." (flutter/engine#40840) 2023-04-01 [email protected] [Impeller] Migrate gaussian blur to half precision. (flutter/engine#40800) 2023-03-31 [email protected] Roll Skia from 33f80c07a09c to 4d1e9cabf0c8 (3 revisions) (flutter/engine#40836) 2023-03-31 [email protected] [web] Move text editing nodes outside of shadowDOM (flutter/engine#39688) 2023-03-31 [email protected] [web] Fix canvasKitVariant test (flutter/engine#40833) 2023-03-31 [email protected] [web] use callConstructor for FinalizationRegistry due to bug in dart2js (flutter/engine#40798) 2023-03-31 [email protected] Roll Fuchsia Mac SDK from kiAbXJ_MIn6CAC9-C... to f16HBH4MJdaKy7Hlf... (flutter/engine#40831) 2023-03-31 [email protected] Remove ios-release-nobitcode from engine v2 builders. (flutter/engine#40830) 2023-03-31 [email protected] Add ui_web to embedder.yaml so that the analyzer knows about it. (flutter/engine#40827) 2023-03-31 [email protected] Roll Skia from 2b86c6d364d0 to 33f80c07a09c (1 revision) (flutter/engine#40826) Also rolling transitive DEPS: fuchsia/sdk/core/mac-amd64 from kiAbXJ_MIn6C to f16HBH4MJdaK If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-engine-flutter-autoroll Please CC [email protected],[email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Password input autofill + suggest password functionality is broken for password inputs inside of a shadowDOM on chrome. This experiment is testing to see if we can lift text editing nodes outside of the shadowDOM while maintaining functionality.
Tree goes from:
to:
Some additional changes:
flt-render-host
(previously being appended toflt-glass-pane
)Simple form w/code changes
Fixes flutter/flutter#87735