Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -2485,6 +2485,7 @@ class EngineSemantics {
final PlatformConfiguration newConfiguration = EnginePlatformDispatcher.instance.configuration
.copyWith(accessibilityFeatures: original.copyWith(accessibleNavigation: value));
EnginePlatformDispatcher.instance.configuration = newConfiguration;
EnginePlatformDispatcher.instance.invokeOnAccessibilityFeaturesChanged();

_semanticsEnabled = value;

Expand Down
13 changes: 13 additions & 0 deletions engine/src/flutter/lib/web_ui/test/engine/window_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,19 @@ Future<void> testMain() async {
EnginePlatformDispatcher.instance.invokeOnAccessibilityFeaturesChanged();
});

test('onAccessibilityFeaturesChanged is called when semantics is enabled', () {
bool a11yChangeInvoked = false;
myWindow.onAccessibilityFeaturesChanged = () {
a11yChangeInvoked = true;
};

expect(EngineSemantics.instance.semanticsEnabled, isFalse);
EngineSemantics.instance.semanticsEnabled = true;

expect(EngineSemantics.instance.semanticsEnabled, isTrue);
expect(a11yChangeInvoked, isTrue);
});

test('onPlatformMessage preserves the zone', () {
final Zone innerZone = Zone.current.fork();

Expand Down
13 changes: 12 additions & 1 deletion packages/flutter/lib/src/semantics/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ library;
import 'dart:ui' as ui show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilder;

import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';

import 'debug.dart';
Expand All @@ -26,7 +27,17 @@ mixin SemanticsBinding on BindingBase {
platformDispatcher
..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
..onSemanticsActionEvent = _handleSemanticsActionEvent
..onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
..onAccessibilityFeaturesChanged = () {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should do this in Widget MediaQuery, not all the listener here care about build phase.

Also when mediaQuery handles it, it should use postframecallback

    if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
      SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
        // do the thing.
      }, debugLabel: 'some meaningful label');
    } else {
      // do the thing
    }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think shuld all listeners be guarded from the build phase.

If we handle to do this on the MediaQuery side, we would call addPostFrameCallback in the didChangeAccessibilityFeatures.
https://github.com/flutter/flutter/blob/3.29.3/packages/flutter/lib/src/widgets/media_query.dart#L1905-L1914

didChangeAccessibilityFeatures is public API, so app developers can use this. Therefore, all code that uses didChangeAccessibilityFeatures will need to be modified.
https://api.flutter.dev/flutter/widgets/WidgetsBindingObserver/didChangeAccessibilityFeatures.html

If we do the build phase for listeners, there will be no breaking change to the public api. Since this breaking change affects only the web, it is difficult to notice and to understand why it is necessary.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this will only happen if the ensureSemantics is called during build phase right? I think this is not a common use case. Now i think we may not even need to have this check. the correct fix will be move ensureSemantics to either a postframecallback or somewhere before runApp

void main() {
  WidgetFlutterBindings.ensureInitialized().ensureSemantics();
  runApp(...)
}

Copy link
Copy Markdown
Contributor Author

@koji-1009 koji-1009 May 21, 2025

Choose a reason for hiding this comment

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

The problem occurred with the example code. I don't have that much experience with accessibility implementations, so let me ask: is calling the ensureSemantics method on a Button's onTap a non-recommended implementation?

import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';

void main() {
  runApp(const MaterialApp(home: MyHomePage()));
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                SemanticsBinding.instance.ensureSemantics();
              },
              child: const Text('Enable a11y'),
            ),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: const Text('Should stay visible'),
                    action: SnackBarAction(label: 'Action', onPressed: () {}),
                  ),
                );
              },
              child: const Text('Show snackbar'),
            ),
            const SizedBox(height: 24),
            Text(
              'MediaQuery.accessibleNavigationOf(context): ${MediaQuery.accessibleNavigationOf(context)}',
            ),
            Text(
              'SemanticsBinding.instance.semanticsEnabled: ${SemanticsBinding.instance.semanticsEnabled}',
            ),
            Text(
              'SemanticsBinding.instance.platformDispatcher.semanticsEnabled: ${SemanticsBinding.instance.platformDispatcher.semanticsEnabled}',
            ),
            Text(
              'SemanticsBinding.instance.accessibilityFeatures: ${SemanticsBinding.instance.accessibilityFeatures}',
            ),
          ],
        ),
      ),
    );
  }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ok i see the issue now, this is mostly a web issue that it enable the semantics in updatesemantics instead of synchronously when ensureSemanitcs is called.

Can you convert the microtask here to check schedule phase and postframe callback, and add a todo to link to this issue #158399

That way we can clean it up later when i fix the web engine

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK, I'll do it. Thanks for the review.

// TODO(chunhtai): Web should not notify accessibility feature changes during updateSemantics
// https://github.com/flutter/flutter/issues/158399
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
handleAccessibilityFeaturesChanged();
}, debugLabel: 'SemanticsBinding.handleAccessibilityFeaturesChanged');
} else {
handleAccessibilityFeaturesChanged();
}
};
_handleSemanticsEnabledChanged();
}

Expand Down
2 changes: 1 addition & 1 deletion packages/flutter/test/material/app_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ void main() {
// didChangeAccessibilityFeatures
tester.platformDispatcher.accessibilityFeaturesTestValue = FakeAccessibilityFeatures.allOn;

await tester.pump();
await tester.pumpAndSettle();

expect(routeBuildCount, equals(1));
expect(dependentBuildCount, equals(5));
Expand Down
4 changes: 2 additions & 2 deletions packages/flutter/test/widgets/media_query_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ void main() {

expect(data.accessibleNavigation, true);
tester.platformDispatcher.accessibilityFeaturesTestValue = const FakeAccessibilityFeatures();
await tester.pump();
await tester.pumpAndSettle();
expect(data.accessibleNavigation, false);
expect(rebuildCount, 4);

Expand Down Expand Up @@ -512,7 +512,7 @@ void main() {

expect(data.accessibleNavigation, true);
tester.platformDispatcher.accessibilityFeaturesTestValue = const FakeAccessibilityFeatures();
await tester.pump();
await tester.pumpAndSettle();
expect(data.accessibleNavigation, true);
expect(rebuildCount, 1);

Expand Down
Loading