Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(date picker): added DatePicker and RangePicker components #821

Merged
merged 14 commits into from
Feb 5, 2025
70 changes: 70 additions & 0 deletions src/components/DatePickers/DatePicker/DatePicker.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Copyright 2024 Mia srl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

import { Meta, StoryObj } from '@storybook/react'
import dayjs from 'dayjs'

import { DatePicker } from './DatePicker'

const meta = {
component: DatePicker,
} satisfies Meta<typeof DatePicker>

export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {
}

export const WithoutTodayButton: Story = {
args: { showNow: false },
}

export const WithTime: Story = {
args: { showTime: true },
}

export const WithoutNowButton: Story = {
args: { showTime: true, showNow: false },
}

export const CustomPlaceholder: Story = {
args: { placeholder: 'Custom placeholder' },
}

export const DefaultValue: Story = {
args: { defaultValue: dayjs() },
}

export const CustomFormat: Story = {
args: { defaultValue: dayjs(), format: 'YYYY-MM-DD' },
}

export const Disabled: Story = {
args: { isDisabled: true, defaultValue: dayjs() },
}

export const MinMaxDates: Story = {
args: { minDate: dayjs().subtract(2, 'day'), maxDate: dayjs().add(2, 'day') },
}

export const ErrorStatus: Story = {
args: { isErrorStatus: true },
}

227 changes: 227 additions & 0 deletions src/components/DatePickers/DatePicker/DatePicker.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/**
* Copyright 2024 Mia srl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

import dayjs from 'dayjs'

import { render, screen, userEvent, within } from '../../../test-utils'
import { DatePicker } from './DatePicker'
import { ShowTimeOptions } from '../types'

const selectedDay = 15
const selectedDate = dayjs('2025-02-03').set('date', selectedDay)
.startOf('day')
const selectedDateFormatted = selectedDate.format('DD/MM/YYYY')
const selectedDateTimeFormatted = selectedDate.format('DD/MM/YYYY HH:mm')

describe('DatePicker Component', () => {
beforeEach(() => {
jest.resetAllMocks()
})

describe('renders correctly', () => {
test('with default props', () => {
const { asFragment } = render(<DatePicker />)
expect(asFragment()).toMatchSnapshot()
})

test('with isErrorStatus prop', () => {
const { asFragment } = render(<DatePicker isErrorStatus />)
expect(asFragment()).toMatchSnapshot()
})

test('with defaultValue prop', () => {
const { asFragment } = render(<DatePicker defaultValue={selectedDate} />)
expect(asFragment()).toMatchSnapshot()
})

test('with custom placeholder', () => {
const { asFragment } = render(<DatePicker placeholder={'Custom placeholder'} />)
expect(asFragment()).toMatchSnapshot()
})

test('with custom format', () => {
const { asFragment } = render(<DatePicker defaultValue={selectedDate} format={'YYYY-MM-DD'} />)
expect(asFragment()).toMatchSnapshot()
})

test('with showTime and custom format', () => {
const { asFragment } = render(<DatePicker defaultValue={selectedDate} format={'YYYY-MM-DD HH.mm.ss'} showTime />)
expect(asFragment()).toMatchSnapshot()
})

test('with allowClear=false', () => {
const { asFragment } = render(<DatePicker allowClear={false} />)
expect(asFragment()).toMatchSnapshot()
})
})

test('calendar show Today button by default', async() => {
render(<DatePicker />)

const input = screen.getByRole<HTMLInputElement>('textbox')

expect(screen.queryByText('Today')).not.toBeInTheDocument()
await userEvent.click(input)
expect(screen.getByText('Today')).toBeInTheDocument()
})

test('calendar does not show Today button if showNow=false', async() => {
render(<DatePicker showNow={false} />)

const input = screen.getByRole<HTMLInputElement>('textbox')

expect(screen.queryByRole('listitem')).not.toBeInTheDocument()
await userEvent.click(input)
expect(screen.queryByText('Today')).not.toBeInTheDocument()
})

describe('show Time', () => {
test('calendar does not show time selectors and ok button by default', async() => {
render(<DatePicker />)

const input = screen.getByRole<HTMLInputElement>('textbox')
expect(screen.queryByRole('listitem')).not.toBeInTheDocument()
await userEvent.click(input)
const lists = screen.getAllByRole('list')
expect(lists).toHaveLength(1)
const listItem = within(lists[0]).getByRole('listitem')
expect(within(listItem).getByText('Today')).toBeInTheDocument()
expect(within(listItem).queryByRole('button', { name: 'Ok' })).not.toBeInTheDocument()
})

test('calendar shows time selectors and ok button if showTime=true', async() => {
render(<DatePicker showTime={true} />)

const input = screen.getByRole<HTMLInputElement>('textbox')
expect(screen.queryByRole('list')).not.toBeInTheDocument()
await userEvent.click(input)
const lists = screen.getAllByRole('list')
// 1 list for the footer buttons
// 2 lists for hour and minutes elements
expect(lists).toHaveLength(3)
expect(screen.getByText('Now')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'OK' })).toBeInTheDocument()
})

test('calendars show only hours if showTime=hours', async() => {
render(<DatePicker showTime={ShowTimeOptions.Hours} />)

const input = screen.getByRole<HTMLInputElement>('textbox')

expect(screen.queryByRole('list')).not.toBeInTheDocument()
await userEvent.click(input)
const lists = screen.getAllByRole('list')
// 1 list for the footer buttons
// 1 list for hour elements
expect(lists).toHaveLength(2)
})

test('calendars show hours and minutes if showTime=minutes', async() => {
render(<DatePicker showTime={ShowTimeOptions.Minutes} />)

const input = screen.getByRole<HTMLInputElement>('textbox')

expect(screen.queryByRole('list')).not.toBeInTheDocument()
await userEvent.click(input)
const lists = screen.getAllByRole('list')
// 1 list for the footer buttons
// 2 list for hour and minute elements
expect(lists).toHaveLength(3)
})

test('calendar show hours, minutes and seconds if showTime=seconds', async() => {
render(<DatePicker showTime={ShowTimeOptions.Seconds} />)

const input = screen.getByRole<HTMLInputElement>('textbox')

expect(screen.queryByRole('list')).not.toBeInTheDocument()
await userEvent.click(input)
const lists = screen.getAllByRole('list')
// 1 list for the footer buttons
// 3 list for hour, minute and second elements
expect(lists).toHaveLength(4)
})
})

describe('onChange', () => {
test('is triggered on clear icon click', async() => {
const onChange = jest.fn()
render(<DatePicker defaultValue={selectedDate} onChange={onChange} />)

const input = screen.getByRole<HTMLInputElement>('textbox')
expect(input.value).toEqual(selectedDateFormatted)
const clearIcon = screen.getByRole('img', { name: 'close-circle' })
expect(clearIcon).toBeVisible()
const calendarIcon = screen.getByRole('img', { name: 'calendar' })
expect(calendarIcon).not.toBeVisible()
expect(onChange).not.toHaveBeenCalled()
await userEvent.click(clearIcon)
expect(onChange).toHaveBeenCalledTimes(1)
expect(onChange).toHaveBeenCalledWith(null, '')
expect(input.value).toEqual('')
expect(calendarIcon).toBeVisible()
expect(clearIcon).not.toBeVisible()
})

test('is triggered on date selection', async() => {
const onChange = jest.fn()
render(<DatePicker onChange={onChange} />)

expect(screen.getByRole('img', { name: 'calendar' })).toBeVisible()
const input = screen.getByRole<HTMLInputElement>('textbox')
await userEvent.click(input)
expect(onChange).not.toHaveBeenCalled()
await userEvent.click(screen.getByText(selectedDay))
expect(onChange).toHaveBeenCalledTimes(1)
expect(onChange).toHaveBeenCalledWith(selectedDate, selectedDateFormatted)
expect(input.value).toEqual(selectedDateFormatted)
expect(screen.queryByRole('img', { name: 'calendar' })).not.toBeVisible()
expect(screen.getByRole('img', { name: 'close-circle' })).toBeVisible()
})

test('is triggered on Ok click if showTime=true', async() => {
const onChange = jest.fn()
render(<DatePicker showTime onChange={onChange} />)

expect(screen.getByRole('img', { name: 'calendar' })).toBeVisible()
const input = screen.getByRole<HTMLInputElement>('textbox')
await userEvent.click(input)
expect(onChange).not.toHaveBeenCalled()
await userEvent.click(screen.getAllByText(selectedDay)[0])
expect(onChange).not.toHaveBeenCalled()
await userEvent.click(screen.getByRole('button', { name: 'OK' }))
expect(onChange).toHaveBeenCalledTimes(1)
expect(onChange).toHaveBeenCalledWith(selectedDate, selectedDateTimeFormatted)
expect(input.value).toEqual(selectedDateTimeFormatted)
expect(screen.queryByRole('img', { name: 'calendar' })).not.toBeVisible()
expect(screen.getByRole('img', { name: 'close-circle' })).toBeVisible()
})
})

test('calendar dates are disabled via minDate and maxDate props', async() => {
const minDate = selectedDate.subtract(1, 'day')
const maxDate = selectedDate.add(1, 'day')

render(<DatePicker maxDate={maxDate} minDate={minDate} />)
const input = screen.getByRole<HTMLInputElement>('textbox')
await userEvent.click(input)
expect(getComputedStyle(screen.getByText(selectedDay - 2).parentElement!).pointerEvents).toBe('none')
expect(getComputedStyle(screen.getByText(selectedDay + 2).parentElement!).pointerEvents).toBe('none')
expect(getComputedStyle(screen.getByText(selectedDay).parentElement!).pointerEvents).not.toBe('none')
})
})
66 changes: 66 additions & 0 deletions src/components/DatePickers/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Copyright 2024 Mia srl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

import { DatePicker as AntDatePicker } from 'antd'
import { ReactNode } from 'react'

import { ShowTimeOptions, defaultDateFormat, defaultTimeFormat } from '../types'
import { DatePickerProps } from '../props'
import { convertToAntShowTimeOptions } from '../utils'

export const defaults = {
allowClear: true,
format: defaultDateFormat,
showTime: false,
hasNowButton: true,
}

export const DatePicker = ({
allowClear = defaults.allowClear,
onChange,
placeholder,
showTime = defaults.showTime,
format = showTime ? `${defaults.format} ${defaultTimeFormat}` : defaults.format,
defaultValue,
isDisabled,
minDate,
maxDate,
isErrorStatus,
showNow = defaults.hasNowButton,
value,
}: DatePickerProps): ReactNode => {
return (
<AntDatePicker
allowClear={allowClear}
defaultValue={defaultValue}
disabled={isDisabled}
format={format}
maxDate={maxDate}
minDate={minDate}
needConfirm={Boolean(showTime)}
placeholder={placeholder}
showNow={showNow}
showTime={convertToAntShowTimeOptions(showTime)}
status={isErrorStatus ? 'error' : undefined}
value={value}
onChange={onChange}
/>
)
}

DatePicker.ShowTimeOptions = ShowTimeOptions
Loading
Loading