Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Conversation

@yjbanov
Copy link
Contributor

@yjbanov yjbanov commented Feb 20, 2024

On Windows, where JAWS is a popular screen reader and, unfortunately, ignores aria-label on empty elements, and for user agents that are known web crawlers (that also ignore aria-label), render semantic text into the DOM using an additional <span> element.

Fixes flutter/flutter#122607

@yjbanov yjbanov requested a review from mdebbar February 20, 2024 20:49
@github-actions github-actions bot added the platform-web Code specifically for the web engine label Feb 20, 2024
Copy link
Contributor

@mdebbar mdebbar left a comment

Choose a reason for hiding this comment

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

LGTM

final List<RegExp> _crawlerRegexes = <RegExp>[
// Covers the search engine and various specialized crawlers: https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers
RegExp(r'googlebot'),
RegExp(r'\-google'),
Copy link
Contributor

Choose a reason for hiding this comment

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

This a good start though it doesn't capture all the user agents that Google lists in that url (e.g. Google-Extended, GoogleOther).

This is fine for now but let's file an issue maybe to improve the regexes?

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'm actually starting to wonder if the engine should contain any crawler patterns at all. Maybe these belong in a package?

@yjbanov yjbanov requested a review from mdebbar February 23, 2024 19:15
Copy link
Contributor

@mdebbar mdebbar left a comment

Choose a reason for hiding this comment

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

I have a few minor suggestions but nothing major.

LGTM!

Comment on lines 81 to 89
DomElement? span = _labelSpan;

// Lazy-create and cache the span.
if (span == null) {
_labelSpan = span = createDomElement('span');
semanticsObject.element.appendChild(span);
}

span.innerText = label;
Copy link
Contributor

Choose a reason for hiding this comment

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

You could do something fancier like this:

Suggested change
DomElement? span = _labelSpan;
// Lazy-create and cache the span.
if (span == null) {
_labelSpan = span = createDomElement('span');
semanticsObject.element.appendChild(span);
}
span.innerText = label;
// Lazy-create and cache the span.
_labelSpan ??= semanticsObject.element.appendChild(createDomElement('span'));
_labelSpan.innerText = label;

but it might hurt readability a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, tried it, but doesn't fully work. The local variable is needed so that the analyzer can prove that it's not null after initialization. _labelSpan is nullable and therefore requires a null assertion (line _labelSpan.innerText = label; does not compile). Also, appendChild return DomNode instead of DomElement, which requires an extra dynamic cast. Keeping the existing variant for now.

DomElement? _labelSpan;

void _updateLabel(String label) {
owner.setAttribute('aria-label', label);
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe a comment here explaining why we are setting aria-label even when needsDomText is true?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You made me realize something. I did this originally so that the aria label rolls up to the parent node, which is correctly sized. Span doesn't match the rect reported by the framework. However, since I'm only using the span for leaf nodes, I shouldn't need the span at all. I think I can just do semanticsObject.element.innerText = label. Let me give it a shot.

Comment on lines 71 to 87
/// It is expected that this function is called early in the app's
/// initialization, before the framework renders the semantics tree, e.g. prior
/// to invoking `runApp`. Calling it too late, such as after the first frame has
/// been rendered or in response to a user action, may result in incorrect
/// rendering of the semantics tree. The crawler may already have crawled the
/// app and failed to find the data it needed to index it.
void enableCrawlerMode() {
assert(
!_isCrawlerModeEnabled,
'`enableCrawlerMode` was called more than once.',
);
_isCrawlerModeEnabled = true;
}

/// Whether the crawler mode is enabled.
bool get isCrawlerModeEnabled => _isCrawlerModeEnabled;
bool _isCrawlerModeEnabled = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's try to enforce the no-late-calls rule:

Suggested change
/// It is expected that this function is called early in the app's
/// initialization, before the framework renders the semantics tree, e.g. prior
/// to invoking `runApp`. Calling it too late, such as after the first frame has
/// been rendered or in response to a user action, may result in incorrect
/// rendering of the semantics tree. The crawler may already have crawled the
/// app and failed to find the data it needed to index it.
void enableCrawlerMode() {
assert(
!_isCrawlerModeEnabled,
'`enableCrawlerMode` was called more than once.',
);
_isCrawlerModeEnabled = true;
}
/// Whether the crawler mode is enabled.
bool get isCrawlerModeEnabled => _isCrawlerModeEnabled;
bool _isCrawlerModeEnabled = false;
/// It is expected that this function is called early in the app's
/// initialization, before the framework renders the semantics tree, e.g. prior
/// to invoking `runApp`. Calling it too late, such as after the first frame has
/// been rendered or in response to a user action, will result in a `StateError`.
/// The crawler may already have crawled the app and failed to find the data
/// it needed to index it.
void enableCrawlerMode() {
assert(
_isCrawlerModeEnabled != null,
'`enableCrawlerMode` was called more than once.',
);
_isCrawlerModeEnabled = true;
}
/// Whether the crawler mode is enabled.
bool get isCrawlerModeEnabled {
// Make sure the value is set so it can't be overriden later.
return _isCrawlerModeEnabled ??= false;
}
bool? _isCrawlerModeEnabled;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Initializing _isCrawlerModeEnabled in the getter seems wrong. It means that you cannot call enableCrawlerMode() after calling the getter, i.e. the getter has a side-effect on the initialization lifecycle. For example, the following would throw an error with an incorrect error message:

if (!isCrawlerModeEnabled) {
  enableCrawlerMode(); // throws '`enableCrawlerMode` was called more than once.', which is wrong; it's called once
}

A better version of the getter might be:

bool get isCrawlerModeEnabled => isCrawlerModeEnabled ?? false;

However, now there's no difference between a nullable version and a non-nullable version. You either go from null to true, or from false to true.

Copy link
Contributor

Choose a reason for hiding this comment

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

What I'm aiming at is making sure that after the engine reads the value of isCrawlModeEnabled, it shouldn't be overridable anymore. Doing it in the getter is easier, but I agree it's not ideal to have side effects in a getter.

Another way of doing it would be to have a 2nd bool isCrawlModeSet and a preventFurtherCrawlModeChanges(). We probably don't want to expose these through dart:ui_web. So you may have to move the _isCrawlModeEnabled into the private part of the engine, and expose it through dart:ui_web via a getter.

Given the extra complexity it would take to add this check, I'm not sure anymore if it's worth doing it.

@yjbanov yjbanov requested a review from mdebbar March 13, 2024 18:04
Copy link
Contributor

@mdebbar mdebbar left a comment

Choose a reason for hiding this comment

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

LGTM and I like the LeafLabelRepresentation enum!

@yjbanov yjbanov added the autosubmit Merge PR when tree becomes green via auto submit App label Mar 13, 2024
@auto-submit auto-submit bot merged commit 6710d10 into flutter:main Mar 13, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Mar 14, 2024
auto-submit bot pushed a commit to flutter/flutter that referenced this pull request Mar 14, 2024
flutter/engine@cb9dead...6710d10

2024-03-13 yjbanov@google.com [web] make text more crawlable; fix a JAWS bug (flutter/engine#50794)

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 bdero@google.com,rmistry@google.com,zra@google.com 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://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

autosubmit Merge PR when tree becomes green via auto submit App platform-web Code specifically for the web engine

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JAWs can no longer read plain Text Widget's content.

2 participants