Skip to content

Commit 521028c

Browse files
authored
Reland "Use semantics label for backbutton and closebutton for Android" (#116676)
* Reland "Use semantics label for backbutton and closebutton for Android" This reverts commit cc256c3. * Makes the semantics slider test more robust
1 parent 8b80552 commit 521028c

File tree

4 files changed

+164
-29
lines changed

4 files changed

+164
-29
lines changed

packages/flutter/lib/src/material/back_button.dart

+42-9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:flutter/foundation.dart';
56
import 'package:flutter/widgets.dart';
67

78
import 'debug.dart';
@@ -27,22 +28,39 @@ class BackButtonIcon extends StatelessWidget {
2728
/// the current platform (as obtained from the [Theme]).
2829
const BackButtonIcon({ super.key });
2930

30-
/// Returns the appropriate "back" icon for the given `platform`.
31-
static IconData _getIconData(TargetPlatform platform) {
32-
switch (platform) {
31+
@override
32+
Widget build(BuildContext context) {
33+
final String? semanticsLabel;
34+
final IconData data;
35+
switch (Theme.of(context).platform) {
3336
case TargetPlatform.android:
3437
case TargetPlatform.fuchsia:
3538
case TargetPlatform.linux:
3639
case TargetPlatform.windows:
37-
return Icons.arrow_back;
40+
data = Icons.arrow_back;
41+
break;
3842
case TargetPlatform.iOS:
3943
case TargetPlatform.macOS:
40-
return Icons.arrow_back_ios;
44+
data = Icons.arrow_back_ios;
45+
break;
46+
}
47+
// This can't use the platform from Theme because it is the Android OS that
48+
// expects the duplicated tooltip and label.
49+
switch (defaultTargetPlatform) {
50+
case TargetPlatform.android:
51+
semanticsLabel = MaterialLocalizations.of(context).backButtonTooltip;
52+
break;
53+
case TargetPlatform.fuchsia:
54+
case TargetPlatform.linux:
55+
case TargetPlatform.windows:
56+
case TargetPlatform.iOS:
57+
case TargetPlatform.macOS:
58+
semanticsLabel = null;
59+
break;
4160
}
42-
}
4361

44-
@override
45-
Widget build(BuildContext context) => Icon(_getIconData(Theme.of(context).platform));
62+
return Icon(data, semanticLabel: semanticsLabel);
63+
}
4664
}
4765

4866
/// A Material Design back button.
@@ -149,8 +167,23 @@ class CloseButton extends StatelessWidget {
149167
@override
150168
Widget build(BuildContext context) {
151169
assert(debugCheckHasMaterialLocalizations(context));
170+
final String? semanticsLabel;
171+
// This can't use the platform from Theme because it is the Android OS that
172+
// expects the duplicated tooltip and label.
173+
switch (defaultTargetPlatform) {
174+
case TargetPlatform.android:
175+
semanticsLabel = MaterialLocalizations.of(context).closeButtonTooltip;
176+
break;
177+
case TargetPlatform.fuchsia:
178+
case TargetPlatform.linux:
179+
case TargetPlatform.windows:
180+
case TargetPlatform.iOS:
181+
case TargetPlatform.macOS:
182+
semanticsLabel = null;
183+
break;
184+
}
152185
return IconButton(
153-
icon: const Icon(Icons.close),
186+
icon: Icon(Icons.close, semanticLabel: semanticsLabel),
154187
color: color,
155188
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
156189
onPressed: () {

packages/flutter/lib/src/widgets/semantics_debugger.dart

+5-1
Original file line numberDiff line numberDiff line change
@@ -288,10 +288,14 @@ class _SemanticsDebuggerPainter extends CustomPainter {
288288

289289
assert(data.attributedLabel != null);
290290
final String message;
291+
// Android will avoid pronouncing duplicating tooltip and label.
292+
// Therefore, having two identical strings is the same as having a single
293+
// string.
294+
final bool shouldIgnoreDuplicatedLabel = defaultTargetPlatform == TargetPlatform.android && data.attributedLabel.string == data.tooltip;
291295
final String tooltipAndLabel = <String>[
292296
if (data.tooltip.isNotEmpty)
293297
data.tooltip,
294-
if (data.attributedLabel.string.isNotEmpty)
298+
if (data.attributedLabel.string.isNotEmpty && !shouldIgnoreDuplicatedLabel)
295299
data.attributedLabel.string,
296300
].join('\n');
297301
if (tooltipAndLabel.isEmpty) {

packages/flutter/test/material/back_button_test.dart

+59-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:flutter/foundation.dart';
56
import 'package:flutter/material.dart';
67
import 'package:flutter_test/flutter_test.dart';
78

@@ -152,17 +153,73 @@ void main() {
152153
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
153154

154155
await tester.pumpAndSettle();
155-
156+
final String? expectedLabel;
157+
switch(defaultTargetPlatform) {
158+
case TargetPlatform.android:
159+
expectedLabel = 'Back';
160+
break;
161+
case TargetPlatform.fuchsia:
162+
case TargetPlatform.iOS:
163+
case TargetPlatform.linux:
164+
case TargetPlatform.macOS:
165+
case TargetPlatform.windows:
166+
expectedLabel = null;
167+
}
156168
expect(tester.getSemantics(find.byType(BackButton)), matchesSemantics(
157169
tooltip: 'Back',
170+
label: expectedLabel,
158171
isButton: true,
159172
hasEnabledState: true,
160173
isEnabled: true,
161174
hasTapAction: true,
162175
isFocusable: true,
163176
));
164177
handle.dispose();
165-
});
178+
}, variant: TargetPlatformVariant.all());
179+
180+
testWidgets('CloseButton semantics', (WidgetTester tester) async {
181+
final SemanticsHandle handle = tester.ensureSemantics();
182+
await tester.pumpWidget(
183+
MaterialApp(
184+
home: const Material(child: Text('Home')),
185+
routes: <String, WidgetBuilder>{
186+
'/next': (BuildContext context) {
187+
return const Material(
188+
child: Center(
189+
child: CloseButton(),
190+
),
191+
);
192+
},
193+
},
194+
),
195+
);
196+
197+
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
198+
199+
await tester.pumpAndSettle();
200+
final String? expectedLabel;
201+
switch(defaultTargetPlatform) {
202+
case TargetPlatform.android:
203+
expectedLabel = 'Close';
204+
break;
205+
case TargetPlatform.fuchsia:
206+
case TargetPlatform.iOS:
207+
case TargetPlatform.linux:
208+
case TargetPlatform.macOS:
209+
case TargetPlatform.windows:
210+
expectedLabel = null;
211+
}
212+
expect(tester.getSemantics(find.byType(CloseButton)), matchesSemantics(
213+
tooltip: 'Close',
214+
label: expectedLabel,
215+
isButton: true,
216+
hasEnabledState: true,
217+
isEnabled: true,
218+
hasTapAction: true,
219+
isFocusable: true,
220+
));
221+
handle.dispose();
222+
}, variant: TargetPlatformVariant.all());
166223

167224
testWidgets('CloseButton color', (WidgetTester tester) async {
168225
await tester.pumpWidget(

packages/flutter/test/widgets/semantics_debugger_test.dart

+58-17
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:flutter/foundation.dart';
56
import 'package:flutter/material.dart';
67
import 'package:flutter_test/flutter_test.dart';
78

@@ -293,26 +294,28 @@ void main() {
293294
double value = 0.75;
294295

295296
await tester.pumpWidget(
296-
Directionality(
297-
textDirection: TextDirection.ltr,
298-
child: SemanticsDebugger(
299-
child: Directionality(
300-
textDirection: TextDirection.ltr,
301-
child: MediaQuery(
302-
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
303-
child: Material(
304-
child: Center(
305-
child: Slider(
306-
value: value,
307-
onChanged: (double newValue) {
308-
value = newValue;
309-
},
297+
MaterialApp(
298+
home: Directionality(
299+
textDirection: TextDirection.ltr,
300+
child: SemanticsDebugger(
301+
child: Directionality(
302+
textDirection: TextDirection.ltr,
303+
child: MediaQuery(
304+
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
305+
child: Material(
306+
child: Center(
307+
child: Slider(
308+
value: value,
309+
onChanged: (double newValue) {
310+
value = newValue;
311+
},
312+
),
310313
),
311314
),
312315
),
313316
),
314317
),
315-
),
318+
)
316319
),
317320
);
318321

@@ -322,8 +325,19 @@ void main() {
322325
// interpreted as a gesture by the semantics debugger and sent to the widget
323326
// as a semantic action that always moves by 10% of the complete track.
324327
await tester.fling(find.byType(Slider), const Offset(-100.0, 0.0), 2000.0, warnIfMissed: false); // hitting the debugger
325-
expect(value, equals(0.70));
326-
});
328+
switch(defaultTargetPlatform) {
329+
case TargetPlatform.iOS:
330+
case TargetPlatform.macOS:
331+
expect(value, equals(0.65));
332+
break;
333+
case TargetPlatform.linux:
334+
case TargetPlatform.windows:
335+
case TargetPlatform.android:
336+
case TargetPlatform.fuchsia:
337+
expect(value, equals(0.70));
338+
break;
339+
}
340+
}, variant: TargetPlatformVariant.all());
327341

328342
testWidgets('SemanticsDebugger checkbox', (WidgetTester tester) async {
329343
final Key keyTop = UniqueKey();
@@ -438,6 +452,33 @@ void main() {
438452
);
439453
});
440454

455+
testWidgets('SemanticsDebugger ignores duplicated label and tooltip for Android', (WidgetTester tester) async {
456+
final Key child = UniqueKey();
457+
final Key debugger = UniqueKey();
458+
final bool isPlatformAndroid = defaultTargetPlatform == TargetPlatform.android;
459+
await tester.pumpWidget(
460+
Directionality(
461+
textDirection: TextDirection.ltr,
462+
child: SemanticsDebugger(
463+
key: debugger,
464+
child: Material(
465+
child: Semantics(
466+
container: true,
467+
key: child,
468+
label: 'text',
469+
tooltip: 'text',
470+
),
471+
),
472+
),
473+
),
474+
);
475+
476+
expect(
477+
_getMessageShownInSemanticsDebugger(widgetKey: child, debuggerKey: debugger, tester: tester),
478+
isPlatformAndroid ? 'text' : 'text\ntext',
479+
);
480+
}, variant: TargetPlatformVariant.all());
481+
441482
testWidgets('SemanticsDebugger textfield', (WidgetTester tester) async {
442483
final UniqueKey textField = UniqueKey();
443484
final UniqueKey debugger = UniqueKey();

0 commit comments

Comments
 (0)