diff --git a/dev/tools/gen_defaults/bin/gen_defaults.dart b/dev/tools/gen_defaults/bin/gen_defaults.dart index ef1f32af64..9c24b27816 100644 --- a/dev/tools/gen_defaults/bin/gen_defaults.dart +++ b/dev/tools/gen_defaults/bin/gen_defaults.dart @@ -120,6 +120,7 @@ Future main(List args) async { ButtonTemplate('md.comp.text-button', 'TextButton', '$materialLib/text_button.dart', tokens).updateFile(); CardTemplate('Card', '$materialLib/card.dart', tokens).updateFile(); CheckboxTemplate('Checkbox', '$materialLib/checkbox.dart', tokens).updateFile(); + DialogFullscreenTemplate('DialogFullscreen', '$materialLib/dialog.dart', tokens).updateFile(); DialogTemplate('Dialog', '$materialLib/dialog.dart', tokens).updateFile(); FABTemplate('FAB', '$materialLib/floating_action_button.dart', tokens).updateFile(); FilterChipTemplate('ChoiceChip', '$materialLib/choice_chip.dart', tokens).updateFile(); diff --git a/dev/tools/gen_defaults/lib/dialog_template.dart b/dev/tools/gen_defaults/lib/dialog_template.dart index 2f2ab654dd..5bb28a2667 100644 --- a/dev/tools/gen_defaults/lib/dialog_template.dart +++ b/dev/tools/gen_defaults/lib/dialog_template.dart @@ -47,3 +47,19 @@ class _${blockName}DefaultsM3 extends DialogTheme { } '''; } + +class DialogFullscreenTemplate extends TokenTemplate { + const DialogFullscreenTemplate(super.blockName, super.fileName, super.tokens); + + @override + String generate() => ''' +class _${blockName}DefaultsM3 extends DialogTheme { + const _${blockName}DefaultsM3(this.context); + + final BuildContext context; + + @override + Color? get backgroundColor => ${componentColor("md.comp.full-screen-dialog.container")}; +} +'''; +} diff --git a/examples/api/lib/material/dialog/dialog.0.dart b/examples/api/lib/material/dialog/dialog.0.dart new file mode 100644 index 0000000000..be8dbd5920 --- /dev/null +++ b/examples/api/lib/material/dialog/dialog.0.dart @@ -0,0 +1,86 @@ +// 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 [Dialog]. + +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( + home: Scaffold( + appBar: AppBar(title: const Text('Dialog Sample')), + body: const Center( + child: DialogExample(), + ), + ), + ); + } +} + +class DialogExample extends StatelessWidget { + const DialogExample({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () => showDialog( + context: context, + builder: (BuildContext context) => Dialog( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('This is a typical dialog.'), + const SizedBox(height: 15), + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Close'), + ), + ], + ), + ), + ), + ), + child: const Text('Show Dialog'), + ), + const SizedBox(height: 10), + TextButton( + onPressed: () => showDialog( + context: context, + builder: (BuildContext context) => Dialog.fullscreen( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('This is a fullscreen dialog.'), + const SizedBox(height: 15), + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Close'), + ), + ], + ), + ), + ), + child: const Text('Show Fullscreen Dialog'), + ), + ], + ); + } +} diff --git a/examples/api/test/material/dialog/dialog.0_test.dart b/examples/api/test/material/dialog/dialog.0_test.dart new file mode 100644 index 0000000000..b6fb588eaf --- /dev/null +++ b/examples/api/test/material/dialog/dialog.0_test.dart @@ -0,0 +1,53 @@ +// 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/dialog/dialog.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Show Dialog', (WidgetTester tester) async { + const String dialogText = 'This is a typical dialog.'; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: example.MyApp(), + ), + ), + ); + + expect(find.text(dialogText), findsNothing); + + await tester.tap(find.text('Show Dialog')); + await tester.pumpAndSettle(); + expect(find.text(dialogText), findsOneWidget); + + await tester.tap(find.text('Close')); + await tester.pumpAndSettle(); + expect(find.text(dialogText), findsNothing); + }); + + testWidgets('Show Dialog.fullscreen', (WidgetTester tester) async { + const String dialogText = 'This is a fullscreen dialog.'; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: example.MyApp(), + ), + ), + ); + + expect(find.text(dialogText), findsNothing); + + await tester.tap(find.text('Show Fullscreen Dialog')); + await tester.pumpAndSettle(); + expect(find.text(dialogText), findsOneWidget); + + await tester.tap(find.text('Close')); + await tester.pumpAndSettle(); + expect(find.text(dialogText), findsNothing); + }); +} diff --git a/packages/flutter/lib/src/material/dialog.dart b/packages/flutter/lib/src/material/dialog.dart index 7d125e4fd9..5b691441bd 100644 --- a/packages/flutter/lib/src/material/dialog.dart +++ b/packages/flutter/lib/src/material/dialog.dart @@ -31,6 +31,12 @@ const EdgeInsets _defaultInsetPadding = EdgeInsets.symmetric(horizontal: 40.0, v /// or [SimpleDialog], which implement specific kinds of Material Design /// dialogs. /// +/// {@tool dartpad} +/// This sample shows the creation of [Dialog] and [Dialog.fullscreen] widgets. +/// +/// ** See code in examples/api/lib/material/dialog/dialog.0.dart ** +/// {@end-tool} +/// /// See also: /// /// * [AlertDialog], for dialogs that have a message and some buttons. @@ -55,7 +61,26 @@ class Dialog extends StatelessWidget { this.alignment, this.child, }) : assert(clipBehavior != null), - assert(elevation == null || elevation >= 0.0); + assert(elevation == null || elevation >= 0.0), + _fullscreen = false; + + /// Creates a fullscreen dialog. + /// + /// Typically used in conjunction with [showDialog]. + const Dialog.fullscreen({ + super.key, + this.backgroundColor, + this.insetAnimationDuration = Duration.zero, + this.insetAnimationCurve = Curves.decelerate, + this.child, + }) : elevation = 0, + shadowColor = null, + surfaceTintColor = null, + insetPadding = EdgeInsets.zero, + clipBehavior = Clip.none, + shape = null, + alignment = null, + _fullscreen = true; /// {@template flutter.material.dialog.backgroundColor} /// The background color of the surface of this [Dialog]. @@ -130,7 +155,8 @@ class Dialog extends StatelessWidget { /// The duration of the animation to show when the system keyboard intrudes /// into the space that the dialog is placed in. /// - /// Defaults to 100 milliseconds. + /// Defaults to 100 milliseconds when [Dialog] is used, and [Duration.zero] + /// when [Dialog.fullscreen] is used. /// {@endtemplate} final Duration insetAnimationDuration; @@ -184,13 +210,44 @@ class Dialog extends StatelessWidget { /// {@macro flutter.widgets.ProxyWidget.child} final Widget? child; + /// This value is used to determine if this is a fullscreen dialog. + final bool _fullscreen; + @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final DialogTheme dialogTheme = DialogTheme.of(context); - final DialogTheme defaults = theme.useMaterial3 ? _DialogDefaultsM3(context) : _DialogDefaultsM2(context); - final EdgeInsets effectivePadding = MediaQuery.of(context).viewInsets + (insetPadding ?? EdgeInsets.zero); + final DialogTheme defaults = theme.useMaterial3 + ? (_fullscreen ? _DialogFullscreenDefaultsM3(context) : _DialogDefaultsM3(context)) + : _DialogDefaultsM2(context); + + Widget dialogChild; + + if (_fullscreen) { + dialogChild = Material( + color: backgroundColor ?? dialogTheme.backgroundColor ?? defaults.backgroundColor, + child: child, + ); + } else { + dialogChild = Align( + alignment: alignment ?? dialogTheme.alignment ?? defaults.alignment!, + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 280.0), + child: Material( + color: backgroundColor ?? dialogTheme.backgroundColor ?? Theme.of(context).dialogBackgroundColor, + elevation: elevation ?? dialogTheme.elevation ?? defaults.elevation!, + shadowColor: shadowColor ?? dialogTheme.shadowColor ?? defaults.shadowColor, + surfaceTintColor: surfaceTintColor ?? dialogTheme.surfaceTintColor ?? defaults.surfaceTintColor, + shape: shape ?? dialogTheme.shape ?? defaults.shape!, + type: MaterialType.card, + clipBehavior: clipBehavior, + child: child, + ), + ), + ); + } + return AnimatedPadding( padding: effectivePadding, duration: insetAnimationDuration, @@ -201,22 +258,7 @@ class Dialog extends StatelessWidget { removeRight: true, removeBottom: true, context: context, - child: Align( - alignment: alignment ?? dialogTheme.alignment ?? defaults.alignment!, - child: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 280.0), - child: Material( - color: backgroundColor ?? dialogTheme.backgroundColor ?? Theme.of(context).dialogBackgroundColor, - elevation: elevation ?? dialogTheme.elevation ?? defaults.elevation!, - shadowColor: shadowColor ?? dialogTheme.shadowColor ?? defaults.shadowColor, - surfaceTintColor: surfaceTintColor ?? dialogTheme.surfaceTintColor ?? defaults.surfaceTintColor, - shape: shape ?? dialogTheme.shape ?? defaults.shape!, - type: MaterialType.card, - clipBehavior: clipBehavior, - child: child, - ), - ), - ), + child: dialogChild, ), ); } @@ -1426,3 +1468,23 @@ class _DialogDefaultsM3 extends DialogTheme { } // END GENERATED TOKEN PROPERTIES - Dialog + +// BEGIN GENERATED TOKEN PROPERTIES - DialogFullscreen + +// Do not edit by hand. The code between the "BEGIN GENERATED" and +// "END GENERATED" comments are generated from data in the Material +// Design token database by the script: +// dev/tools/gen_defaults/bin/gen_defaults.dart. + +// Token database version: v0_132 + +class _DialogFullscreenDefaultsM3 extends DialogTheme { + const _DialogFullscreenDefaultsM3(this.context); + + final BuildContext context; + + @override + Color? get backgroundColor => Theme.of(context).colorScheme.surface; +} + +// END GENERATED TOKEN PROPERTIES - DialogFullscreen diff --git a/packages/flutter/test/material/dialog_test.dart b/packages/flutter/test/material/dialog_test.dart index 9a94021ee0..091f64e623 100644 --- a/packages/flutter/test/material/dialog_test.dart +++ b/packages/flutter/test/material/dialog_test.dart @@ -6,6 +6,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../widgets/semantics_tester.dart'; @@ -138,6 +139,53 @@ void main() { expect(material3Widget.elevation, 6.0); }); + testWidgets('Dialog.fullscreen Defaults', (WidgetTester tester) async { + const String dialogTextM2 = 'Fullscreen Dialog - M2'; + const String dialogTextM3 = 'Fullscreen Dialog - M3'; + + await tester.pumpWidget(_buildAppWithDialog( + theme: material2Theme, + const Dialog.fullscreen( + child: Text(dialogTextM2), + ), + )); + + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + expect(find.text(dialogTextM2), findsOneWidget); + + Material materialWidget = _getMaterialFromDialog(tester); + expect(materialWidget.color, Colors.grey[800]); + + // Try to dismiss the fullscreen dialog with the escape key. + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await tester.pumpAndSettle(); + + expect(find.text(dialogTextM2), findsNothing); + + await tester.pumpWidget(_buildAppWithDialog( + theme: material3Theme, + const Dialog.fullscreen( + child: Text(dialogTextM3), + ), + )); + + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + expect(find.text(dialogTextM3), findsOneWidget); + + materialWidget = _getMaterialFromDialog(tester); + expect(materialWidget.color, material3Theme.colorScheme.surface); + + // Try to dismiss the fullscreen dialog with the escape key. + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await tester.pumpAndSettle(); + + expect(find.text(dialogTextM3), findsNothing); + }); + testWidgets('Custom dialog elevation', (WidgetTester tester) async { const double customElevation = 12.0; const Color shadowColor = Color(0xFF000001);