Skip to content

Commit

Permalink
feat(component): add Datepicker component (#408)
Browse files Browse the repository at this point in the history
  • Loading branch information
animesh1987 authored Jun 25, 2020
1 parent bd1e220 commit f23176a
Show file tree
Hide file tree
Showing 17 changed files with 697 additions and 4 deletions.
2 changes: 2 additions & 0 deletions packages/big-design/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
"@bigcommerce/big-design-icons": "^0.12.0",
"@bigcommerce/big-design-theme": "^0.10.0",
"@popperjs/core": "^2.2.1",
"@types/react-datepicker": "^2.11.0",
"downshift": "5.2.2",
"focus-trap": "^5.1.0",
"polished": "^3.0.3",
"react-datepicker": "^2.16.0",
"react-popper": "^2.1.0",
"react-uid": "^2.2.0"
},
Expand Down
90 changes: 90 additions & 0 deletions packages/big-design/src/components/Datepicker/Datepicker.tsx
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} />),
);
48 changes: 48 additions & 0 deletions packages/big-design/src/components/Datepicker/Header.tsx
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;
4 changes: 4 additions & 0 deletions packages/big-design/src/components/Datepicker/index.ts
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;
130 changes: 130 additions & 0 deletions packages/big-design/src/components/Datepicker/spec.tsx
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);
});
Loading

0 comments on commit f23176a

Please sign in to comment.