Skip to content

Commit

Permalink
Rewrite DatePicker, TimePicker and DateTimePicker in TypeScript
Browse files Browse the repository at this point in the history
  • Loading branch information
noisysocks committed May 3, 2022
1 parent 27835a4 commit 2d24470
Show file tree
Hide file tree
Showing 12 changed files with 168 additions and 51 deletions.
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
"@types/npm-package-arg": "6.1.1",
"@types/prettier": "2.4.4",
"@types/qs": "6.9.7",
"@types/react-dates": "17.1.10",
"@types/requestidlecallback": "0.3.4",
"@types/semver": "7.3.8",
"@types/sprintf-js": "1.1.2",
Expand Down
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@emotion/serialize": "^1.0.2",
"@emotion/styled": "^11.6.0",
"@emotion/utils": "1.0.0",
"@types/react-dates": "17.1.10",
"@use-gesture/react": "^10.2.6",
"@wordpress/a11y": "file:../a11y",
"@wordpress/compose": "file:../compose",
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/button-group/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* External dependencies
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
*/
import moment from 'moment';
import classnames from 'classnames';
import type { Moment } from 'moment';

// react-dates doesn't tree-shake correctly, so we import from the individual
// component here, to avoid including too much of the library
import DayPickerSingleDateController from 'react-dates/lib/components/DayPickerSingleDateController';
// component here, to avoid including too much of the library.
// @ts-ignore
import UntypedDayPickerSingleDateController from 'react-dates/lib/components/DayPickerSingleDateController';
import type { DayPickerSingleDateController } from 'react-dates';
const TypedDayPickerSingleDateController = UntypedDayPickerSingleDateController as DayPickerSingleDateController;

/**
* WordPress dependencies
Expand All @@ -18,15 +22,21 @@ import { isRTL, _n, sprintf } from '@wordpress/i18n';
* Internal dependencies
*/
import { getMomentDate } from './utils';
import type { DatePickerEvent, DatePickerProps } from './types';

/**
* Module Constants
*/
const TIMEZONELESS_FORMAT = 'YYYY-MM-DDTHH:mm:ss';
const ARIAL_LABEL_TIME_FORMAT = 'dddd, LL';

function DatePickerDay( { day, events = [] } ) {
const ref = useRef();
interface DatePickerDayProps {
day: Moment;
events?: DatePickerEvent[];
}

function DatePickerDay( { day, events = [] }: DatePickerDayProps ) {
const ref = useRef< HTMLDivElement >( null );

/*
* a11y hack to make the `There is/are n events` string
Expand All @@ -36,7 +46,7 @@ function DatePickerDay( { day, events = [] } ) {
*/
useEffect( () => {
// Bail when no parent node.
if ( ! ref?.current?.parentNode ) {
if ( ! ( ref?.current?.parentNode instanceof Element ) ) {
return;
}

Expand Down Expand Up @@ -81,9 +91,9 @@ function DatePicker( {
events,
isInvalidDate,
onMonthPreviewed,
} ) {
const nodeRef = useRef();
const onMonthPreviewedHandler = ( newMonthDate ) => {
}: DatePickerProps ) {
const nodeRef = useRef< HTMLDivElement >( null );
const onMonthPreviewedHandler = ( newMonthDate: Moment ) => {
onMonthPreviewed?.( newMonthDate.toISOString() );
keepFocusInside();
};
Expand Down Expand Up @@ -111,15 +121,19 @@ function DatePicker( {
const focusRegion = nodeRef.current.querySelector(
'.DayPicker_focusRegion'
);
if ( ! focusRegion ) {
if ( ! ( focusRegion instanceof HTMLElement ) ) {
return;
}
// Keep the focus on focus region.
focusRegion.focus();
}
};

const onChangeMoment = ( newDate ) => {
const onChangeMoment = ( newDate: Moment | null ) => {
if ( ! newDate ) {
return;
}

// If currentDate is null, use now as momentTime to designate hours, minutes, seconds.
const momentDate = currentDate ? moment( currentDate ) : moment();
const momentTime = {
Expand All @@ -128,13 +142,13 @@ function DatePicker( {
seconds: 0,
};

onChange( newDate.set( momentTime ).format( TIMEZONELESS_FORMAT ) );
onChange?.( newDate.set( momentTime ).format( TIMEZONELESS_FORMAT ) );

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

const getEventsPerDay = ( day ) => {
const getEventsPerDay = ( day: Moment ) => {
if ( ! events?.length ) {
return [];
}
Expand All @@ -148,7 +162,7 @@ function DatePicker( {

return (
<div className="components-datetime__date" ref={ nodeRef }>
<DayPickerSingleDateController
<TypedDayPickerSingleDateController
date={ momentDate }
daySize={ 30 }
focused
Expand All @@ -166,7 +180,7 @@ function DatePicker( {
dayAriaLabelFormat={ ARIAL_LABEL_TIME_FORMAT }
isRTL={ isRTL() }
isOutsideRange={ ( date ) => {
return isInvalidDate && isInvalidDate( date.toDate() );
return !! isInvalidDate && isInvalidDate( date.toDate() );
} }
onPrevMonthClick={ onMonthPreviewedHandler }
onNextMonthClick={ onMonthPreviewedHandler }
Expand All @@ -176,6 +190,7 @@ function DatePicker( {
events={ getEventsPerDay( day ) }
/>
) }
onFocusChange={ () => {} }
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// See: https://github.com/airbnb/react-dates#initialize
import 'react-dates/initialize';
import { noop } from 'lodash';
import type { ForwardedRef } from 'react';

/**
* WordPress dependencies
Expand All @@ -18,6 +19,7 @@ import { __, _x } from '@wordpress/i18n';
import Button from '../button';
import { default as DatePicker } from './date';
import { default as TimePicker } from './time';
import type { DateTimePickerProps } from './types';

export { DatePicker, TimePicker };

Expand All @@ -29,8 +31,8 @@ function DateTimePicker(
onMonthPreviewed = noop,
onChange,
events,
},
ref
}: DateTimePickerProps,
ref: ForwardedRef< any >
) {
const [ calendarHelpIsVisible, setCalendarHelpIsVisible ] = useState(
false
Expand Down Expand Up @@ -148,7 +150,7 @@ function DateTimePicker(
<Button
className="components-datetime__date-reset-button"
variant="link"
onClick={ () => onChange( null ) }
onClick={ () => onChange?.( null ) }
>
{ __( 'Reset' ) }
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import classnames from 'classnames';
import { isInteger } from 'lodash';
import moment from 'moment';
import type { FocusEvent, ReactNode } from 'react';
import type { Moment } from 'moment';

/**
* WordPress dependencies
Expand All @@ -22,13 +24,22 @@ import { __ } from '@wordpress/i18n';
import Button from '../button';
import ButtonGroup from '../button-group';
import TimeZone from './timezone';
import type { WordPressComponentProps } from '../ui/context';
import type { TimePickerProps } from './types';

/**
* Module Constants
*/
const TIMEZONELESS_FORMAT = 'YYYY-MM-DDTHH:mm:ss';

function from12hTo24h( hours, isPm ) {
interface UpdateOnBlurAsIntegerFieldProps {
value: number | string;
onUpdate: ( value: number ) => void;
className?: string;
children?: ReactNode;
}

function from12hTo24h( hours: number, isPm: boolean ) {
return isPm ? ( ( hours % 12 ) + 12 ) % 24 : hours % 12;
}

Expand All @@ -48,11 +59,11 @@ function UpdateOnBlurAsIntegerField( {
onUpdate,
className,
...props
} ) {
function handleBlur( event ) {
}: WordPressComponentProps< UpdateOnBlurAsIntegerFieldProps, 'input', true > ) {
function handleBlur( event: FocusEvent< HTMLInputElement > ) {
const { target } = event;

if ( value === target.value ) {
if ( String( value ) === target.value ) {
return;
}

Expand All @@ -65,10 +76,10 @@ function UpdateOnBlurAsIntegerField( {
( typeof props.min !== 'undefined' && parsedValue < props.min )
) {
// If validation failed, reset the value to the previous valid value.
target.value = value;
target.value = String( value );
} else {
// Otherwise, it's valid, call onUpdate.
onUpdate( target.name, parsedValue );
onUpdate( parsedValue );
}
}

Expand All @@ -95,7 +106,11 @@ function UpdateOnBlurAsIntegerField( {
* @param {WPValidDateTimeFormat} props.currentTime The initial current time the time picker should render.
* @param {Function} props.onChange Callback function when the date changed.
*/
export function TimePicker( { is12Hour, currentTime, onChange } ) {
export function TimePicker( {
is12Hour,
currentTime,
onChange,
}: TimePickerProps ) {
const [ date, setDate ] = useState( () =>
// Truncate the date at the minutes, see: #15495.
moment( currentTime ).startOf( 'minutes' )
Expand All @@ -115,7 +130,7 @@ export function TimePicker( { is12Hour, currentTime, onChange } ) {
year: date.format( 'YYYY' ),
minutes: date.format( 'mm' ),
hours: date.format( is12Hour ? 'hh' : 'HH' ),
am: date.format( 'H' ) <= 11 ? 'AM' : 'PM',
am: Number( date.format( 'H' ) ) <= 11 ? 'AM' : 'PM',
} ),
[ date, is12Hour ]
);
Expand All @@ -124,28 +139,30 @@ export function TimePicker( { is12Hour, currentTime, onChange } ) {
* Function that sets the date state and calls the onChange with a new date.
* The date is truncated at the minutes.
*
* @param {Object} newDate The date object.
* @param {Moment} newDate The date object.
*/
function changeDate( newDate ) {
function changeDate( newDate: Moment ) {
setDate( newDate );
onChange( newDate.format( TIMEZONELESS_FORMAT ) );
onChange?.( newDate.format( TIMEZONELESS_FORMAT ) );
}

function update( name, value ) {
// If the 12-hour format is being used and the 'PM' period is selected, then
// the incoming value (which ranges 1-12) should be increased by 12 to match
// the expected 24-hour format.
let adjustedValue = value;
if ( name === 'hours' && is12Hour ) {
adjustedValue = from12hTo24h( value, am === 'PM' );
}
function update( name: 'date' | 'month' | 'year' | 'hours' | 'minutes' ) {
return ( value: number ) => {
// If the 12-hour format is being used and the 'PM' period is selected, then
// the incoming value (which ranges 1-12) should be increased by 12 to match
// the expected 24-hour format.
let adjustedValue = value;
if ( name === 'hours' && is12Hour ) {
adjustedValue = from12hTo24h( value, am === 'PM' );
}

// Clone the date and call the specific setter function according to `name`.
const newDate = date.clone()[ name ]( adjustedValue );
changeDate( newDate );
// Clone the date and call the specific setter function according to `name`.
const newDate = date.clone()[ name ]( adjustedValue );
changeDate( newDate );
};
}

function updateAmPm( value ) {
function updateAmPm( value: 'AM' | 'PM' ) {
return () => {
if ( am === value ) {
return;
Expand Down Expand Up @@ -173,7 +190,7 @@ export function TimePicker( { is12Hour, currentTime, onChange } ) {
step={ 1 }
min={ 1 }
max={ 31 }
onUpdate={ update }
onUpdate={ update( 'date' ) }
/>
</div>
);
Expand All @@ -187,7 +204,7 @@ export function TimePicker( { is12Hour, currentTime, onChange } ) {
name="month"
value={ month }
// The value starts from 0, so we have to -1 when setting month.
onUpdate={ ( key, value ) => update( key, value - 1 ) }
onUpdate={ ( value ) => update( 'month' )( value - 1 ) }
>
<option value="01">{ __( 'January' ) }</option>
<option value="02">{ __( 'February' ) }</option>
Expand Down Expand Up @@ -236,7 +253,7 @@ export function TimePicker( { is12Hour, currentTime, onChange } ) {
min={ 0 }
max={ 9999 }
value={ year }
onUpdate={ update }
onUpdate={ update( 'year' ) }
/>
</div>
</div>
Expand All @@ -257,7 +274,7 @@ export function TimePicker( { is12Hour, currentTime, onChange } ) {
min={ is12Hour ? 1 : 0 }
max={ is12Hour ? 12 : 23 }
value={ hours }
onUpdate={ update }
onUpdate={ update( 'hours' ) }
/>
<span
className="components-datetime__time-separator"
Expand All @@ -274,7 +291,7 @@ export function TimePicker( { is12Hour, currentTime, onChange } ) {
min={ 0 }
max={ 59 }
value={ minutes }
onUpdate={ update }
onUpdate={ update( 'minutes' ) }
/>
</div>
{ is12Hour && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,9 @@ const TimeZone = () => {
return null;
}

const offsetSymbol = timezone.offset >= 0 ? '+' : '';
const offsetSymbol = Number( timezone.offset ) >= 0 ? '+' : '';
const zoneAbbr =
'' !== timezone.abbr && isNaN( timezone.abbr )
? timezone.abbr
: `UTC${ offsetSymbol }${ timezone.offset }`;
timezone.abbr || `UTC${ offsetSymbol }${ timezone.offset }`;

const timezoneDetail =
'UTC' === timezone.string
Expand Down

0 comments on commit 2d24470

Please sign in to comment.