diff --git a/.changeset/fruity-crabs-smoke.md b/.changeset/fruity-crabs-smoke.md new file mode 100644 index 0000000000..99b6bd1c32 --- /dev/null +++ b/.changeset/fruity-crabs-smoke.md @@ -0,0 +1,5 @@ +--- +"@ultraviolet/ui": minor +--- + +`DateInput`: change date format from MMDDYYYY to DDMMYYYY and fix date input when min/max date \ No newline at end of file diff --git a/packages/ui/src/components/DateInput/__stories__/Clearable.stories.tsx b/packages/ui/src/components/DateInput/__stories__/Clearable.stories.tsx index 439d6fcac6..da2a9f4700 100644 --- a/packages/ui/src/components/DateInput/__stories__/Clearable.stories.tsx +++ b/packages/ui/src/components/DateInput/__stories__/Clearable.stories.tsx @@ -5,7 +5,7 @@ export const Clearable = Template.bind({}) Clearable.args = { clearable: true, label: 'Clearable', - placeholder: 'MM-DD-YYYY', + placeholder: 'DD-MM-YYYY', } Clearable.decorators = [ diff --git a/packages/ui/src/components/DateInput/__stories__/MinMax.stories.tsx b/packages/ui/src/components/DateInput/__stories__/MinMax.stories.tsx index 8bf833dd98..416035dea2 100644 --- a/packages/ui/src/components/DateInput/__stories__/MinMax.stories.tsx +++ b/packages/ui/src/components/DateInput/__stories__/MinMax.stories.tsx @@ -12,9 +12,10 @@ MinMax.parameters = { MinMax.args = { label: 'Date', - maxDate: new Date('December 25, 1995 03:24:00'), - minDate: new Date('December 12, 1995 03:24:00'), + maxDate: new Date('December 25, 1995 00:00:00'), + minDate: new Date('December 12, 1995 00:00:00'), value: new Date('1995-12-17T03:24:00'), + onChange: console.log, } MinMax.decorators = [ diff --git a/packages/ui/src/components/DateInput/__stories__/Playground.stories.tsx b/packages/ui/src/components/DateInput/__stories__/Playground.stories.tsx index c5c6bee128..ab86535d94 100644 --- a/packages/ui/src/components/DateInput/__stories__/Playground.stories.tsx +++ b/packages/ui/src/components/DateInput/__stories__/Playground.stories.tsx @@ -3,7 +3,7 @@ import { Template } from './Template' export const Playground = Template.bind({}) Playground.args = { - placeholder: 'MM-DD-YYYY', + placeholder: 'DD-MM-YYYY', } Playground.decorators = [ diff --git a/packages/ui/src/components/DateInput/__stories__/Template.tsx b/packages/ui/src/components/DateInput/__stories__/Template.tsx index 6d126b4c4b..20b8623036 100644 --- a/packages/ui/src/components/DateInput/__stories__/Template.tsx +++ b/packages/ui/src/components/DateInput/__stories__/Template.tsx @@ -8,6 +8,6 @@ export const Template: StoryFn = props => ( Template.args = { label: 'Date Input', - placeholder: 'MM-DD-YYYY', + placeholder: 'DD-MM-YYYY', required: true, } diff --git a/packages/ui/src/components/DateInput/__tests__/__snapshots__/index.test.tsx.snap b/packages/ui/src/components/DateInput/__tests__/__snapshots__/index.test.tsx.snap index 5334cfa20c..92f289d6f8 100644 --- a/packages/ui/src/components/DateInput/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/ui/src/components/DateInput/__tests__/__snapshots__/index.test.tsx.snap @@ -1226,7 +1226,7 @@ exports[`dateInput > renders correctly with a array of dates to exclude 1`] = ` id="_r_af_" placeholder="YYYY-MM-DD" type="text" - value="12/11/1995" + value="11/12/1995" />
{ }) test('formatValue should work with default formatting', () => { - expect(formatValue(date, null, false, false)).toBe('11/20/2000') + expect(formatValue(date, null, false, false)).toBe('20/11/2000') expect(formatValue(date, null, true, false)).toBe('11/2000') - expect(formatValue(date, null, false, true)).toBe('11/20/2000') + expect(formatValue(date, null, false, true)).toBe('20/11/2000') expect(formatValue(null, rangeDate, false, false)).toBe(undefined) expect(formatValue(null, rangeDate, true, true)).toBe('10/2000 - 10/2000') expect(formatValue(null, rangeDate, false, true)).toBe( - '10/20/2000 - 10/31/2000', + '20/10/2000 - 31/10/2000', ) }) @@ -87,4 +88,25 @@ describe('helper functions dateInput', () => { '2000 - 2000', ) }) + + test('createDate should work', () => { + expect(createDate('12/02/2020', false)).toEqual(new Date(2020, 1, 12)) + expect(createDate('12-02-2020', false)).toEqual(new Date(2020, 1, 12)) + expect(createDate('12 may 2020', false)).toEqual(new Date(2020, 4, 12)) + }) + + test('createDate should work with min and maxDate', () => { + expect(createDate('12/02/2020', false, new Date(2020, 1, 15))).toEqual( + new Date(2020, 1, 15), + ) + expect( + createDate('12/02/2020', false, undefined, new Date(2020, 1, 10)), + ).toEqual(new Date(2020, 1, 10)) + }) + + test('createdate should work with showMonthYearPicker', () => { + expect(createDate('12/2020', true)).toEqual(new Date(2020, 11, 1)) + expect(createDate('12-2020', true)).toEqual(new Date(2020, 11, 1)) + expect(createDate('2020/12', true)).toEqual(new Date(2020, 11, 1)) + }) }) diff --git a/packages/ui/src/components/DateInput/__tests__/index.test.tsx b/packages/ui/src/components/DateInput/__tests__/index.test.tsx index 008fd57578..1fcca97dd0 100644 --- a/packages/ui/src/components/DateInput/__tests__/index.test.tsx +++ b/packages/ui/src/components/DateInput/__tests__/index.test.tsx @@ -9,6 +9,9 @@ import { DateInput } from '..' tk.freeze(new Date(1_609_503_120_000)) +const DECEMBER = /December/i +const YEAR = /1995/i + describe('dateInput', () => { test('renders correctly with default props', () => { const { asFragment } = renderWithTheme( @@ -203,7 +206,7 @@ describe('dateInput', () => { const calendar = screen.getByRole('dialog') expect(calendar).toBeVisible() - const visibleMonth = screen.getByText(/December/i) + const visibleMonth = screen.getByText(DECEMBER) await userEvent.click(screen.getByTestId('previous-month')) expect(visibleMonth.textContent).toContain('November') @@ -230,7 +233,7 @@ describe('dateInput', () => { const calendar = screen.getByRole('dialog') expect(calendar).toBeVisible() - const visibleMonth = screen.getByText(/1995/i) + const visibleMonth = screen.getByText(YEAR) await userEvent.click(screen.getByTestId('previous-month')) expect(visibleMonth.textContent).toContain('1994') @@ -257,7 +260,7 @@ describe('dateInput', () => { await userEvent.click(input) await userEvent.click(screen.getByText('15')) - expect(input.value).toBe('12/15/1995') + expect(input.value).toBe('15/12/1995') await userEvent.click(input) @@ -265,12 +268,12 @@ describe('dateInput', () => { await userEvent.click(dayFromLastMonth) await userEvent.click(input) - expect(input.value).toBe('11/30/1995') + expect(input.value).toBe('30/11/1995') await userEvent.click(input) const dayFromNextMonth = screen.getAllByText('1')[1] await userEvent.click(dayFromNextMonth) - expect(input.value).toBe('12/01/1995') + expect(input.value).toBe('01/12/1995') }) test('handle correctly click on date range', async () => { @@ -290,21 +293,21 @@ describe('dateInput', () => { expect(calendar).toBeVisible() await userEvent.click(screen.getByText('15')) - expect(input.value).toBe('02/15/1995 - ') + expect(input.value).toBe('15/02/1995 - ') const day = screen.getByText('27') await userEvent.hover(day) await userEvent.unhover(day) await userEvent.click(day) - expect(input.value).toBe('02/15/1995 - 02/27/1995') + expect(input.value).toBe('15/02/1995 - 27/02/1995') await userEvent.click(input) await userEvent.click(screen.getByText('31')) - expect(input.value).toBe('01/31/1995 - ') + expect(input.value).toBe('31/01/1995 - ') await userEvent.click(screen.getByText('20')) - expect(input.value).toBe('01/20/1995 - 01/31/1995') + expect(input.value).toBe('20/01/1995 - 31/01/1995') }) test('render correctly with showMonthYearPicker with excluded months', async () => { @@ -408,7 +411,7 @@ describe('dateInput', () => { const input = screen.getByPlaceholderText('YYYY-MM-DD') await userEvent.click(input) - await userEvent.type(input, '08/21/1995') + await userEvent.type(input, '21/08/1995') input.blur() expect(mockOnChange).toBeCalled() expect(screen.getByText('August', { exact: false })).toBeInTheDocument() @@ -428,7 +431,7 @@ describe('dateInput', () => { const input = screen.getByPlaceholderText('YYYY-MM-DD') await userEvent.click(input) - await userEvent.type(input, '08/21/1995') + await userEvent.type(input, '21/08/1995') input.blur() expect(mockOnChange).toBeCalled() expect(screen.getByText('August', { exact: false })).toBeInTheDocument() diff --git a/packages/ui/src/components/DateInput/helpers.ts b/packages/ui/src/components/DateInput/helpers.ts index a4008538fa..b6f5fbfae7 100644 --- a/packages/ui/src/components/DateInput/helpers.ts +++ b/packages/ui/src/components/DateInput/helpers.ts @@ -1,3 +1,5 @@ +const SPLIT_REGEX = /\D+/ +const DATE_SEPARATOR_REGEX = /[^a-zA-Z0-9]+/ // First day of the month for a given year export const getMonthFirstDay = (month: number, year: number) => { const firstDay = new Date(year, month - 1, 1).getDay() @@ -43,8 +45,8 @@ const getDateISO = (showMonthYearPicker: boolean, date?: Date) => { } return [ - addZero(date.getMonth() + 1), addZero(date.getDate()), + addZero(date.getMonth() + 1), date.getFullYear(), ].join('/') } @@ -84,25 +86,49 @@ export const formatValue = ( return undefined } -export const createDate = (value: string, showMonthYearPicker: boolean) => { +const returnValidDate = ( + computedDate: Date, + minDate?: Date, + maxDate?: Date, +) => { + const isValidDate = !!computedDate.getTime() + + const isTooSoon = isValidDate && minDate && computedDate < minDate + const isTooLate = isValidDate && maxDate && computedDate > maxDate + + if (isTooLate) { + return maxDate + } + if (isTooSoon) { + return minDate + } + + return isValidDate ? computedDate : null +} + +export const createDate = ( + value: string, + showMonthYearPicker: boolean, + minDate?: Date, + maxDate?: Date, +) => { if (showMonthYearPicker) { // Force YYYY/MM (since MM/YYYY not recognised as a date in typescript) - // oxlint-disable: to fix - const res = value.split(/\D+/).map(val => Number.parseInt(val, 10)) + const res = value.split(SPLIT_REGEX).map(val => Number.parseInt(val, 10)) const year = Math.max(...res) < 100 ? Math.max(...res) + 2000 : Math.max(...res) // MM/YY should be seen as MM/20YY instead of MM/19YY const month = Math.min(...res) - 1 const computedDate = new Date(year, month) - const isValidDate = !!computedDate.getTime() - return isValidDate ? computedDate : null + return returnValidDate(computedDate, minDate, maxDate) } - const computedDate = new Date(value) - const isValidDate = !!computedDate.getTime() + // Cannot simply use new Date(value) since its base format is MM/DD/YYYY whereas the component uses DD/MM/YYYY + const [day, month, year] = value.split(DATE_SEPARATOR_REGEX) + const computedDate = new Date(`${month} ${day} ${year}`) - return isValidDate ? computedDate : null + return returnValidDate(computedDate, minDate, maxDate) } export const createDateRange = ( diff --git a/packages/ui/src/components/DateInput/index.tsx b/packages/ui/src/components/DateInput/index.tsx index 9957df04bc..704dc9ff9b 100644 --- a/packages/ui/src/components/DateInput/index.tsx +++ b/packages/ui/src/components/DateInput/index.tsx @@ -261,7 +261,12 @@ export const DateInput = ({ setYearToShow(computedNewRange[0].getFullYear()) } } else { - const computedDate = createDate(newValue, showMonthYearPicker) + const computedDate = createDate( + newValue, + showMonthYearPicker, + minDate, + maxDate, + ) if (computedDate) { setMonthToShow(computedDate.getMonth() + 1) @@ -287,7 +292,12 @@ export const DateInput = ({ ) => void )?.(computedNewRange, undefined) } else { - const computedDate = createDate(inputValue, showMonthYearPicker) + const computedDate = createDate( + inputValue, + showMonthYearPicker, + minDate, + maxDate, + ) ;( onChange as (date: Date | null, event?: React.SyntheticEvent) => void )?.(computedDate, undefined) diff --git a/packages/ui/src/components/DateInput/styles.css.ts b/packages/ui/src/components/DateInput/styles.css.ts index 8d88148657..a53f9b4a40 100644 --- a/packages/ui/src/components/DateInput/styles.css.ts +++ b/packages/ui/src/components/DateInput/styles.css.ts @@ -8,7 +8,10 @@ import { dayMonth, } from './components/styles.css' -const container = style({ width: '100%' }) +const container = style({ + width: '100%', + height: 'fit-content', +}) const calendarContentWrapper = recipe({ base: {