diff --git a/angular_components/lib/material_datepicker/date_range_editor.dart b/angular_components/lib/material_datepicker/date_range_editor.dart index c7a606814..dbb899f00 100644 --- a/angular_components/lib/material_datepicker/date_range_editor.dart +++ b/angular_components/lib/material_datepicker/date_range_editor.dart @@ -630,8 +630,6 @@ class DateRangeEditorComponent implements OnInit, AfterViewInit, Focusable { desc: 'Message that explains why a date range is invalid.'); } -typedef NextPrevCallback = void Function(); - class DateRangeEditorNextPrevModel implements Sequential { final NextPrevCallback onNext; final NextPrevCallback onPrev; diff --git a/angular_components/lib/material_datepicker/material_datepicker.dart b/angular_components/lib/material_datepicker/material_datepicker.dart index c6999f377..a996a4d8a 100644 --- a/angular_components/lib/material_datepicker/material_datepicker.dart +++ b/angular_components/lib/material_datepicker/material_datepicker.dart @@ -6,6 +6,12 @@ import 'dart:async'; import 'dart:html'; import 'package:angular/angular.dart'; +import 'package:angular_components/material_datepicker/material_month_picker.dart'; +import 'package:angular_components/material_datepicker/next_prev_buttons.dart'; +import 'package:angular_components/material_icon/material_icon.dart'; +import 'package:angular_components/model/observable/observable.dart'; +import 'package:angular_components/utils/browser/dom_service/dom_service.dart'; +import 'package:angular_components/utils/showhide/showhide.dart'; import 'package:intl/intl.dart'; import 'package:quiver/time.dart'; import 'package:angular_components/button_decorator/button_decorator.dart'; @@ -61,6 +67,10 @@ import 'package:angular_components/utils/angular/css/css.dart'; NgFor, NgIf, PopupSourceDirective, + MaterialMonthPickerComponent, + ShowHideDirective, + MaterialIconComponent, + NextPrevComponent, ], providers: [ExistingProvider(HasDisabled, MaterialDatepickerComponent)], styleUrls: ['material_datepicker.scss.css'], @@ -91,7 +101,14 @@ class MaterialDatepickerComponent /// date which makes sense in your domain context. e.g. For apps which analyse /// historical data, this could be the current day. @Input() - Date maxDate; + set maxDate(Date d) { + _maxDate = d; + nextPrevModel.update(_visibleMonth, minDate, maxDate); + } + + Date _maxDate; + + Date get maxDate => _maxDate; /// Dates earlier than `minDate` cannot be chosen. /// @@ -99,7 +116,14 @@ class MaterialDatepickerComponent /// makes sense in your domain context. e.g. The earliest date for which data /// is available for analysis. @Input() - Date minDate; + set minDate(Date d) { + _minDate = d; + nextPrevModel.update(_visibleMonth, minDate, maxDate); + } + + Date _minDate; + + Date get minDate => _minDate; /// Whether to enable compact calendar styles. @Input() @@ -286,11 +310,77 @@ class MaterialDatepickerComponent @Input() String error; + /// Whether to display the month selector dropdown. + /// + /// Defaults to true. + @Input() + bool supportsMonthSelector = true; + + @ViewChild(MaterialCalendarPickerComponent) + MaterialCalendarPickerComponent calendarPicker; + + @ViewChild(MaterialMonthPickerComponent) + MaterialMonthPickerComponent monthSelector; + + void onMonthSelectorDropdownClicked() { + showMonthSelector = !showMonthSelector; + if (showMonthSelector) { + _domService.scheduleWrite(() { + monthSelector.scrollToYear(_visibleMonth.year); + }); + } + } + + set monthSelectorState(CalendarState state) { + _monthSelectorState = state; + if (state.has(state.currentSelection)) { + // A month was selected - switch back to the calendar picker and scroll + // the month into view. + showMonthSelector = false; + _monthSelectorState = + CalendarState.empty(resolution: CalendarResolution.months); + final selectedMonth = state.selection(state.currentSelection); + _domService.scheduleWrite(() { + calendarPicker.scrollToDate(selectedMonth.start); + }); + } + } + + CalendarState get monthSelectorState => _monthSelectorState; + CalendarState _monthSelectorState = + CalendarState.empty(resolution: CalendarResolution.months); + + static final _monthFormatter = DateFormat.yMMM(); + Date _visibleMonth; + + String get visibleMonthName => _visibleMonthName; + String _visibleMonthName = ''; + + void onVisibleMonthChange(Date month) { + _visibleMonth = month; + _visibleMonthName = _monthFormatter.format(month.asUtcTime()); + nextPrevModel.update(_visibleMonth, minDate, maxDate); + } + + /// The model for scrolling to the next or previous month. + DatepickerNextPrevModel nextPrevModel; + + bool showMonthSelector = false; + + final DomService _domService; + MaterialDatepickerComponent( - HtmlElement element, - @Attribute('popupClass') String popupClass, - @Optional() @Inject(datepickerClock) Clock clock) - : popupClassName = constructEncapsulatedCss(popupClass, element.classes) { + HtmlElement element, + @Attribute('popupClass') String popupClass, + @Optional() @Inject(datepickerClock) Clock clock, + this._domService, + ) : popupClassName = constructEncapsulatedCss(popupClass, element.classes) { + nextPrevModel = DatepickerNextPrevModel(onNext: () { + calendarPicker.scrollToDate(_visibleMonth.add(months: 1)); + }, onPrev: () { + calendarPicker.scrollToDate(_visibleMonth.add(months: -1)); + }); + clock ??= Clock(); // Init minDate and maxDate to sensible defaults @@ -299,3 +389,35 @@ class MaterialDatepickerComponent maxDate = Date(now.year + 10, DateTime.december, 31); } } + +class DatepickerNextPrevModel implements Sequential { + final NextPrevCallback onNext; + final NextPrevCallback onPrev; + + DatepickerNextPrevModel({this.onNext, this.onPrev}); + + @override + ObservableReference hasNext = ObservableReference(false); + + @override + ObservableReference hasPrev = ObservableReference(false); + + @override + void next() => onNext(); + + @override + void prev() => onPrev(); + + void update(Date visibleMonth, Date minDate, Date maxDate) { + if (visibleMonth == null) return; + + hasPrev.value = minDate != null && + compareDatesAtResolution( + visibleMonth, minDate, CalendarResolution.months) > + 0; + hasNext.value = maxDate != null && + compareDatesAtResolution( + visibleMonth, maxDate, CalendarResolution.months) < + 0; + } +} diff --git a/angular_components/lib/material_datepicker/material_datepicker.html b/angular_components/lib/material_datepicker/material_datepicker.html index bfd5b65c8..486aa99ae 100644 --- a/angular_components/lib/material_datepicker/material_datepicker.html +++ b/angular_components/lib/material_datepicker/material_datepicker.html @@ -43,6 +43,21 @@ +
+
+ {{visibleMonthName}} + +
+ + +
+
@@ -54,15 +69,27 @@
- +
+ + + +
diff --git a/angular_components/lib/material_datepicker/material_datepicker.scss b/angular_components/lib/material_datepicker/material_datepicker.scss index d98597e66..414201cd2 100644 --- a/angular_components/lib/material_datepicker/material_datepicker.scss +++ b/angular_components/lib/material_datepicker/material_datepicker.scss @@ -40,8 +40,13 @@ $main-font-size: 13px; } } -.popup-content.compact .date-input { - padding: 0 $picker-compact-horizontal-padding; +.popup-content.compact { + + .date-input, + .month-selector-toolbar { + padding: 0 $picker-compact-horizontal-padding; + } + } .icon { @@ -79,3 +84,74 @@ material-select-item { padding-bottom: 0; } } + +// note(lejard-h) share month selector style with date_range_editor ? +.picker-container { + @include calendar-height(7); + position: relative; + overflow: hidden; + flex-grow: 1; + + &.compact { + @include calendar-compact-height(7); + } +} + +.calendar-picker { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + transform: translateY(0); + transition: transform $mat-transition $mat-transition-standard; + will-change: transform; + + &.acx-showhide-hide { + transform: translateY(100%); + } + + &.acx-showhide-hidden { + visibility: hidden; + } +} + +.month-selector { + border-top: 1px solid $mat-border-light; + + &.acx-showhide-hide { + transform: translateY(-100%); + } +} + +.month-selector-toolbar { + align-items: center; + color: $mat-transparent-black; + display: flex; + flex-shrink: 0; + margin-bottom: $picker-horizontal-padding; + padding: 0 $picker-horizontal-padding; +} + +.month-selector-dropdown { + display: flex; + align-items: center; + margin-right: auto; + cursor: pointer; +} + +.month-selector-dropdown-icon { + will-change: transform; + transition: transform $mat-transition $mat-transition-standard; + + &.flipped { + transform: scaleY(-1); + } +} + +.visible-month { + // TODO(google): Migrate to extended mixin mat-font-body-2 + font-size: $mat-font-size-body; + font-weight: $mat-font-weight-medium; + text-transform: uppercase; +} \ No newline at end of file diff --git a/angular_components/lib/material_datepicker/next_prev_buttons.dart b/angular_components/lib/material_datepicker/next_prev_buttons.dart index c72404ef8..2b3cca182 100644 --- a/angular_components/lib/material_datepicker/next_prev_buttons.dart +++ b/angular_components/lib/material_datepicker/next_prev_buttons.dart @@ -129,3 +129,5 @@ class NextPrevComponent implements OnDestroy { _modelListeners.dispose(); } } + +typedef void NextPrevCallback();