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

Rewrite DatePicker, TimePicker and DateTimePicker in TypeScript #40775

Merged
merged 13 commits into from
May 10, 2022
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
noisysocks marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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 ) ) {
noisysocks marked this conversation as resolved.
Show resolved Hide resolved
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;