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
This commit is contained in:
Taha Tesser 2024-03-26 20:36:09 +02:00 committed by GitHub
parent 388f3217e4
commit 81f969e807
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 55 additions and 2 deletions

View file

@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
@ -571,6 +574,7 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
bool _isExpanded = false;
late ExpansionTileController _tileController;
Timer? _timer;
@override
void initState() {
@ -597,6 +601,8 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
void dispose() {
_tileController._state = null;
_animationController.dispose();
_timer?.cancel();
_timer = null;
super.dispose();
}
@ -621,7 +627,19 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
PageStorage.maybeOf(context)?.writeState(context, _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() {

View file

@ -7,6 +7,7 @@
@Tags(<String>['reduced-test-set'])
library;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
@ -788,7 +789,41 @@ void main() {
// "Collapsed".
expect(tester.takeAnnouncements().first.message, localizations.expandedHint);
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 {
// This is a regression test for https://github.com/flutter/flutter/pull/121624