mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
[Material] Relanding fix to ensure time picker input mode lays out correctly in RTL (#64097)
This commit is contained in:
parent
db705b81e3
commit
021cf56fc9
@ -1414,13 +1414,19 @@ class _TimePickerInputState extends State<_TimePickerInput> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 12.0),
|
const SizedBox(width: 12.0),
|
||||||
],
|
],
|
||||||
Expanded(child: Column(
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
// Hour/minutes should not change positions in RTL locales.
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const SizedBox(height: 8.0),
|
const SizedBox(height: 8.0),
|
||||||
_HourMinuteTextField(
|
_HourTextField(
|
||||||
selectedTime: _selectedTime,
|
selectedTime: _selectedTime,
|
||||||
isHour: true,
|
|
||||||
style: hourMinuteStyle,
|
style: hourMinuteStyle,
|
||||||
validator: _validateHour,
|
validator: _validateHour,
|
||||||
onSavedSubmitted: _handleHourSavedSubmitted,
|
onSavedSubmitted: _handleHourSavedSubmitted,
|
||||||
@ -1437,19 +1443,20 @@ class _TimePickerInputState extends State<_TimePickerInput> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.only(top: 8.0),
|
margin: const EdgeInsets.only(top: 8.0),
|
||||||
height: _kTimePickerHeaderControlHeight,
|
height: _kTimePickerHeaderControlHeight,
|
||||||
child: _StringFragment(timeOfDayFormat: timeOfDayFormat),
|
child: _StringFragment(timeOfDayFormat: timeOfDayFormat),
|
||||||
),
|
),
|
||||||
Expanded(child: Column(
|
Expanded(
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const SizedBox(height: 8.0),
|
const SizedBox(height: 8.0),
|
||||||
_HourMinuteTextField(
|
_MinuteTextField(
|
||||||
selectedTime: _selectedTime,
|
selectedTime: _selectedTime,
|
||||||
isHour: false,
|
|
||||||
style: hourMinuteStyle,
|
style: hourMinuteStyle,
|
||||||
validator: _validateMinute,
|
validator: _validateMinute,
|
||||||
onSavedSubmitted: _handleMinuteSavedSubmitted,
|
onSavedSubmitted: _handleMinuteSavedSubmitted,
|
||||||
@ -1465,7 +1472,11 @@ class _TimePickerInputState extends State<_TimePickerInput> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
if (!use24HourDials && timeOfDayFormat != TimeOfDayFormat.a_space_h_colon_mm) ...<Widget>[
|
if (!use24HourDials && timeOfDayFormat != TimeOfDayFormat.a_space_h_colon_mm) ...<Widget>[
|
||||||
const SizedBox(width: 12.0),
|
const SizedBox(width: 12.0),
|
||||||
_DayPeriodControl(
|
_DayPeriodControl(
|
||||||
@ -1489,6 +1500,61 @@ class _TimePickerInputState extends State<_TimePickerInput> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _HourTextField extends StatelessWidget {
|
||||||
|
const _HourTextField({
|
||||||
|
Key key,
|
||||||
|
@required this.selectedTime,
|
||||||
|
@required this.style,
|
||||||
|
@required this.validator,
|
||||||
|
@required this.onSavedSubmitted,
|
||||||
|
@required this.onChanged,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final TimeOfDay selectedTime;
|
||||||
|
final TextStyle style;
|
||||||
|
final FormFieldValidator<String> validator;
|
||||||
|
final ValueChanged<String> onSavedSubmitted;
|
||||||
|
final ValueChanged<String> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _HourMinuteTextField(
|
||||||
|
selectedTime: selectedTime,
|
||||||
|
isHour: true,
|
||||||
|
style: style,
|
||||||
|
validator: validator,
|
||||||
|
onSavedSubmitted: onSavedSubmitted,
|
||||||
|
onChanged: onChanged,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MinuteTextField extends StatelessWidget {
|
||||||
|
const _MinuteTextField({
|
||||||
|
Key key,
|
||||||
|
@required this.selectedTime,
|
||||||
|
@required this.style,
|
||||||
|
@required this.validator,
|
||||||
|
@required this.onSavedSubmitted,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final TimeOfDay selectedTime;
|
||||||
|
final TextStyle style;
|
||||||
|
final FormFieldValidator<String> validator;
|
||||||
|
final ValueChanged<String> onSavedSubmitted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _HourMinuteTextField(
|
||||||
|
selectedTime: selectedTime,
|
||||||
|
isHour: false,
|
||||||
|
style: style,
|
||||||
|
validator: validator,
|
||||||
|
onSavedSubmitted: onSavedSubmitted,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _HourMinuteTextField extends StatefulWidget {
|
class _HourMinuteTextField extends StatefulWidget {
|
||||||
const _HourMinuteTextField({
|
const _HourMinuteTextField({
|
||||||
Key key,
|
Key key,
|
||||||
|
@ -806,6 +806,16 @@ void _testsInput() {
|
|||||||
await finishPicker(tester);
|
await finishPicker(tester);
|
||||||
expect(result, equals(const TimeOfDay(hour: 8, minute: 15)));
|
expect(result, equals(const TimeOfDay(hour: 8, minute: 15)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fixes regression that was reverted in https://github.com/flutter/flutter/pull/64094#pullrequestreview-469836378.
|
||||||
|
testWidgets('Ensure hour/minute fields are top-aligned with the separator', (WidgetTester tester) async {
|
||||||
|
await startPicker(tester, (TimeOfDay time) { }, entryMode: TimePickerEntryMode.input);
|
||||||
|
final double hourFieldTop = tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourTextField')).dy;
|
||||||
|
final double minuteFieldTop = tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_MinuteTextField')).dy;
|
||||||
|
final double separatorTop = tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_StringFragment')).dy;
|
||||||
|
expect(hourFieldTop, separatorTop);
|
||||||
|
expect(minuteFieldTop, separatorTop);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final Finder findDialPaint = find.descendant(
|
final Finder findDialPaint = find.descendant(
|
||||||
|
@ -8,10 +8,16 @@ 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 {
|
class _TimePickerLauncher extends StatelessWidget {
|
||||||
const _TimePickerLauncher({ Key key, this.onChanged, this.locale }) : super(key: key);
|
const _TimePickerLauncher({
|
||||||
|
Key key,
|
||||||
|
this.onChanged,
|
||||||
|
this.locale,
|
||||||
|
this.entryMode = TimePickerEntryMode.dial,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
final ValueChanged<TimeOfDay> onChanged;
|
final ValueChanged<TimeOfDay> onChanged;
|
||||||
final Locale locale;
|
final Locale locale;
|
||||||
|
final TimePickerEntryMode entryMode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -28,6 +34,7 @@ class _TimePickerLauncher extends StatelessWidget {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
onChanged(await showTimePicker(
|
onChanged(await showTimePicker(
|
||||||
context: context,
|
context: context,
|
||||||
|
initialEntryMode: entryMode,
|
||||||
initialTime: const TimeOfDay(hour: 7, minute: 0),
|
initialTime: const TimeOfDay(hour: 7, minute: 0),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@ -207,6 +214,73 @@ void main() {
|
|||||||
tester.binding.window.devicePixelRatioTestValue = null;
|
tester.binding.window.devicePixelRatioTestValue = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('can localize input mode in all known formats', (WidgetTester tester) async {
|
||||||
|
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>[
|
||||||
|
const Locale('en', 'US'), //'h:mm a'
|
||||||
|
const Locale('en', 'GB'), //'HH:mm'
|
||||||
|
const Locale('es', 'ES'), //'H:mm'
|
||||||
|
const Locale('fr', 'CA'), //'HH \'h\' mm'
|
||||||
|
const Locale('zh', 'ZH'), //'ah:mm'
|
||||||
|
const Locale('fa', 'IR'), //'H:mm' but RTL
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final Locale locale in locales) {
|
||||||
|
await tester.pumpWidget(_TimePickerLauncher(onChanged: (TimeOfDay time) { }, locale: locale, entryMode: TimePickerEntryMode.input));
|
||||||
|
await tester.tap(find.text('X'));
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
final Text stringFragmentText = tester.widget(stringFragmentTextFinder);
|
||||||
|
final double hourLeftOffset = tester.getTopLeft(hourControlFinder).dx;
|
||||||
|
final double minuteLeftOffset = tester.getTopLeft(minuteControlFinder).dx;
|
||||||
|
final double stringFragmentLeftOffset = tester.getTopLeft(stringFragmentTextFinder).dx;
|
||||||
|
|
||||||
|
if (locale == const Locale('en', 'US')) {
|
||||||
|
final double dayPeriodLeftOffset = tester.getTopLeft(dayPeriodControlFinder).dx;
|
||||||
|
expect(stringFragmentText.data, ':');
|
||||||
|
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||||
|
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||||
|
expect(minuteLeftOffset, lessThan(dayPeriodLeftOffset));
|
||||||
|
} else if (locale == const Locale('en', 'GB')) {
|
||||||
|
expect(stringFragmentText.data, ':');
|
||||||
|
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||||
|
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||||
|
expect(dayPeriodControlFinder, findsNothing);
|
||||||
|
} else if (locale == const Locale('es', 'ES')) {
|
||||||
|
expect(stringFragmentText.data, ':');
|
||||||
|
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||||
|
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||||
|
expect(dayPeriodControlFinder, findsNothing);
|
||||||
|
} else if (locale == const Locale('fr', 'CA')) {
|
||||||
|
expect(stringFragmentText.data, 'h');
|
||||||
|
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||||
|
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||||
|
expect(dayPeriodControlFinder, findsNothing);
|
||||||
|
} else if (locale == const Locale('zh', 'ZH')) {
|
||||||
|
final double dayPeriodLeftOffset = tester.getTopLeft(dayPeriodControlFinder).dx;
|
||||||
|
expect(stringFragmentText.data, ':');
|
||||||
|
expect(dayPeriodLeftOffset, lessThan(hourLeftOffset));
|
||||||
|
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||||
|
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||||
|
} else if (locale == const Locale('fa', 'IR')) {
|
||||||
|
// Even though this is an RTL locale, the hours and minutes positions should remain the same.
|
||||||
|
expect(stringFragmentText.data, ':');
|
||||||
|
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||||
|
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||||
|
expect(dayPeriodControlFinder, findsNothing);
|
||||||
|
}
|
||||||
|
await finishPicker(tester);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('uses single-ring 24-hour dial for all formats', (WidgetTester tester) async {
|
testWidgets('uses single-ring 24-hour dial for all formats', (WidgetTester tester) async {
|
||||||
const List<Locale> locales = <Locale>[
|
const List<Locale> locales = <Locale>[
|
||||||
Locale('en', 'US'), // h
|
Locale('en', 'US'), // h
|
||||||
|
Loading…
Reference in New Issue
Block a user