Merge branch 'improve-datepicker' into 'develop'

Add new custom datepicker for improved UX

See merge request soapbox-pub/soapbox-fe!1507
chats-fixes
Justin 2022-06-08 17:12:54 +00:00
commit cedbc468bd
8 zmienionych plików z 196 dodań i 26 usunięć

Wyświetl plik

@ -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);
});
});

Wyświetl plik

@ -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;

Wyświetl plik

@ -4,6 +4,7 @@ export { Card, CardBody, CardHeader, CardTitle } from './card/card';
export { default as Checkbox } from './checkbox/checkbox'; export { default as Checkbox } from './checkbox/checkbox';
export { default as Column } from './column/column'; export { default as Column } from './column/column';
export { default as Counter } from './counter/counter'; export { default as Counter } from './counter/counter';
export { default as Datepicker } from './datepicker/datepicker';
export { default as Emoji } from './emoji/emoji'; export { default as Emoji } from './emoji/emoji';
export { default as EmojiSelector } from './emoji-selector/emoji-selector'; export { default as EmojiSelector } from './emoji-selector/emoji-selector';
export { default as FileInput } from './file-input/file-input'; export { default as FileInput } from './file-input/file-input';

Wyświetl plik

@ -1,13 +1,17 @@
import * as React from 'react'; import * as React from 'react';
interface ISelect extends React.SelectHTMLAttributes<HTMLSelectElement> {
children: Iterable<React.ReactNode>,
}
/** Multiple-select dropdown. */ /** Multiple-select dropdown. */
const Select = React.forwardRef<HTMLSelectElement>((props, ref) => { const Select = React.forwardRef<HTMLSelectElement, ISelect>((props, ref) => {
const { children, ...filteredProps } = props; const { children, ...filteredProps } = props;
return ( return (
<select <select
ref={ref} 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} {...filteredProps}
> >
{children} {children}

Wyświetl plik

@ -39,7 +39,10 @@ describe('<AgeVerification />', () => {
store, store,
); );
await userEvent.type(screen.getByLabelText('Birth Date'), '{enter}'); await userEvent.selectOptions(
screen.getByTestId('datepicker-year'),
screen.getByRole('option', { name: '2020' }),
);
fireEvent.submit( fireEvent.submit(
screen.getByRole('button'), { screen.getByRole('button'), {

Wyświetl plik

@ -1,12 +1,11 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as React from 'react'; import * as React from 'react';
import DatePicker from 'react-datepicker';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import snackbar from 'soapbox/actions/snackbar'; import snackbar from 'soapbox/actions/snackbar';
import { verifyAge } from 'soapbox/actions/verification'; import { verifyAge } from 'soapbox/actions/verification';
import { Button, Form, FormGroup, Text } from 'soapbox/components/ui'; import { Button, Datepicker, Form, Text } from 'soapbox/components/ui';
const messages = defineMessages({ const messages = defineMessages({
fail: { fail: {
@ -23,13 +22,6 @@ function meetsAgeMinimum(birthday, ageMinimum) {
return new Date(year + ageMinimum, month, day) <= new Date(); return new Date(year + ageMinimum, month, day) <= new Date();
} }
function getMaximumDate(ageMinimum) {
const date = new Date();
date.setUTCFullYear(date.getUTCFullYear() - ageMinimum);
return date;
}
const AgeVerification = () => { const AgeVerification = () => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -67,21 +59,9 @@ const AgeVerification = () => {
</h1> </h1>
</div> </div>
<div className='sm:pt-10 sm:w-2/3 md:w-1/2 mx-auto'> <div className='sm:pt-10 sm:w-2/3 mx-auto'>
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<FormGroup labelText='Birth Date'> <Datepicker onChange={onChange} />
<DatePicker
selected={date}
dateFormat='MMMM d, yyyy'
onChange={onChange}
showMonthDropdown
showYearDropdown
maxDate={getMaximumDate(ageMinimum)}
className='block w-full sm:text-sm border-gray-300 rounded-md focus:ring-indigo-500 focus:border-indigo-500'
dropdownMode='select'
required
/>
</FormGroup>
<Text theme='muted' size='sm'> <Text theme='muted' size='sm'>
{siteTitle} requires users to be at least {ageMinimum} years old to {siteTitle} requires users to be at least {ageMinimum} years old to

Wyświetl plik

@ -344,6 +344,9 @@
"crypto_donate_panel.actions.view": "Click to see {count} {count, plural, one {wallet} other {wallets}}", "crypto_donate_panel.actions.view": "Click to see {count} {count, plural, one {wallet} other {wallets}}",
"crypto_donate_panel.heading": "Donate Cryptocurrency", "crypto_donate_panel.heading": "Donate Cryptocurrency",
"crypto_donate_panel.intro.message": "{siteTitle} accepts cryptocurrency donations to fund our service. Thank you for your support!", "crypto_donate_panel.intro.message": "{siteTitle} accepts cryptocurrency donations to fund our service. Thank you for your support!",
"datepicker.month": "Month",
"datepicker.day": "Day",
"datepicker.year": "Year",
"datepicker.hint": "Scheduled to post at…", "datepicker.hint": "Scheduled to post at…",
"datepicker.next_month": "Next month", "datepicker.next_month": "Next month",
"datepicker.next_year": "Next year", "datepicker.next_year": "Next year",

Wyświetl plik

@ -1,5 +1,7 @@
select { select {
@apply pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md; @apply pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
} }
.form-error::before, .form-error::before,