Skip to content

Commit 59c8eab

Browse files
authored
Remove use of announce in time_picker and remove double Feedback.forTap calls (#167241)
<!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> This pull request replaces the use of the deprecated announce calls with idiomatic Semantic widgets and also removes the extra `Feedback.forTap` call. The `InkWell` widget already wraps the onTap with the `Feedback.forTap`. I also made sure sure that existing functionality is not impacted. Resolves flutter/flutter#166448 Partially addresses flutter/flutter#165510 Physical Testing: Android ✅ iOS ✅ Web ✅ ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent b33f8e8 commit 59c8eab

2 files changed

Lines changed: 166 additions & 166 deletions

File tree

packages/flutter/lib/src/material/time_picker.dart

Lines changed: 95 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -250,81 +250,89 @@ class _TimePickerHeader extends StatelessWidget {
250250
).timeOfDayFormat(alwaysUse24HourFormat: _TimePickerModel.use24HourFormatOf(context));
251251

252252
final _HourDialType hourDialType = _TimePickerModel.hourDialTypeOf(context);
253-
switch (_TimePickerModel.orientationOf(context)) {
254-
case Orientation.portrait:
255-
return Column(
256-
crossAxisAlignment: CrossAxisAlignment.start,
257-
children: <Widget>[
258-
Padding(
259-
padding: EdgeInsetsDirectional.only(
260-
bottom: _TimePickerModel.useMaterial3Of(context) ? 20 : 24,
261-
),
262-
child: Text(
263-
helpText,
264-
style:
265-
_TimePickerModel.themeOf(context).helpTextStyle ??
266-
_TimePickerModel.defaultThemeOf(context).helpTextStyle,
253+
final RenderObjectWidget orientationSpecificHeader = switch (_TimePickerModel.orientationOf(
254+
context,
255+
)) {
256+
Orientation.portrait => Column(
257+
crossAxisAlignment: CrossAxisAlignment.start,
258+
children: <Widget>[
259+
Padding(
260+
padding: EdgeInsetsDirectional.only(
261+
bottom: _TimePickerModel.useMaterial3Of(context) ? 20 : 24,
262+
),
263+
child: Text(
264+
helpText,
265+
style:
266+
_TimePickerModel.themeOf(context).helpTextStyle ??
267+
_TimePickerModel.defaultThemeOf(context).helpTextStyle,
268+
),
269+
),
270+
Row(
271+
textDirection:
272+
timeOfDayFormat == TimeOfDayFormat.a_space_h_colon_mm
273+
? TextDirection.rtl
274+
: TextDirection.ltr,
275+
spacing: 12,
276+
children: <Widget>[
277+
Expanded(
278+
child: Row(
279+
// Hour/minutes should not change positions in RTL locales.
280+
textDirection: TextDirection.ltr,
281+
children: <Widget>[
282+
const Expanded(child: _HourControl()),
283+
_TimeSelectorSeparator(timeOfDayFormat: timeOfDayFormat),
284+
const Expanded(child: _MinuteControl()),
285+
],
286+
),
267287
),
288+
if (hourDialType == _HourDialType.twelveHour) const _DayPeriodControl(),
289+
],
290+
),
291+
],
292+
),
293+
Orientation.landscape => SizedBox(
294+
width: _kTimePickerHeaderLandscapeWidth,
295+
child: Stack(
296+
children: <Widget>[
297+
Text(
298+
helpText,
299+
style:
300+
_TimePickerModel.themeOf(context).helpTextStyle ??
301+
_TimePickerModel.defaultThemeOf(context).helpTextStyle,
268302
),
269-
Row(
270-
textDirection:
303+
Column(
304+
verticalDirection:
271305
timeOfDayFormat == TimeOfDayFormat.a_space_h_colon_mm
272-
? TextDirection.rtl
273-
: TextDirection.ltr,
306+
? VerticalDirection.up
307+
: VerticalDirection.down,
308+
mainAxisAlignment: MainAxisAlignment.center,
309+
crossAxisAlignment: CrossAxisAlignment.start,
274310
spacing: 12,
275311
children: <Widget>[
276-
Expanded(
277-
child: Row(
278-
// Hour/minutes should not change positions in RTL locales.
279-
textDirection: TextDirection.ltr,
280-
children: <Widget>[
281-
const Expanded(child: _HourControl()),
282-
_TimeSelectorSeparator(timeOfDayFormat: timeOfDayFormat),
283-
const Expanded(child: _MinuteControl()),
284-
],
285-
),
312+
Row(
313+
// Hour/minutes should not change positions in RTL locales.
314+
textDirection: TextDirection.ltr,
315+
children: <Widget>[
316+
const Expanded(child: _HourControl()),
317+
_TimeSelectorSeparator(timeOfDayFormat: timeOfDayFormat),
318+
const Expanded(child: _MinuteControl()),
319+
],
286320
),
287321
if (hourDialType == _HourDialType.twelveHour) const _DayPeriodControl(),
288322
],
289323
),
290324
],
291-
);
292-
case Orientation.landscape:
293-
return SizedBox(
294-
width: _kTimePickerHeaderLandscapeWidth,
295-
child: Stack(
296-
children: <Widget>[
297-
Text(
298-
helpText,
299-
style:
300-
_TimePickerModel.themeOf(context).helpTextStyle ??
301-
_TimePickerModel.defaultThemeOf(context).helpTextStyle,
302-
),
303-
Column(
304-
verticalDirection:
305-
timeOfDayFormat == TimeOfDayFormat.a_space_h_colon_mm
306-
? VerticalDirection.up
307-
: VerticalDirection.down,
308-
mainAxisAlignment: MainAxisAlignment.center,
309-
crossAxisAlignment: CrossAxisAlignment.start,
310-
spacing: 12,
311-
children: <Widget>[
312-
Row(
313-
// Hour/minutes should not change positions in RTL locales.
314-
textDirection: TextDirection.ltr,
315-
children: <Widget>[
316-
const Expanded(child: _HourControl()),
317-
_TimeSelectorSeparator(timeOfDayFormat: timeOfDayFormat),
318-
const Expanded(child: _MinuteControl()),
319-
],
320-
),
321-
if (hourDialType == _HourDialType.twelveHour) const _DayPeriodControl(),
322-
],
323-
),
324-
],
325-
),
326-
);
327-
}
325+
),
326+
),
327+
};
328+
329+
return Semantics(
330+
label: MaterialLocalizations.of(context).formatTimeOfDay(
331+
_TimePickerModel.selectedTimeOf(context),
332+
alwaysUse24HourFormat: MediaQuery.alwaysUse24HourFormatOf(context),
333+
),
334+
child: orientationSpecificHeader,
335+
);
328336
}
329337
}
330338

@@ -443,11 +451,7 @@ class _HourControl extends StatelessWidget {
443451
child: _HourMinuteControl(
444452
isSelected: _TimePickerModel.hourMinuteModeOf(context) == _HourMinuteMode.hour,
445453
text: formattedHour,
446-
onTap:
447-
Feedback.wrapForTap(
448-
() => _TimePickerModel.setHourMinuteMode(context, _HourMinuteMode.hour),
449-
context,
450-
)!,
454+
onTap: () => _TimePickerModel.setHourMinuteMode(context, _HourMinuteMode.hour),
451455
onDoubleTap:
452456
_TimePickerModel.of(context, _TimePickerAspect.onHourDoubleTapped).onHourDoubleTapped,
453457
),
@@ -559,11 +563,7 @@ class _MinuteControl extends StatelessWidget {
559563
child: _HourMinuteControl(
560564
isSelected: _TimePickerModel.hourMinuteModeOf(context) == _HourMinuteMode.minute,
561565
text: formattedMinute,
562-
onTap:
563-
Feedback.wrapForTap(
564-
() => _TimePickerModel.setHourMinuteMode(context, _HourMinuteMode.minute),
565-
context,
566-
)!,
566+
onTap: () => _TimePickerModel.setHourMinuteMode(context, _HourMinuteMode.minute),
567567
onDoubleTap:
568568
_TimePickerModel.of(
569569
context,
@@ -597,19 +597,6 @@ class _DayPeriodControl extends StatelessWidget {
597597
if (selectedTime.period == DayPeriod.am) {
598598
return;
599599
}
600-
switch (Theme.of(context).platform) {
601-
case TargetPlatform.android:
602-
case TargetPlatform.fuchsia:
603-
case TargetPlatform.linux:
604-
case TargetPlatform.windows:
605-
_announceToAccessibility(
606-
context,
607-
MaterialLocalizations.of(context).anteMeridiemAbbreviation,
608-
);
609-
case TargetPlatform.iOS:
610-
case TargetPlatform.macOS:
611-
break;
612-
}
613600
_togglePeriod(context);
614601
}
615602

@@ -618,19 +605,6 @@ class _DayPeriodControl extends StatelessWidget {
618605
if (selectedTime.period == DayPeriod.pm) {
619606
return;
620607
}
621-
switch (Theme.of(context).platform) {
622-
case TargetPlatform.android:
623-
case TargetPlatform.fuchsia:
624-
case TargetPlatform.linux:
625-
case TargetPlatform.windows:
626-
_announceToAccessibility(
627-
context,
628-
MaterialLocalizations.of(context).postMeridiemAbbreviation,
629-
);
630-
case TargetPlatform.iOS:
631-
case TargetPlatform.macOS:
632-
break;
633-
}
634608
_togglePeriod(context);
635609
}
636610

@@ -758,7 +732,7 @@ class _AmPmButton extends StatelessWidget {
758732
return Material(
759733
color: resolvedBackgroundColor,
760734
child: InkWell(
761-
onTap: Feedback.wrapForTap(onPressed, context),
735+
onTap: onPressed,
762736
child: Semantics(
763737
checked: selected,
764738
inMutuallyExclusiveGroup: true,
@@ -1328,18 +1302,9 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
13281302
_center = box.size.center(Offset.zero);
13291303
_dialSize = box.size;
13301304
_updateThetaForPan(roundMinutes: true);
1331-
final TimeOfDay newTime = _notifyOnChangedIfNeeded(roundMinutes: true);
1305+
_notifyOnChangedIfNeeded(roundMinutes: true);
13321306
if (widget.hourMinuteMode == _HourMinuteMode.hour) {
1333-
switch (widget.hourDialType) {
1334-
case _HourDialType.twentyFourHour:
1335-
case _HourDialType.twentyFourHourDoubleRing:
1336-
_announceToAccessibility(context, localizations.formatDecimal(newTime.hour));
1337-
case _HourDialType.twelveHour:
1338-
_announceToAccessibility(context, localizations.formatDecimal(newTime.hourOfPeriod));
1339-
}
13401307
widget.onHourSelected?.call();
1341-
} else {
1342-
_announceToAccessibility(context, localizations.formatDecimal(newTime.minute));
13431308
}
13441309
final TimeOfDay time = _getTimeForTheta(
13451310
_theta.value,
@@ -1354,7 +1319,6 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
13541319
}
13551320

13561321
void _selectHour(int hour) {
1357-
_announceToAccessibility(context, localizations.formatDecimal(hour));
13581322
final TimeOfDay time;
13591323

13601324
TimeOfDay getAmPmTime() {
@@ -1387,7 +1351,6 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
13871351
}
13881352

13891353
void _selectMinute(int minute) {
1390-
_announceToAccessibility(context, localizations.formatDecimal(minute));
13911354
final TimeOfDay time = TimeOfDay(hour: widget.selectedTime.hour, minute: minute);
13921355
final double angle = _getThetaForTime(time);
13931356
_thetaTween
@@ -2775,7 +2738,6 @@ class _TimePickerState extends State<_TimePicker> with RestorationMixin {
27752738
);
27762739
final RestorableBoolN _autofocusHour = RestorableBoolN(null);
27772740
final RestorableBoolN _autofocusMinute = RestorableBoolN(null);
2778-
final RestorableBool _announcedInitialTime = RestorableBool(false);
27792741
late final RestorableEnumN<Orientation> _orientation = RestorableEnumN<Orientation>(
27802742
widget.orientation,
27812743
values: Orientation.values,
@@ -2793,16 +2755,13 @@ class _TimePickerState extends State<_TimePicker> with RestorationMixin {
27932755
_lastModeAnnounced.dispose();
27942756
_autofocusHour.dispose();
27952757
_autofocusMinute.dispose();
2796-
_announcedInitialTime.dispose();
27972758
super.dispose();
27982759
}
27992760

28002761
@override
28012762
void didChangeDependencies() {
28022763
super.didChangeDependencies();
28032764
localizations = MaterialLocalizations.of(context);
2804-
_announceInitialTimeOnce();
2805-
_announceModeOnce();
28062765
}
28072766

28082767
@override
@@ -2829,7 +2788,6 @@ class _TimePickerState extends State<_TimePicker> with RestorationMixin {
28292788
registerForRestoration(_lastModeAnnounced, 'last_mode_announced');
28302789
registerForRestoration(_autofocusHour, 'autofocus_hour');
28312790
registerForRestoration(_autofocusMinute, 'autofocus_minute');
2832-
registerForRestoration(_announcedInitialTime, 'announced_initial_time');
28332791
registerForRestoration(_selectedTime, 'selected_time');
28342792
registerForRestoration(_orientation, 'orientation');
28352793
}
@@ -2855,7 +2813,6 @@ class _TimePickerState extends State<_TimePicker> with RestorationMixin {
28552813
_vibrate();
28562814
setState(() {
28572815
_hourMinuteMode.value = mode;
2858-
_announceModeOnce();
28592816
});
28602817
}
28612818

@@ -2877,37 +2834,6 @@ class _TimePickerState extends State<_TimePicker> with RestorationMixin {
28772834
});
28782835
}
28792836

2880-
void _announceModeOnce() {
2881-
if (_lastModeAnnounced.value == _hourMinuteMode.value) {
2882-
// Already announced it.
2883-
return;
2884-
}
2885-
2886-
switch (_hourMinuteMode.value) {
2887-
case _HourMinuteMode.hour:
2888-
_announceToAccessibility(context, localizations.timePickerHourModeAnnouncement);
2889-
case _HourMinuteMode.minute:
2890-
_announceToAccessibility(context, localizations.timePickerMinuteModeAnnouncement);
2891-
}
2892-
_lastModeAnnounced.value = _hourMinuteMode.value;
2893-
}
2894-
2895-
void _announceInitialTimeOnce() {
2896-
if (_announcedInitialTime.value) {
2897-
return;
2898-
}
2899-
2900-
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
2901-
_announceToAccessibility(
2902-
context,
2903-
localizations.formatTimeOfDay(
2904-
_selectedTime.value,
2905-
alwaysUse24HourFormat: MediaQuery.alwaysUse24HourFormatOf(context),
2906-
),
2907-
);
2908-
_announcedInitialTime.value = true;
2909-
}
2910-
29112837
void _handleTimeChanged(TimeOfDay value) {
29122838
_vibrate();
29132839
setState(() {
@@ -2969,17 +2895,24 @@ class _TimePickerState extends State<_TimePicker> with RestorationMixin {
29692895
};
29702896
final Widget dial = Padding(
29712897
padding: dialPadding,
2972-
child: ExcludeSemantics(
2973-
child: SizedBox.fromSize(
2974-
size: defaultTheme.dialSize,
2975-
child: AspectRatio(
2976-
aspectRatio: 1,
2977-
child: _Dial(
2978-
hourMinuteMode: _hourMinuteMode.value,
2979-
hourDialType: hourMode,
2980-
selectedTime: _selectedTime.value,
2981-
onChanged: _handleTimeChanged,
2982-
onHourSelected: _handleHourSelected,
2898+
child: Semantics(
2899+
label: switch (_hourMinuteMode.value) {
2900+
_HourMinuteMode.hour => localizations.timePickerHourModeAnnouncement,
2901+
_HourMinuteMode.minute => localizations.timePickerMinuteModeAnnouncement,
2902+
},
2903+
liveRegion: true,
2904+
child: ExcludeSemantics(
2905+
child: SizedBox.fromSize(
2906+
size: defaultTheme.dialSize,
2907+
child: AspectRatio(
2908+
aspectRatio: 1,
2909+
child: _Dial(
2910+
hourMinuteMode: _hourMinuteMode.value,
2911+
hourDialType: hourMode,
2912+
selectedTime: _selectedTime.value,
2913+
onChanged: _handleTimeChanged,
2914+
onHourSelected: _handleHourSelected,
2915+
),
29832916
),
29842917
),
29852918
),
@@ -3233,10 +3166,6 @@ Future<TimeOfDay?> showTimePicker({
32333166
);
32343167
}
32353168

3236-
void _announceToAccessibility(BuildContext context, String message) {
3237-
SemanticsService.announce(message, Directionality.of(context));
3238-
}
3239-
32403169
// An abstract base class for the M2 and M3 defaults below, so that their return
32413170
// types can be non-nullable.
32423171
abstract class _TimePickerDefaults extends TimePickerThemeData {

0 commit comments

Comments
 (0)