-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(component): add Datepicker component (#408)
- Loading branch information
1 parent
bd1e220
commit f23176a
Showing
17 changed files
with
697 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
90 changes: 90 additions & 0 deletions
90
packages/big-design/src/components/Datepicker/Datepicker.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import React, { forwardRef, memo, Ref, useEffect, useState } from 'react'; | ||
import ReactDatePicker, { registerLocale } from 'react-datepicker'; | ||
|
||
import { createLocalizationProvider } from '../../utils'; | ||
import { Input } from '../Input'; | ||
|
||
import Header from './Header'; | ||
import { StyledDatepicker } from './styled'; | ||
|
||
interface Props { | ||
dateFormat?: string; | ||
error?: React.ReactNode; | ||
label?: string; | ||
locale?: string; | ||
onDateChange(date: string): void; | ||
} | ||
|
||
export interface PrivateProps { | ||
forwardedRef: Ref<ReactDatePicker>; | ||
} | ||
|
||
export type DatepickerProps = Props & React.InputHTMLAttributes<HTMLInputElement>; | ||
|
||
const RawDatepicker: React.FC<DatepickerProps & PrivateProps> = ({ | ||
dateFormat = 'EE, dd MMM, yyyy', | ||
error, | ||
forwardedRef, | ||
label, | ||
locale = 'en-US', | ||
min, | ||
max, | ||
onDateChange, | ||
required, | ||
placeholder, | ||
value, | ||
...props | ||
}) => { | ||
const [selected, setSelected] = useState<Date>(); | ||
const localization = createLocalizationProvider(locale); | ||
|
||
registerLocale(locale, localization); | ||
const updateDate = (value: Date) => onDateChange(value.toISOString()); | ||
|
||
useEffect(() => { | ||
if (typeof value === 'string') { | ||
setSelected(new Date(value)); | ||
} else { | ||
setSelected(undefined); | ||
} | ||
}, [value]); | ||
|
||
return ( | ||
<StyledDatepicker> | ||
<ReactDatePicker | ||
renderCustomHeader={({ | ||
date, | ||
decreaseMonth, | ||
increaseMonth, | ||
prevMonthButtonDisabled, | ||
nextMonthButtonDisabled, | ||
}) => ( | ||
<Header | ||
months={localization.monthsLong} | ||
date={date} | ||
decreaseMonth={decreaseMonth} | ||
increaseMonth={increaseMonth} | ||
prevMonthButtonDisabled={prevMonthButtonDisabled} | ||
nextMonthButtonDisabled={nextMonthButtonDisabled} | ||
/> | ||
)} | ||
customInput={<Input label={label} error={error} {...props} />} | ||
className="calendar-input" | ||
calendarClassName="bc-datepicker" | ||
dateFormat={dateFormat || 'EE, dd MMM, yyyy'} | ||
locale={locale} | ||
maxDate={max ? new Date(max) : undefined} | ||
minDate={min ? new Date(min) : undefined} | ||
selected={selected} | ||
placeholderText={placeholder} | ||
required={required} | ||
onChange={updateDate} | ||
ref={forwardedRef} | ||
/> | ||
</StyledDatepicker> | ||
); | ||
}; | ||
|
||
export const Datepicker = memo( | ||
forwardRef<ReactDatePicker, DatepickerProps>((props, ref) => <RawDatepicker {...props} forwardedRef={ref} />), | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { ChevronLeftIcon, ChevronRightIcon } from '@bigcommerce/big-design-icons'; | ||
import React from 'react'; | ||
|
||
import { Flex } from '..'; | ||
import { Button } from '../Button'; | ||
import { Text } from '../Typography'; | ||
|
||
interface HeaderProps { | ||
date?: Date; | ||
nextMonthButtonDisabled?: boolean; | ||
months: string[]; | ||
prevMonthButtonDisabled?: boolean; | ||
decreaseMonth?(): void; | ||
increaseMonth?(): void; | ||
} | ||
|
||
const Header: React.FC<HeaderProps> = ({ | ||
date = new Date(), | ||
decreaseMonth, | ||
increaseMonth, | ||
months, | ||
prevMonthButtonDisabled, | ||
nextMonthButtonDisabled, | ||
}) => ( | ||
<Flex alignItems="center" justifyContent="space-between"> | ||
<Button | ||
title="View previous month." | ||
type="button" | ||
iconOnly={<ChevronLeftIcon title="View previous month." />} | ||
onClick={decreaseMonth} | ||
disabled={prevMonthButtonDisabled} | ||
variant="subtle" | ||
/> | ||
|
||
<Text as="span" marginBottom="none" bold>{`${months[date.getMonth()]} ${date.getFullYear()}`}</Text> | ||
|
||
<Button | ||
title="View next month." | ||
type="button" | ||
iconOnly={<ChevronRightIcon title="View next month." />} | ||
onClick={increaseMonth} | ||
disabled={nextMonthButtonDisabled} | ||
variant="subtle" | ||
/> | ||
</Flex> | ||
); | ||
|
||
export default Header; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { DatepickerProps as _DatepickerProps } from './Datepicker'; | ||
|
||
export { Datepicker } from './Datepicker'; | ||
export type DatepickerProps = _DatepickerProps; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import 'jest-styled-components'; | ||
|
||
import React, { createRef } from 'react'; | ||
import ReactDatePicker from 'react-datepicker'; | ||
import { act } from 'react-dom/test-utils'; | ||
|
||
import { fireEvent, render } from '@test/utils'; | ||
|
||
import { FormGroup } from '..'; | ||
|
||
import { Datepicker } from './index'; | ||
|
||
jest.mock( | ||
'popper.js', | ||
() => | ||
class Popper { | ||
static placements = [ | ||
'auto', | ||
'auto-end', | ||
'auto-start', | ||
'bottom', | ||
'bottom-end', | ||
'bottom-start', | ||
'left', | ||
'left-end', | ||
'left-start', | ||
'right', | ||
'right-end', | ||
'right-start', | ||
'top', | ||
'top-end', | ||
'top-start', | ||
]; | ||
|
||
constructor() { | ||
return { | ||
destroy: () => jest.fn(), | ||
scheduleUpdate: () => jest.fn(), | ||
}; | ||
} | ||
}, | ||
); | ||
|
||
test('should use the passed in ref object if provided', () => { | ||
const ref = createRef<ReactDatePicker>(); | ||
const { container } = render(<Datepicker ref={ref} onDateChange={jest.fn()} />); | ||
|
||
const input = container.querySelector('input'); | ||
|
||
fireEvent.focus(input as HTMLInputElement); | ||
const datepicker = container.querySelector('.react-datepicker'); | ||
|
||
expect(datepicker?.className.includes(ref.current?.props.calendarClassName as string)).toBeTruthy(); | ||
}); | ||
|
||
test('renders select label', () => { | ||
const { getByText } = render(<Datepicker data-testid="datepicker" label={'test'} onDateChange={jest.fn()} />); | ||
|
||
expect(getByText('test')).toBeInTheDocument(); | ||
}); | ||
|
||
test('calls onDateChange function when a date cell is clicked', () => { | ||
const changeFunction = jest.fn(); | ||
const { container } = render(<Datepicker onDateChange={changeFunction} />); | ||
|
||
const input = container.querySelector('input'); | ||
|
||
act(() => { | ||
fireEvent.focus(input as HTMLInputElement); | ||
}); | ||
|
||
const datepicker = container.querySelector('.react-datepicker'); | ||
|
||
const cell = datepicker?.querySelector('.react-datepicker__day--today'); | ||
|
||
act(() => { | ||
fireEvent.click(cell as HTMLElement); | ||
}); | ||
|
||
expect(changeFunction).toHaveBeenCalled(); | ||
}); | ||
|
||
test('renders an error if one is provided', () => { | ||
const { getByText } = render( | ||
<FormGroup> | ||
<Datepicker onDateChange={jest.fn()} error="Required" /> | ||
</FormGroup>, | ||
); | ||
|
||
expect(getByText('Required')).toBeInTheDocument(); | ||
}); | ||
|
||
test('appends (optional) text to label if select is not required', () => { | ||
const { container } = render(<Datepicker onDateChange={jest.fn()} label="label" />); | ||
const label = container.querySelector('label'); | ||
|
||
expect(label).toHaveStyleRule('content', "' (optional)'", { modifier: '::after' }); | ||
}); | ||
|
||
test('dates before minimum date passed are disabled', () => { | ||
const selectedDate = '2020/1/5'; | ||
const minimumDate = '2020/1/4'; | ||
const { container } = render( | ||
<Datepicker onDateChange={jest.fn()} value={selectedDate} min={minimumDate} label="label" />, | ||
); | ||
const input = container.querySelector('input'); | ||
|
||
act(() => { | ||
fireEvent.focus(input as HTMLInputElement); | ||
}); | ||
|
||
const disabledDate = container.querySelector('.react-datepicker__day--003'); | ||
expect(disabledDate?.classList.contains('react-datepicker__day--disabled')).toBe(true); | ||
}); | ||
|
||
test('dates after max date passed are disabled', () => { | ||
const selectedDate = '2020/1/5'; | ||
const maximumDate = '2020/1/10'; | ||
const { container } = render( | ||
<Datepicker onDateChange={jest.fn()} value={selectedDate} max={maximumDate} label="label" />, | ||
); | ||
const input = container.querySelector('input'); | ||
|
||
act(() => { | ||
fireEvent.focus(input as HTMLInputElement); | ||
}); | ||
|
||
const disabledDate = container.querySelector('.react-datepicker__day--011'); | ||
expect(disabledDate?.classList.contains('react-datepicker__day--disabled')).toBe(true); | ||
}); |
Oops, something went wrong.