Skip to content

Commit

Permalink
feat(datepickers): Enable setting locale from prop. (#3626)
Browse files Browse the repository at this point in the history
* feat(datepickers): Enable setting locale from prop. Resolves #3596

* feat(datepickers): Check for navigator-existence to work with SSR

* docs(datepickers): Document how to specify locale
  • Loading branch information
yusijs authored Sep 16, 2024
1 parent 5f8015c commit 01184ec
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,17 @@ describe('DatePicker', () => {

expect(screen.getByRole('presentation')).toHaveTextContent('04.05.2024')

rerender(
<DatePicker
locale={'no'}
label={'Datepicker'}
value={date}
isDateUnavailable={(d) => d.getDate() === 31}
/>,
)

expect(screen.getByRole('presentation')).toHaveTextContent('04.05.2024')

rerender(
<I18nProvider locale={'zh-Hans-SG'}>
<DatePicker
Expand Down
102 changes: 53 additions & 49 deletions packages/eds-core-react/src/components/Datepicker/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ import {
toCalendarDate,
toCalendarDateTime,
} from '@internationalized/date'
import { useDatePicker, useLocale } from 'react-aria'
import { I18nProvider, useDatePicker } from 'react-aria'
import { useDatePickerState } from '@react-stately/datepicker'
import { DatePickerProvider, defaultTimezone } from './utils/context'
import { tokens } from '@equinor/eds-tokens'
import { Icon } from '../Icon'
import { getCalendarDate } from './utils/get-calendar-date'
import { useGetLocale } from './utils/useGetLocale'

/**
* DatePicker component encapsulates the logic for selecting a single date.
Expand All @@ -44,6 +45,7 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
Footer,
Header,
timezone,
locale: propLocale,
defaultValue,
showTimeInput,
granularity,
Expand Down Expand Up @@ -119,15 +121,15 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(

const _value = getCalendarDate(value, timezone, showTimeInput) ?? innerValue

const { locale } = useLocale()
const locale = useGetLocale(propLocale)

const dateCreateProps = {
helperProps,
variant,
isDisabled,
value: _value,
hideTimeZone: true,
locale,
locale: locale,
createCalendar,
onChange: _onChange,
minValue: _minValue,
Expand Down Expand Up @@ -158,55 +160,57 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
}, [defaultValue, value])

return (
<DatePickerProvider timezone={timezone} formatOptions={formatOptions}>
<FieldWrapper
{...props}
isOpen={isOpen}
readonly={fieldProps.isReadOnly}
pickerRef={pickerRef}
ref={ref}
setIsOpen={setIsOpen}
label={label}
calendar={
<Calendar
ref={pickerRef}
Footer={Footer}
Header={Header}
{...calendarProps}
/>
}
disabled={isDisabled}
readOnly={isReadOnly}
color={pickerState.isInvalid ? 'warning' : variant}
helperProps={helperPropsInvalid ?? helperProps}
>
<DateField
fieldProps={fieldProps}
groupProps={groupProps}
dateCreateProps={dateCreateProps}
<I18nProvider locale={locale}>
<DatePickerProvider timezone={timezone} formatOptions={formatOptions}>
<FieldWrapper
{...props}
isOpen={isOpen}
readonly={fieldProps.isReadOnly}
pickerRef={pickerRef}
ref={ref}
onChange={_onChange}
rightAdornments={
<Toggle
showClearButton={showClearButton}
setOpen={setIsOpen}
open={isOpen}
icon={calendar}
disabled={isDisabled}
readonly={isReadOnly}
reset={() => _onChange(null)}
buttonProps={buttonProps}
valueString={pickerState.formatValue(locale, {
year: 'numeric',
month: 'short',
day: '2-digit',
})}
setIsOpen={setIsOpen}
label={label}
calendar={
<Calendar
ref={pickerRef}
Footer={Footer}
Header={Header}
{...calendarProps}
/>
}
variant={variant}
/>
</FieldWrapper>
</DatePickerProvider>
disabled={isDisabled}
readOnly={isReadOnly}
color={pickerState.isInvalid ? 'warning' : variant}
helperProps={helperPropsInvalid ?? helperProps}
>
<DateField
fieldProps={fieldProps}
groupProps={groupProps}
dateCreateProps={dateCreateProps}
ref={ref}
onChange={_onChange}
rightAdornments={
<Toggle
showClearButton={showClearButton}
setOpen={setIsOpen}
open={isOpen}
icon={calendar}
disabled={isDisabled}
readonly={isReadOnly}
reset={() => _onChange(null)}
buttonProps={buttonProps}
valueString={pickerState.formatValue(locale, {
year: 'numeric',
month: 'short',
day: '2-digit',
})}
/>
}
variant={variant}
/>
</FieldWrapper>
</DatePickerProvider>
</I18nProvider>
)
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { calendar_date_range, warning_outlined } from '@equinor/eds-icons'
import { useConvertedValidationFunctions } from './utils/useConvertedValidationFunctions'
import { FieldWrapper } from './fields/FieldWrapper'
import { Toggle } from './fields/Toggle'
import { DateValue, useDateRangePicker, useLocale } from 'react-aria'
import { DateValue, I18nProvider, useDateRangePicker } from 'react-aria'
import {
DateRangePickerStateOptions,
useDateRangePickerState,
Expand All @@ -24,6 +24,7 @@ import { DatePickerProvider, defaultTimezone } from './utils/context'
import { tokens } from '@equinor/eds-tokens'
import { Icon } from '../Icon'
import { getCalendarDate } from './utils/get-calendar-date'
import { useGetLocale } from './utils/useGetLocale'

/**
* DateRangePicker component encapsulates the logic for selecting a range of dates
Expand All @@ -42,6 +43,7 @@ export const DateRangePicker = forwardRef(
Header,
timezone,
defaultValue,
locale: propLocale,
formatOptions,
hideClearButton,
disabled: isDisabled,
Expand Down Expand Up @@ -73,7 +75,7 @@ export const DateRangePicker = forwardRef(
const inputRef = useRef(null)
const pickerRef = useRef(null)
const ref = forwardedRef || inputRef
const { locale } = useLocale()
const locale = useGetLocale(propLocale)

const { _minValue, _maxValue, _isDateUnavailable } =
useConvertedValidationFunctions(
Expand Down Expand Up @@ -158,54 +160,56 @@ export const DateRangePicker = forwardRef(
}, [defaultValue, value])

return (
<DatePickerProvider timezone={timezone} formatOptions={formatOptions}>
<FieldWrapper
{...props}
isOpen={isOpen}
color={state.isInvalid ? 'warning' : props.variant}
helperProps={helperProps ?? props.helperProps}
readonly={startFieldProps.isReadOnly}
ref={ref}
pickerRef={pickerRef}
setIsOpen={setIsOpen}
label={label}
calendar={
<RangeCalendar
ref={pickerRef}
maxValue={_maxValue}
minValue={_minValue}
isDateUnavailable={_isDateUnavailable}
Footer={Footer}
Header={Header}
{...calendarProps}
/>
}
>
<DateRangeField
startFieldProps={startFieldProps}
endFieldProps={endFieldProps}
groupProps={groupProps}
<I18nProvider locale={locale}>
<DatePickerProvider timezone={timezone} formatOptions={formatOptions}>
<FieldWrapper
{...props}
isOpen={isOpen}
color={state.isInvalid ? 'warning' : props.variant}
helperProps={helperProps ?? props.helperProps}
readonly={startFieldProps.isReadOnly}
ref={ref}
variant={props.variant}
disabled={isDisabled}
rightAdornments={
<Toggle
showClearButton={showClearButton}
buttonProps={buttonProps}
disabled={isDisabled}
readonly={isReadOnly}
reset={() => {
_onChange(null)
}}
setOpen={setIsOpen}
open={isOpen}
icon={calendar_date_range}
valueString={valueString}
pickerRef={pickerRef}
setIsOpen={setIsOpen}
label={label}
calendar={
<RangeCalendar
ref={pickerRef}
maxValue={_maxValue}
minValue={_minValue}
isDateUnavailable={_isDateUnavailable}
Footer={Footer}
Header={Header}
{...calendarProps}
/>
}
/>
</FieldWrapper>
</DatePickerProvider>
>
<DateRangeField
startFieldProps={startFieldProps}
endFieldProps={endFieldProps}
groupProps={groupProps}
ref={ref}
variant={props.variant}
disabled={isDisabled}
rightAdornments={
<Toggle
showClearButton={showClearButton}
buttonProps={buttonProps}
disabled={isDisabled}
readonly={isReadOnly}
reset={() => {
_onChange(null)
}}
setOpen={setIsOpen}
open={isOpen}
icon={calendar_date_range}
valueString={valueString}
/>
}
/>
</FieldWrapper>
</DatePickerProvider>
</I18nProvider>
)
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@ For example, to display the date as `dd MMMM yyyy`:
### Custom locale

In some cases it's useful to display the date in a different locale than the default, e.g. if your app has a locale-picker.
This can be done by wrapping the component with [I18nProvider](https://react-spectrum.adobe.com/react-aria/I18nProvider.html) from react-aria, which supports all the locales that Intl.DateTimeFormat does.

Currently the locale is selected on a priority basis:
* The locale passed to the DatePicker component as a prop: `<DatePicker locale="en-US" />`
* The locale defined in react-aria's `I18nProvider`:
```
<I18nProvider locale={'en-US'}>
<DatePicker />
</I18nProvider>
```
* The locale resolved from your system setup (`Intl.DateTimeFormat().resolvedOptions().locale`)

<Canvas of={ComponentStories.CustomLocale} />

Expand Down
5 changes: 5 additions & 0 deletions packages/eds-core-react/src/components/Datepicker/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ export type DatePickerProps = Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> &
* Only applies when input is blurred
*/
formatOptions?: DateFormatterOptions
/**
* The locale to use for formatting the date.
* Defaults to browser's language setting
*/
locale?: string
}>

export type DateTimePickerProps = Omit<
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useLocale } from 'react-aria'

export const useGetLocale = (locale?: string) => {
const { locale: externalLocale } = useLocale()
// react-aria defaults to navigator.language if no locale is provided. If these are equal, we override by using the system default locale
const defaultLocale =
(typeof navigator !== 'undefined' && navigator.language) || 'en-US'
const fallbackLocale = new Intl.DateTimeFormat().resolvedOptions().locale
return (
locale ??
(externalLocale === defaultLocale ? undefined : externalLocale) ??
fallbackLocale
)
}

0 comments on commit 01184ec

Please sign in to comment.