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';
import 'package:gen_defaults/switch_template.dart';
import 'package:gen_defaults/tabs_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';
Map<String, dynamic> _readTokenFile(String fileName) {
......@@ -167,6 +168,7 @@ Future<void> main(List<String> args) async {
SliderTemplate('md.comp.slider', 'Slider', '$materialLib/slider.dart', tokens).updateFile();
SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.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();
TabsTemplate('Tabs', '$materialLib/tabs.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.
......@@ -75,21 +75,21 @@ void main() {
expect(description, <String>[
'backgroundColor: Color(0xffffffff)',
'hourMinuteTextColor: Color(0xffffffff)',
'hourMinuteColor: Color(0xffffffff)',
'dayPeriodTextColor: Color(0xffffffff)',
'dayPeriodBorderSide: BorderSide',
'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)',
'dialHandColor: Color(0xffffffff)',
'dialTextColor: Color(0xffffffff)',
'entryModeIconColor: Color(0xffffffff)',
'hourMinuteTextStyle: TextStyle(<all styles inherited>)',
'dayPeriodTextStyle: 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)',
'dayPeriodShape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)',
'dayPeriodBorderSide: BorderSide',
'hourMinuteTextColor: Color(0xffffffff)',
'hourMinuteTextStyle: TextStyle(<all styles inherited>)',
'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)'
]);
});
......@@ -104,10 +104,11 @@ void main() {
expect(dialogMaterial.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))));
final RenderBox dial = tester.firstRenderObject<RenderBox>(find.byType(CustomPaint));
debugPrint('Color: ${defaultTheme.colorScheme.onSurface.withOpacity(0.08)}');
expect(
dial,
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.
);
......@@ -162,10 +163,10 @@ void main() {
.copyWith(color: defaultTheme.colorScheme.onSurface),
);
// ignore: avoid_dynamic_calls
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
final List<dynamic> selectedLabels = dialPainter.selectedLabels as List<dynamic>;
expect(
// ignore: avoid_dynamic_calls
secondaryLabels.first.painter.text.style,
selectedLabels.first.painter.text.style,
Typography.material2014().englishLike.bodyLarge!
.merge(Typography.material2014().white.bodyLarge)
.copyWith(color: defaultTheme.colorScheme.onPrimary),
......@@ -186,7 +187,7 @@ void main() {
expect(pmMaterial.color, Colors.transparent);
final Color expectedBorderColor = Color.alphaBlend(
defaultTheme.colorScheme.onBackground.withOpacity(0.38),
defaultTheme.colorScheme.onSurface.withOpacity(0.38),
defaultTheme.colorScheme.surface,
);
final Material dayPeriodMaterial = _dayPeriodMaterial(tester);
......@@ -220,7 +221,7 @@ void main() {
final InputDecoration hourDecoration = _textField(tester, '7').decoration!;
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.errorBorder, OutlineInputBorder(borderSide: BorderSide(color: defaultTheme.colorScheme.error, width: 2)));
expect(hourDecoration.focusedBorder, OutlineInputBorder(borderSide: BorderSide(color: defaultTheme.colorScheme.primary, width: 2)));
......@@ -307,10 +308,10 @@ void main() {
.copyWith(color: _unselectedColor),
);
// ignore: avoid_dynamic_calls
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
final List<dynamic> selectedLabels = dialPainter.selectedLabels as List<dynamic>;
expect(
// ignore: avoid_dynamic_calls
secondaryLabels.first.painter.text.style,
selectedLabels.first.painter.text.style,
Typography.material2014().englishLike.bodyLarge!
.merge(Typography.material2014().white.bodyLarge)
.copyWith(color: _selectedColor),
......
......@@ -6,62 +6,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.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() {
testWidgets('can localize the header in all known formats - portrait', (WidgetTester tester) async {
// Ensure picker is displayed in portrait mode.
......@@ -213,13 +157,13 @@ void main() {
});
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(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_StringFragment'),
matching: find.byType(Text),
).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
final List<Locale> locales = <Locale>[
......@@ -276,6 +220,7 @@ void main() {
expect(dayPeriodControlFinder, findsNothing);
}
await finishPicker(tester);
expect(tester.takeException(), isNot(throwsFlutterError));
}
});
......@@ -353,10 +298,10 @@ void main() {
);
// ignore: avoid_dynamic_calls
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
final List<dynamic> selectedLabels = dialPainter.selectedLabels as List<dynamic>;
expect(
// 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,
);
});
......@@ -375,11 +320,72 @@ void main() {
);
// ignore: avoid_dynamic_calls
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
final List<dynamic> selectedLabels = dialPainter.selectedLabels as List<dynamic>;
expect(
// 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,
);
});
}
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