From 82e2e9c7ea1867a941c6fa98b4e727e642e48c5f Mon Sep 17 00:00:00 2001 From: Joshua Allen Date: Tue, 15 Jan 2019 10:48:12 -0500 Subject: [PATCH 1/5] Add locale prop for Date Picker i18n --- UNRELEASED.md | 2 + package.json | 3 +- src/components/DatePicker/DatePicker.tsx | 61 ++++----- .../DatePicker/components/Day/Day.tsx | 20 +-- .../components/Day/tests/Day.test.tsx | 16 +++ .../DatePicker/components/Month/Month.tsx | 67 +++++---- .../components/Month/tests/Month.test.tsx | 127 ++++++++++++++++-- .../DatePicker/tests/DatePicker.test.tsx | 122 ++++++++++++++--- yarn.lock | 5 + 9 files changed, 322 insertions(+), 101 deletions(-) diff --git a/UNRELEASED.md b/UNRELEASED.md index 85e772118ff..38cfe8ff023 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -10,6 +10,8 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f ### Enhancements +- Added `locale` prop to `DatePicker` to support date format localization + ### Bug fixes - Removed a duplicate `activatorWrapper` in `Popover` when destructuring props ([#916](https://github.com/Shopify/polaris-react/pull/916)) diff --git a/package.json b/package.json index b1ba039017e..8c12a2ec2c2 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "lint": "sewing-kit lint", "format": "sewing-kit format", "ts": "tsc --noEmit", - "test": "sewing-kit test", + "test": "env NODE_ICU_DATA=$(node-full-icu-path) sewing-kit test", "test:coverage": "yarn test --coverage", "test:ci": "yarn test --coverage", "check": "npm-run-all lint ts test", @@ -121,6 +121,7 @@ "enzyme": "^3.7.0", "enzyme-adapter-react-16": "^1.6.0", "fs-extra": "^4.0.2", + "full-icu": "^1.2.1", "generic-names": "^1.0.2", "glob": "^7.1.2", "gray-matter": "^4.0.1", diff --git a/src/components/DatePicker/DatePicker.tsx b/src/components/DatePicker/DatePicker.tsx index f93caa68569..c318f0e4c8a 100644 --- a/src/components/DatePicker/DatePicker.tsx +++ b/src/components/DatePicker/DatePicker.tsx @@ -7,10 +7,6 @@ import { Year, isDateAfter, isDateBefore, - getNextDisplayYear, - getNextDisplayMonth, - getPreviousDisplayYear, - getPreviousDisplayMonth, Weekdays, isSameDay, } from '@shopify/javascript-utilities/dates'; @@ -42,6 +38,8 @@ export interface BaseProps { multiMonth?: boolean; /** First day of week. Sunday by default */ weekStartsOn?: Weekdays; + /** Locale for date formatting. 'en' by default */ + locale?: string; /** Callback when date is selected. */ onChange?(date: Range): void; /** Callback when month is changed. */ @@ -84,32 +82,21 @@ export class DatePicker extends React.PureComponent { disableDatesBefore, disableDatesAfter, weekStartsOn = Weekdays.Sunday, - polaris: {intl}, + locale = 'en', } = 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 = Months[showPreviousMonth]; - const nextMonth = multiMonth - ? Months[showNextToNextMonth] - : Months[showNextMonth]; - const nextYear = multiMonth ? showNextToNextYear : showNextYear; + const visibleMonth = new Date(year, month); + const previousVisibleMonth = new Date(year, month - 1); + const nextVisibleMonth = new Date(year, month + 1); + const nextToNextVisibleMonth = new Date(year, month + 2); const secondDatePicker = multiMonth ? ( { disableDatesAfter={disableDatesAfter} allowRange={allowRange} weekStartsOn={weekStartsOn} + locale={locale} /> ) : null; @@ -132,41 +120,38 @@ export class DatePicker extends React.PureComponent { ); } diff --git a/src/components/DatePicker/components/Day/tests/Day.test.tsx b/src/components/DatePicker/components/Day/tests/Day.test.tsx index e0e6ae519ed..6d4cf5772fd 100644 --- a/src/components/DatePicker/components/Day/tests/Day.test.tsx +++ b/src/components/DatePicker/components/Day/tests/Day.test.tsx @@ -68,4 +68,20 @@ describe('', () => { expect(spy).toHaveBeenCalledTimes(1); }); }); + + describe('locale', () => { + it('defaults to en locale if no locale is set', () => { + const day = mountWithAppProvider(); + + expect(day.text()).toEqual('14'); + }); + + it('day is formatted to specified locale', () => { + const day = mountWithAppProvider( + , + ); + + expect(day.text()).toEqual('14日'); + }); + }); }); diff --git a/src/components/DatePicker/components/Month/Month.tsx b/src/components/DatePicker/components/Month/Month.tsx index 0c953f6016e..f4962195364 100644 --- a/src/components/DatePicker/components/Month/Month.tsx +++ b/src/components/DatePicker/components/Month/Month.tsx @@ -2,8 +2,6 @@ import * as React from 'react'; import { Range, Weekdays, - Months, - Year, isDateBefore, isDateAfter, isSameDay, @@ -11,7 +9,6 @@ import { dateIsInRange, dateIsSelected, getNewRange, - abbreviationForWeekday, } from '@shopify/javascript-utilities/dates'; import {noop} from '@shopify/javascript-utilities/other'; import {classNames} from '@shopify/react-utilities/styles'; @@ -20,11 +17,11 @@ import Day from '../Day'; import Weekday from '../Weekday'; export interface Props { + locale?: string; focusedDate?: Date; selected?: Range; hoverDate?: Date; - month: Months; - year: Year; + visibleMonth: Date; disableDatesBefore?: Date; disableDatesAfter?: Date; allowRange?: Boolean; @@ -32,8 +29,6 @@ export interface Props { onChange?(date: Range): void; onHover?(hoverEnd: Date): void; onFocus?(date: Date): void; - monthName?(month: Months): string; - weekdayName?(weekday: Weekdays): string; } const WEEKDAYS = [ @@ -47,6 +42,7 @@ const WEEKDAYS = [ ]; export default function Month({ + locale = 'en', focusedDate, selected, hoverDate, @@ -56,26 +52,40 @@ export default function Month({ onChange = noop, onHover = noop, onFocus = noop, - month, - year, + visibleMonth, weekStartsOn, }: Props) { const isInHoveringRange = allowRange ? hoveringDateIsInRange : () => false; const now = new Date(); - const current = now.getMonth() === month && now.getFullYear() === year; + const current = + now.getMonth() === visibleMonth.getMonth() && + now.getFullYear() === visibleMonth.getFullYear(); const className = classNames( styles.Title, current && styles['Month-current'], ); - const weeks = getWeeksForMonth(month, year, weekStartsOn); - const weekdays = getWeekdaysOrdered(weekStartsOn).map((weekday) => ( - - )); + + const weeks = getWeeksForMonth( + visibleMonth.getMonth(), + visibleMonth.getFullYear(), + weekStartsOn, + ); + + const weekdayFormat = Intl.DateTimeFormat(locale, {weekday: 'short'}); + + const weekdays = getWeekdaysOrdered(weekStartsOn).map((weekday) => { + // October 1, 2017 is a Sunday + const arbitraryWeekdayDate = new Date(2017, 9, weekday + 1); + + return ( + + ); + }); function handleDateClick(selectedDate: Date) { onChange(getNewRange(allowRange && selected, selectedDate)); @@ -83,10 +93,18 @@ export default function Month({ function renderWeek(day: Date, dayIndex: number) { if (day == null) { - const lastDayOfMonth = new Date(year, (month as number) + 1, 0); + const lastDayOfMonth = new Date( + visibleMonth.getFullYear(), + visibleMonth.getMonth() + 1, + 0, + ); return ( - // eslint-disable-next-line react/jsx-no-bind - + ); } @@ -96,6 +114,7 @@ export default function Month({ return (
- {Months[month]} {year} + {Intl.DateTimeFormat(locale, {month: 'long', year: 'numeric'}).format( + visibleMonth, + )}
{weekdays} diff --git a/src/components/DatePicker/components/Month/tests/Month.test.tsx b/src/components/DatePicker/components/Month/tests/Month.test.tsx index 6ecdb186e15..793539b8afb 100644 --- a/src/components/DatePicker/components/Month/tests/Month.test.tsx +++ b/src/components/DatePicker/components/Month/tests/Month.test.tsx @@ -9,21 +9,27 @@ describe('', () => { describe('title', () => { it('passes the correct value to Weekday', () => { const month = mountWithAppProvider( - , + , ); expect( month .find(Weekday) .first() .prop('title'), - ).toBe('Mo'); + ).toBe('Mon'); }); }); describe('label', () => { it('passes the correct value to Weekday', () => { const month = mountWithAppProvider( - , + , ); expect( month @@ -35,15 +41,9 @@ describe('', () => { }); describe('current', () => { - const currentDay = new Date().getDay(); - const currentMonth = new Date().getMonth(); - const currentYear = new Date().getFullYear(); + const today = new Date(); const month = mountWithAppProvider( - , + , ); it('passes true to Weekday if month year and weekStartsOn are today', () => { @@ -57,7 +57,10 @@ describe('', () => { it('passes false to Weekday if month year and weekStartsOn are not today', () => { const month = mountWithAppProvider( - , + , ); expect( month @@ -73,8 +76,7 @@ describe('', () => { const hoverDate = new Date('05 Jan 2018 00:00:00 GMT'); const month = mountWithAppProvider( ', () => { expect(month.find(Day).get(10).props.inHoveringRange).toBeFalsy(); }); }); + + describe('locale', () => { + it('passes locale prop to all Day components', () => { + const month = mountWithAppProvider( + , + ); + + const daysWithExpectedLocale = month.find(Day).filter({locale: 'ja'}); + + expect(daysWithExpectedLocale).toHaveLength(35); + }); + + it('weekday labels default to en locale if no locale is set', () => { + const month = mountWithAppProvider( + , + ); + + expect( + month + .find('div') + .filter({role: 'rowheader'}) + .text(), + ).toEqual('MonTueWedThuFriSatSun'); + }); + + it('weekday labels are formatted to match specified locale', () => { + const month = mountWithAppProvider( + , + ); + + expect( + month + .find('div') + .filter({role: 'rowheader'}) + .text(), + ).toEqual('月火水木金土日'); + }); + + it('contains visible month / year label formatted to en locale if no locale is set', () => { + const month = mountWithAppProvider( + , + ); + + expect(month.text()).toContain('January 2018'); + }); + + it('contains visible month / year label formatted to specified locale', () => { + const month = mountWithAppProvider( + , + ); + + expect(month.text()).toContain('2018年1月'); + }); + }); }); diff --git a/src/components/DatePicker/tests/DatePicker.test.tsx b/src/components/DatePicker/tests/DatePicker.test.tsx index a6bc1b8d3ad..c43a69a645d 100644 --- a/src/components/DatePicker/tests/DatePicker.test.tsx +++ b/src/components/DatePicker/tests/DatePicker.test.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import {noop} from '@shopify/javascript-utilities/other'; import {Weekdays} from '@shopify/javascript-utilities/dates'; -import {mountWithAppProvider} from 'test-utilities'; +import {mountWithAppProvider, trigger} from 'test-utilities'; +import Button from '../../Button'; import {Day, Month, Weekday} from '../components'; import DatePicker from '../DatePicker'; @@ -20,7 +21,7 @@ describe('', () => { ); const weekday = datePicker.find(Weekday); - expect(weekday.first().text()).toEqual('Su'); + expect(weekday.first().text()).toEqual('Sun'); }); describe('when weekStartsOn is passed', () => { @@ -30,7 +31,7 @@ describe('', () => { ); const weekday = datePicker.find(Weekday); - expect(weekday.first().text()).toEqual('Mo'); + expect(weekday.first().text()).toEqual('Mon'); }); it('renders Saturday as first day of the week', () => { @@ -39,7 +40,7 @@ describe('', () => { ); const weekday = datePicker.find(Weekday); - expect(weekday.first().text()).toEqual('Sa'); + expect(weekday.first().text()).toEqual('Sat'); }); }); @@ -50,18 +51,9 @@ describe('', () => { ); const month = datePicker.find(Month); - expect(month.prop('month')).toEqual(1); - }); - }); - - describe('year', () => { - it('passes the correct year to Month', () => { - const datePicker = mountWithAppProvider( - , + expect(month.prop('visibleMonth').valueOf()).toEqual( + new Date(2018, 1).valueOf(), ); - - const year = datePicker.find(Month); - expect(year.prop('year')).toEqual(2016); }); }); @@ -89,8 +81,7 @@ describe('', () => { focusedDate={new Date()} selected={selected} hoverDate={hoverDate} - month={month} - year={year} + visibleMonth={new Date(year, month)} onChange={spy} weekStartsOn={Weekdays.Sunday} />, @@ -139,6 +130,103 @@ describe('', () => { }); }); + describe('onMonthChange', () => { + it('calls onMonthChange with next month arguments when next month button is clicked', () => { + const spy = jest.fn(); + const datePicker = mountWithAppProvider( + , + ); + + const nextMonthButton = datePicker + .find(Button) + .filter({icon: 'arrowRight'}); + + trigger(nextMonthButton, 'onClick'); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(0, 2019); + }); + + it('calls onMonthChange with previous month arguments when previous month button is clicked', () => { + const spy = jest.fn(); + const datePicker = mountWithAppProvider( + , + ); + + const previousMonthButton = datePicker + .find(Button) + .filter({icon: 'arrowLeft'}); + + trigger(previousMonthButton, 'onClick'); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(11, 2017); + }); + }); + + describe('locale', () => { + it('passes locale to Month component', () => { + const datePicker = mountWithAppProvider( + , + ); + + expect(datePicker.find(Month).prop('locale')).toEqual('ja'); + }); + + it('passes locale to both Month components if multiMonth is true', () => { + const datePicker = mountWithAppProvider( + , + ); + + const monthsWithExpectedLocale = datePicker + .find(Month) + .filter({locale: 'ja'}); + + expect(monthsWithExpectedLocale).toHaveLength(2); + }); + + it('button accessibility labels default to en locale if no locale is set', () => { + const datePicker = mountWithAppProvider( + , + ); + + const nextMonthButton = datePicker + .find(Button) + .filter({icon: 'arrowRight'}); + + const previousMonthButton = datePicker + .find(Button) + .filter({icon: 'arrowLeft'}); + + expect(nextMonthButton.prop('accessibilityLabel')).toEqual( + 'December 2018', + ); + expect(previousMonthButton.prop('accessibilityLabel')).toEqual( + 'October 2018', + ); + }); + + it('button accessibility labels are formatted to specified locale', () => { + const datePicker = mountWithAppProvider( + , + ); + + const nextMonthButton = datePicker + .find(Button) + .filter({icon: 'arrowRight'}); + + const previousMonthButton = datePicker + .find(Button) + .filter({icon: 'arrowLeft'}); + + expect(nextMonthButton.prop('accessibilityLabel')).toEqual('2018年12月'); + + expect(previousMonthButton.prop('accessibilityLabel')).toEqual( + '2018年10月', + ); + }); + }); + it('unfocuses currently focused day when selected prop is updated', () => { const selected = { start: new Date(2016, 11, 8), diff --git a/yarn.lock b/yarn.lock index 5349fc1613e..0b72cf888d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7455,6 +7455,11 @@ fstream@^1.0.0, fstream@^1.0.2: mkdirp ">=0.5 0" rimraf "2" +full-icu@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/full-icu/-/full-icu-1.2.1.tgz#28d7f1caafb14cac262364ca584fe7f2044a4ab2" + integrity sha512-E2s1b4GVbt8PyG+iaRN6ks8N0Oy2LOJz7SIMUwWWWx7Mr5Z08hKkfpkKQbOtOGqzkFpckDJHjjZ8qfigN2W86A== + function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" From efad034512b33bc684ef2bc92dca19b0d29c16e3 Mon Sep 17 00:00:00 2001 From: Joshua Allen Date: Wed, 23 Jan 2019 12:17:07 -0500 Subject: [PATCH 2/5] Update Weekday spacing --- src/components/DatePicker/DatePicker.scss | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/DatePicker/DatePicker.scss b/src/components/DatePicker/DatePicker.scss index 7c22a6f4cd8..e46ddcb8e97 100644 --- a/src/components/DatePicker/DatePicker.scss +++ b/src/components/DatePicker/DatePicker.scss @@ -129,15 +129,11 @@ $in-range-border-color: #9ca6de; .Weekday { display: block; flex: 1 0 0%; - padding: spacing(tight); + margin: spacing(tight) auto; background: transparent; font-size: $font-size; color: color('ink', 'lighter'); text-align: center; - - + .Weekday { - margin-left: -1px; - } } .Weekday-current { From 99ef5f2426f73ec71c9af739abcbd93305bb50aa Mon Sep 17 00:00:00 2001 From: Joshua Allen Date: Wed, 23 Jan 2019 12:28:16 -0500 Subject: [PATCH 3/5] Fix re-render of Month on mount --- src/components/DatePicker/DatePicker.tsx | 6 +++-- .../DatePicker/components/Month/Month.tsx | 26 +++++++------------ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/components/DatePicker/DatePicker.tsx b/src/components/DatePicker/DatePicker.tsx index c318f0e4c8a..04a7e8f4b91 100644 --- a/src/components/DatePicker/DatePicker.tsx +++ b/src/components/DatePicker/DatePicker.tsx @@ -96,7 +96,8 @@ export class DatePicker extends React.PureComponent { { locale={locale} onFocus={this.handleFocus} focusedDate={focusDate} - visibleMonth={visibleMonth} + month={visibleMonth.getMonth()} + year={visibleMonth.getFullYear()} selected={deriveRange(selected)} hoverDate={hoverDate} onChange={this.handleDateSelection} diff --git a/src/components/DatePicker/components/Month/Month.tsx b/src/components/DatePicker/components/Month/Month.tsx index f4962195364..517bd11179d 100644 --- a/src/components/DatePicker/components/Month/Month.tsx +++ b/src/components/DatePicker/components/Month/Month.tsx @@ -2,6 +2,8 @@ import * as React from 'react'; import { Range, Weekdays, + Months, + Year, isDateBefore, isDateAfter, isSameDay, @@ -21,7 +23,8 @@ export interface Props { focusedDate?: Date; selected?: Range; hoverDate?: Date; - visibleMonth: Date; + month: Months; + year: Year; disableDatesBefore?: Date; disableDatesAfter?: Date; allowRange?: Boolean; @@ -52,24 +55,19 @@ export default function Month({ onChange = noop, onHover = noop, onFocus = noop, - visibleMonth, + month, + year, weekStartsOn, }: Props) { const isInHoveringRange = allowRange ? hoveringDateIsInRange : () => false; const now = new Date(); - const current = - now.getMonth() === visibleMonth.getMonth() && - now.getFullYear() === visibleMonth.getFullYear(); + const current = now.getMonth() === month && now.getFullYear() === year; const className = classNames( styles.Title, current && styles['Month-current'], ); - const weeks = getWeeksForMonth( - visibleMonth.getMonth(), - visibleMonth.getFullYear(), - weekStartsOn, - ); + const weeks = getWeeksForMonth(month, year, weekStartsOn); const weekdayFormat = Intl.DateTimeFormat(locale, {weekday: 'short'}); @@ -93,11 +91,7 @@ export default function Month({ function renderWeek(day: Date, dayIndex: number) { if (day == null) { - const lastDayOfMonth = new Date( - visibleMonth.getFullYear(), - visibleMonth.getMonth() + 1, - 0, - ); + const lastDayOfMonth = new Date(year, month + 1, 0); return (
{Intl.DateTimeFormat(locale, {month: 'long', year: 'numeric'}).format( - visibleMonth, + new Date(year, month), )}
From 06b780ff07bfa8731de13c89567ef00e5ff33ecb Mon Sep 17 00:00:00 2001 From: Joshua Allen Date: Wed, 23 Jan 2019 16:06:15 -0500 Subject: [PATCH 4/5] fix Month tests --- .../components/Month/tests/Month.test.tsx | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/components/DatePicker/components/Month/tests/Month.test.tsx b/src/components/DatePicker/components/Month/tests/Month.test.tsx index 793539b8afb..8a9b7410d13 100644 --- a/src/components/DatePicker/components/Month/tests/Month.test.tsx +++ b/src/components/DatePicker/components/Month/tests/Month.test.tsx @@ -9,10 +9,7 @@ describe('', () => { describe('title', () => { it('passes the correct value to Weekday', () => { const month = mountWithAppProvider( - , + , ); expect( month @@ -26,10 +23,7 @@ describe('', () => { describe('label', () => { it('passes the correct value to Weekday', () => { const month = mountWithAppProvider( - , + , ); expect( month @@ -43,7 +37,11 @@ describe('', () => { describe('current', () => { const today = new Date(); const month = mountWithAppProvider( - , + , ); it('passes true to Weekday if month year and weekStartsOn are today', () => { @@ -57,10 +55,7 @@ describe('', () => { it('passes false to Weekday if month year and weekStartsOn are not today', () => { const month = mountWithAppProvider( - , + , ); expect( month @@ -76,7 +71,8 @@ describe('', () => { const hoverDate = new Date('05 Jan 2018 00:00:00 GMT'); const month = mountWithAppProvider( ', () => { const month = mountWithAppProvider( ', () => { it('weekday labels default to en locale if no locale is set', () => { const month = mountWithAppProvider( ', () => { const month = mountWithAppProvider( ', () => { it('contains visible month / year label formatted to en locale if no locale is set', () => { const month = mountWithAppProvider( ', () => { const month = mountWithAppProvider( Date: Thu, 24 Jan 2019 11:10:10 -0500 Subject: [PATCH 5/5] Fix DatePicker tests --- src/components/DatePicker/tests/DatePicker.test.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/DatePicker/tests/DatePicker.test.tsx b/src/components/DatePicker/tests/DatePicker.test.tsx index c43a69a645d..7bee44d9dc2 100644 --- a/src/components/DatePicker/tests/DatePicker.test.tsx +++ b/src/components/DatePicker/tests/DatePicker.test.tsx @@ -51,9 +51,9 @@ describe('', () => { ); const month = datePicker.find(Month); - expect(month.prop('visibleMonth').valueOf()).toEqual( - new Date(2018, 1).valueOf(), - ); + + expect(month.prop('month')).toEqual(1); + expect(month.prop('year')).toEqual(2018); }); }); @@ -81,7 +81,8 @@ describe('', () => { focusedDate={new Date()} selected={selected} hoverDate={hoverDate} - visibleMonth={new Date(year, month)} + month={month} + year={year} onChange={spy} weekStartsOn={Weekdays.Sunday} />,