Skip to content
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

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

mdebbar
Copy link
Contributor

@mdebbar mdebbar commented May 10, 2024

  • Better support for semantics.
  • Better support for clicks with a modifier key (e.g. cmd+click).
  • More robust handling of events (can now correctly handle events received in a different order).
  • Apply the target attribute on semantic links.
  • Improve tests.

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

@@ -42,13 +47,42 @@ class WebLinkDelegate extends StatefulWidget {
WebLinkDelegateState createState() => WebLinkDelegateState();
}

extension on Uri {
Copy link
Contributor

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?

Copy link
Contributor Author

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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: docs?

Copy link
Contributor Author

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());
Copy link
Contributor

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?

Copy link
Contributor Author

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;
Copy link
Contributor

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.

Copy link
Contributor Author

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++}';
Copy link
Contributor

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++}'?

Copy link
Contributor Author

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`.
Copy link
Contributor

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?

Copy link
Contributor Author

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:

  1. The engine is listening to keyboard events in the capture phase.
  2. 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

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.
Copy link
Contributor

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.

Copy link
Contributor Author

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();
}
Copy link
Contributor

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).

Copy link
Contributor Author

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);
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants