Skip to content

Commit

Permalink
Rewrite DatePicker, TimePicker and DateTimePicker in TypeScript (#40775)
Browse files Browse the repository at this point in the history
* Rewrite DatePicker, TimePicker and DateTimePicker in TypeScript

* Does this fix anything?

* Move DatePickerDayProps to types.ts

* Use noop

* Move UpdateOnBlurAsIntegerFieldProps to types.ts

* Use type instead of interface

* Update props format in README.md

* Improve @ts-ignore comment

* Move prop destructuring to function body

* Remove unnecessary className prop

* Go back to using isNaN

* Remove unnecessary @types/react-dates from package.json

* Add CHANGELOG.md entry
  • Loading branch information
noisysocks committed May 10, 2022
1 parent f1c4882 commit d888491
Show file tree
Hide file tree
Showing 14 changed files with 279 additions and 108 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
4 changes: 4 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- `BorderControl` now only displays the reset button in its popover when selections have already been made. [#40917](https://github.com/WordPress/gutenberg/pull/40917)

### Internal

- `DateTimePicker`: Convert to TypeScript ([#40775](https://github.com/WordPress/gutenberg/pull/40775)).

## 19.10.0 (2022-05-04)

### Internal
Expand Down
8 changes: 6 additions & 2 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 All @@ -8,10 +9,13 @@ import classnames from 'classnames';
*/
import { forwardRef } from '@wordpress/element';

function ButtonGroup( { className, ...props }, ref ) {
function ButtonGroup( props, ref ) {
const { className, ...restProps } = props;
const classes = classnames( 'components-button-group', className );

return <div ref={ ref } role="group" className={ classes } { ...props } />;
return (
<div ref={ ref } role="group" className={ classes } { ...restProps } />
);
}

export default forwardRef( ButtonGroup );
23 changes: 13 additions & 10 deletions packages/components/src/date-time/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,39 +35,42 @@ const MyDateTimePicker = () => {

The component accepts the following props:

### currentDate
### `currentDate`: `Date | string | number | null`

The current date and time at initialization. Optionally pass in a `null` value to specify no date is currently selected.

- Type: `string`
- Required: No
- Default: today's date

### onChange
### `onChange`: `( date: string | null ) => void`

The function called when a new date or time has been selected. It is passed the `currentDate` as an argument.

- Type: `Function`
- Required: Yes
- Required: No

### is12Hour
### `is12Hour`: `boolean`

Whether we use a 12-hour clock. With a 12-hour clock, an AM/PM widget is displayed and the time format is assumed to be `MM-DD-YYYY` (as opposed to the default format `DD-MM-YYYY`).

- Type: `bool`
- Required: No
- Default: false

### isInvalidDate
### `isInvalidDate`: `( date: Date ) => boolean`

A callback function which receives a Date object representing a day as an argument, and should return a Boolean to signify if the day is valid or not.

- Type: `Function`
- Required: No

### onMonthPreviewed
### `onMonthPreviewed`: `( date: Date ) => void`

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

### `events`: `{ date: Date }[]`

List of events to show in the date picker. Each event will appear as a dot on the day of the event.

- Type: `Array`
- Required: No
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
*/
import moment from 'moment';
import classnames from 'classnames';

// 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';
import type { Moment } from 'moment';
import { noop } from 'lodash';

// `react-dates` doesn't tree-shake correctly, so we import from the individual
// component here.
// @ts-expect-error TypeScript won't find any type declarations at
// `react-dates/lib/components/DayPickerSingleDateController` as they're located
// at `react-dates`.
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 +25,13 @@ import { isRTL, _n, sprintf } from '@wordpress/i18n';
* Internal dependencies
*/
import { getMomentDate } from './utils';
import type { DatePickerDayProps, 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();
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 +41,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 +86,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 +116,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 +137,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 +157,7 @@ function DatePicker( {

return (
<div className="components-datetime__date" ref={ nodeRef }>
<DayPickerSingleDateController
<TypedDayPickerSingleDateController
date={ momentDate }
daySize={ 30 }
focused
Expand All @@ -166,7 +175,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 +185,7 @@ function DatePicker( {
events={ getEventsPerDay( day ) }
/>
) }
onFocusChange={ noop }
/>
</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,19 +19,20 @@ 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 };

function DateTimePicker(
function UnforwardedDateTimePicker(
{
currentDate,
is12Hour,
isInvalidDate,
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 All @@ -167,4 +169,29 @@ function DateTimePicker(
);
}

export default forwardRef( DateTimePicker );
/**
* DateTimePicker is a React component that renders a calendar and clock for
* date and time selection. The calendar and clock components can be accessed
* individually using the `DatePicker` and `TimePicker` components respectively.
*
* @example
* ```jsx
* import { DateTimePicker } from '@wordpress/components';
* import { useState } from '@wordpress/element';
*
* const MyDateTimePicker = () => {
* const [ date, setDate ] = useState( new Date() );
*
* return (
* <DateTimePicker
* currentDate={ date }
* onChange={ ( newDate ) => setDate( newDate ) }
* is12Hour={ true }
* />
* );
* };
* ```
*/
export const DateTimePicker = forwardRef( UnforwardedDateTimePicker );

export default DateTimePicker;

0 comments on commit d888491

Please sign in to comment.