Repository navigation
[material_ui] Add day and weekday builders to CalendarDatePicker and showDatePicker - #12587
Conversation
CalendarDatePicker does not provide a way to customize individual day cells or weekday headers while retaining the default picker behavior. Add dayBuilder and weekdayBuilder callbacks for customizing individual day cells and weekday headers. Expose the callbacks through CalendarDatePicker, DatePickerDialog, and showDatePicker. The day builder receives the current widget states while the picker retains its semantics, focus, and tap handling. Add tests for customized days and weekday headers, day widget states, and showDatePicker parameter forwarding.
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
There was a problem hiding this comment.
Code Review
This pull request introduces dayBuilder and weekdayBuilder custom builders to CalendarDatePicker and showDatePicker to allow customization of individual days and weekday headers. The review feedback suggests optimizing performance by avoiding the overhead of DefaultTextStyle.merge and ListenableBuilder when these builders are not provided.
| final child = Text(weekday); | ||
|
|
||
| final Widget weekdayWidget = | ||
| widget.weekdayBuilder?.call(context, dateTimeWeekday, child) ?? child; | ||
|
|
||
| result.add( | ||
| ExcludeSemantics( | ||
| child: Center(child: Text(weekday, style: headerStyle)), | ||
| child: DefaultTextStyle.merge( | ||
| style: headerStyle, | ||
| child: Center(child: weekdayWidget), | ||
| ), | ||
| ), | ||
| ); |
There was a problem hiding this comment.
When weekdayBuilder is null, we can avoid the overhead of creating a DefaultTextStyle.merge widget (which internally allocates a Builder and a DefaultTextStyle widget) for every weekday header. Applying the headerStyle directly to the Text widget as before is more efficient and keeps the widget tree flatter.
if (widget.weekdayBuilder != null) {
final Widget child = Text(weekday);
final Widget weekdayWidget = widget.weekdayBuilder!(context, dateTimeWeekday, child);
result.add(
ExcludeSemantics(
child: DefaultTextStyle.merge(
style: headerStyle,
child: Center(child: weekdayWidget),
),
),
);
} else {
result.add(
ExcludeSemantics(
child: Center(child: Text(weekday, style: headerStyle)),
),
);
}There was a problem hiding this comment.
This seems valid, please address. Thanks!
| Widget dayWidget = Ink( | ||
| decoration: decoration, | ||
| child: Center( | ||
| child: Text( | ||
| localizations.formatDecimal(widget.day.day), | ||
| style: dayStyle?.apply(color: dayForegroundColor), | ||
| ), | ||
| ), | ||
| child: Center(child: Text(localizations.formatDecimal(widget.day.day))), | ||
| ); | ||
|
|
||
| if (widget.dayBuilder != null) { | ||
| dayWidget = ListenableBuilder( | ||
| listenable: _statesController, | ||
| builder: (BuildContext context, Widget? child) { | ||
| return widget.dayBuilder!( | ||
| context, | ||
| widget.day, | ||
| Set<WidgetState>.unmodifiable(_statesController.value), | ||
| child!, | ||
| ); | ||
| }, | ||
| child: dayWidget, | ||
| ); | ||
| } | ||
|
|
||
| dayWidget = DefaultTextStyle.merge( | ||
| style: dayStyle?.apply(color: dayForegroundColor), | ||
| child: dayWidget, | ||
| ); |
There was a problem hiding this comment.
When dayBuilder is null, we can completely avoid wrapping every single day cell (up to 31 cells per month view) in a ListenableBuilder and a DefaultTextStyle.merge. Applying the text style directly to the Text widget as before is much more efficient and avoids unnecessary widget tree depth and rebuild overhead.
Widget dayWidget;
if (widget.dayBuilder != null) {
dayWidget = Ink(
decoration: decoration,
child: Center(child: Text(localizations.formatDecimal(widget.day.day))),
);
dayWidget = ListenableBuilder(
listenable: _statesController,
builder: (BuildContext context, Widget? child) {
return widget.dayBuilder!(
context,
widget.day,
Set<WidgetState>.unmodifiable(_statesController.value),
child!,
);
},
child: dayWidget,
);
dayWidget = DefaultTextStyle.merge(
style: dayStyle?.apply(color: dayForegroundColor),
child: dayWidget,
);
} else {
dayWidget = Ink(
decoration: decoration,
child: Center(
child: Text(
localizations.formatDecimal(widget.day.day),
style: dayStyle?.apply(color: dayForegroundColor),
),
),
);
}There was a problem hiding this comment.
This seems valid, please address. Thanks!
|
Thanks a lot for your contribution! We should also add a changelog entry for this change. |
|
Thanks! I've added a changelog entry. |
elliette
left a comment
There was a problem hiding this comment.
Thanks for working on this feature request! Left a couple comments
| Widget dayWidget = Ink( | ||
| decoration: decoration, | ||
| child: Center( | ||
| child: Text( | ||
| localizations.formatDecimal(widget.day.day), | ||
| style: dayStyle?.apply(color: dayForegroundColor), | ||
| ), | ||
| ), | ||
| child: Center(child: Text(localizations.formatDecimal(widget.day.day))), | ||
| ); | ||
|
|
||
| if (widget.dayBuilder != null) { | ||
| dayWidget = ListenableBuilder( | ||
| listenable: _statesController, | ||
| builder: (BuildContext context, Widget? child) { | ||
| return widget.dayBuilder!( | ||
| context, | ||
| widget.day, | ||
| Set<WidgetState>.unmodifiable(_statesController.value), | ||
| child!, | ||
| ); | ||
| }, | ||
| child: dayWidget, | ||
| ); | ||
| } | ||
|
|
||
| dayWidget = DefaultTextStyle.merge( | ||
| style: dayStyle?.apply(color: dayForegroundColor), | ||
| child: dayWidget, | ||
| ); |
There was a problem hiding this comment.
This seems valid, please address. Thanks!
| final child = Text(weekday); | ||
|
|
||
| final Widget weekdayWidget = | ||
| widget.weekdayBuilder?.call(context, dateTimeWeekday, child) ?? child; | ||
|
|
||
| result.add( | ||
| ExcludeSemantics( | ||
| child: Center(child: Text(weekday, style: headerStyle)), | ||
| child: DefaultTextStyle.merge( | ||
| style: headerStyle, | ||
| child: Center(child: weekdayWidget), | ||
| ), | ||
| ), | ||
| ); |
There was a problem hiding this comment.
This seems valid, please address. Thanks!
|
Thanks! I've addressed both comments and pushed the changes. |
QuncCccccc
left a comment
There was a problem hiding this comment.
Thanks for your contribution! I left a few comments:)
| /// | ||
| /// The date picker's semantics, focus, and tap handling are applied to the | ||
| /// widget returned by this function. | ||
| typedef CalendarDatePickerDayBuilder = |
There was a problem hiding this comment.
Do we consider using separate CalendarDatePickerDayDetails and CalendarDatePickerWeekdayDetails objects for these callbacks? That would keep both callback signatures stable if we need to expose additional metadata later, while allowing the two builders to evolve independently.
Something like
final class CalendarDatePickerDayDetails {
const CalendarDatePickerDayDetails._({
required this.day,
required this.states,
required this.child,
});
final DateTime day;
final Set<WidgetState> states;
final Widget child;
}There was a problem hiding this comment.
Thanks!
I updated this to use separate CalendarDatePickerDayDetails and CalendarDatePickerWeekdayDetails classes.
| return widget.dayBuilder!( | ||
| context, | ||
| widget.day, | ||
| Set<WidgetState>.unmodifiable(_statesController.value), |
There was a problem hiding this comment.
Should this state set reliably preserve hovered, focused, and pressed? InkResponse can add those states, but _statesController.value = states earlier in build replaces the set with only selected and disabled whenever the widget rebuilds.
There was a problem hiding this comment.
Thanks for pointing this out. I updated the state handling so hovered, focused, and pressed are preserved across rebuilds.
| ), | ||
| ); | ||
|
|
||
| expect(find.text('Custom day 1'), findsOneWidget); |
There was a problem hiding this comment.
The builder documentation guarantees that the date picker retains semantics, focus, and tap handling around the returned widget. Could this test also tap “Custom day 1” and verify onDateChanged, as well as assert that it still has the expected button/date semantics?
There was a problem hiding this comment.
Done. I added a tap assertion for “Custom day 1” and verified the expected semantics as well.
| Text(weekday), | ||
| ); | ||
| result.add( | ||
| ExcludeSemantics( |
There was a problem hiding this comment.
Could ExcludeSemantics wrap only the default child passed to weekdayBuilder, rather than the complete widget returned by the builder? That would preserve the existing behavior when the builder returns child, while allowing a custom builder to provide meaningful semantics explicitly. With the current structure, even semantics intentionally added by the builder are discarded.
There was a problem hiding this comment.
Updated. ExcludeSemantics now wraps only the default child passed to weekdayBuilder, so custom semantics provided by the builder are preserved.
elliette
left a comment
There was a problem hiding this comment.
Thank you, this is looking good! One small suggestion for testing.
| }); | ||
| } | ||
|
|
||
| testWidgets('weekdayBuilder customizes headers and uses DateTime weekday values', ( |
There was a problem hiding this comment.
Could we also cover a locale where the week starts on Monday (for example Locale('en', 'GB')), and check the order of details.weekday values the builder receives?
There was a problem hiding this comment.
Thanks! I've added tests for both en_US (Sunday-first) and en_GB (Monday-first) to verify the order of details.weekday values received by weekdayBuilder.
QuncCccccc
left a comment
There was a problem hiding this comment.
It looks like the Linux repo_checks job failed on the latest commit. Other than the PR feedback of adding tests, could you also take a look the repo_checks failures? Thanks!
|
Thanks! After pulling the latest changes, I re-ran the formatter and pushed the formatting fixes. |
QuncCccccc
left a comment
There was a problem hiding this comment.
Sorry for the late response. Overall looks great! Just left one comment. Thanks a lot!
| dayWidget = ListenableBuilder( | ||
| listenable: _statesController, | ||
| builder: (BuildContext context, Widget? child) { | ||
| return widget.dayBuilder!( |
There was a problem hiding this comment.
semantics that a dayBuilder adds are dropped, because the widget returned here ends up inside Semantics(excludeSemantics: true, ...) in _DayState.build (L1451), which removes everything below it. For example, if a builder returns Semantics(label: 'has events', child: details.child) for day 12, a screen reader still only reads "12, Tuesday, January 12, 2016". Since the original issue also mentioned a use case with appointments, screen reader users would miss that. (weekdayBuilder doesn't have this problem, because only its default child is wrapped in ExcludeSemantics.)
Maybe we can keep the builder's semantics by only wrapping the default Text in ExcludeSemantics, and don't set excludeSemantics when a dayBuilder is provided.
There was a problem hiding this comment.
Thanks! I've wrapped only the default day text in ExcludeSemantics and removed excludeSemantics from the outer Semantics. I also extended the existing test to verify that labels added by dayBuilder are preserved without duplicating the day text.
CalendarDatePicker does not provide a way to customize individual day cells or weekday headers while retaining the default picker behavior. Add dayBuilder and weekdayBuilder callbacks for customizing individual day cells and weekday headers. Expose the callbacks through CalendarDatePicker, DatePickerDialog, and showDatePicker. The day builder receives the current widget states while the picker retains its semantics, focus, and tap handling. Add tests for customized days and weekday headers, day widget states, and showDatePicker parameter forwarding.
48f92bc to
713fad1
Compare
…kages into date-picker-day-style
QuncCccccc
left a comment
There was a problem hiding this comment.
LGTM. Thank you for your contribution!
elliette
left a comment
There was a problem hiding this comment.
Thanks for your patience! Looks good
flutter/packages@93baed9...108dad0 2026-10-22 [email protected] [material_ui] Migrate M3 IconButton template to use new gen_defaults (flutter/packages#12922) 2026-10-22 [email protected] [material_ui] Migrate M3 NavigationBar template to use new gen_defaults (flutter/packages#13067) 2026-10-22 [email protected] [material_ui] Remove obsolete `SurfaceTint` defaults template (flutter/packages#13276) 2026-10-22 [email protected] [material_ui] Migrate M3 Radio template to use new gen_defaults (flutter/packages#13210) 2026-10-22 [email protected] [material_ui] Migrate M3 NavigationDrawer template to use new gen_defaults (flutter/packages#13068) 2026-10-22 [email protected] [image_picker_ios] PR 3/5 Split SPM packaging and migrate the plugin class to Swift (flutter/packages#13235) 2026-10-22 [email protected] [ci] Increase check-discovery timeout in release workflows (flutter/packages#13392) 2026-10-22 [email protected] [material_ui] Migrate M3 `Switch` template to use new gen_defaults (flutter/packages#13274) 2026-10-22 [email protected] [material_ui] Migrate M3 PopupMenu template to use new gen_defaults (flutter/packages#13209) 2026-10-22 [email protected] [material_ui] Add day and weekday builders to CalendarDatePicker and showDatePicker (flutter/packages#12587) 2026-10-22 [email protected] [material_ui] Migrate M3 `SearchView` template to use new gen_defaults (flutter/packages#13272) 2026-10-22 [email protected] [material_ui] Migrate M3 `TextField` template to use new gen_defaults (flutter/packages#13275) 2026-10-22 [email protected] [material_ui] Migrate M3 RangeSlider template to use new gen_defaults (flutter/packages#13215) 2026-10-22 [email protected] [go_router] Keep web autocomplete focused in shell routes (flutter/packages#13348) 2026-10-22 [email protected] [shared_preferences_android] Fix web compilation of apps that import the package (flutter/packages#13380) 2026-10-22 [email protected] [material_ui] Migrate M3 NavigationRail template to use new gen_defaults (flutter/packages#13069) 2026-10-22 [email protected] [material_ui] Migrate M3 SearchBar template to use new gen_defaults (flutter/packages#13216) 2026-10-22 [email protected] [material_ui] Fix for time picker error in locales that do not use Latin digits (flutter/packages#13035) 2026-10-22 [email protected] [material_ui] Fix MenuAnchor scrollbar safe-area padding (flutter/packages#12470) 2026-10-22 [email protected] [material_ui] Document Material 3 fallback for Divider.color and VerticalDivider.color (flutter/packages#12967) 2026-10-22 [email protected] [material_ui] Migrate M3 tabs template to use new gen_defaults (flutter/packages#13139) 2026-10-22 [email protected] [material_ui] Migrate M3 progress indicator defaults to new gen_defaults (flutter/packages#13026) 2026-10-22 [email protected] [google_sign_in] Convert the iOS example app and UI tests to Swift (flutter/packages#13271) If this roll has caused a breakage, revert this CL and set the roller to dry run mode using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC [email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
flutter/packages@93baed9...108dad0 2026-10-22 [email protected] [material_ui] Migrate M3 IconButton template to use new gen_defaults (flutter/packages#12922) 2026-10-22 [email protected] [material_ui] Migrate M3 NavigationBar template to use new gen_defaults (flutter/packages#13067) 2026-10-22 [email protected] [material_ui] Remove obsolete `SurfaceTint` defaults template (flutter/packages#13276) 2026-10-22 [email protected] [material_ui] Migrate M3 Radio template to use new gen_defaults (flutter/packages#13210) 2026-10-22 [email protected] [material_ui] Migrate M3 NavigationDrawer template to use new gen_defaults (flutter/packages#13068) 2026-10-22 [email protected] [image_picker_ios] PR 3/5 Split SPM packaging and migrate the plugin class to Swift (flutter/packages#13235) 2026-10-22 [email protected] [ci] Increase check-discovery timeout in release workflows (flutter/packages#13392) 2026-10-22 [email protected] [material_ui] Migrate M3 `Switch` template to use new gen_defaults (flutter/packages#13274) 2026-10-22 [email protected] [material_ui] Migrate M3 PopupMenu template to use new gen_defaults (flutter/packages#13209) 2026-10-22 [email protected] [material_ui] Add day and weekday builders to CalendarDatePicker and showDatePicker (flutter/packages#12587) 2026-10-22 [email protected] [material_ui] Migrate M3 `SearchView` template to use new gen_defaults (flutter/packages#13272) 2026-10-22 [email protected] [material_ui] Migrate M3 `TextField` template to use new gen_defaults (flutter/packages#13275) 2026-10-22 [email protected] [material_ui] Migrate M3 RangeSlider template to use new gen_defaults (flutter/packages#13215) 2026-10-22 [email protected] [go_router] Keep web autocomplete focused in shell routes (flutter/packages#13348) 2026-10-22 [email protected] [shared_preferences_android] Fix web compilation of apps that import the package (flutter/packages#13380) 2026-10-22 [email protected] [material_ui] Migrate M3 NavigationRail template to use new gen_defaults (flutter/packages#13069) 2026-10-22 [email protected] [material_ui] Migrate M3 SearchBar template to use new gen_defaults (flutter/packages#13216) 2026-10-22 [email protected] [material_ui] Fix for time picker error in locales that do not use Latin digits (flutter/packages#13035) 2026-10-22 [email protected] [material_ui] Fix MenuAnchor scrollbar safe-area padding (flutter/packages#12470) 2026-10-22 [email protected] [material_ui] Document Material 3 fallback for Divider.color and VerticalDivider.color (flutter/packages#12967) 2026-10-22 [email protected] [material_ui] Migrate M3 tabs template to use new gen_defaults (flutter/packages#13139) 2026-10-22 [email protected] [material_ui] Migrate M3 progress indicator defaults to new gen_defaults (flutter/packages#13026) 2026-10-22 [email protected] [google_sign_in] Convert the iOS example app and UI tests to Swift (flutter/packages#13271) If this roll has caused a breakage, revert this CL and set the roller to dry run mode using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC [email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
CalendarDatePickerdoes not provide a way to customize individual day cells or weekday headers while retaining the default picker behavior.This PR adds
dayBuilderandweekdayBuildercallbacks for customizing individual day cells and weekday headers.The callbacks are exposed through
CalendarDatePicker,DatePickerDialog, andshowDatePicker. The day builder receives the current widget states while the picker retains its semantics, focus, and tap handling.Tests cover customized days and weekday headers, day widget states, and
showDatePickerparameter forwarding.Fixes flutter/flutter#68106
Pre-Review Checklist
[shared_preferences]///).If you need help, consider asking for advice on the #hackers-new channel on Discord.
Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the
gemini-code-assistbot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.Footnotes
Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. ↩ ↩2