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.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 { diff --git a/src/components/DatePicker/DatePicker.tsx b/src/components/DatePicker/DatePicker.tsx index f93caa68569..04a7e8f4b91 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,22 @@ 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 +121,39 @@ 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..517bd11179d 100644 --- a/src/components/DatePicker/components/Month/Month.tsx +++ b/src/components/DatePicker/components/Month/Month.tsx @@ -11,7 +11,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,6 +19,7 @@ import Day from '../Day'; import Weekday from '../Weekday'; export interface Props { + locale?: string; focusedDate?: Date; selected?: Range; hoverDate?: Date; @@ -32,8 +32,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 +45,7 @@ const WEEKDAYS = [ ]; export default function Month({ + locale = 'en', focusedDate, selected, hoverDate, @@ -67,15 +66,24 @@ export default function Month({ styles.Title, current && styles['Month-current'], ); + const weeks = getWeeksForMonth(month, year, weekStartsOn); - const weekdays = getWeekdaysOrdered(weekStartsOn).map((weekday) => ( - - )); + + 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 +91,14 @@ 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(year, month + 1, 0); return ( - // eslint-disable-next-line react/jsx-no-bind - + ); } @@ -96,6 +108,7 @@ export default function Month({ return (
- {Months[month]} {year} + {Intl.DateTimeFormat(locale, {month: 'long', year: 'numeric'}).format( + new Date(year, month), + )}
{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..8a9b7410d13 100644 --- a/src/components/DatePicker/components/Month/tests/Month.test.tsx +++ b/src/components/DatePicker/components/Month/tests/Month.test.tsx @@ -16,7 +16,7 @@ describe('', () => { .find(Weekday) .first() .prop('title'), - ).toBe('Mo'); + ).toBe('Mon'); }); }); @@ -35,14 +35,12 @@ 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( , ); @@ -57,7 +55,7 @@ describe('', () => { it('passes false to Weekday if month year and weekStartsOn are not today', () => { const month = mountWithAppProvider( - , + , ); expect( month @@ -89,4 +87,106 @@ describe('', () => { 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..7bee44d9dc2 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( - , - ); - - const year = datePicker.find(Month); - expect(year.prop('year')).toEqual(2016); + expect(month.prop('month')).toEqual(1); + expect(month.prop('year')).toEqual(2018); }); }); @@ -139,6 +131,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"