Add error message and documentation when a SnackBar is off screen (#102073)

This commit is contained in:
Bruno Leroux 2022-05-26 14:15:07 +02:00 committed by GitHub
parent 5764b5dd67
commit bc53e6202e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 221 additions and 0 deletions

View file

@ -0,0 +1,76 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flutter code sample for SnackBar
import 'package:flutter/material.dart';
void main() => runApp(const SnackBarApp());
class SnackBarApp extends StatelessWidget {
const SnackBarApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: SnackBarExample(),
);
}
}
class SnackBarExample extends StatefulWidget {
const SnackBarExample({super.key});
@override
State<SnackBarExample> createState() => _SnackBarExampleState();
}
class _SnackBarExampleState extends State<SnackBarExample> {
bool _largeLogo = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('SnackBar Sample')),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
ElevatedButton(
onPressed: () {
const SnackBar snackBar = SnackBar(
content: Text('A SnackBar has been shown.'),
behavior: SnackBarBehavior.floating,
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
},
child: const Text('Show SnackBar'),
),
const SizedBox(height: 8.0),
ElevatedButton(
onPressed: () {
setState(() => _largeLogo = !_largeLogo);
},
child: Text(_largeLogo ? 'Shrink Logo' : 'Grow Logo'),
),
],
),
),
// A floating [SnackBar] is positioned above [Scaffold.floatingActionButton].
// If the Widget provided to the floatingActionButton slot takes up too much space
// for the SnackBar to be visible, an error will be thrown.
floatingActionButton: Container(
constraints: BoxConstraints.tightFor(
width: 150,
height: _largeLogo ? double.infinity : 150,
),
decoration: const BoxDecoration(
color: Colors.blueGrey,
borderRadius: BorderRadius.all(Radius.circular(20)),
),
child: const FlutterLogo(),
),
);
}
}

View file

@ -0,0 +1,36 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/material/scaffold/scaffold_messenger_state.show_snack_bar.1.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Floating SnackBar is visible', (WidgetTester tester) async {
await tester.pumpWidget(
const example.SnackBarApp(),
);
final Finder buttonFinder = find.byType(ElevatedButton);
await tester.tap(buttonFinder.first);
// Have the SnackBar fully animate out.
await tester.pumpAndSettle();
final Finder snackBarFinder = find.byType(SnackBar);
expect(snackBarFinder, findsOneWidget);
// Grow logo to send SnackBar off screen.
await tester.tap(buttonFinder.last);
await tester.pumpAndSettle();
final AssertionError exception = tester.takeException() as AssertionError;
const String message = 'Floating SnackBar presented off screen.\n'
'A SnackBar with behavior property set to SnackBarBehavior.floating is fully '
'or partially off screen because some or all the widgets provided to '
'Scaffold.floatingActionButton, Scaffold.persistentFooterButtons and '
'Scaffold.bottomNavigationBar take up too much vertical space.\n'
'Consider constraining the size of these widgets to allow room for the SnackBar to be visible.';
expect(exception.message, message);
});
}

View file

@ -264,6 +264,23 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide
///
/// ** See code in examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.0.dart **
/// {@end-tool}
///
/// ## Relative positioning of floating SnackBars
///
/// A [SnackBar] with [SnackBar.behavior] set to [SnackBarBehavior.floating] is
/// positioned above the widgets provided to [Scaffold.floatingActionButton],
/// [Scaffold.persistentFooterButtons], and [Scaffold.bottomNavigationBar].
/// If some or all of these widgets take up enough space such that the SnackBar
/// would not be visible when positioned above them, an error will be thrown.
/// In this case, consider constraining the size of these widgets to allow room for
/// the SnackBar to be visible.
///
/// {@tool dartpad}
/// Here is an example showing that a floating [SnackBar] appears above [Scaffold.floatingActionButton].
///
/// ** See code in examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.1.dart **
/// {@end-tool}
///
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(SnackBar snackBar) {
assert(
_scaffolds.isNotEmpty,
@ -1118,6 +1135,32 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
final double xOffset = hasCustomWidth ? (size.width - snackBarWidth!) / 2 : 0.0;
positionChild(_ScaffoldSlot.snackBar, Offset(xOffset, snackBarYOffsetBase - snackBarSize.height));
assert((){
// Whether a floating SnackBar has been offsetted too high.
//
// To improve the developper experience, this assert is done after the call to positionChild.
// if we assert sooner the SnackBar is visible because its defaults position is (0,0) and
// it can cause confusion to the user as the error message states that the SnackBar is off screen.
if (isSnackBarFloating) {
final bool snackBarVisible = (snackBarYOffsetBase - snackBarSize.height) > 0;
if (!snackBarVisible) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Floating SnackBar presented off screen.'),
ErrorDescription(
'A SnackBar with behavior property set to SnackBarBehavior.floating is fully '
'or partially off screen because some or all the widgets provided to '
'Scaffold.floatingActionButton, Scaffold.persistentFooterButtons and '
'Scaffold.bottomNavigationBar take up too much vertical space.\n'
),
ErrorHint(
'Consider constraining the size of these widgets to allow room for the SnackBar to be visible.',
),
]);
}
}
return true;
}());
}
if (hasChild(_ScaffoldSlot.statusBar)) {

View file

@ -1652,6 +1652,72 @@ void main() {
},
);
void openFloatingSnackBar(WidgetTester tester) {
final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
scaffoldMessengerState.showSnackBar(
const SnackBar(
content: Text('SnackBar text'),
behavior: SnackBarBehavior.floating,
),
);
}
void expectSnackBarNotVisibleError(WidgetTester tester) {
final AssertionError exception = tester.takeException() as AssertionError;
const String message = 'Floating SnackBar presented off screen.\n'
'A SnackBar with behavior property set to SnackBarBehavior.floating is fully '
'or partially off screen because some or all the widgets provided to '
'Scaffold.floatingActionButton, Scaffold.persistentFooterButtons and '
'Scaffold.bottomNavigationBar take up too much vertical space.\n'
'Consider constraining the size of these widgets to allow room for the SnackBar to be visible.';
expect(exception.message, message);
}
testWidgets('Snackbar with SnackBarBehavior.floating will assert when offsetted too high by a large Scaffold.floatingActionButton', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/84263
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
floatingActionButton: Container(),
),
),
);
openFloatingSnackBar(tester);
await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
expectSnackBarNotVisibleError(tester);
});
testWidgets('Snackbar with SnackBarBehavior.floating will assert when offsetted too high by a large Scaffold.persistentFooterButtons', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/84263
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
persistentFooterButtons: <Widget>[SizedBox(height: 1000)],
),
),
);
openFloatingSnackBar(tester);
await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
expectSnackBarNotVisibleError(tester);
});
testWidgets('Snackbar with SnackBarBehavior.floating will assert when offsetted too high by a large Scaffold.bottomNavigationBar', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/84263
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
bottomNavigationBar: SizedBox(height: 1000),
),
),
);
openFloatingSnackBar(tester);
await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
expectSnackBarNotVisibleError(tester);
});
testWidgets(
'SnackBar has correct end padding when it contains an action with fixed behavior',
(WidgetTester tester) async {