Unverified Commit fae458b9 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Convert TimePicker to Material 3 (#116396)

* Make some minor changes in preparation for updating the Time Picker to M3

* Revert OutlineInputBorder.borderRadius type change

* Revert more OutlineInputBorder.borderRadius changes.

* Convert TimePicker to Material 3

* Add example test

* Revert OutlineInputBorder.borderRadius type change

* Fix test

* Review Changes

* Merge changes

* Some sizing and elevation fixes

* Fix localization tests
parent a59dd83d
...@@ -49,6 +49,7 @@ import 'package:gen_defaults/surface_tint.dart'; ...@@ -49,6 +49,7 @@ import 'package:gen_defaults/surface_tint.dart';
import 'package:gen_defaults/switch_template.dart'; import 'package:gen_defaults/switch_template.dart';
import 'package:gen_defaults/tabs_template.dart'; import 'package:gen_defaults/tabs_template.dart';
import 'package:gen_defaults/text_field_template.dart'; import 'package:gen_defaults/text_field_template.dart';
import 'package:gen_defaults/time_picker_template.dart';
import 'package:gen_defaults/typography_template.dart'; import 'package:gen_defaults/typography_template.dart';
Map<String, dynamic> _readTokenFile(String fileName) { Map<String, dynamic> _readTokenFile(String fileName) {
...@@ -167,6 +168,7 @@ Future<void> main(List<String> args) async { ...@@ -167,6 +168,7 @@ Future<void> main(List<String> args) async {
SliderTemplate('md.comp.slider', 'Slider', '$materialLib/slider.dart', tokens).updateFile(); SliderTemplate('md.comp.slider', 'Slider', '$materialLib/slider.dart', tokens).updateFile();
SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile(); SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile();
SwitchTemplate('Switch', '$materialLib/switch.dart', tokens).updateFile(); SwitchTemplate('Switch', '$materialLib/switch.dart', tokens).updateFile();
TimePickerTemplate('TimePicker', '$materialLib/time_picker.dart', tokens).updateFile();
TextFieldTemplate('TextField', '$materialLib/text_field.dart', tokens).updateFile(); TextFieldTemplate('TextField', '$materialLib/text_field.dart', tokens).updateFile();
TabsTemplate('Tabs', '$materialLib/tabs.dart', tokens).updateFile(); TabsTemplate('Tabs', '$materialLib/tabs.dart', tokens).updateFile();
TypographyTemplate('Typography', '$materialLib/typography.dart', tokens).updateFile(); TypographyTemplate('Typography', '$materialLib/typography.dart', tokens).updateFile();
......
This diff is collapsed.
This diff is collapsed.
// 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/time_picker/show_time_picker.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Can open and modify time picker', (WidgetTester tester) async {
const String openPicker = 'Open time picker';
final List<String> options = <String>[
'$TimePickerEntryMode',
... TimePickerEntryMode.values.map<String>((TimePickerEntryMode value) => value.name),
'$ThemeMode',
... ThemeMode.values.map<String>((ThemeMode value) => value.name),
'$TextDirection',
... TextDirection.values.map<String>((TextDirection value) => value.name),
'$MaterialTapTargetSize',
... MaterialTapTargetSize.values.map<String>((MaterialTapTargetSize value) => value.name),
'$Orientation',
... Orientation.values.map<String>((Orientation value) => value.name),
'Time Mode',
'12-hour am/pm time',
'24-hour time',
'Material Version',
'Material 2',
'Material 3',
openPicker,
];
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: example.ShowTimePickerApp(),
),
),
);
for (final String option in options) {
expect(find.text(option), findsOneWidget, reason: 'Unable to find $option widget in example.');
}
// Open time picker
await tester.tap(find.text(openPicker));
await tester.pumpAndSettle();
expect(find.text('Select time'), findsOneWidget);
expect(find.text('Cancel'), findsOneWidget);
expect(find.text('OK'), findsOneWidget);
// Close time picker
await tester.tapAt(const Offset(1, 1));
await tester.pumpAndSettle();
expect(find.text('Select time'), findsNothing);
expect(find.text('Cancel'), findsNothing);
expect(find.text('OK'), findsNothing);
// Change an option.
await tester.tap(find.text('Material 2'));
await tester.pumpAndSettle();
await tester.tap(find.text(openPicker));
await tester.pumpAndSettle();
expect(find.text('SELECT TIME'), findsOneWidget);
expect(find.text('CANCEL'), findsOneWidget);
expect(find.text('OK'), findsOneWidget);
});
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -75,21 +75,21 @@ void main() { ...@@ -75,21 +75,21 @@ void main() {
expect(description, <String>[ expect(description, <String>[
'backgroundColor: Color(0xffffffff)', 'backgroundColor: Color(0xffffffff)',
'hourMinuteTextColor: Color(0xffffffff)', 'dayPeriodBorderSide: BorderSide',
'hourMinuteColor: Color(0xffffffff)',
'dayPeriodTextColor: Color(0xffffffff)',
'dayPeriodColor: Color(0xffffffff)', 'dayPeriodColor: Color(0xffffffff)',
'dialHandColor: Color(0xffffffff)', 'dayPeriodShape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)',
'dayPeriodTextColor: Color(0xffffffff)',
'dayPeriodTextStyle: TextStyle(<all styles inherited>)',
'dialBackgroundColor: Color(0xffffffff)', 'dialBackgroundColor: Color(0xffffffff)',
'dialHandColor: Color(0xffffffff)',
'dialTextColor: Color(0xffffffff)', 'dialTextColor: Color(0xffffffff)',
'entryModeIconColor: Color(0xffffffff)', 'entryModeIconColor: Color(0xffffffff)',
'hourMinuteTextStyle: TextStyle(<all styles inherited>)',
'dayPeriodTextStyle: TextStyle(<all styles inherited>)',
'helpTextStyle: TextStyle(<all styles inherited>)', 'helpTextStyle: TextStyle(<all styles inherited>)',
'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)', 'hourMinuteColor: Color(0xffffffff)',
'hourMinuteShape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)', 'hourMinuteShape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)',
'dayPeriodShape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)', 'hourMinuteTextColor: Color(0xffffffff)',
'dayPeriodBorderSide: BorderSide', 'hourMinuteTextStyle: TextStyle(<all styles inherited>)',
'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)'
]); ]);
}); });
...@@ -104,10 +104,11 @@ void main() { ...@@ -104,10 +104,11 @@ void main() {
expect(dialogMaterial.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)))); expect(dialogMaterial.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))));
final RenderBox dial = tester.firstRenderObject<RenderBox>(find.byType(CustomPaint)); final RenderBox dial = tester.firstRenderObject<RenderBox>(find.byType(CustomPaint));
debugPrint('Color: ${defaultTheme.colorScheme.onSurface.withOpacity(0.08)}');
expect( expect(
dial, dial,
paints paints
..circle(color: defaultTheme.colorScheme.onBackground.withOpacity(0.12)) // Dial background color. ..circle(color: defaultTheme.colorScheme.onSurface.withOpacity(0.08)) // Dial background color.
..circle(color: Color(defaultTheme.colorScheme.primary.value)), // Dial hand color. ..circle(color: Color(defaultTheme.colorScheme.primary.value)), // Dial hand color.
); );
...@@ -162,10 +163,10 @@ void main() { ...@@ -162,10 +163,10 @@ void main() {
.copyWith(color: defaultTheme.colorScheme.onSurface), .copyWith(color: defaultTheme.colorScheme.onSurface),
); );
// ignore: avoid_dynamic_calls // ignore: avoid_dynamic_calls
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>; final List<dynamic> selectedLabels = dialPainter.selectedLabels as List<dynamic>;
expect( expect(
// ignore: avoid_dynamic_calls // ignore: avoid_dynamic_calls
secondaryLabels.first.painter.text.style, selectedLabels.first.painter.text.style,
Typography.material2014().englishLike.bodyLarge! Typography.material2014().englishLike.bodyLarge!
.merge(Typography.material2014().white.bodyLarge) .merge(Typography.material2014().white.bodyLarge)
.copyWith(color: defaultTheme.colorScheme.onPrimary), .copyWith(color: defaultTheme.colorScheme.onPrimary),
...@@ -186,7 +187,7 @@ void main() { ...@@ -186,7 +187,7 @@ void main() {
expect(pmMaterial.color, Colors.transparent); expect(pmMaterial.color, Colors.transparent);
final Color expectedBorderColor = Color.alphaBlend( final Color expectedBorderColor = Color.alphaBlend(
defaultTheme.colorScheme.onBackground.withOpacity(0.38), defaultTheme.colorScheme.onSurface.withOpacity(0.38),
defaultTheme.colorScheme.surface, defaultTheme.colorScheme.surface,
); );
final Material dayPeriodMaterial = _dayPeriodMaterial(tester); final Material dayPeriodMaterial = _dayPeriodMaterial(tester);
...@@ -220,7 +221,7 @@ void main() { ...@@ -220,7 +221,7 @@ void main() {
final InputDecoration hourDecoration = _textField(tester, '7').decoration!; final InputDecoration hourDecoration = _textField(tester, '7').decoration!;
expect(hourDecoration.filled, true); expect(hourDecoration.filled, true);
expect(hourDecoration.fillColor, defaultTheme.colorScheme.onSurface.withOpacity(0.12)); expect(hourDecoration.fillColor, MaterialStateColor.resolveWith((Set<MaterialState> states) => defaultTheme.colorScheme.onSurface.withOpacity(0.12)));
expect(hourDecoration.enabledBorder, const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent))); expect(hourDecoration.enabledBorder, const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)));
expect(hourDecoration.errorBorder, OutlineInputBorder(borderSide: BorderSide(color: defaultTheme.colorScheme.error, width: 2))); expect(hourDecoration.errorBorder, OutlineInputBorder(borderSide: BorderSide(color: defaultTheme.colorScheme.error, width: 2)));
expect(hourDecoration.focusedBorder, OutlineInputBorder(borderSide: BorderSide(color: defaultTheme.colorScheme.primary, width: 2))); expect(hourDecoration.focusedBorder, OutlineInputBorder(borderSide: BorderSide(color: defaultTheme.colorScheme.primary, width: 2)));
...@@ -307,10 +308,10 @@ void main() { ...@@ -307,10 +308,10 @@ void main() {
.copyWith(color: _unselectedColor), .copyWith(color: _unselectedColor),
); );
// ignore: avoid_dynamic_calls // ignore: avoid_dynamic_calls
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>; final List<dynamic> selectedLabels = dialPainter.selectedLabels as List<dynamic>;
expect( expect(
// ignore: avoid_dynamic_calls // ignore: avoid_dynamic_calls
secondaryLabels.first.painter.text.style, selectedLabels.first.painter.text.style,
Typography.material2014().englishLike.bodyLarge! Typography.material2014().englishLike.bodyLarge!
.merge(Typography.material2014().white.bodyLarge) .merge(Typography.material2014().white.bodyLarge)
.copyWith(color: _selectedColor), .copyWith(color: _selectedColor),
......
...@@ -6,62 +6,6 @@ import 'package:flutter/material.dart'; ...@@ -6,62 +6,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
class _TimePickerLauncher extends StatelessWidget {
const _TimePickerLauncher({
this.onChanged,
required this.locale,
this.entryMode = TimePickerEntryMode.dial,
});
final ValueChanged<TimeOfDay?>? onChanged;
final Locale locale;
final TimePickerEntryMode entryMode;
@override
Widget build(BuildContext context) {
return MaterialApp(
locale: locale,
supportedLocales: <Locale>[locale],
localizationsDelegates: GlobalMaterialLocalizations.delegates,
home: Material(
child: Center(
child: Builder(
builder: (BuildContext context) {
return ElevatedButton(
child: const Text('X'),
onPressed: () async {
onChanged?.call(await showTimePicker(
context: context,
initialEntryMode: entryMode,
initialTime: const TimeOfDay(hour: 7, minute: 0),
));
},
);
}
),
),
),
);
}
}
Future<Offset> startPicker(
WidgetTester tester,
ValueChanged<TimeOfDay?> onChanged, {
Locale locale = const Locale('en', 'US'),
}) async {
await tester.pumpWidget(_TimePickerLauncher(onChanged: onChanged, locale: locale,));
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
return tester.getCenter(find.byKey(const Key('time-picker-dial')));
}
Future<void> finishPicker(WidgetTester tester) async {
final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(tester.element(find.byType(ElevatedButton)));
await tester.tap(find.text(materialLocalizations.okButtonLabel));
await tester.pumpAndSettle(const Duration(seconds: 1));
}
void main() { void main() {
testWidgets('can localize the header in all known formats - portrait', (WidgetTester tester) async { testWidgets('can localize the header in all known formats - portrait', (WidgetTester tester) async {
// Ensure picker is displayed in portrait mode. // Ensure picker is displayed in portrait mode.
...@@ -213,13 +157,13 @@ void main() { ...@@ -213,13 +157,13 @@ void main() {
}); });
testWidgets('can localize input mode in all known formats', (WidgetTester tester) async { testWidgets('can localize input mode in all known formats', (WidgetTester tester) async {
final Finder hourControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourTextField');
final Finder minuteControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_MinuteTextField');
final Finder dayPeriodControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DayPeriodControl');
final Finder stringFragmentTextFinder = find.descendant( final Finder stringFragmentTextFinder = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_StringFragment'), of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_StringFragment'),
matching: find.byType(Text), matching: find.byType(Text),
).first; ).first;
final Finder hourControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourTextField');
final Finder minuteControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_MinuteTextField');
final Finder dayPeriodControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DayPeriodControl');
// TODO(yjbanov): also test `HH.mm` (in_ID), `a h:mm` (ko_KR) and `HH:mm น.` (th_TH) when we have .arb files for them // TODO(yjbanov): also test `HH.mm` (in_ID), `a h:mm` (ko_KR) and `HH:mm น.` (th_TH) when we have .arb files for them
final List<Locale> locales = <Locale>[ final List<Locale> locales = <Locale>[
...@@ -276,6 +220,7 @@ void main() { ...@@ -276,6 +220,7 @@ void main() {
expect(dayPeriodControlFinder, findsNothing); expect(dayPeriodControlFinder, findsNothing);
} }
await finishPicker(tester); await finishPicker(tester);
expect(tester.takeException(), isNot(throwsFlutterError));
} }
}); });
...@@ -353,10 +298,10 @@ void main() { ...@@ -353,10 +298,10 @@ void main() {
); );
// ignore: avoid_dynamic_calls // ignore: avoid_dynamic_calls
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>; final List<dynamic> selectedLabels = dialPainter.selectedLabels as List<dynamic>;
expect( expect(
// ignore: avoid_dynamic_calls // ignore: avoid_dynamic_calls
secondaryLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text! as TextSpan).text!), selectedLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text! as TextSpan).text!),
labels12To11, labels12To11,
); );
}); });
...@@ -375,11 +320,72 @@ void main() { ...@@ -375,11 +320,72 @@ void main() {
); );
// ignore: avoid_dynamic_calls // ignore: avoid_dynamic_calls
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>; final List<dynamic> selectedLabels = dialPainter.selectedLabels as List<dynamic>;
expect( expect(
// ignore: avoid_dynamic_calls // ignore: avoid_dynamic_calls
secondaryLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text! as TextSpan).text!), selectedLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text! as TextSpan).text!),
labels00To22TwoDigit, labels00To22TwoDigit,
); );
}); });
} }
class _TimePickerLauncher extends StatelessWidget {
const _TimePickerLauncher({
this.onChanged,
required this.locale,
this.entryMode = TimePickerEntryMode.dial,
});
final ValueChanged<TimeOfDay?>? onChanged;
final Locale locale;
final TimePickerEntryMode entryMode;
@override
Widget build(BuildContext context) {
return MaterialApp(
locale: locale,
supportedLocales: <Locale>[locale],
localizationsDelegates: GlobalMaterialLocalizations.delegates,
home: Material(
child: Center(
child: Builder(
builder: (BuildContext context) {
return ElevatedButton(
child: const Text('X'),
onPressed: () async {
onChanged?.call(await showTimePicker(
context: context,
initialEntryMode: entryMode,
initialTime: const TimeOfDay(hour: 7, minute: 0),
));
},
);
}
),
),
),
);
}
}
Future<Offset> startPicker(
WidgetTester tester,
ValueChanged<TimeOfDay?> onChanged, {
Locale locale = const Locale('en', 'US'),
}) async {
await tester.pumpWidget(
_TimePickerLauncher(
onChanged: onChanged,
locale: locale,
),
);
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
return tester.getCenter(find.byKey(const Key('time-picker-dial')));
}
Future<void> finishPicker(WidgetTester tester) async {
final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(tester.element(find.byType(ElevatedButton)));
await tester.tap(find.text(materialLocalizations.okButtonLabel));
await tester.pumpAndSettle(const Duration(seconds: 1));
}
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