Unverified Commit 57333748 authored by Taha Tesser's avatar Taha Tesser Committed by GitHub

Migrate `Dialog` to Material 3 (#98919)

parent 671aa9e9
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:gen_defaults/dialog_template.dart';
import 'package:gen_defaults/fab_template.dart'; import 'package:gen_defaults/fab_template.dart';
import 'package:gen_defaults/navigation_bar_template.dart'; import 'package:gen_defaults/navigation_bar_template.dart';
import 'package:gen_defaults/typography_template.dart'; import 'package:gen_defaults/typography_template.dart';
...@@ -67,4 +68,5 @@ Future<void> main(List<String> args) async { ...@@ -67,4 +68,5 @@ Future<void> main(List<String> args) async {
FABTemplate('$materialLib/floating_action_button.dart', tokens).updateFile(); FABTemplate('$materialLib/floating_action_button.dart', tokens).updateFile();
NavigationBarTemplate('$materialLib/navigation_bar.dart', tokens).updateFile(); NavigationBarTemplate('$materialLib/navigation_bar.dart', tokens).updateFile();
TypographyTemplate('$materialLib/typography.dart', tokens).updateFile(); TypographyTemplate('$materialLib/typography.dart', tokens).updateFile();
DialogTemplate('$materialLib/dialog.dart', tokens).updateFile();
} }
// 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 'template.dart';
class DialogTemplate extends TokenTemplate {
const DialogTemplate(String fileName, Map<String, dynamic> tokens) : super(fileName, tokens);
@override
String generate() => '''
// Generated version ${tokens["version"]}
class _TokenDefaultsM3 extends DialogTheme {
_TokenDefaultsM3(this.context)
: _colors = Theme.of(context).colorScheme,
_textTheme = Theme.of(context).textTheme,
super(
alignment: Alignment.center,
elevation: ${elevation("md.comp.dialog.container")},
shape: ${shape("md.comp.dialog.container")},
);
final BuildContext context;
final ColorScheme _colors;
final TextTheme _textTheme;
// TODO(darrenaustin): overlay should be handled by Material widget: https://github.com/flutter/flutter/issues/9160
@override
Color? get backgroundColor => ElevationOverlay.colorWithOverlay(_colors.${color("md.comp.dialog.container")}, _colors.primary, ${elevation("md.comp.dialog.container")});
@override
TextStyle? get titleTextStyle => _textTheme.${textStyle("md.comp.dialog.subhead")};
@override
TextStyle? get contentTextStyle => _textTheme.${textStyle("md.comp.dialog.supporting-text")};
}
''';
}
...@@ -6,12 +6,15 @@ import 'dart:ui'; ...@@ -6,12 +6,15 @@ import 'dart:ui';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'color_scheme.dart';
import 'colors.dart'; import 'colors.dart';
import 'debug.dart'; import 'debug.dart';
import 'dialog_theme.dart'; import 'dialog_theme.dart';
import 'elevation_overlay.dart';
import 'ink_well.dart'; import 'ink_well.dart';
import 'material.dart'; import 'material.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'text_theme.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart'; import 'theme_data.dart';
...@@ -128,13 +131,12 @@ class Dialog extends StatelessWidget { ...@@ -128,13 +131,12 @@ class Dialog extends StatelessWidget {
/// {@macro flutter.widgets.ProxyWidget.child} /// {@macro flutter.widgets.ProxyWidget.child}
final Widget? child; final Widget? child;
static const RoundedRectangleBorder _defaultDialogShape =
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
static const double _defaultElevation = 24.0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final DialogTheme dialogTheme = DialogTheme.of(context); final DialogTheme dialogTheme = DialogTheme.of(context);
final DialogTheme defaults = theme.useMaterial3 ? _TokenDefaultsM3(context) : _DefaultsM2(context);
final EdgeInsets effectivePadding = MediaQuery.of(context).viewInsets + (insetPadding ?? EdgeInsets.zero); final EdgeInsets effectivePadding = MediaQuery.of(context).viewInsets + (insetPadding ?? EdgeInsets.zero);
return AnimatedPadding( return AnimatedPadding(
padding: effectivePadding, padding: effectivePadding,
...@@ -147,13 +149,13 @@ class Dialog extends StatelessWidget { ...@@ -147,13 +149,13 @@ class Dialog extends StatelessWidget {
removeBottom: true, removeBottom: true,
context: context, context: context,
child: Align( child: Align(
alignment: alignment ?? dialogTheme.alignment ?? Alignment.center, alignment: alignment ?? dialogTheme.alignment ?? defaults.alignment!,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 280.0), constraints: const BoxConstraints(minWidth: 280.0),
child: Material( child: Material(
color: backgroundColor ?? dialogTheme.backgroundColor ?? Theme.of(context).dialogBackgroundColor, color: backgroundColor ?? dialogTheme.backgroundColor ?? Theme.of(context).dialogBackgroundColor,
elevation: elevation ?? dialogTheme.elevation ?? _defaultElevation, elevation: elevation ?? dialogTheme.elevation ?? defaults.elevation!,
shape: shape ?? dialogTheme.shape ?? _defaultDialogShape, shape: shape ?? dialogTheme.shape ?? defaults.shape!,
type: MaterialType.card, type: MaterialType.card,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
child: child, child: child,
...@@ -481,6 +483,7 @@ class AlertDialog extends StatelessWidget { ...@@ -481,6 +483,7 @@ class AlertDialog extends StatelessWidget {
assert(debugCheckHasMaterialLocalizations(context)); assert(debugCheckHasMaterialLocalizations(context));
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final DialogTheme dialogTheme = DialogTheme.of(context); final DialogTheme dialogTheme = DialogTheme.of(context);
final DialogTheme defaults = theme.useMaterial3 ? _TokenDefaultsM3(context) : _DefaultsM2(context);
String? label = semanticLabel; String? label = semanticLabel;
switch (theme.platform) { switch (theme.platform) {
...@@ -498,6 +501,8 @@ class AlertDialog extends StatelessWidget { ...@@ -498,6 +501,8 @@ class AlertDialog extends StatelessWidget {
// children. // children.
final double paddingScaleFactor = _paddingScaleFactor(MediaQuery.of(context).textScaleFactor); final double paddingScaleFactor = _paddingScaleFactor(MediaQuery.of(context).textScaleFactor);
final TextDirection? textDirection = Directionality.maybeOf(context); final TextDirection? textDirection = Directionality.maybeOf(context);
const double m3ActionEndPadding = 18.0;
const double m3ActionBottomPadding = 12.0;
Widget? titleWidget; Widget? titleWidget;
Widget? contentWidget; Widget? contentWidget;
...@@ -513,7 +518,7 @@ class AlertDialog extends StatelessWidget { ...@@ -513,7 +518,7 @@ class AlertDialog extends StatelessWidget {
bottom: effectiveTitlePadding.bottom, bottom: effectiveTitlePadding.bottom,
), ),
child: DefaultTextStyle( child: DefaultTextStyle(
style: titleTextStyle ?? dialogTheme.titleTextStyle ?? theme.textTheme.headline6!, style: titleTextStyle ?? dialogTheme.titleTextStyle ?? defaults.titleTextStyle!,
child: Semantics( child: Semantics(
// For iOS platform, the focus always lands on the title. // For iOS platform, the focus always lands on the title.
// Set nameRoute to false to avoid title being announce twice. // Set nameRoute to false to avoid title being announce twice.
...@@ -535,7 +540,7 @@ class AlertDialog extends StatelessWidget { ...@@ -535,7 +540,7 @@ class AlertDialog extends StatelessWidget {
bottom: effectiveContentPadding.bottom, bottom: effectiveContentPadding.bottom,
), ),
child: DefaultTextStyle( child: DefaultTextStyle(
style: contentTextStyle ?? dialogTheme.contentTextStyle ?? theme.textTheme.subtitle1!, style: contentTextStyle ?? dialogTheme.contentTextStyle ?? defaults.contentTextStyle!,
child: Semantics( child: Semantics(
container: true, container: true,
child: content, child: content,
...@@ -547,7 +552,13 @@ class AlertDialog extends StatelessWidget { ...@@ -547,7 +552,13 @@ class AlertDialog extends StatelessWidget {
if (actions != null) { if (actions != null) {
final double spacing = (buttonPadding?.horizontal ?? 16) / 2; final double spacing = (buttonPadding?.horizontal ?? 16) / 2;
actionsWidget = Padding( actionsWidget = Padding(
padding: actionsPadding.add(EdgeInsets.all(spacing)), padding: theme.useMaterial3
? actionsPadding.add(EdgeInsets.all(spacing))
.add(const EdgeInsets.only(
right: m3ActionEndPadding,
bottom: m3ActionBottomPadding,
))
: actionsPadding.add(EdgeInsets.all(spacing)),
child: OverflowBar( child: OverflowBar(
alignment: actionsAlignment ?? MainAxisAlignment.end, alignment: actionsAlignment ?? MainAxisAlignment.end,
spacing: spacing, spacing: spacing,
...@@ -1145,3 +1156,63 @@ double _paddingScaleFactor(double textScaleFactor) { ...@@ -1145,3 +1156,63 @@ double _paddingScaleFactor(double textScaleFactor) {
// a non-scaled padding of 24 will produce a padding between 24 and 8. // a non-scaled padding of 24 will produce a padding between 24 and 8.
return lerpDouble(1.0, 1.0 / 3.0, clampedTextScaleFactor - 1.0)!; return lerpDouble(1.0, 1.0 / 3.0, clampedTextScaleFactor - 1.0)!;
} }
// Generate a DialogTheme that represents the M2 default values.
// This was generated by hand from the previous hand coded defaults
// for M2. It uses get method overrides instead of properties to
// avoid computing values that we may not need upfront.
class _DefaultsM2 extends DialogTheme {
_DefaultsM2(this.context)
: _textTheme = Theme.of(context).textTheme,
super(
alignment: Alignment.center,
elevation: 24.0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))),
);
final BuildContext context;
final TextTheme _textTheme;
@override
Color? get backgroundColor => Theme.of(context).dialogBackgroundColor;
@override
TextStyle? get titleTextStyle => _textTheme.headline6;
@override
TextStyle? get contentTextStyle => _textTheme.subtitle1;
}
// BEGIN GENERATED TOKEN PROPERTIES
// Generated code to the end of this file. Do not edit by hand.
// These defaults are generated from the Material Design Token
// database by the script dev/tools/gen_defaults/bin/gen_defaults.dart.
// Generated version v0_86
class _TokenDefaultsM3 extends DialogTheme {
_TokenDefaultsM3(this.context)
: _colors = Theme.of(context).colorScheme,
_textTheme = Theme.of(context).textTheme,
super(
alignment: Alignment.center,
elevation: 6.0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(28.0), topRight: Radius.circular(28.0), bottomLeft: Radius.circular(28.0), bottomRight: Radius.circular(28.0))),
);
final BuildContext context;
final ColorScheme _colors;
final TextTheme _textTheme;
// TODO(darrenaustin): overlay should be handled by Material widget: https://github.com/flutter/flutter/issues/9160
@override
Color? get backgroundColor => ElevationOverlay.colorWithOverlay(_colors.surface, _colors.primary, 6.0);
@override
TextStyle? get titleTextStyle => _textTheme.headlineSmall;
@override
TextStyle? get contentTextStyle => _textTheme.bodyMedium;
}
// END GENERATED TOKEN PROPERTIES
...@@ -1157,6 +1157,8 @@ class ThemeData with Diagnosticable { ...@@ -1157,6 +1157,8 @@ class ThemeData with Diagnosticable {
/// ///
/// * [FloatingActionButton] /// * [FloatingActionButton]
/// * [NavigationBar] /// * [NavigationBar]
/// * [Dialog]
/// * [AlertDialog]
/// ///
/// See also: /// See also:
/// ///
......
...@@ -53,9 +53,14 @@ Finder _findButtonBar() { ...@@ -53,9 +53,14 @@ Finder _findButtonBar() {
return find.ancestor(of: find.byType(OverflowBar), matching: find.byType(Padding)).first; return find.ancestor(of: find.byType(OverflowBar), matching: find.byType(Padding)).first;
} }
const ShapeBorder _defaultDialogShape = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))); const ShapeBorder _defaultM2DialogShape = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
final ShapeBorder _defaultM3DialogShape = RoundedRectangleBorder(borderRadius: BorderRadius.circular(28.0));
void main() { void main() {
final ThemeData material3Theme = ThemeData(useMaterial3: true, brightness: Brightness.dark);
final ThemeData material2Theme = ThemeData(useMaterial3: false, brightness: Brightness.dark);
testWidgets('Dialog is scrollable', (WidgetTester tester) async { testWidgets('Dialog is scrollable', (WidgetTester tester) async {
bool didPressOk = false; bool didPressOk = false;
final AlertDialog dialog = AlertDialog( final AlertDialog dialog = AlertDialog(
...@@ -104,20 +109,33 @@ void main() { ...@@ -104,20 +109,33 @@ void main() {
content: Text('Y'), content: Text('Y'),
actions: <Widget>[ ], actions: <Widget>[ ],
); );
await tester.pumpWidget(_buildAppWithDialog(dialog, theme: ThemeData(brightness: Brightness.dark))); await tester.pumpWidget(_buildAppWithDialog(dialog, theme: material2Theme));
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final Material materialWidget = _getMaterialFromDialog(tester); final Material materialWidget = _getMaterialFromDialog(tester);
expect(materialWidget.color, Colors.grey[800]); expect(materialWidget.color, Colors.grey[800]);
expect(materialWidget.shape, _defaultDialogShape); expect(materialWidget.shape, _defaultM2DialogShape);
expect(materialWidget.elevation, 24.0); expect(materialWidget.elevation, 24.0);
final Offset bottomLeft = tester.getBottomLeft( final Offset bottomLeft = tester.getBottomLeft(
find.descendant(of: find.byType(Dialog), matching: find.byType(Material)), find.descendant(of: find.byType(Dialog), matching: find.byType(Material)),
); );
expect(bottomLeft.dy, 360.0); expect(bottomLeft.dy, 360.0);
await tester.tapAt(const Offset(10.0, 10.0));
await tester.pumpAndSettle();
await tester.pumpWidget(_buildAppWithDialog(dialog, theme: material3Theme));
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
final Material material3Widget = _getMaterialFromDialog(tester);
expect(material3Widget.color, const Color(0xff424242));
expect(material3Widget.shape, _defaultM3DialogShape);
expect(material3Widget.elevation, 6.0);
}); });
testWidgets('Custom dialog elevation', (WidgetTester tester) async { testWidgets('Custom dialog elevation', (WidgetTester tester) async {
...@@ -223,7 +241,7 @@ void main() { ...@@ -223,7 +241,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final Material materialWidget = _getMaterialFromDialog(tester); final Material materialWidget = _getMaterialFromDialog(tester);
expect(materialWidget.shape, _defaultDialogShape); expect(materialWidget.shape, _defaultM2DialogShape);
}); });
testWidgets('Rectangular dialog shape', (WidgetTester tester) async { testWidgets('Rectangular dialog shape', (WidgetTester tester) async {
...@@ -585,9 +603,7 @@ void main() { ...@@ -585,9 +603,7 @@ void main() {
], ],
); );
await tester.pumpWidget( await tester.pumpWidget(_buildAppWithDialog(dialog, theme: material2Theme));
_buildAppWithDialog(dialog),
);
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -622,6 +638,46 @@ void main() { ...@@ -622,6 +638,46 @@ void main() {
tester.getBottomRight(find.byKey(key2)).dx, tester.getBottomRight(find.byKey(key2)).dx,
tester.getBottomRight(_findButtonBar()).dx - 8.0, tester.getBottomRight(_findButtonBar()).dx - 8.0,
); // right ); // right
// Dismiss it and test materail 3 dialog
await tester.tapAt(const Offset(10.0, 10.0));
await tester.pumpAndSettle();
await tester.pumpWidget(_buildAppWithDialog(dialog, theme: material3Theme));
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
// Padding between both buttons
expect(
tester.getBottomLeft(find.byKey(key2)).dx,
tester.getBottomRight(find.byKey(key1)).dx + 8.0,
);
// Padding between button and edges of the button bar
// First button
expect(
tester.getTopRight(find.byKey(key1)).dy,
tester.getTopRight(_findButtonBar()).dy + 8.0,
); // top
expect(
tester.getBottomRight(find.byKey(key1)).dy,
tester.getBottomRight(_findButtonBar()).dy - 20.0,
); // bottom
// // Second button
expect(
tester.getTopRight(find.byKey(key2)).dy,
tester.getTopRight(_findButtonBar()).dy + 8.0,
); // top
expect(
tester.getBottomRight(find.byKey(key2)).dy,
tester.getBottomRight(_findButtonBar()).dy - 20.0,
); // bottom
expect(
tester.getBottomRight(find.byKey(key2)).dx,
tester.getBottomRight(_findButtonBar()).dx - 26.0,
); // right
}); });
testWidgets('AlertDialog.buttonPadding custom values', (WidgetTester tester) async { testWidgets('AlertDialog.buttonPadding custom values', (WidgetTester tester) async {
......
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