Skip to content

Commit 5d71ff0

Browse files
authored
feat: datepicker improvements (#1224)
In this change, we update the Datepicker * to accept and set a `defaultValue` when `enableRangeSelection` is set. For example, `defaultValue='01/01/2019 - 01/01/2020'` * add story for above use case * to allow spreading props into the `<InputGroup.Addon>`, footer, and footer `<Button>`
1 parent dc456f5 commit 5d71ff0

File tree

5 files changed

+241
-83
lines changed

5 files changed

+241
-83
lines changed

src/DatePicker/DatePicker.js

Lines changed: 60 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,22 @@ const dateRangeSeparator = ' - ';
2525
class DatePicker extends Component {
2626
constructor(props) {
2727
super(props);
28-
const formattedDate = props.defaultValue.length > 0 ? this.getFormattedDateStr(props.defaultValue) : '';
29-
const isoFormattedDate = props.defaultValue.length > 0
30-
? moment(props.defaultValue, props.dateFormat).format(ISO_DATE_FORMAT)
31-
: '';
28+
3229
this.state = {
33-
isExpanded: false,
34-
selectedDate: formattedDate.length === 0 ? null : this.getMomentDateObj(formattedDate),
35-
startAndEndDates: [],
36-
formattedDate,
37-
isoFormattedDate
30+
formattedDate: this.props?.defaultValue || '',
31+
isExpanded: false
3832
};
3933

4034
this.calendarRef = React.createRef();
4135
this.popoverRef = React.createRef();
4236
}
4337

38+
39+
toISOFormat = (dateStr) => {
40+
if (!dateStr || !dateStr?.trim().length) return '';
41+
return moment(dateStr, this.resolveFormat()).format(ISO_DATE_FORMAT);
42+
}
43+
4444
/**
4545
* Function tries to format any date string into the format specified by dateFormat.
4646
* It will use format derived from locale if dateFormat is not specified.
@@ -122,22 +122,20 @@ class DatePicker extends Component {
122122
this.setState({ formattedDate: e.target.value });
123123
}
124124

125+
componentDidMount() {
126+
this.validateDates();
127+
}
128+
125129
componentDidUpdate(prevProps) {
126130
if (prevProps.defaultValue !== this.props.defaultValue) {
127131
this.handleNewDefault();
128132
}
129133
}
130134

131135
handleNewDefault = () => {
132-
const { dateFormat, defaultValue } = this.props;
133-
const formattedNewDefault = defaultValue && defaultValue.length > 0 ? this.getFormattedDateStr(defaultValue) : '';
136+
const { defaultValue } = this.props;
134137
this.setState({
135-
selectedDate: formattedNewDefault.length === 0 ? null : this.getMomentDateObj(formattedNewDefault),
136-
isoFormattedDate: defaultValue && defaultValue.length > 0
137-
? moment(defaultValue, dateFormat).format(ISO_DATE_FORMAT)
138-
: '',
139-
formattedDate: formattedNewDefault,
140-
startAndEndDates: []
138+
formattedDate: defaultValue
141139
}, () => {
142140
this.validateDates();
143141
});
@@ -147,9 +145,7 @@ class DatePicker extends Component {
147145
e.stopPropagation();
148146
this.setState({
149147
formattedDate: e.target.value,
150-
isoFormattedDate: e.target.value
151-
? moment(e.target.value, this.props.dateFormat).format(ISO_DATE_FORMAT)
152-
: ''
148+
isoFormattedDate: this.toISOFormat(e.target?.value)
153149
}, () => {
154150
this.props.onChange(this.getCallbackData(), 'inputChange');
155151
});
@@ -183,7 +179,9 @@ class DatePicker extends Component {
183179
validateDates = (postValidationCallback) => {
184180
const { formattedDate } = this.state;
185181

186-
if (this.props.enableRangeSelection) {
182+
if (!formattedDate || !formattedDate?.trim().length) {
183+
this.resetState(postValidationCallback);
184+
} else if (this.props.enableRangeSelection) {
187185
const dateRange = formattedDate.split(dateRangeSeparator);
188186
const firstDate = this.getMomentDateObj(dateRange[0]);
189187
const secondDate = this.getMomentDateObj(dateRange[1]);
@@ -195,9 +193,10 @@ class DatePicker extends Component {
195193
: (arrSelected = [firstDate, secondDate]);
196194
const newFormattedDateRangeStr = this.getFormattedDateRangeStr(arrSelected);
197195
this.setState({
196+
formattedDate: newFormattedDateRangeStr,
197+
isoFormatDate: arrSelected[0].format(ISO_DATE_FORMAT) + dateRangeSeparator + arrSelected[1].format(ISO_DATE_FORMAT),
198198
selectedDate: null,
199-
startAndEndDates: arrSelected,
200-
formattedDate: newFormattedDateRangeStr
199+
startAndEndDates: arrSelected
201200
}, () => {
202201
if (formattedDate !== newFormattedDateRangeStr) {
203202
this.executeCallback(this.props.onChange, 'autoFormatDateRange');
@@ -214,9 +213,7 @@ class DatePicker extends Component {
214213
this.setState({
215214
selectedDate: newDate,
216215
formattedDate: newFormattedDateStr,
217-
isoFormattedDate: formattedDate
218-
? moment(formattedDate, this.props.dateFormat).format(ISO_DATE_FORMAT)
219-
: '',
216+
isoFormattedDate: newDate.format(ISO_DATE_FORMAT),
220217
startAndEndDates: []
221218
}, () => {
222219
if (formattedDate !== newFormattedDateStr) {
@@ -267,16 +264,17 @@ class DatePicker extends Component {
267264
const { formattedDate } = this.state;
268265

269266
if (this.props.enableRangeSelection) {
270-
let isoFormatDate = date[0].format(ISO_DATE_FORMAT);
267+
let isoFormatDateRange = date[0].format(ISO_DATE_FORMAT);
271268
if (!!date[1]) {
272-
isoFormatDate += dateRangeSeparator + date[1].format(ISO_DATE_FORMAT);
269+
isoFormatDateRange += dateRangeSeparator + date[1].format(ISO_DATE_FORMAT);
273270
closeCalendar = true;
274271
}
275272
const newFormattedDateRangeStr = this.getFormattedDateRangeStr(date);
276273
this.setState({
277-
startAndEndDates: date,
278274
formattedDate: newFormattedDateRangeStr,
279-
isoFormattedDate: isoFormatDate
275+
isoFormattedDate: isoFormatDateRange,
276+
selectedDate: null,
277+
startAndEndDates: date
280278
}, () => {
281279
if (formattedDate !== newFormattedDateRangeStr) {
282280
this.props.onChange(this.getCallbackData(), reason);
@@ -286,9 +284,9 @@ class DatePicker extends Component {
286284
closeCalendar = true;
287285
const newFormattedDate = this.getFormattedDateStr(date);
288286
this.setState({
289-
selectedDate: date,
290287
formattedDate: newFormattedDate,
291-
isoFormattedDate: date.format(ISO_DATE_FORMAT),
288+
isoFormattedDate: this.toISOFormat(newFormattedDate),
289+
selectedDate: date,
292290
startAndEndDates: []
293291
}, () => {
294292
if (formattedDate !== newFormattedDate) {
@@ -341,6 +339,7 @@ class DatePicker extends Component {
341339

342340
render() {
343341
const {
342+
addonProps,
344343
blockedDates,
345344
buttonLabel,
346345
buttonProps,
@@ -356,6 +355,8 @@ class DatePicker extends Component {
356355
disableWeekday,
357356
disableWeekends,
358357
enableRangeSelection,
358+
footerButtonProps,
359+
footerClasses,
359360
inputProps,
360361
inputGroupProps,
361362
locale,
@@ -380,7 +381,8 @@ class DatePicker extends Component {
380381
'fd-input-group--control',
381382
{
382383
[`is-${validationState?.state}`]: validationState?.state
383-
}
384+
},
385+
inputGroupProps?.className
384386
);
385387

386388
const datepickerFooterClassName = classnames(
@@ -390,7 +392,13 @@ class DatePicker extends Component {
390392
{
391393
'fd-bar--cozy': !compact,
392394
'fd-bar--compact': compact
393-
}
395+
},
396+
footerClasses
397+
);
398+
399+
const footerButtonClassnames = classnames(
400+
'fd-dialog__decisive-button',
401+
footerButtonProps?.className
394402
);
395403

396404
const disableButton = disabled || readOnly;
@@ -414,7 +422,9 @@ class DatePicker extends Component {
414422
placeholder={this.getPlaceHolder(dateFormat)}
415423
readOnly={readOnly}
416424
value={this.state.formattedDate} />
417-
<InputGroup.Addon isButton>
425+
<InputGroup.Addon
426+
{...addonProps}
427+
isButton>
418428
<Button {...buttonProps}
419429
aria-label={buttonLabel}
420430
disabled={disableButton}
@@ -471,8 +481,8 @@ class DatePicker extends Component {
471481
...localizedText,
472482
todayLabel: todayAction.label
473483
}}
474-
onChange={ (e, todayNavigated) => {
475-
this.updateDate(e, todayNavigated, todayNavigated ? 'todayNavigated' : 'calendarDateClicked');
484+
onChange={ (date, todayNavigated) => {
485+
this.updateDate(date, todayNavigated, todayNavigated ? 'todayNavigated' : 'calendarDateClicked');
476486
}}
477487
openToDate={openToDate}
478488
ref={this.calendarRef}
@@ -484,7 +494,8 @@ class DatePicker extends Component {
484494
<div className='fd-bar__right'>
485495
<div className='fd-bar__element'>
486496
<Button
487-
className='fd-dialog__decisive-button'
497+
{...footerButtonProps}
498+
className={footerButtonClassnames}
488499
compact={compact}
489500
onClick={this.setTodayDate}>
490501
{todayAction.label}
@@ -513,6 +524,8 @@ DatePicker.displayName = 'DatePicker';
513524

514525
DatePicker.propTypes = {
515526
...Calendar.PropTypes,
527+
/** Additional props to be spread to the input group addon button container i.e. `<InputGroup.Addon {...addonProps} >` */
528+
addonProps: PropTypes.object,
516529
/** aria-label for datepicker button */
517530
buttonLabel: PropTypes.string,
518531
/** Additional props to be spread to the `<button>` element */
@@ -529,12 +542,20 @@ DatePicker.propTypes = {
529542
/** Format to use for displaying the inputted or selected date. E.g. "YYYY.M.D", "DD-MM-YYYY", "MM/DD/YYYY" etc.
530543
* This overrides the date format derived from any set locale. */
531544
dateFormat: PropTypes.string,
532-
/** Default value to be shown in the Datepicker */
545+
/**
546+
* Default value to be shown in the Datepicker
547+
* for example, `defaultValue='12/04/1993'`
548+
* or when range selection is enabled `defaultValue='12/04/1993 - 12/30/1992'` (will auto sort chronologically)
549+
* */
533550
defaultValue: PropTypes.string,
534551
/** Set to **true** to mark component as disabled and make it non-interactive */
535552
disabled: PropTypes.bool,
536553
/** Set to **true** to enable the selection of a date range (begin and end) */
537554
enableRangeSelection: PropTypes.bool,
555+
/** Additional props to to apply to calendar footer button*/
556+
footerButtonProps: PropTypes.string,
557+
/** Classnames to apply to calendar footer that will contain the 'Today' action */
558+
footerClasses: PropTypes.string,
538559
/** Additional props to be spread to the `InputGroup` component */
539560
inputGroupProps: PropTypes.object,
540561
/** Additional props to be spread to the `<input>` element */

src/DatePicker/DatePicker.test.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,10 +249,32 @@ describe('<DatePicker />', () => {
249249
defaultValue='3.16.20'
250250
locale='hi' />
251251
);
252-
wrapper = mount(compToTest);
252+
253+
act(() => {
254+
wrapper = mount(compToTest);
255+
});
256+
253257
expect(wrapper.state('formattedDate')).toEqual('०३/१६/२०२०');
254258
});
255259

260+
describe('Date range', () => {
261+
test('default value with dateFormat and locale set', ()=>{
262+
const compToTest = (
263+
<DatePicker
264+
dateFormat='MM/DD/YYYY'
265+
defaultValue='3.20.20 - 3.16.20'
266+
enableRangeSelection
267+
locale='hi' />
268+
);
269+
act(()=>{
270+
wrapper = mount(compToTest);
271+
});
272+
273+
expect(wrapper.state('formattedDate')).toEqual('०३/१६/२०२० - ०३/२०/२०२०');
274+
});
275+
});
276+
277+
256278
test('set defaultDate, unset dateFormat, set locale', ()=>{
257279
const compToTest = (
258280
<DatePicker

src/DatePicker/__stories__/DatePicker.stories.js

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,37 @@ export const localized = () => (
110110
localized.storyName = 'Localized DatePicker';
111111

112112
export const rangeSelection = () => (
113-
<DatePicker enableRangeSelection />
113+
<Container>
114+
<Row>
115+
<Column>
116+
<div>
117+
<FormLabel
118+
htmlFor='rangeSelection'>
119+
Select a date range
120+
</FormLabel>
121+
<DatePicker
122+
enableRangeSelection
123+
inputProps={{
124+
id: 'rangeSelection'
125+
}} />
126+
</div>
127+
</Column>
128+
<Column>
129+
<div>
130+
<FormLabel
131+
htmlFor='rangeSelectionDefaultValue'>
132+
Select a date range (default set)
133+
</FormLabel>
134+
<DatePicker
135+
defaultValue='12/04/1993 - 12/30/1992'
136+
enableRangeSelection
137+
inputProps={{
138+
id: 'rangeSelectionDefaultValue'
139+
}} />
140+
</div>
141+
</Column>
142+
</Row>
143+
</Container>
114144
);
115145

116146
rangeSelection.storyName = 'Enabled Range Selection';
@@ -411,7 +441,7 @@ export const dev = () => (
411441
dateFormat={
412442
select(dateFormatOptionsLabel, dateFormatOptions, 'DD/MM/YYYY')
413443
}
414-
defaultValue={text('Default Value', '20/06/2020')}
444+
defaultValue={text('Default Value', '20/06/2020 - 18/06/2020')}
415445
disableAfterDate={dateKnobToDate('disable after date', afterDateDefault)}
416446
disableBeforeDate={dateKnobToDate('disable before date', beforeDateDefault)}
417447
disableFutureDates={boolean('disable future dates', false)}
@@ -420,8 +450,8 @@ export const dev = () => (
420450
disableWeekends={boolean('disable weekends', false)}
421451
disabledDates={[dateKnobToDate('disable between dates (1)', disabledDateFirstDefault),
422452
dateKnobToDate('disable between dates (2)', disabledDateSecondDefault)]}
423-
enableRangeSelection={boolean('enableRangeSelection', false)}
424-
locale={text('locale', 'en')}
453+
enableRangeSelection={boolean('enableRangeSelection', true)}
454+
locale={text('locale', 'hi')}
425455
onChange={action('on-change')}
426456
onDatePickerClose={action('on-date-picker-close')}
427457
onInputBlur={action('on-input-blur')}

0 commit comments

Comments
 (0)