Skip to content
This repository was archived by the owner on May 20, 2023. It is now read-only.

Commit bea391d

Browse files
Googlernshahan
authored andcommitted
Dynamic Date Picker Comparison Options
- Allow user create their own comparison option - Provide default comparison options [previous period, previous year, custom] - New interface “SupportedComparisonOptions“ for MaterialDateRangePickerComponent - New interface “Disabled” for DateRangeInput ▶ Screencast - with default setting go/screencastlink/NTE2NzAyNTYwMTExODIwOHxkMjA3MjQ5My0wMw ▶ Screencast - with new option and no custom go/screencastlink/NTE0MzkwMTQ5NzE5NjU0NHxkNWY5MmY2Yy02YQ (Demo code is not checked in, this is just showing how it looks like) ▶ Design doc: go/date-picker-comparison-options PiperOrigin-RevId: 202718417
1 parent b2f5296 commit bea391d

File tree

8 files changed

+247
-130
lines changed

8 files changed

+247
-130
lines changed

lib/material_datepicker/comparison.dart

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
import 'package:intl/intl.dart';
5+
import 'package:angular_components/material_datepicker/comparison_option.dart';
66
import 'package:angular_components/material_datepicker/range.dart';
77
import 'package:angular_components/model/date/date.dart';
88

@@ -15,14 +15,17 @@ class DatepickerComparison implements DateRangeComparison {
1515
/// The selected comparison range, if any.
1616
final DatepickerDateRange comparison;
1717

18+
DatepickerComparison(DatepickerDateRange range, ComparisonOption option)
19+
: this.custom(range, option.computeComparisonRange(range));
20+
1821
DatepickerComparison.noComparison(DatepickerDateRange range)
1922
: this.custom(range, null);
2023

2124
DatepickerComparison.previousPeriod(DatepickerDateRange range)
22-
: this.custom(range, _getPreviousRange(range));
25+
: this(range, ComparisonOption.previousPeriod);
2326

2427
DatepickerComparison.samePeriodLastYear(DatepickerDateRange range)
25-
: this.custom(range, _getSamePeriodLastYearRange(range));
28+
: this(range, ComparisonOption.samePeriodLastYear);
2629

2730
/// Construct a copy of `original` clamped to the given `min`/`max` dates.
2831
/// Existing clamping is removed before the new clamping is applied.
@@ -34,12 +37,19 @@ class DatepickerComparison implements DateRangeComparison {
3437

3538
bool get isComparisonEnabled => comparison != null;
3639

40+
@Deprecated('use comparesTo instead')
3741
bool comparesToPreviousPeriod() =>
38-
comparison != null && rangeEqual(comparison, _getPreviousRange(range));
42+
comparesTo(ComparisonOption.previousPeriod);
3943

44+
@Deprecated('use comparesTo instead')
4045
bool comparesToSamePeriodLastYear() =>
46+
comparesTo(ComparisonOption.samePeriodLastYear);
47+
48+
/// Checks the comparison date range has same logic as given comparisonOption.
49+
bool comparesTo(ComparisonOption option) =>
4150
comparison != null &&
42-
comparison.unclamped() == _getSamePeriodLastYearRange(range);
51+
comparison.unclamped() ==
52+
option.computeComparisonRange(range.unclamped());
4353

4454
bool operator ==(o) =>
4555
o is DatepickerComparison &&
@@ -49,32 +59,4 @@ class DatepickerComparison implements DateRangeComparison {
4959
? rangeHash(range) ^ rangeHash(comparison)
5060
: rangeHash(range);
5161
String toString() => 'DatepickerComparison -- $range / $comparison';
52-
53-
/// If the previous date range has an interesting title like '3 weeks ago',
54-
/// keep it; if it's the generic 'Custom' title, replace it with 'Previous
55-
/// period'.
56-
static DatepickerDateRange _getPreviousRange(DatepickerDateRange range) {
57-
var prev = range.prev;
58-
if (prev != null && !prev.isPredefined) {
59-
return new DatepickerDateRange(_prevPeriodMsg(), prev.start, prev.end);
60-
}
61-
return prev;
62-
}
63-
64-
static DatepickerDateRange _getSamePeriodLastYearRange(
65-
DatepickerDateRange range) =>
66-
new DatepickerDateRange(
67-
_lastYearMsg(), range.start.add(years: -1), range.end.add(years: -1));
68-
69-
static String _prevPeriodMsg() => Intl.message('Previous period',
70-
name: '_prevPeriodMsg',
71-
meaning: 'Name of a date range',
72-
desc: 'Generic name for the period before a time range. E.g. if someone '
73-
'has selected the last 30 days, this represents the 30 days previous.');
74-
static String _lastYearMsg() => Intl.message('Same period last year',
75-
name: '_lastYearMsg',
76-
meaning: 'Name of a date range',
77-
desc: 'Generic name for a time period matching a selected date range, '
78-
'but one year prior. E.g. if someone has selected Feb 2015, this '
79-
'represents Feb 2014.');
8062
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:intl/intl.dart';
6+
import 'package:angular_components/material_datepicker/range.dart';
7+
8+
/// Converts current date range to comparison date range.
9+
///
10+
/// Return null if this is a "custom".
11+
typedef DatepickerDateRange ComparisonFn(DatepickerDateRange range);
12+
13+
/// The [ComparisonOption]s the component provides to the user by default.
14+
List<ComparisonOption> defaultComparisonOptions = [
15+
ComparisonOption.previousPeriod,
16+
ComparisonOption.samePeriodLastYear,
17+
ComparisonOption.custom
18+
];
19+
20+
/// Comparison option for comparing [DatepickerDateRange].
21+
class ComparisonOption {
22+
/// The label to display for the preset (e.g. "Previous period").
23+
final String displayName;
24+
25+
/// Get date shifting base on the given date range.
26+
final ComparisonFn computeComparisonRange;
27+
28+
const ComparisonOption(this.displayName, this.computeComparisonRange);
29+
30+
static final ComparisonOption previousPeriod =
31+
new ComparisonOption(_prevPeriodMsg, (DatepickerDateRange range) {
32+
var prev = range.prev;
33+
if (prev != null && !prev.isPredefined) {
34+
return new DatepickerDateRange(_prevPeriodMsg, prev.start, prev.end);
35+
}
36+
return prev;
37+
});
38+
39+
static final ComparisonOption samePeriodLastYear = new ComparisonOption(
40+
_previousYearMsg,
41+
(DatepickerDateRange range) => new DatepickerDateRange(_previousYearMsg,
42+
range.start.add(years: -1), range.end.add(years: -1)));
43+
44+
static final ComparisonOption custom =
45+
new ComparisonOption(_customMsg, (DatepickerDateRange range) => null);
46+
47+
static final String _prevPeriodMsg = Intl.message('Previous period',
48+
name: '_prevPeriodMsg',
49+
meaning: 'An option name, the date range before the selected date '
50+
'range',
51+
desc: 'Setting to compare the selected date range with the previous '
52+
'period. E.g. if the selected range were May, this would be April'
53+
'.');
54+
static final String _previousYearMsg = Intl.message('Previous year',
55+
name: '_previousYearMsg',
56+
meaning: 'An option name, the same date range in the last year',
57+
desc: 'Setting to compare the selected date range with the same range '
58+
'last year. E.g. if the selected range were May 2015, this would'
59+
' be May 2014.');
60+
61+
static final String _customMsg = Intl.message('Custom',
62+
name: '_customMsg',
63+
meaning: 'An option name, user could enter the date range they want',
64+
desc: 'Setting to compare the selected date range with another '
65+
'arbitrary user-selected date range.');
66+
67+
bool operator ==(o) =>
68+
o is ComparisonOption &&
69+
this.displayName == o.displayName &&
70+
this.computeComparisonRange == o.computeComparisonRange;
71+
72+
int get hashCode => displayName.hashCode ^ computeComparisonRange.hashCode;
73+
74+
String toString() => displayName;
75+
}

lib/material_datepicker/date_range_editor.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ class DateRangeEditorComponent implements OnInit, AfterViewInit, Focusable {
109109

110110
bool _supportsComparison = true;
111111

112+
/// Checks if custom comparison is a valid option.
113+
bool get isCustomComparisonValid => model.isCustomComparisonValid;
114+
112115
static final comparisonHeaderMsg = Intl.message('Compare',
113116
name: 'comparisonHeaderMsg',
114117
desc: 'Label for a toggle that turns time comparison on/off.');

lib/material_datepicker/date_range_editor.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@
125125
[isClearRangeSelected]="isClearRangeSelected"
126126
[rangeId]="model.comparisonId"
127127
[(state)]="model.calendar.value"
128-
[(range)]="model.comparison.value">
128+
[(range)]="model.comparison.value"
129+
[disabled]="!isCustomComparisonValid">
129130
</date-range-input>
130131
</div>
131132

lib/material_datepicker/date_range_input.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import 'package:angular_components/model/observable/observable.dart';
3939
[class.active]="isStartActive"
4040
[(date)]="start"
4141
(focus)="onStartFocused()"
42+
[disabled]="disabled"
4243
class="start date-input">
4344
</material-input>
4445
<span class="separator">—</span>
@@ -52,6 +53,7 @@ import 'package:angular_components/model/observable/observable.dart';
5253
[(date)]="end"
5354
[rangeEnd]="true"
5455
(focus)="onEndFocused()"
56+
[disabled]="disabled"
5557
class="end date-input">
5658
</material-input>
5759
''',
@@ -64,6 +66,17 @@ class DateRangeInputComponent implements OnInit, OnDestroy {
6466

6567
DateRangeInputComponent(this._changeDetector);
6668

69+
/// Whether the input field is disabled.
70+
///
71+
/// If true, the component disable both start and end input field.
72+
@Input()
73+
set disabled(bool isDisabled) {
74+
_disabled = isDisabled;
75+
}
76+
77+
bool get disabled => _disabled;
78+
bool _disabled = false;
79+
6780
@override
6881
void ngOnInit() {
6982
_calendarStream = _model.stream.listen(_onCalendarChange);
@@ -79,12 +92,18 @@ class DateRangeInputComponent implements OnInit, OnDestroy {
7992
}
8093

8194
void onStartFocused() {
95+
if (_disabled) {
96+
return;
97+
}
8298
if (state.currentSelection == rangeId && !state.previewAnchoredAtStart)
8399
return;
84100
_model.value = state.select(rangeId, previewAnchoredAtStart: false);
85101
}
86102

87103
void onEndFocused() {
104+
if (_disabled) {
105+
return;
106+
}
88107
if (state.currentSelection == rangeId && state.previewAnchoredAtStart)
89108
return;
90109
_model.value = state.select(rangeId, previewAnchoredAtStart: true);

lib/material_datepicker/material_date_range_picker.dart

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import 'package:angular_components/laminate/enums/alignment.dart';
1818
import 'package:angular_components/laminate/popup/popup.dart';
1919
import 'package:angular_components/material_button/material_button.dart';
2020
import 'package:angular_components/material_datepicker/comparison.dart';
21+
import 'package:angular_components/material_datepicker/comparison_option.dart';
2122
import 'package:angular_components/material_datepicker/date_range_editor.dart';
2223
import 'package:angular_components/material_datepicker/module.dart';
2324
import 'package:angular_components/material_datepicker/next_prev_buttons.dart';
@@ -84,7 +85,12 @@ import 'package:angular_components/utils/disposer/disposer.dart';
8485
],
8586
)
8687
class MaterialDateRangePickerComponent extends KeyboardHandlerMixin
87-
implements HasDisabled, OnInit, OnDestroy, DateRangeEditorHost {
88+
implements
89+
HasDisabled,
90+
OnInit,
91+
AfterChanges,
92+
OnDestroy,
93+
DateRangeEditorHost {
8894
DateRangeEditorComponent _dateRangeEditor;
8995
bool _focusOnDateRangeEditorInit = false;
9096

@@ -286,6 +292,22 @@ class MaterialDateRangePickerComponent extends KeyboardHandlerMixin
286292
String get placeHolderMsg => _customPlaceHolderMsg ?? _placeHolderMsg;
287293
String _customPlaceHolderMsg;
288294

295+
/// [ComparisonOption]s the user can choose from.
296+
///
297+
/// By default, this is "Previous period", "Previous year", and "Custom".
298+
/// This can only be set once. Null or empty values are ignored.
299+
@Input()
300+
set comparisonOptions(List<ComparisonOption> options) {
301+
if (options != null && options.isNotEmpty) {
302+
// User cannot change this value after setting it.
303+
assert(_comparisonOptions == null || _comparisonOptions == options);
304+
_comparisonOptions = options;
305+
model.supportedComparisonOptions = _comparisonOptions;
306+
}
307+
}
308+
309+
List<ComparisonOption> _comparisonOptions;
310+
289311
@ViewChild('focusOnClose')
290312
KeyboardOnlyFocusIndicatorDirective focusOnClose;
291313

@@ -381,6 +403,19 @@ class MaterialDateRangePickerComponent extends KeyboardHandlerMixin
381403
.listen((v) => selection.value = v.date));
382404
}
383405

406+
@override
407+
void ngAfterChanges() {
408+
// Checks the edge case if user enter a wrong comparison range that
409+
// is not supported.
410+
if (supportsComparison &&
411+
_comparisonOptions != null &&
412+
selection.value != null &&
413+
!_isComparisonOptionsSupported(selection.value)) {
414+
throw UnsupportedError('Your comparisonOptions don\'t support your'
415+
' input datePickerComparison: ${selection.value}');
416+
}
417+
}
418+
384419
@override
385420
void ngOnDestroy() => _disposer.dispose();
386421

@@ -550,6 +585,13 @@ class MaterialDateRangePickerComponent extends KeyboardHandlerMixin
550585
}
551586
}
552587

588+
/// Whether the given [DatepickerComparison] is supported by this picker's
589+
/// current configuration.
590+
bool _isComparisonOptionsSupported(DatepickerComparison cmp) =>
591+
!cmp.isComparisonEnabled ||
592+
_comparisonOptions.contains(ComparisonOption.custom) ||
593+
_comparisonOptions.any((option) => cmp.comparesTo(option));
594+
553595
static final cancelButtonMsg = Intl.message('Cancel',
554596
meaning: 'Button in a date picker',
555597
desc: 'Label for a "cancel" button -- abandon the current date selection '

lib/src/material_datepicker/comparison_range_editor.dart

Lines changed: 19 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:core';
6+
57
import 'package:angular/angular.dart';
68
import 'package:intl/intl.dart';
9+
import 'package:angular_components/material_datepicker/comparison_option.dart';
710
import 'package:angular_components/material_datepicker/date_range_input.dart';
11+
import 'package:angular_components/material_datepicker/range.dart';
812
import 'package:angular_components/src/material_datepicker/date_range_editor_model.dart';
913
import 'package:angular_components/material_list/material_list.dart';
1014
import 'package:angular_components/material_list/material_list_item.dart';
@@ -41,53 +45,27 @@ class ComparisonRangeEditorComponent {
4145
/// non-test implementation is [DateRangeEditorModel].
4246
@Input()
4347
HasComparisonRange model;
48+
Map<ComparisonOption, String> _optionMsgCache = {};
49+
DatepickerDateRange _primaryDateRange;
4450

45-
final List<ComparisonOption> options = [
46-
ComparisonOption.previousPeriod,
47-
ComparisonOption.samePeriodLastYear,
48-
ComparisonOption.custom
49-
];
50-
51-
static final comparisonHeaderMsg = Intl.message('Compare',
51+
String get comparisonHeaderMsg => Intl.message('Compare',
5252
name: 'comparisonHeaderMsg',
5353
desc: 'Label for a toggle that turns time comparison on/off.');
5454

55-
String get previousPeriodMsg {
56-
// If we have a nice title ("3 weeks ago"), use that. Otherwise fall back
57-
// to "Previous period".
58-
var prev = model.prevRange;
59-
return prev?.isPredefined == true ? prev.title : _previousPeriodMsg;
55+
/// Gets display message from given option.
56+
String comparisonOptionMsg(ComparisonOption option) {
57+
if (_primaryDateRange != model.primaryRange) {
58+
_updateOptionMsg();
59+
_primaryDateRange = model.primaryRange;
60+
}
61+
return _optionMsgCache[option];
6062
}
6163

62-
static final _previousPeriodMsg = Intl.message('Previous period',
63-
name: '_previousPeriodMsg',
64-
meaning: 'Name for a time comparison option',
65-
desc: 'Setting to compare the selected date range with the previous '
66-
'period. E.g. if the selected range were May, this would be April.');
67-
68-
static final samePeriodLastYearMsg = Intl.message('Previous year',
69-
name: 'samePeriodLastYearMsg',
70-
meaning: 'Name for a time comparison option',
71-
desc: 'Setting to compare the selected date range with the same range '
72-
'last year. E.g. if the selected range were May 2015, this would be '
73-
'May 2014.');
74-
75-
static final customMsg = Intl.message('Custom',
76-
name: 'customMsg',
77-
meaning: 'Name for a time comparison option',
78-
desc: 'Setting to compare the selected date range with another arbitrary '
79-
'user-selected date range.');
80-
81-
String comparisonOptionMsg(ComparisonOption option) {
82-
switch (option) {
83-
case ComparisonOption.previousPeriod:
84-
return previousPeriodMsg;
85-
case ComparisonOption.samePeriodLastYear:
86-
return samePeriodLastYearMsg;
87-
case ComparisonOption.custom:
88-
return customMsg;
89-
default:
90-
return '<unknown comparison option>';
64+
void _updateOptionMsg() {
65+
for (var option in model.validComparisonOptions) {
66+
_optionMsgCache[option] =
67+
option.computeComparisonRange(model.primaryRange)?.title ??
68+
option.displayName;
9169
}
9270
}
9371
}

0 commit comments

Comments
 (0)