-
Notifications
You must be signed in to change notification settings - Fork 2.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
[url_launcher][web] Better support for semantics in the Link widget #6711
base: main
Are you sure you want to change the base?
Conversation
937ed37
to
979fc2a
Compare
@@ -42,13 +47,42 @@ class WebLinkDelegate extends StatefulWidget { | |||
WebLinkDelegateState createState() => WebLinkDelegateState(); | |||
} | |||
|
|||
extension on Uri { |
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: do extensions need docs?
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.
Well, this is a "nameless" extension, which is private. So I don't think docs are needed, but I'll add docs to the method.
@@ -42,13 +47,42 @@ class WebLinkDelegate extends StatefulWidget { | |||
WebLinkDelegateState createState() => WebLinkDelegateState(); | |||
} | |||
|
|||
extension on Uri { | |||
String getHref() { |
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: docs?
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 wanted to make sure first that the code is starting to look good before I spend time on writing and re-writing docs.
|
||
// In case an internal uri is given, the uri must be properly encoded | ||
// using the currently used UrlStrategy. | ||
return ui_web.urlStrategy!.prepareExternalUrl(toString()); |
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.
Is this related to flutter/flutter#147857 by any chance?
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.
Nope. That issue is purely a UrlStrategy and/or engine issue.
/// The link delegate used on the web platform. | ||
/// | ||
/// For external URIs, it lets the browser do its thing. For app route names, it | ||
/// pushes the route name to the framework. | ||
class WebLinkDelegateState extends State<WebLinkDelegate> { | ||
late LinkViewController _controller; | ||
|
||
@visibleForTesting | ||
late final String semanticIdentifier; |
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: we always use plural "semantics" even in singular context, e.g. SemanticsNode
.
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 keep jumping back and forth since I'm not sure if there's already a convention. I'll change all of them to plural.
@override | ||
void initState() { | ||
super.initState(); | ||
semanticIdentifier = 'sem-id-${_nextSemanticsIdentifier++}'; |
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 wonder if widget-specific identifiers should prefix their ids with their own names rather than the generic "sem". That it is a "semantics" and "ID" is already implied by the API through which this value flows. The useful extra information that could be provided here is that the ID belongs to a link widget. So maybe do 'link-${_nextSemanticsIdentifier++}'
?
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 agree!
// Why listen in the capture phase? | ||
// | ||
// To ensure we always receive the event even if the engine calls | ||
// `stopPropagation`. |
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.
Why would the engine stop propagating the events? Seems like code smell in the engine, no?
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.
For some reason, it already does here.
The problem is a combination of two things:
- The engine is listening to keyboard events in the capture phase.
- The engine is calling
stopPropagation
on the event when the framework handles it.
I wanted to make the Link widget robust in all kinds of scenarios, at least until we fix the above 2 issues.
@@ -197,41 +343,99 @@ class LinkViewController extends PlatformViewController { | |||
// - If `_hitTestedViewId` is set, it means the app triggered the link. |
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 for line https://github.com/flutter/packages/pull/6711/files#diff-77a04b3b00b00a4480b7d5e75e089dde7cca99ccebcc4f43e0e25d2bba5346e0R338: typo in "therefor"
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.
TIL; I always thought "therefor" and "therefore" were the same :)
/// Keeps track of the signals required to trigger a link. | ||
/// | ||
/// Automatically resets the signals after a certain delay. This is to prevent | ||
/// the signals from getting stale. |
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 description is very generic, even though the class does something very specific. AFAICT this class is resolving some race where the viewId
and the mouse event can come in at unpredictable times and in unpredictable order. The dart docs could talk about the specifics, and also explain how this race arises.
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.
Good idea. I'll revisit this when I do the pass to fill in missing docs.
_hitTestedViewId = null; | ||
if (_triggerSignals.isReadyToTrigger) { | ||
_triggerLink(); | ||
} |
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 pattern of this if
block looks the same everywhere. Can we pass the _triggerLink
function to LinkTriggerSignals
and have it call it at the appropriate time automatically? It already has the knowledge of when it is appropriate to call it. LinkTriggerSignals
will no longer need isReadyToTrigger
(except maybe for testing).
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 cases where I also check for isValid
first and do something else. But I think that also can be moved to the LinkTriggerSignals
class. I like the idea so I'll take a stab at it.
// We only want to handle clicks that land on *our* links, whether that's a | ||
// platform view link or a semantics link. | ||
final int? viewIdFromTarget = _getViewIdFromLink(target) ?? | ||
_getViewIdFromSemanticLink(target); |
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.
Does this work if <flutter-view>
is inside shadow DOM? In embedded case in particular, we don't know how exactly a FlutterView is embedded. It could be inside a web component's shadow DOM. In that case event.target
caught at window
level will simply point to the web component. It won't tell you the exact element the mouse event happened on.
Another issue is that this relies on the semantic DOM internals, so it can easily break if the structure changes.
Can we use https://api.flutter.dev/flutter/semantics/SemanticsProperties/onTap.html instead?
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.
Regarding <flutter-view>
being inside shadow DOM, that's a very good point that I hadn't considered. I'll test it and fix it.
Regarding relying on the semantic DOM internals, I couldn't find a better way to implement it. Using onTap
doesn't work because the children of the Link
widget typically have their own onTap
which absorbs the tap event so it never reaches the Link
's onTap
.
target
attribute on semantic links.To get the complete fix, the following engine PR is also needed (this PR doesn't strictly depend on the engine PR, but the fix won't be complete without it).
Fixes flutter/flutter#143164
Fixes flutter/flutter#146291