diff --git a/packages/eds-core-react/jest.setup.ts b/packages/eds-core-react/jest.setup.ts index 60c86044c2..2c0c6bf101 100644 --- a/packages/eds-core-react/jest.setup.ts +++ b/packages/eds-core-react/jest.setup.ts @@ -7,3 +7,6 @@ expect.extend(toHaveNoViolations) // Workaround for jest-axe error: https://github.com/nickcolley/jest-axe/issues/147 const { getComputedStyle } = window window.getComputedStyle = (elt) => getComputedStyle(elt) + +HTMLElement.prototype.showPopover = jest.fn() +HTMLElement.prototype.hidePopover = jest.fn() diff --git a/packages/eds-core-react/src/components/Autocomplete/Autocomplete.test.tsx b/packages/eds-core-react/src/components/Autocomplete/Autocomplete.test.tsx index 5b958c5f36..b67233820e 100644 --- a/packages/eds-core-react/src/components/Autocomplete/Autocomplete.test.tsx +++ b/packages/eds-core-react/src/components/Autocomplete/Autocomplete.test.tsx @@ -21,8 +21,6 @@ const mockResizeObserver = jest.fn(() => ({ beforeAll(() => { window.ResizeObserver = mockResizeObserver - HTMLDivElement.prototype.showPopover = jest.fn() - HTMLDivElement.prototype.hidePopover = jest.fn() //https://github.com/TanStack/virtual/issues/641 // eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/packages/eds-core-react/src/components/Datepicker/DatePicker.spec.tsx b/packages/eds-core-react/src/components/Datepicker/DatePicker.spec.tsx index a374b74876..75aa77da31 100644 --- a/packages/eds-core-react/src/components/Datepicker/DatePicker.spec.tsx +++ b/packages/eds-core-react/src/components/Datepicker/DatePicker.spec.tsx @@ -3,6 +3,7 @@ import { axe } from 'jest-axe' import { DatePicker } from './DatePicker' import userEvent from '@testing-library/user-event' import { I18nProvider } from 'react-aria' +import { useState } from 'react' describe('DatePicker', () => { it('Can render', () => { @@ -27,6 +28,24 @@ describe('DatePicker', () => { expect(screen.getByText('dd')).toHaveAttribute('aria-disabled', 'true') }) + it('should be nullable from outside', async () => { + const Comp = () => { + const [date, setDate] = useState(new Date(2024, 4, 1)) + return ( + + + + + ) + } + render() + expect(screen.getByRole('presentation')).toHaveTextContent('05/01/2024') + await userEvent.click(screen.getByText('Reset')) + expect(screen.getByRole('presentation')).toHaveTextContent('mm/dd/yyyy') + }) + it('Should be possible to type', async () => { const onChange = jest.fn() @@ -195,6 +214,6 @@ describe('DatePicker', () => { , ) - expect(screen.getByRole('presentation')).toHaveTextContent('2024年05月04日') + expect(screen.getByRole('presentation')).toHaveTextContent('2024年05年04年') }) }) diff --git a/packages/eds-core-react/src/components/Datepicker/DatePicker.stories.tsx b/packages/eds-core-react/src/components/Datepicker/DatePicker.stories.tsx index a8e458dada..74bbdcbed8 100644 --- a/packages/eds-core-react/src/components/Datepicker/DatePicker.stories.tsx +++ b/packages/eds-core-react/src/components/Datepicker/DatePicker.stories.tsx @@ -8,6 +8,7 @@ import { Autocomplete } from '../Autocomplete' import { NativeSelect } from '../Select' import { action } from '@storybook/addon-actions' import { CalendarDate } from '@internationalized/date' +import { I18nProvider } from 'react-aria' const meta: Meta = { title: 'Inputs/Dates/Datepicker', @@ -46,7 +47,7 @@ export const Introduction: StoryFn = (props: DatePickerProps) => { { - const str = v.toISOString() + const str = v?.toISOString() action('onChange')(str) }} /> @@ -82,6 +83,67 @@ export const DateTime: StoryFn = () => { ) } +export const CustomDisplayFormat: StoryFn = () => { + const [val, setValue] = useState(new Date()) + return ( + { + setValue(v) + action('onChange')(v?.toISOString()) + }} + /> + ) +} + +export const CustomLocale: StoryFn = () => { + const [val, setValue] = useState(new Date()) + const [locale, setLocale] = useState('en-US') + const locales = [ + { value: 'en-US', label: 'English' }, + { value: 'uk', label: 'Ukrainian' }, + { value: 'sv-SE', label: 'Swedish' }, + { value: 'zh-Hans', label: 'Chinese (Simplified)' }, + { value: 'zh-Hant', label: 'Chinese (Traditional)' }, + ] + return ( +
+ setLocale(e.currentTarget.value)} + value={locale} + > + {locales.map((l) => ( + + ))} + {' '} +
+ + { + setValue(v) + action('onChange')(v?.toISOString()) + }} + /> + +
+ ) +} + const minusOneMonth = new Date() minusOneMonth.setMonth(minusOneMonth.getMonth() - 1) const plusOneMonth = new Date() diff --git a/packages/eds-core-react/src/components/Datepicker/DatePicker.tsx b/packages/eds-core-react/src/components/Datepicker/DatePicker.tsx index 60feea4351..d017fd0790 100644 --- a/packages/eds-core-react/src/components/Datepicker/DatePicker.tsx +++ b/packages/eds-core-react/src/components/Datepicker/DatePicker.tsx @@ -1,4 +1,11 @@ -import { forwardRef, RefObject, useCallback, useRef, useState } from 'react' +import { + forwardRef, + RefObject, + useCallback, + useEffect, + useRef, + useState, +} from 'react' import { DatePickerProps } from './props' import { Calendar } from './calendars/Calendar' import { DateField } from './fields/DateField' @@ -42,11 +49,17 @@ export const DatePicker = forwardRef( granularity, disabled: isDisabled, readOnly: isReadOnly, + formatOptions, ...props }: DatePickerProps, forwardedRef: RefObject, ) => { timezone = timezone ?? defaultTimezone + formatOptions = formatOptions ?? { + year: 'numeric', + month: '2-digit', + day: '2-digit', + } const [innerValue, setInnerValue] = useState< CalendarDate | CalendarDateTime >(() => { @@ -133,8 +146,13 @@ export const DatePicker = forwardRef( } : undefined + // innerValue is used as a fallback, especially for uncontrolled inputs, so it needs to be reset when value / defaultValue is reset + useEffect(() => { + if (!defaultValue && !value) setInnerValue(null) + }, [defaultValue, value]) + return ( - + , ) => { timezone = timezone ?? defaultTimezone + formatOptions = formatOptions ?? { + year: 'numeric', + month: '2-digit', + day: '2-digit', + } const [innerValue, setInnerValue] = useState>(() => { const initialValue = value ?? defaultValue if (initialValue) { @@ -134,8 +147,15 @@ export const DateRangePicker = forwardRef( ? Object.values(formattedValue).join(' - ') : null + // innerValue is used as a fallback, especially for uncontrolled inputs, so it needs to be reset when value / defaultValue is reset + useEffect(() => { + const val = defaultValue ?? value + if (!defaultValue && !value) setInnerValue(null) + if (!val?.from && !val?.to) setInnerValue(null) + }, [defaultValue, value]) + return ( - + +``` + + + +### 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. + +``` + + + +``` + + + ### Min / Max values Limits on min/max can be set to restrict the dates that can be selected. In this example, it's set as the current month excluding first/last date diff --git a/packages/eds-core-react/src/components/Datepicker/fields/DateSegment.tsx b/packages/eds-core-react/src/components/Datepicker/fields/DateSegment.tsx index d9be492cad..2a8c30a49e 100644 --- a/packages/eds-core-react/src/components/Datepicker/fields/DateSegment.tsx +++ b/packages/eds-core-react/src/components/Datepicker/fields/DateSegment.tsx @@ -2,10 +2,11 @@ import { DateFieldState, DateSegment as IDateSegment, } from '@react-stately/datepicker' -import { KeyboardEvent, useRef } from 'react' -import { useDateSegment } from 'react-aria' +import { KeyboardEvent, useRef, useState } from 'react' +import { useDateFormatter, useDateSegment } from 'react-aria' import styled from 'styled-components' import { tokens } from '@equinor/eds-tokens' +import { useDatePickerContext } from '../utils/context' const Segment = styled.div<{ $placeholder: boolean @@ -32,12 +33,29 @@ export function DateSegment({ state: DateFieldState segment: IDateSegment }) { + const { formatOptions, timezone } = useDatePickerContext() + const formatter = useDateFormatter(formatOptions) + const parts = state.value + ? formatter.formatToParts(state.value.toDate(timezone)) + : [] + const part = parts.find((p) => p.type === segment.type) + const value = part?.value ?? segment.text + + const [focus, setFocus] = useState(false) const ref = useRef(null) const { segmentProps } = useDateSegment(segment, state, ref) return ( { + setFocus(true) + segmentProps.onFocus(e) + }} + onBlur={(e) => { + setFocus(false) + segmentProps.onBlur(e) + }} $invalid={state.isInvalid} $disabled={state.isDisabled} $placeholder={segment.isPlaceholder} @@ -53,9 +71,11 @@ export function DateSegment({ ref={ref} className={`segment ${segment.isPlaceholder ? 'placeholder' : ''}`} > - {segment.isPlaceholder || segment.type === 'literal' - ? segment.text - : segment.text.padStart(segment.type === 'year' ? 4 : 2, '0')} + {focus + ? segment.isPlaceholder || segment.type === 'literal' + ? segment.text + : segment.text.padStart(segment.type === 'year' ? 4 : 2, '0') + : value} ) } diff --git a/packages/eds-core-react/src/components/Datepicker/props.ts b/packages/eds-core-react/src/components/Datepicker/props.ts index 5f86947d07..d2abdfe3f5 100644 --- a/packages/eds-core-react/src/components/Datepicker/props.ts +++ b/packages/eds-core-react/src/components/Datepicker/props.ts @@ -2,6 +2,7 @@ import { MutableRefObject, ReactNode } from 'react' import { CalendarState, RangeCalendarState } from '@react-stately/calendar' import { Variants } from '../types' import { HelperTextProps } from '../InputWrapper/HelperText' +import { DateFormatterOptions } from 'react-aria' type DateRange = { from: Date | null; to: Date | null } @@ -83,6 +84,11 @@ export type DatePickerProps = Partial<{ * Granularity of the time field if enabled */ granularity?: 'hour' | 'minute' | 'second' + /** + * Format options for the datepicker input field + * Only applies when input is blurred + */ + formatOptions?: DateFormatterOptions }> export type DateTimePickerProps = Omit< diff --git a/packages/eds-core-react/src/components/Datepicker/utils/context.tsx b/packages/eds-core-react/src/components/Datepicker/utils/context.tsx index b7b0d3406f..776f3d993e 100644 --- a/packages/eds-core-react/src/components/Datepicker/utils/context.tsx +++ b/packages/eds-core-react/src/components/Datepicker/utils/context.tsx @@ -1,7 +1,9 @@ -import { createContext, ReactNode } from 'react' +import { createContext, ReactNode, useContext } from 'react' +import { DateFormatterOptions } from 'react-aria' type DatePickerContextValue = { timezone: string + formatOptions?: DateFormatterOptions } const intl = new Intl.DateTimeFormat() @@ -13,13 +15,18 @@ const DatePickerContext = createContext({ export const DatePickerProvider = ({ timezone, + formatOptions, children, }: DatePickerContextValue & { children: ReactNode }) => { return ( {children} ) } + +export const useDatePickerContext = () => useContext(DatePickerContext) +export const useTimezone = () => useDatePickerContext().timezone +export const useCustomFormat = () => useDatePickerContext().formatOptions diff --git a/packages/eds-core-react/src/components/Datepicker/utils/timezone.ts b/packages/eds-core-react/src/components/Datepicker/utils/timezone.ts deleted file mode 100644 index e31ec610ba..0000000000 --- a/packages/eds-core-react/src/components/Datepicker/utils/timezone.ts +++ /dev/null @@ -1,2 +0,0 @@ -const intl = new Intl.DateTimeFormat() -export const tz = intl.resolvedOptions().timeZone diff --git a/packages/eds-data-grid-react/jest.setup.ts b/packages/eds-data-grid-react/jest.setup.ts index 3dd4737bee..ed0cdee1a8 100644 --- a/packages/eds-data-grid-react/jest.setup.ts +++ b/packages/eds-data-grid-react/jest.setup.ts @@ -7,3 +7,6 @@ expect.extend(toHaveNoViolations) // Workaround for jest-axe error: https://github.com/nickcolley/jest-axe/issues/147 const { getComputedStyle } = window window.getComputedStyle = (elt) => getComputedStyle(elt) + +HTMLElement.prototype.showPopover = jest.fn() +HTMLElement.prototype.hidePopover = jest.fn() diff --git a/packages/eds-lab-react/jest.setup.ts b/packages/eds-lab-react/jest.setup.ts index 625b3f5c9a..4bc62c1324 100644 --- a/packages/eds-lab-react/jest.setup.ts +++ b/packages/eds-lab-react/jest.setup.ts @@ -1,3 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies */ import '@testing-library/jest-dom' import 'jest-styled-components' + +HTMLElement.prototype.showPopover = jest.fn() +HTMLElement.prototype.hidePopover = jest.fn()