Merge branch 'improve-datepicker' into 'develop'
Add new custom datepicker for improved UX See merge request soapbox-pub/soapbox-fe!1507
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { queryAllByRole, render, screen } from '../../../../jest/test-helpers';
|
||||
import Datepicker from '../datepicker';
|
||||
|
||||
describe('<Datepicker />', () => {
|
||||
it('defaults to the current date', () => {
|
||||
const handler = jest.fn();
|
||||
render(<Datepicker onChange={handler} />);
|
||||
const today = new Date();
|
||||
|
||||
expect(screen.getByTestId('datepicker-month')).toHaveValue(String(today.getMonth()));
|
||||
expect(screen.getByTestId('datepicker-day')).toHaveValue(String(today.getDate()));
|
||||
expect(screen.getByTestId('datepicker-year')).toHaveValue(String(today.getFullYear()));
|
||||
});
|
||||
|
||||
it('changes number of days based on selected month and year', async() => {
|
||||
const handler = jest.fn();
|
||||
render(<Datepicker onChange={handler} />);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('datepicker-month'),
|
||||
screen.getByRole('option', { name: 'February' }),
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('datepicker-year'),
|
||||
screen.getByRole('option', { name: '2020' }),
|
||||
);
|
||||
|
||||
let daySelect: HTMLElement;
|
||||
daySelect = document.querySelector('[data-testid="datepicker-day"]');
|
||||
expect(queryAllByRole(daySelect, 'option')).toHaveLength(29);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('datepicker-year'),
|
||||
screen.getByRole('option', { name: '2021' }),
|
||||
);
|
||||
|
||||
daySelect = document.querySelector('[data-testid="datepicker-day"]') as HTMLElement;
|
||||
expect(queryAllByRole(daySelect, 'option')).toHaveLength(28);
|
||||
});
|
||||
|
||||
it('ranges from the current year to 120 years ago', () => {
|
||||
const handler = jest.fn();
|
||||
render(<Datepicker onChange={handler} />);
|
||||
const today = new Date();
|
||||
|
||||
const yearSelect = document.querySelector('[data-testid="datepicker-year"]') as HTMLElement;
|
||||
expect(queryAllByRole(yearSelect, 'option')).toHaveLength(121);
|
||||
expect(queryAllByRole(yearSelect, 'option')[0]).toHaveValue(String(today.getFullYear()));
|
||||
expect(queryAllByRole(yearSelect, 'option')[120]).toHaveValue(String(today.getFullYear() - 120));
|
||||
});
|
||||
|
||||
it('calls the onChange function when the inputs change', async() => {
|
||||
const handler = jest.fn();
|
||||
render(<Datepicker onChange={handler} />);
|
||||
|
||||
expect(handler.mock.calls.length).toEqual(1);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('datepicker-month'),
|
||||
screen.getByRole('option', { name: 'February' }),
|
||||
);
|
||||
|
||||
expect(handler.mock.calls.length).toEqual(2);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('datepicker-year'),
|
||||
screen.getByRole('option', { name: '2020' }),
|
||||
);
|
||||
|
||||
expect(handler.mock.calls.length).toEqual(3);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('datepicker-day'),
|
||||
screen.getByRole('option', { name: '5' }),
|
||||
);
|
||||
|
||||
expect(handler.mock.calls.length).toEqual(4);
|
||||
});
|
||||
});
|
||||
94
app/soapbox/components/ui/datepicker/datepicker.tsx
Normal file
94
app/soapbox/components/ui/datepicker/datepicker.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import Select from '../select/select';
|
||||
import Stack from '../stack/stack';
|
||||
import Text from '../text/text';
|
||||
|
||||
const getDaysInMonth = (month: number, year: number) => new Date(year, month + 1, 0).getDate();
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
interface IDatepicker {
|
||||
onChange(date: Date): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Datepicker that allows a user to select month, day, and year.
|
||||
*/
|
||||
const Datepicker = ({ onChange }: IDatepicker) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [month, setMonth] = useState<number>(new Date().getMonth());
|
||||
const [day, setDay] = useState<number>(new Date().getDate());
|
||||
const [year, setYear] = useState<number>(2022);
|
||||
|
||||
const numberOfDays = useMemo(() => {
|
||||
return getDaysInMonth(month, year);
|
||||
}, [month, year]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(new Date(year, month, day));
|
||||
}, [month, day, year]);
|
||||
|
||||
return (
|
||||
<div className='grid grid-cols-1 gap-y-2 gap-x-2 sm:grid-cols-3'>
|
||||
<div className='sm:col-span-1'>
|
||||
<Stack>
|
||||
<Text size='sm' weight='medium' theme='muted'>
|
||||
<FormattedMessage id='datepicker.month' defaultMessage='Month' />
|
||||
</Text>
|
||||
|
||||
<Select
|
||||
value={month}
|
||||
onChange={(event) => setMonth(Number(event.target.value))}
|
||||
data-testid='datepicker-month'
|
||||
>
|
||||
{[...Array(12)].map((_, idx) => (
|
||||
<option key={idx} value={idx}>
|
||||
{intl.formatDate(new Date(year, idx, 1), { month: 'long' })}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<div className='sm:col-span-1'>
|
||||
<Stack>
|
||||
<Text size='sm' weight='medium' theme='muted'>
|
||||
<FormattedMessage id='datepicker.day' defaultMessage='Day' />
|
||||
</Text>
|
||||
|
||||
<Select
|
||||
value={day}
|
||||
onChange={(event) => setDay(Number(event.target.value))}
|
||||
data-testid='datepicker-day'
|
||||
>
|
||||
{[...Array(numberOfDays)].map((_, idx) => (
|
||||
<option key={idx} value={idx + 1}>{idx + 1}</option>
|
||||
))}
|
||||
</Select>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<div className='sm:col-span-1'>
|
||||
<Stack>
|
||||
<Text size='sm' weight='medium' theme='muted'>
|
||||
<FormattedMessage id='datepicker.year' defaultMessage='Year' />
|
||||
</Text>
|
||||
|
||||
<Select
|
||||
value={year}
|
||||
onChange={(event) => setYear(Number(event.target.value))}
|
||||
data-testid='datepicker-year'
|
||||
>
|
||||
{[...Array(121)].map((_, idx) => (
|
||||
<option key={idx} value={currentYear - idx}>{currentYear - idx}</option>
|
||||
))}
|
||||
</Select>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Datepicker;
|
||||
@@ -4,6 +4,7 @@ export { Card, CardBody, CardHeader, CardTitle } from './card/card';
|
||||
export { default as Checkbox } from './checkbox/checkbox';
|
||||
export { default as Column } from './column/column';
|
||||
export { default as Counter } from './counter/counter';
|
||||
export { default as Datepicker } from './datepicker/datepicker';
|
||||
export { default as Emoji } from './emoji/emoji';
|
||||
export { default as EmojiSelector } from './emoji-selector/emoji-selector';
|
||||
export { default as FileInput } from './file-input/file-input';
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import * as React from 'react';
|
||||
|
||||
interface ISelect extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||
children: Iterable<React.ReactNode>,
|
||||
}
|
||||
|
||||
/** Multiple-select dropdown. */
|
||||
const Select = React.forwardRef<HTMLSelectElement>((props, ref) => {
|
||||
const Select = React.forwardRef<HTMLSelectElement, ISelect>((props, ref) => {
|
||||
const { children, ...filteredProps } = props;
|
||||
|
||||
return (
|
||||
<select
|
||||
ref={ref}
|
||||
className='pl-3 pr-10 py-2 text-base border-gray-300 dark:border-slate-700 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-800 sm:text-sm rounded-md'
|
||||
className='w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-slate-700 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-800 dark:text-white sm:text-sm rounded-md disabled:opacity-50'
|
||||
{...filteredProps}
|
||||
>
|
||||
{children}
|
||||
|
||||
Reference in New Issue
Block a user