Skip to content

Commit 9139ffc

Browse files
[LENS-606] Update Calendar components to accept external localization strings (#1019)
1 parent 28cdb2d commit 9139ffc

File tree

18 files changed

+459
-275
lines changed

18 files changed

+459
-275
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
### Fixed
1616

1717
- `ButtonGroup` and `ButtonToggle` accessibility issues due to hidden `input`s (they now render a list of `button`s instead)
18+
- `Calendar`, `InputDate`, and `InputDateRange` localization props
1819

1920
### Removed
2021

packages/components/src/Calendar/Calendar.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,26 @@
2424
2525
*/
2626
import React, { FC } from 'react'
27-
import DayPicker, { RangeModifier } from 'react-day-picker'
27+
import DayPicker, { RangeModifier, LocaleUtils } from 'react-day-picker'
2828
import 'react-day-picker/lib/style.css'
2929
import styled from 'styled-components'
3030
import has from 'lodash/has'
3131
import { mix } from 'polished'
3232
import noop from 'lodash/noop'
3333
import { reset } from '@looker/design-tokens'
34-
import { LocaleCodes } from '../utils/i18n'
3534
import { inputTextFocus } from '../Form/Inputs/InputText'
3635
import { CalendarSize, calendarSize, calendarSpacing } from './calendar-size'
3736
import { CalendarContext } from './CalendarContext'
3837
import { CalendarNav } from './CalendarNav'
3938

39+
export interface CalendarLocalization {
40+
months: string[]
41+
weekdaysShort: string[]
42+
firstDayOfWeek: number
43+
}
44+
4045
interface CalendarProps {
41-
locale?: LocaleCodes
46+
localization?: CalendarLocalization
4247
selectedDates?: Date | Date[] | RangeModifier
4348
onDayClick?: (day: Date) => void
4449
className?: string
@@ -56,7 +61,7 @@ interface CalendarProps {
5661
const NoopComponent: FC = () => null
5762

5863
const InternalCalendar: FC<CalendarProps> = ({
59-
locale = 'en',
64+
localization = {},
6065
onDayClick,
6166
className,
6267
size,
@@ -73,11 +78,19 @@ const InternalCalendar: FC<CalendarProps> = ({
7378
const renderDateRange = selectedDates && has(selectedDates, 'from')
7479
const modifiers = renderDateRange ? selectedDates : {}
7580

76-
const disableCallback = (cb: any) => {
81+
const disableCallback = (cb: Function = noop) => {
7782
// allows provided callback to be circumvented by disabled prop
7883
return (...args: any[]) => (disabled ? noop() : cb(...args)) // eslint-disable-line standard/no-callback-literal
7984
}
8085

86+
const formatMonthTitle = (month: Date) => {
87+
if (localization.months) {
88+
return `${localization.months[month.getMonth()]} ${month.getFullYear()}`
89+
} else {
90+
return LocaleUtils.formatMonthTitle(month)
91+
}
92+
}
93+
8194
return (
8295
<CalendarContext.Provider
8396
value={{
@@ -90,9 +103,10 @@ const InternalCalendar: FC<CalendarProps> = ({
90103
}}
91104
>
92105
<DayPicker
106+
{...localization}
107+
localeUtils={{ ...LocaleUtils, formatMonthTitle }}
93108
className={`${renderDateRange && 'render-date-range'} ${className}`}
94109
selectedDays={selectedDates}
95-
locale={locale}
96110
month={viewMonth}
97111
showOutsideDays={true}
98112
onDayClick={disableCallback(onDayClick)}

packages/components/src/Calendar/CalendarNav.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ export const CalendarNav: FC<NavbarElementProps> = ({
5050
month,
5151
labels,
5252
localeUtils,
53-
locale,
5453
nextMonth,
5554
previousMonth,
5655
}) => {
@@ -94,7 +93,7 @@ export const CalendarNav: FC<NavbarElementProps> = ({
9493
<Tooltip content="View Current Month">
9594
<ButtonTransparent onClick={handleLabelClick} color="neutral">
9695
<Heading as={headingSizeMap(size)} fontWeight="semiBold">
97-
{localeUtils.formatMonthTitle(month, locale)}
96+
{localeUtils.formatMonthTitle(month)}
9897
</Heading>
9998
</ButtonTransparent>
10099
</Tooltip>

packages/components/src/Form/Inputs/InputDate/InputDate.test.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import React from 'react'
2828
import { fireEvent } from '@testing-library/react'
2929
import { renderWithTheme } from '@looker/components-test-utils'
3030

31+
import { Locales } from '../../../utils'
3132
import { InputDate } from './InputDate'
3233

3334
const realDateNow = Date.now.bind(global.Date)
@@ -114,3 +115,63 @@ test('validates text input to match localized date format', () => {
114115

115116
expect(mockProps.onValidationFail).toHaveBeenCalledTimes(1)
116117
})
118+
119+
test('localizes calendar', () => {
120+
const months = [
121+
'Gennaio',
122+
'Febbraio',
123+
'Marzo',
124+
'Aprile',
125+
'Maggio',
126+
'Giugno',
127+
'Luglio',
128+
'Agosto',
129+
'Settembre',
130+
'Ottobre',
131+
'Novembre',
132+
'Dicembre',
133+
]
134+
const weekdaysShort = ['Do', 'Lu', 'Ma', 'Me', 'Gi', 'Ve', 'Sa']
135+
const firstDayOfWeek = 1 // monday
136+
const localizationProps = { firstDayOfWeek, months, weekdaysShort }
137+
138+
const { getByText, container } = renderWithTheme(
139+
<InputDate localization={localizationProps} />
140+
)
141+
142+
expect(getByText('Febbraio 2020')).toBeInTheDocument()
143+
expect(
144+
(container.querySelector('.DayPicker-WeekdaysRow') as HTMLElement)
145+
.textContent
146+
).toMatchInlineSnapshot(`"LuMaMeGiVeSaDo"`)
147+
})
148+
149+
describe('localizes text input', () => {
150+
test('Korean', () => {
151+
const { getByDisplayValue } = renderWithTheme(
152+
<InputDate
153+
dateStringLocale={Locales.Korean}
154+
defaultValue={new Date(Date.now())}
155+
/>
156+
)
157+
expect(getByDisplayValue('2020.02.01')).toBeInTheDocument()
158+
})
159+
test('Italian', () => {
160+
const { getByDisplayValue } = renderWithTheme(
161+
<InputDate
162+
dateStringLocale={Locales.Italian}
163+
defaultValue={new Date(Date.now())}
164+
/>
165+
)
166+
expect(getByDisplayValue('01/02/2020')).toBeInTheDocument()
167+
})
168+
test('English', () => {
169+
const { getByDisplayValue } = renderWithTheme(
170+
<InputDate
171+
dateStringLocale={Locales.English}
172+
defaultValue={new Date(Date.now())}
173+
/>
174+
)
175+
expect(getByDisplayValue('02/01/2020')).toBeInTheDocument()
176+
})
177+
})

packages/components/src/Form/Inputs/InputDate/InputDate.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,9 @@ import isFunction from 'lodash/isFunction'
3636
import isEqual from 'lodash/isEqual'
3737
import { BorderProps, SpaceProps } from '@looker/design-tokens'
3838
import { InputText } from '../InputText'
39-
import { Calendar } from '../../../Calendar'
39+
import { Calendar, CalendarLocalization } from '../../../Calendar'
4040
import { ValidationType } from '../../ValidationMessage'
4141
import {
42-
LocaleCodes,
4342
Locales,
4443
formatDateString,
4544
parseDateFromString,
@@ -52,7 +51,8 @@ export interface InputDateProps extends SpaceProps, BorderProps {
5251
onChange?: (date?: Date) => void
5352
validationType?: ValidationType
5453
onValidationFail?: (value: string) => void
55-
locale?: LocaleCodes
54+
localization?: CalendarLocalization
55+
dateStringLocale?: Locales
5656
id?: string
5757
ref?: Ref<HTMLInputElement>
5858
disabled?: boolean
@@ -78,7 +78,8 @@ export const InputDate: FC<InputDateProps> = forwardRef(
7878
{
7979
onChange,
8080
defaultValue,
81-
locale = Locales.English,
81+
localization,
82+
dateStringLocale,
8283
validationType,
8384
onValidationFail,
8485
value,
@@ -92,7 +93,7 @@ export const InputDate: FC<InputDateProps> = forwardRef(
9293
const [selectedDate, setSelectedDate] = useState(value || defaultValue)
9394
const [validDate, setValidDate] = useState(validationType !== 'error')
9495
const [textInputValue, setTextInputValue] = useState(
95-
selectedDate ? formatDateString(selectedDate, locale) : ''
96+
selectedDate ? formatDateString(selectedDate, dateStringLocale) : ''
9697
)
9798
const [viewMonth, setViewMonth] = useState<Date | undefined>(
9899
value || defaultValue || new Date(Date.now())
@@ -110,7 +111,7 @@ export const InputDate: FC<InputDateProps> = forwardRef(
110111
}
111112

112113
const handleDayClick = (date: Date) => {
113-
setTextInputValue(formatDateString(date, locale))
114+
setTextInputValue(formatDateString(date, dateStringLocale))
114115
handleDateChange(date)
115116
}
116117

@@ -121,7 +122,7 @@ export const InputDate: FC<InputDateProps> = forwardRef(
121122
if (value.length === 0) {
122123
handleDateChange()
123124
} else {
124-
const parsedValue = parseDateFromString(value, locale)
125+
const parsedValue = parseDateFromString(value, dateStringLocale)
125126
if (parsedValue) {
126127
handleDateChange(parsedValue)
127128
}
@@ -134,7 +135,7 @@ export const InputDate: FC<InputDateProps> = forwardRef(
134135
const value = (e.target as HTMLInputElement).value
135136
// is valid if text input is blank or parseDateFromString returns a date object
136137
const isValid =
137-
value.length === 0 || !!parseDateFromString(value, locale)
138+
value.length === 0 || !!parseDateFromString(value, dateStringLocale)
138139
setValidDate(isValid)
139140
if (!isValid && isFunction(onValidationFail)) {
140141
onValidationFail(value)
@@ -153,7 +154,7 @@ export const InputDate: FC<InputDateProps> = forwardRef(
153154
// controlled component: update state when value changes externally
154155
if (value && !isEqual(value, selectedDate)) {
155156
setSelectedDate(value)
156-
value && setTextInputValue(formatDateString(value, locale))
157+
value && setTextInputValue(formatDateString(value, dateStringLocale))
157158
value &&
158159
viewMonth &&
159160
!isDateInView(value, viewMonth) &&
@@ -167,7 +168,7 @@ export const InputDate: FC<InputDateProps> = forwardRef(
167168
<InputText
168169
placeholder={`Date (${formatDateString(
169170
new Date(Date.now()),
170-
locale
171+
dateStringLocale
171172
)})`}
172173
value={textInputValue}
173174
onChange={handleTextInputChange}
@@ -183,7 +184,7 @@ export const InputDate: FC<InputDateProps> = forwardRef(
183184
<Calendar
184185
selectedDates={selectedDate}
185186
onDayClick={handleDayClick}
186-
locale={locale}
187+
localization={localization}
187188
viewMonth={viewMonth}
188189
onNowClick={handleNavClick}
189190
onNextClick={handleNavClick}

packages/components/src/Form/Inputs/InputDateRange/InputDateRange.test.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import React from 'react'
2828
import { fireEvent } from '@testing-library/react'
2929
import { renderWithTheme } from '@looker/components-test-utils'
3030

31+
import { Locales } from '../../../utils'
3132
import { InputDateRange } from './InputDateRange'
3233

3334
const realDateNow = Date.now.bind(global.Date)
@@ -205,3 +206,72 @@ test('validates TO text input to match localized date format', () => {
205206

206207
expect(mockProps.onValidationFail).toHaveBeenCalledTimes(1)
207208
})
209+
210+
test('localizes calendar', () => {
211+
const months = [
212+
'Gennaio',
213+
'Febbraio',
214+
'Marzo',
215+
'Aprile',
216+
'Maggio',
217+
'Giugno',
218+
'Luglio',
219+
'Agosto',
220+
'Settembre',
221+
'Ottobre',
222+
'Novembre',
223+
'Dicembre',
224+
]
225+
const weekdaysShort = ['Do', 'Lu', 'Ma', 'Me', 'Gi', 'Ve', 'Sa']
226+
const firstDayOfWeek = 1 // monday
227+
const localizationProps = { firstDayOfWeek, months, weekdaysShort }
228+
229+
const { getByText, container } = renderWithTheme(
230+
<InputDateRange localization={localizationProps} />
231+
)
232+
233+
expect(getByText('Febbraio 2020')).toBeInTheDocument()
234+
expect(
235+
(container.querySelector('.DayPicker-WeekdaysRow') as HTMLElement)
236+
.textContent
237+
).toMatchInlineSnapshot(`"LuMaMeGiVeSaDo"`)
238+
})
239+
240+
describe('localizes text input', () => {
241+
test('Korean', () => {
242+
const { getByDisplayValue } = renderWithTheme(
243+
<InputDateRange
244+
dateStringLocale={Locales.Korean}
245+
defaultValue={{
246+
from: new Date(Date.now()),
247+
to: new Date('May 2, 2020'),
248+
}}
249+
/>
250+
)
251+
expect(getByDisplayValue('2020.02.01')).toBeInTheDocument()
252+
})
253+
test('Italian', () => {
254+
const { getByDisplayValue } = renderWithTheme(
255+
<InputDateRange
256+
dateStringLocale={Locales.Italian}
257+
defaultValue={{
258+
from: new Date(Date.now()),
259+
to: new Date('May 2, 2020'),
260+
}}
261+
/>
262+
)
263+
expect(getByDisplayValue('01/02/2020')).toBeInTheDocument()
264+
})
265+
test('English', () => {
266+
const { getByDisplayValue } = renderWithTheme(
267+
<InputDateRange
268+
dateStringLocale={Locales.English}
269+
defaultValue={{
270+
from: new Date(Date.now()),
271+
to: new Date('May 2, 2020'),
272+
}}
273+
/>
274+
)
275+
expect(getByDisplayValue('02/01/2020')).toBeInTheDocument()
276+
})
277+
})

0 commit comments

Comments
 (0)