Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor DatePicker component to react hooks and function component #36835

Merged
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
### Enhancements

- Wrapped `Modal` in a `forwardRef` call ([#36831](https://github.com/WordPress/gutenberg/pull/36831)).
- Refactor `DateTime` class component to functional component ([#36835](https://github.com/WordPress/gutenberg/pull/36835))
- Unify styles for `ColorIndicator` with how they appear in Global Styles ([#37028](https://github.com/WordPress/gutenberg/pull/37028))
- Add support for rendering the `ColorPalette` in a `Dropdown` when opened in the sidebar ([#37067](https://github.com/WordPress/gutenberg/pull/37067))
- Show an incremental sequence of numbers (1/2/3/4/5) as a label of the font size, when we have at most five font sizes, where at least one the them contains a complex css value(clamp, var, etc..). We do this because complex css values cannot be calculated properly and the incremental sequence of numbers as labels can help the user better mentally map the different available font sizes. ([#37038](https://github.com/WordPress/gutenberg/pull/37038))
Expand Down
7 changes: 7 additions & 0 deletions packages/components/src/date-time/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,10 @@ A callback function which receives a Date object representing a day as an argume

- Type: `Function`
- Required: No

### onMonthPreviewed

A callback invoked when selecting the previous/next month in the date picker. The callback receives the new month date in the ISO format as an argument.

- Type: `Function`
- Required: No
151 changes: 67 additions & 84 deletions packages/components/src/date-time/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ import DayPickerSingleDateController from 'react-dates/lib/components/DayPickerS
/**
* WordPress dependencies
*/
import { Component, createRef, useEffect, useRef } from '@wordpress/element';
import { useEffect, useRef } from '@wordpress/element';
import { isRTL, _n, sprintf } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { getMomentDate } from './utils';

/**
* Module Constants
*/
Expand Down Expand Up @@ -70,43 +75,40 @@ function DatePickerDay( { day, events = [] } ) {
);
}

class DatePicker extends Component {
constructor() {
super( ...arguments );

this.onChangeMoment = this.onChangeMoment.bind( this );
this.nodeRef = createRef();
this.onMonthPreviewedHandler = this.onMonthPreviewedHandler.bind(
this
);
}

onMonthPreviewedHandler( newMonthDate ) {
this.props.onMonthPreviewed?.( newMonthDate.toISOString() );
this.keepFocusInside();
}
function DatePicker( {
currentDate,
onChange,
events,
isInvalidDate,
onMonthPreviewed,
ciampo marked this conversation as resolved.
Show resolved Hide resolved
} ) {
const nodeRef = useRef();
const onMonthPreviewedHandler = ( newMonthDate ) => {
onMonthPreviewed?.( newMonthDate.toISOString() );
keepFocusInside();
};

/*
* Todo: We should remove this function ASAP.
* It is kept because focus is lost when we click on the previous and next month buttons.
* This focus loss closes the date picker popover.
* Ideally we should add an upstream commit on react-dates to fix this issue.
*/
keepFocusInside() {
if ( ! this.nodeRef.current ) {
const keepFocusInside = () => {
amustaque97 marked this conversation as resolved.
Show resolved Hide resolved
if ( ! nodeRef.current ) {
return;
}

const { ownerDocument } = this.nodeRef.current;
const { ownerDocument } = nodeRef.current;
const { activeElement } = ownerDocument;

// If focus was lost.
if (
! activeElement ||
! this.nodeRef.current.contains( ownerDocument.activeElement )
! nodeRef.current.contains( ownerDocument.activeElement )
) {
// Retrieve the focus region div.
const focusRegion = this.nodeRef.current.querySelector(
const focusRegion = nodeRef.current.querySelector(
'.DayPicker_focusRegion'
);
if ( ! focusRegion ) {
Expand All @@ -115,11 +117,9 @@ class DatePicker extends Component {
// Keep the focus on focus region.
focusRegion.focus();
}
}

onChangeMoment( newDate ) {
const { currentDate, onChange } = this.props;
};

const onChangeMoment = ( newDate ) => {
// If currentDate is null, use now as momentTime to designate hours, minutes, seconds.
const momentDate = currentDate ? moment( currentDate ) : moment();
const momentTime = {
Expand All @@ -131,71 +131,54 @@ class DatePicker extends Component {
onChange( newDate.set( momentTime ).format( TIMEZONELESS_FORMAT ) );

// Keep focus on the date picker.
this.keepFocusInside();
}

/**
* Create a Moment object from a date string. With no currentDate supplied, default to a Moment
* object representing now. If a null value is passed, return a null value.
*
* @param {?string} currentDate Date representing the currently selected date or null to signify no selection.
* @return {?moment.Moment} Moment object for selected date or null.
*/
getMomentDate( currentDate ) {
if ( null === currentDate ) {
return null;
}
return currentDate ? moment( currentDate ) : moment();
}
keepFocusInside();
};

getEventsPerDay( day ) {
if ( ! this.props.events?.length ) {
const getEventsPerDay = ( day ) => {
if ( ! events?.length ) {
return [];
}

return this.props.events.filter( ( eventDay ) =>
return events.filter( ( eventDay ) =>
day.isSame( eventDay.date, 'day' )
);
}

render() {
const { currentDate, isInvalidDate } = this.props;
const momentDate = this.getMomentDate( currentDate );

return (
<div className="components-datetime__date" ref={ this.nodeRef }>
<DayPickerSingleDateController
date={ momentDate }
daySize={ 30 }
focused
hideKeyboardShortcutsPanel
// This is a hack to force the calendar to update on month or year change
// https://github.com/airbnb/react-dates/issues/240#issuecomment-361776665
key={ `datepicker-controller-${
momentDate ? momentDate.format( 'MM-YYYY' ) : 'null'
}` }
noBorder
numberOfMonths={ 1 }
onDateChange={ this.onChangeMoment }
transitionDuration={ 0 }
weekDayFormat="ddd"
dayAriaLabelFormat={ ARIAL_LABEL_TIME_FORMAT }
isRTL={ isRTL() }
isOutsideRange={ ( date ) => {
return isInvalidDate && isInvalidDate( date.toDate() );
} }
onPrevMonthClick={ this.onMonthPreviewedHandler }
onNextMonthClick={ this.onMonthPreviewedHandler }
renderDayContents={ ( day ) => (
<DatePickerDay
day={ day }
events={ this.getEventsPerDay( day ) }
/>
) }
/>
</div>
);
}
};

const momentDate = getMomentDate( currentDate );

return (
<div className="components-datetime__date" ref={ nodeRef }>
<DayPickerSingleDateController
date={ momentDate }
daySize={ 30 }
focused
hideKeyboardShortcutsPanel
// This is a hack to force the calendar to update on month or year change
// https://github.com/airbnb/react-dates/issues/240#issuecomment-361776665
key={ `datepicker-controller-${
momentDate ? momentDate.format( 'MM-YYYY' ) : 'null'
}` }
noBorder
numberOfMonths={ 1 }
onDateChange={ onChangeMoment }
transitionDuration={ 0 }
weekDayFormat="ddd"
dayAriaLabelFormat={ ARIAL_LABEL_TIME_FORMAT }
isRTL={ isRTL() }
isOutsideRange={ ( date ) => {
return isInvalidDate && isInvalidDate( date.toDate() );
} }
onPrevMonthClick={ onMonthPreviewedHandler }
onNextMonthClick={ onMonthPreviewedHandler }
renderDayContents={ ( day ) => (
<DatePickerDay
day={ day }
events={ getEventsPerDay( day ) }
/>
) }
/>
</div>
);
}

export default DatePicker;
32 changes: 3 additions & 29 deletions packages/components/src/date-time/test/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,32 +31,6 @@ describe( 'DatePicker', () => {
expect( moment.isMoment( date ) ).toBe( true );
} );

describe( 'getMomentDate', () => {
it( 'should return a Moment object representing a given date string', () => {
const currentDate = '1986-10-18T23:00:00';
const wrapper = shallow( <DatePicker /> );
const momentDate = wrapper.instance().getMomentDate( currentDate );

expect( moment.isMoment( momentDate ) ).toBe( true );
expect( momentDate.isSame( moment( currentDate ) ) ).toBe( true );
} );

it( 'should return null when given a null agrument', () => {
const currentDate = null;
const wrapper = shallow( <DatePicker /> );
const momentDate = wrapper.instance().getMomentDate( currentDate );

expect( momentDate ).toBeNull();
} );

it( 'should return a Moment object representing now when given an undefined argument', () => {
const wrapper = shallow( <DatePicker /> );
const momentDate = wrapper.instance().getMomentDate();

expect( moment.isMoment( momentDate ) ).toBe( true );
} );
} );

describe( 'onChangeMoment', () => {
it( 'should call onChange with a formated date of the input', () => {
const onChangeSpy = jest.fn();
Expand All @@ -69,7 +43,7 @@ describe( 'DatePicker', () => {
);
const newDate = moment();

wrapper.instance().onChangeMoment( newDate );
wrapper.childAt( 0 ).props().onDateChange( newDate );

expect( onChangeSpy ).toHaveBeenCalledWith(
newDate.format( TIMEZONELESS_FORMAT )
Expand All @@ -87,7 +61,7 @@ describe( 'DatePicker', () => {
minutes: current.minutes(),
seconds: current.seconds(),
} );
wrapper.instance().onChangeMoment( newDate );
wrapper.childAt( 0 ).props().onDateChange( newDate );

expect(
moment( onChangeSpyArgument ).isSame(
Expand All @@ -110,7 +84,7 @@ describe( 'DatePicker', () => {
minutes: current.minutes(),
seconds: current.seconds(),
} );
wrapper.instance().onChangeMoment( newDate );
wrapper.childAt( 0 ).props().onDateChange( newDate );

expect(
moment( onChangeSpyArgument ).isSame(
Expand Down
32 changes: 32 additions & 0 deletions packages/components/src/date-time/test/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* External dependencies
*/
import moment from 'moment';

/**
* Internal dependencies
*/
import { getMomentDate } from '../utils';

describe( 'getMomentDate', () => {
it( 'should return a Moment object representing a given date string', () => {
const currentDate = '1986-10-18T23:00:00';
const momentDate = getMomentDate( currentDate );

expect( moment.isMoment( momentDate ) ).toBe( true );
expect( momentDate.isSame( moment( currentDate ) ) ).toBe( true );
} );

it( 'should return null when given a null argument', () => {
const currentDate = null;
const momentDate = getMomentDate( currentDate );

expect( momentDate ).toBeNull();
} );

it( 'should return a Moment object representing now when given an undefined argument', () => {
const momentDate = getMomentDate();

expect( moment.isMoment( momentDate ) ).toBe( true );
} );
} );
18 changes: 18 additions & 0 deletions packages/components/src/date-time/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* External dependencies
*/
import moment from 'moment';

/**
* Create a Moment object from a date string. With no date supplied, default to a Moment
* object representing now. If a null value is passed, return a null value.
*
* @param {?string} date Date representing the currently selected date or null to signify no selection.
* @return {?moment.Moment} Moment object for selected date or null.
*/
export const getMomentDate = ( date ) => {
if ( null === date ) {
return null;
}
return date ? moment( date ) : moment();
};