Unverified Commit 81f969e8 authored by Taha Tesser's avatar Taha Tesser Committed by GitHub

Fix `ExpansionTile` Expanded/Collapsed announcement is interrupted by VoiceOver (#143936)

fixes [`ExpansionTile` accessibility information doesn't read Expanded/Collapsed (iOS)](https://github.com/flutter/flutter/issues/132264)

### Code sample

<details>
<summary>expand to view the code sample</summary> 

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

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('ExpansionTile'),
        ),
        body: const ExpansionTile(
          title: Text("Title"),
          children: <Widget>[
            Placeholder(),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {},
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}
```

</details>

### Before

https://github.com/flutter/flutter/assets/48603081/542d8392-52dc-4319-92ba-215a7164db49

### After

https://github.com/flutter/flutter/assets/48603081/c9225144-4c12-4e92-bc41-4ff82b370ad7
parent 388f3217
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -571,6 +574,7 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider ...@@ -571,6 +574,7 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
bool _isExpanded = false; bool _isExpanded = false;
late ExpansionTileController _tileController; late ExpansionTileController _tileController;
Timer? _timer;
@override @override
void initState() { void initState() {
...@@ -597,6 +601,8 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider ...@@ -597,6 +601,8 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
void dispose() { void dispose() {
_tileController._state = null; _tileController._state = null;
_animationController.dispose(); _animationController.dispose();
_timer?.cancel();
_timer = null;
super.dispose(); super.dispose();
} }
...@@ -621,7 +627,19 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider ...@@ -621,7 +627,19 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
PageStorage.maybeOf(context)?.writeState(context, _isExpanded); PageStorage.maybeOf(context)?.writeState(context, _isExpanded);
}); });
widget.onExpansionChanged?.call(_isExpanded); widget.onExpansionChanged?.call(_isExpanded);
SemanticsService.announce(stateHint, textDirection);
if (defaultTargetPlatform == TargetPlatform.iOS) {
// TODO(tahatesser): This is a workaround for VoiceOver interrupting
// semantic announcements on iOS. https://github.com/flutter/flutter/issues/122101.
_timer?.cancel();
_timer = Timer(const Duration(seconds: 1), () {
SemanticsService.announce(stateHint, textDirection);
_timer?.cancel();
_timer = null;
});
} else {
SemanticsService.announce(stateHint, textDirection);
}
} }
void _handleTap() { void _handleTap() {
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
@Tags(<String>['reduced-test-set']) @Tags(<String>['reduced-test-set'])
library; library;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -788,7 +789,41 @@ void main() { ...@@ -788,7 +789,41 @@ void main() {
// "Collapsed". // "Collapsed".
expect(tester.takeAnnouncements().first.message, localizations.expandedHint); expect(tester.takeAnnouncements().first.message, localizations.expandedHint);
handle.dispose(); handle.dispose();
}); }, skip: defaultTargetPlatform == TargetPlatform.iOS); // [intended] https://github.com/flutter/flutter/issues/122101.
// This is a regression test for https://github.com/flutter/flutter/issues/132264.
testWidgets('ExpansionTile Semantics announcement is delayed on iOS', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
const DefaultMaterialLocalizations localizations = DefaultMaterialLocalizations();
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: ExpansionTile(
title: Text('Title'),
children: <Widget>[
SizedBox(height: 100, width: 100),
],
),
),
),
);
// There is no semantics announcement without tap action.
expect(tester.takeAnnouncements(), isEmpty);
// Tap the title to expand ExpansionTile.
await tester.tap(find.text('Title'));
await tester.pump(const Duration(seconds: 1)); // Wait for the announcement to be made.
expect(tester.takeAnnouncements().first.message, localizations.collapsedHint);
// Tap the title to collapse ExpansionTile.
await tester.tap(find.text('Title'));
await tester.pump(const Duration(seconds: 1)); // Wait for the announcement to be made.
expect(tester.takeAnnouncements().first.message, localizations.expandedHint);
handle.dispose();
}, variant: TargetPlatformVariant.only(TargetPlatform.iOS));
testWidgets('Semantics with the onTapHint is an ancestor of ListTile', (WidgetTester tester) async { testWidgets('Semantics with the onTapHint is an ancestor of ListTile', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/pull/121624 // This is a regression test for https://github.com/flutter/flutter/pull/121624
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment