From 7c81ee46e6a5a26dbc3a3802ea180dcb720c63f6 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Tue, 3 May 2022 15:22:07 +1000 Subject: [PATCH 01/13] Rewrite DatePicker, TimePicker and DateTimePicker in TypeScript --- package-lock.json | 10 ++ package.json | 1 + packages/components/package.json | 1 + packages/components/src/button-group/index.js | 1 + packages/components/src/date-time/README.md | 7 ++ .../src/date-time/{date.js => date.tsx} | 46 +++++---- .../src/date-time/{index.js => index.tsx} | 37 ++++++- .../src/date-time/{time.js => time.tsx} | 98 +++++++++---------- .../date-time/{timezone.js => timezone.tsx} | 9 +- packages/components/src/date-time/types.ts | 72 ++++++++++++++ packages/components/src/date-time/utils.js | 18 ---- packages/components/src/date-time/utils.ts | 25 +++++ packages/components/tsconfig.json | 2 + packages/date/src/index.js | 2 +- 14 files changed, 233 insertions(+), 96 deletions(-) rename packages/components/src/date-time/{date.js => date.tsx} (75%) rename packages/components/src/date-time/{index.js => index.tsx} (80%) rename packages/components/src/date-time/{time.js => time.tsx} (74%) rename packages/components/src/date-time/{timezone.js => timezone.tsx} (84%) create mode 100644 packages/components/src/date-time/types.ts delete mode 100644 packages/components/src/date-time/utils.js create mode 100644 packages/components/src/date-time/utils.ts diff --git a/package-lock.json b/package-lock.json index f836c69f0400f..3144b9120410a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16305,6 +16305,15 @@ } } }, + "@types/react-dates": { + "version": "17.1.10", + "resolved": "https://registry.npmjs.org/@types/react-dates/-/react-dates-17.1.10.tgz", + "integrity": "sha512-vMMO4bw88PJ5yLfeo1WR0peR7hkYIsso1wGLiO5Xkpcb/B86LK2P1l3YbNWXD1M7BqFekezkuWaO1qKGf8uX4Q==", + "requires": { + "@types/react": "*", + "moment": ">=2.17.1" + } + }, "@types/react-dom": { "version": "17.0.11", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz", @@ -17253,6 +17262,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:packages/a11y", "@wordpress/compose": "file:packages/compose", diff --git a/package.json b/package.json index 123da1ec51229..1656862823c5a 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/components/package.json b/packages/components/package.json index d587d1889e085..b093cb9106aee 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -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", diff --git a/packages/components/src/button-group/index.js b/packages/components/src/button-group/index.js index 2ef2b5e94c601..8ffe13824973d 100644 --- a/packages/components/src/button-group/index.js +++ b/packages/components/src/button-group/index.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * External dependencies */ diff --git a/packages/components/src/date-time/README.md b/packages/components/src/date-time/README.md index a3567c2719880..7eedc087815b0 100644 --- a/packages/components/src/date-time/README.md +++ b/packages/components/src/date-time/README.md @@ -71,3 +71,10 @@ A callback invoked when selecting the previous/next month in the date picker. Th - Type: `Function` - Required: No + +### events + +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 diff --git a/packages/components/src/date-time/date.js b/packages/components/src/date-time/date.tsx similarity index 75% rename from packages/components/src/date-time/date.js rename to packages/components/src/date-time/date.tsx index fd6869ac84333..302dab4110301 100644 --- a/packages/components/src/date-time/date.js +++ b/packages/components/src/date-time/date.tsx @@ -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 @@ -18,15 +22,18 @@ 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 @@ -36,7 +43,7 @@ function DatePickerDay( { day, events = [] } ) { */ useEffect( () => { // Bail when no parent node. - if ( ! ref?.current?.parentNode ) { + if ( ! ( ref?.current?.parentNode instanceof Element ) ) { return; } @@ -81,9 +88,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(); }; @@ -111,7 +118,7 @@ function DatePicker( { const focusRegion = nodeRef.current.querySelector( '.DayPicker_focusRegion' ); - if ( ! focusRegion ) { + if ( ! ( focusRegion instanceof HTMLElement ) ) { return; } // Keep the focus on focus region. @@ -119,7 +126,11 @@ function DatePicker( { } }; - 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 = { @@ -128,13 +139,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 []; } @@ -148,7 +159,7 @@ function DatePicker( { return (
- { - return isInvalidDate && isInvalidDate( date.toDate() ); + return !! isInvalidDate && isInvalidDate( date.toDate() ); } } onPrevMonthClick={ onMonthPreviewedHandler } onNextMonthClick={ onMonthPreviewedHandler } @@ -176,6 +187,7 @@ function DatePicker( { events={ getEventsPerDay( day ) } /> ) } + onFocusChange={ () => {} } />
); diff --git a/packages/components/src/date-time/index.js b/packages/components/src/date-time/index.tsx similarity index 80% rename from packages/components/src/date-time/index.js rename to packages/components/src/date-time/index.tsx index 2623b72d28244..9801d4c9407fa 100644 --- a/packages/components/src/date-time/index.js +++ b/packages/components/src/date-time/index.tsx @@ -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 @@ -18,10 +19,11 @@ 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, @@ -29,8 +31,8 @@ function DateTimePicker( onMonthPreviewed = noop, onChange, events, - }, - ref + }: DateTimePickerProps, + ref: ForwardedRef< any > ) { const [ calendarHelpIsVisible, setCalendarHelpIsVisible ] = useState( false @@ -148,7 +150,7 @@ function DateTimePicker( @@ -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 ( + * setDate( newDate ) } + * is12Hour={ true } + * /> + * ); + * }; + * ``` + */ +export const DateTimePicker = forwardRef( UnforwardedDateTimePicker ); + +export default DateTimePicker; diff --git a/packages/components/src/date-time/time.js b/packages/components/src/date-time/time.tsx similarity index 74% rename from packages/components/src/date-time/time.js rename to packages/components/src/date-time/time.tsx index 3fc7da4fe3970..c3598d59ae8dc 100644 --- a/packages/components/src/date-time/time.js +++ b/packages/components/src/date-time/time.tsx @@ -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 @@ -22,25 +24,25 @@ 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 ) { +function from12hTo24h( hours: number, isPm: boolean ) { return isPm ? ( ( hours % 12 ) + 12 ) % 24 : hours % 12; } +interface UpdateOnBlurAsIntegerFieldProps { + value: number | string; + onUpdate: ( value: number ) => void; + className?: string; + children?: ReactNode; +} + /** - * - * A shared component to parse, validate, and handle remounting of the underlying form field element like and and and