From 770d1e07ebf570850e201687a4a6a11cb704b977 Mon Sep 17 00:00:00 2001 From: Andre Rocha Date: Mon, 23 Sep 2019 07:59:21 -0700 Subject: [PATCH] [DatePicker] Hookify (#2089) * Hookify Day * Hookify DatePicker --- UNRELEASED.md | 2 + src/components/DatePicker/DatePicker.tsx | 429 ++++++++---------- .../DatePicker/components/Day/Day.tsx | 132 +++--- .../DatePicker/components/Day/index.ts | 4 +- .../components/Day/tests/Day.test.tsx | 2 +- src/components/DatePicker/index.ts | 4 +- .../DatePicker/tests/DatePicker.test.tsx | 2 +- 7 files changed, 252 insertions(+), 323 deletions(-) diff --git a/UNRELEASED.md b/UNRELEASED.md index e52108b4687..081b1f689ab 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -58,4 +58,6 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f - Migrated `TextField` and `Resizer` to use hooks ([#1997](https://github.com/Shopify/polaris-react/pull/1997)) - Migrated `Avatar` to use hooks instead of withAppProvider ([#2067](https://github.com/Shopify/polaris-react/pull/2067)) +- Update `Day` and `DatePicker` to use hooks ([#2089](https://github.com/Shopify/polaris-react/pull/2089)) + ### Deprecations diff --git a/src/components/DatePicker/DatePicker.tsx b/src/components/DatePicker/DatePicker.tsx index 820bb730e34..becc2f4a613 100644 --- a/src/components/DatePicker/DatePicker.tsx +++ b/src/components/DatePicker/DatePicker.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useState, useEffect, useCallback} from 'react'; import {ArrowLeftMinor, ArrowRightMinor} from '@shopify/polaris-icons'; import { Range, @@ -11,14 +11,10 @@ import { getPreviousDisplayYear, getPreviousDisplayMonth, Weekdays, - isSameDay, } from '@shopify/javascript-utilities/dates'; -import { - withAppProvider, - WithAppProviderProps, -} from '../../utilities/with-app-provider'; import {Button} from '../Button'; +import {useI18n} from '../../utilities/i18n'; import {monthName} from './utilities'; import {Month} from './components'; @@ -52,235 +48,214 @@ export interface BaseProps { } export interface DatePickerProps extends BaseProps {} -type CombinedProps = DatePickerProps & WithAppProviderProps; - -interface State { - hoverDate?: Date; - focusDate?: Date; -} - -class DatePicker extends React.PureComponent { - state: State = { - hoverDate: undefined, - focusDate: undefined, - }; - - componentDidUpdate(prevProps: DatePickerProps) { - const selectedPropDidChange = !isSameSelectedDate( - prevProps.selected, - this.props.selected, - ); - - if (selectedPropDidChange) { - this.resetFocus(); - } - } - - render() { - const { - id, - selected, - month, - year, - allowRange, - multiMonth, - disableDatesBefore, - disableDatesAfter, - weekStartsOn = Weekdays.Sunday, - polaris: {intl}, - } = this.props; - const {hoverDate, focusDate} = this.state; - - const showNextYear = getNextDisplayYear(month, year); - const showNextMonth = getNextDisplayMonth(month); - - const showNextToNextYear = getNextDisplayYear(showNextMonth, showNextYear); - const showNextToNextMonth = getNextDisplayMonth(showNextMonth); - - const showPreviousYear = getPreviousDisplayYear(month, year); - const showPreviousMonth = getPreviousDisplayMonth(month); - - const previousMonthName = intl.translate( - `Polaris.DatePicker.months.${monthName(showPreviousMonth)}`, - ); - const nextMonth = multiMonth - ? intl.translate( - `Polaris.DatePicker.months.${monthName(showNextToNextMonth)}`, - ) - : intl.translate(`Polaris.DatePicker.months.${monthName(showNextMonth)}`); - const nextYear = multiMonth ? showNextToNextYear : showNextYear; - - const secondDatePicker = multiMonth ? ( - - ) : null; +export function DatePicker({ + id, + selected, + month, + year, + allowRange, + multiMonth, + disableDatesBefore, + disableDatesAfter, + weekStartsOn = Weekdays.Sunday, + onMonthChange, + onChange = noop, +}: DatePickerProps) { + const i18n = useI18n(); + const [hoverDate, setHoverDate] = useState(undefined); + const [focusDate, setFocusDate] = useState(undefined); + + useEffect(() => { + setFocusDate(undefined); + }, [selected]); + + const handleFocus = useCallback((date: Date) => { + setFocusDate(date); + }, []); + + const setFocusDateAndHandleMonthChange = useCallback( + (date: Date) => { + if (onMonthChange) { + onMonthChange(date.getMonth(), date.getFullYear()); + } + setHoverDate(date); + setFocusDate(date); + }, + [onMonthChange], + ); - return ( -
-
-
-
- - {secondDatePicker} -
-
- ); - } + const handleDateSelection = useCallback( + (range: Range) => { + const {end} = range; - private handleFocus = (date: Date) => { - this.setState({focusDate: date}); - }; + setHoverDate(end); + setFocusDate(new Date(end)); + onChange(range); + }, + [onChange], + ); - private resetFocus = () => { - this.setState({focusDate: undefined}); - }; + const handleMonthChangeClick = useCallback( + (month: Months, year: Year) => { + if (!onMonthChange) { + return; + } + setFocusDate(undefined); + onMonthChange(month, year); + }, + [onMonthChange], + ); - private handleKeyUp = (event: React.KeyboardEvent) => { - const {key} = event; - const {selected, disableDatesBefore, disableDatesAfter} = this.props; + const handleHover = useCallback((date: Date) => { + setHoverDate(date); + }, []); - const {focusDate} = this.state; - const range = deriveRange(selected); - const focusedDate = focusDate || (range && range.start); + const handleKeyUp = useCallback( + (event: React.KeyboardEvent) => { + const {key} = event; - if (focusedDate == null) { - return; - } + const range = deriveRange(selected); + const focusedDate = focusDate || (range && range.start); - if (key === 'ArrowUp') { - const previousWeek = new Date(focusedDate); - previousWeek.setDate(focusedDate.getDate() - 7); - if ( - !(disableDatesBefore && isDateBefore(previousWeek, disableDatesBefore)) - ) { - this.setFocusDateAndHandleMonthChange(previousWeek); + if (focusedDate == null) { + return; } - } - if (key === 'ArrowDown') { - const nextWeek = new Date(focusedDate); - nextWeek.setDate(focusedDate.getDate() + 7); - if (!(disableDatesAfter && isDateAfter(nextWeek, disableDatesAfter))) { - this.setFocusDateAndHandleMonthChange(nextWeek); + if (key === 'ArrowUp') { + const previousWeek = new Date(focusedDate); + previousWeek.setDate(focusedDate.getDate() - 7); + if ( + !( + disableDatesBefore && isDateBefore(previousWeek, disableDatesBefore) + ) + ) { + setFocusDateAndHandleMonthChange(previousWeek); + } } - } - if (key === 'ArrowRight') { - const tomorrow = new Date(focusedDate); - tomorrow.setDate(focusedDate.getDate() + 1); - if (!(disableDatesAfter && isDateAfter(tomorrow, disableDatesAfter))) { - this.setFocusDateAndHandleMonthChange(tomorrow); + if (key === 'ArrowDown') { + const nextWeek = new Date(focusedDate); + nextWeek.setDate(focusedDate.getDate() + 7); + if (!(disableDatesAfter && isDateAfter(nextWeek, disableDatesAfter))) { + setFocusDateAndHandleMonthChange(nextWeek); + } } - } - if (key === 'ArrowLeft') { - const yesterday = new Date(focusedDate); - yesterday.setDate(focusedDate.getDate() - 1); - if ( - !(disableDatesBefore && isDateBefore(yesterday, disableDatesBefore)) - ) { - this.setFocusDateAndHandleMonthChange(yesterday); + if (key === 'ArrowRight') { + const tomorrow = new Date(focusedDate); + tomorrow.setDate(focusedDate.getDate() + 1); + if (!(disableDatesAfter && isDateAfter(tomorrow, disableDatesAfter))) { + setFocusDateAndHandleMonthChange(tomorrow); + } } - } - }; - private setFocusDateAndHandleMonthChange = (date: Date) => { - const {onMonthChange} = this.props; - if (onMonthChange) { - onMonthChange(date.getMonth(), date.getFullYear()); - } - this.setState({ - hoverDate: date, - focusDate: date, - }); - }; + if (key === 'ArrowLeft') { + const yesterday = new Date(focusedDate); + yesterday.setDate(focusedDate.getDate() - 1); + if ( + !(disableDatesBefore && isDateBefore(yesterday, disableDatesBefore)) + ) { + setFocusDateAndHandleMonthChange(yesterday); + } + } + }, + [ + disableDatesAfter, + disableDatesBefore, + focusDate, + selected, + setFocusDateAndHandleMonthChange, + ], + ); - private handleDateSelection = (range: Range) => { - const {end} = range; - const {onChange = noop} = this.props; + const showNextYear = getNextDisplayYear(month, year); + const showNextMonth = getNextDisplayMonth(month); - this.setState({hoverDate: end, focusDate: new Date(end)}, () => - onChange(range), - ); - }; + const showNextToNextYear = getNextDisplayYear(showNextMonth, showNextYear); + const showNextToNextMonth = getNextDisplayMonth(showNextMonth); - private handleMonthChangeClick = (month: Months, year: Year) => { - const {onMonthChange} = this.props; - if (!onMonthChange) { - return; - } - this.setState({ - focusDate: undefined, - }); + const showPreviousYear = getPreviousDisplayYear(month, year); + const showPreviousMonth = getPreviousDisplayMonth(month); - onMonthChange(month, year); - }; + const previousMonthName = i18n.translate( + `Polaris.DatePicker.months.${monthName(showPreviousMonth)}`, + ); + const nextMonth = multiMonth + ? i18n.translate( + `Polaris.DatePicker.months.${monthName(showNextToNextMonth)}`, + ) + : i18n.translate(`Polaris.DatePicker.months.${monthName(showNextMonth)}`); + const nextYear = multiMonth ? showNextToNextYear : showNextYear; + + const secondDatePicker = multiMonth ? ( + + ) : null; - private handleHover = (date: Date) => { - this.setState({ - hoverDate: date, - }); - }; + return ( +
+
+
+
+ + {secondDatePicker} +
+
+ ); } function noop() {} @@ -299,32 +274,6 @@ function handleKeyDown(event: React.KeyboardEvent) { } } -function isSameSelectedDate( - previousDate?: DatePickerProps['selected'], - currentDate?: DatePickerProps['selected'], -) { - if (previousDate == null || currentDate == null) { - return previousDate == null && currentDate == null; - } - - if (previousDate instanceof Date || currentDate instanceof Date) { - return ( - previousDate instanceof Date && - currentDate instanceof Date && - isSameDay(previousDate, currentDate) - ); - } - - return ( - isSameDay(previousDate.start, currentDate.start) && - isSameDay(previousDate.end, currentDate.end) - ); -} - function deriveRange(selected?: Date | Range) { return selected instanceof Date ? {start: selected, end: selected} : selected; } - -// Use named export once withAppProvider is refactored away -// eslint-disable-next-line import/no-default-export -export default withAppProvider()(DatePicker); diff --git a/src/components/DatePicker/components/Day/Day.tsx b/src/components/DatePicker/components/Day/Day.tsx index 7e5c2b7d490..8413e5f0087 100644 --- a/src/components/DatePicker/components/Day/Day.tsx +++ b/src/components/DatePicker/components/Day/Day.tsx @@ -1,10 +1,7 @@ -import React from 'react'; +import React, {useRef, useEffect} from 'react'; import {Months, isSameDay} from '@shopify/javascript-utilities/dates'; import {classNames} from '../../../../utilities/css'; -import { - withAppProvider, - WithAppProviderProps, -} from '../../../../utilities/with-app-provider'; +import {useI18n} from '../../../../utilities/i18n'; import styles from '../../DatePicker.scss'; @@ -16,84 +13,69 @@ export interface DayProps { inHoveringRange?: boolean; disabled?: boolean; onClick?(day: Date): void; - onHover?(day: Date): void; + onHover?(day?: Date): void; onFocus?(day: Date): void; } -type CombinedProps = DayProps & WithAppProviderProps; +export function Day({ + day, + focused, + onClick, + onHover = noop, + onFocus = noop, + selected, + inRange, + inHoveringRange, + disabled, +}: DayProps) { + const i18n = useI18n(); + const dayNode = useRef(null); -class Day extends React.PureComponent { - private dayNode: HTMLElement | null = null; - - componentDidUpdate(prevProps: CombinedProps) { - if (!prevProps.focused && this.props.focused && this.dayNode) { - this.dayNode.focus(); - } - } - - render() { - const { - day, - focused, - onClick, - onHover = noop, - onFocus = noop, - selected, - inRange, - inHoveringRange, - disabled, - polaris: {intl}, - } = this.props; - - const handleHover = onHover.bind(null, day); - if (!day) { - return
; + useEffect(() => { + if (focused && dayNode.current) { + dayNode.current.focus(); } - const handleClick = onClick && !disabled ? onClick.bind(null, day) : noop; - const today = isSameDay(new Date(), day); - const className = classNames( - styles.Day, - selected && styles['Day-selected'], - disabled && styles['Day-disabled'], - today && styles['Day-today'], - (inRange || inHoveringRange) && !disabled && styles['Day-inRange'], - ); - const date = day.getDate(); - const tabIndex = - (focused || selected || today || date === 1) && !disabled ? 0 : -1; - const ariaLabel = [ - `${today ? intl.translate('Polaris.DatePicker.today') : ''}`, - `${Months[day.getMonth()]} `, - `${date} `, - `${day.getFullYear()}`, - ].join(''); + }, [focused]); - return ( - - ); + if (!day) { + return
onHover(day)} />; } + const handleClick = onClick && !disabled ? onClick.bind(null, day) : noop; + const today = isSameDay(new Date(), day); + const className = classNames( + styles.Day, + selected && styles['Day-selected'], + disabled && styles['Day-disabled'], + today && styles['Day-today'], + (inRange || inHoveringRange) && !disabled && styles['Day-inRange'], + ); + const date = day.getDate(); + const tabIndex = + (focused || selected || today || date === 1) && !disabled ? 0 : -1; + const ariaLabel = [ + `${today ? i18n.translate('Polaris.DatePicker.today') : ''}`, + `${Months[day.getMonth()]} `, + `${date} `, + `${day.getFullYear()}`, + ].join(''); - private setNode = (node: HTMLElement | null) => { - this.dayNode = node; - }; + return ( + + ); } -// Use named export once withAppProvider is refactored away -// eslint-disable-next-line import/no-default-export -export default withAppProvider()(Day); - function noop() {} diff --git a/src/components/DatePicker/components/Day/index.ts b/src/components/DatePicker/components/Day/index.ts index 38e5ab00475..9726cab545a 100644 --- a/src/components/DatePicker/components/Day/index.ts +++ b/src/components/DatePicker/components/Day/index.ts @@ -1,3 +1 @@ -import Day, {DayProps} from './Day'; - -export {Day, DayProps}; +export {Day, DayProps} from './Day'; diff --git a/src/components/DatePicker/components/Day/tests/Day.test.tsx b/src/components/DatePicker/components/Day/tests/Day.test.tsx index 9179418a72e..d270c48868f 100644 --- a/src/components/DatePicker/components/Day/tests/Day.test.tsx +++ b/src/components/DatePicker/components/Day/tests/Day.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mountWithAppProvider} from 'test-utilities/legacy'; -import Day from '../Day'; +import {Day} from '../Day'; describe('', () => { it('renders a button', () => { diff --git a/src/components/DatePicker/index.ts b/src/components/DatePicker/index.ts index 11960e8c0ef..451e2cad281 100644 --- a/src/components/DatePicker/index.ts +++ b/src/components/DatePicker/index.ts @@ -1,3 +1 @@ -import DatePicker, {DatePickerProps, Range, Months, Year} from './DatePicker'; - -export {DatePicker, DatePickerProps, Range, Months, Year}; +export {DatePicker, DatePickerProps, Range, Months, Year} from './DatePicker'; diff --git a/src/components/DatePicker/tests/DatePicker.test.tsx b/src/components/DatePicker/tests/DatePicker.test.tsx index 26038ccc208..3a8e512dba4 100644 --- a/src/components/DatePicker/tests/DatePicker.test.tsx +++ b/src/components/DatePicker/tests/DatePicker.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {Weekdays} from '@shopify/javascript-utilities/dates'; import {mountWithAppProvider} from 'test-utilities/legacy'; import {Day, Month, Weekday} from '../components'; -import DatePicker from '../DatePicker'; +import {DatePicker} from '../DatePicker'; describe('', () => { const selected = {