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

Switch react-datepicker for react-dates #59

Merged
merged 3 commits into from
Nov 6, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 6 additions & 3 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@babel/runtime": "^7.12.1",
"@fortawesome/fontawesome-free": "^5.15.1",
"@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-solid-svg-icons": "^5.15.1",
Expand All @@ -11,16 +12,18 @@
"@trussworks/react-uswds": "^1.9.1",
"http-proxy-middleware": "^1.0.5",
"lodash": "^4.17.20",
"moment": "^2.29.1",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-datepicker": "^3.3.0",
"react-dom": "^16.13.1",
"react": "^16.14.0",
"react-dates": "^21.8.0",
"react-dom": "^16.14.0",
"react-dropzone": "^11.2.0",
"react-hook-form": "^6.9.0",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-router-prop-types": "^1.0.5",
"react-scripts": "^3.4.4",
"react-with-direction": "^1.3.1",
"url-join": "^4.0.1",
"uswds": "^2.9.0"
},
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,17 @@ body {
font-size: 2.13rem;
font-weight: bold;
}

.SingleDatePickerInput__withBorder {
border: 1px solid #565c65;
border-radius: 0px;
}

.DateInput_input {
color: #1b1b1b;
font-weight: 400;
}

.SingleDatePickerInput__disabled {
opacity: 0.7;
}
2 changes: 2 additions & 0 deletions frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import Admin from './pages/Admin';
import Unauthenticated from './pages/Unauthenticated';
import Home from './pages/Home';
import ActivityReport from './pages/ActivityReport';
import 'react-dates/initialize';
import 'react-dates/lib/css/_datepicker.css';
import './App.css';

function App() {
Expand Down
15 changes: 12 additions & 3 deletions frontend/src/components/DatePicker.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
.usa-date-picker__button {
margin-top: 0.5rem
}
.smart-hub--date-picker-input {
/*
We want the open button on the right of the date input and
want the next focusable item after the button to be the calendar.
row-reverse to deal with tab ordering in the case where
the open button is clicked and then the user changes focus
with the tab key. Since the open button has tabindex of -1
we won't run into an issue of focus moving "backwards"
*/
flex-direction: row-reverse;
width: fit-content;
}
91 changes: 38 additions & 53 deletions frontend/src/components/DatePicker.js
Original file line number Diff line number Diff line change
@@ -1,88 +1,69 @@
/*
This component requires being embedded into a `react-hook-form` form

Uses ReactDatePicker styled as the USWDS date picker. The react USWDS library does
Uses react-dates styled as the USWDS date picker. The react USWDS library does
not have a date picker component. We could have used USWDS component directly here
instead of ReactDatePicker but I decided against for a couple reasons:
instead of react-dates but I decided against for a couple reasons:
1. I was having a hard time getting input back into react hook form using the USWDS
code directly. Issue centered around the USWDS code not sending `onChange` events
when an invalid date was input
2. Related to #1, ReactDatePicker handles invalid dates by removing the invalid input
on blur, which is nicer then how the USWDS component handled invalid dates.
3. ReactDatePicker had easily readable documentation and conveniences such as `maxDate`
2. react-dates had easily readable documentation and conveniences such as `maxDate`
and `minDate`. I couldn't find great docs using the USWDS datepicker javascript
*/

import React, { forwardRef } from 'react';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { TextInput, Label } from '@trussworks/react-uswds';
import ReactDatePicker from 'react-datepicker';
import { Label } from '@trussworks/react-uswds';
import { SingleDatePicker } from 'react-dates';
import { OPEN_UP, OPEN_DOWN } from 'react-dates/constants';
import { Controller } from 'react-hook-form';
import moment from 'moment';

import 'react-datepicker/dist/react-datepicker.css';
import './DatePicker.css';

const DateInput = ({
control, label, minDate, name, disabled, maxDate,
control, label, minDate, name, disabled, maxDate, openUp, required,
}) => {
const labelId = `${name}-id`;
const hintId = `${name}-hint`;
const [isFocused, updateFocus] = useState(false);
const openDirection = openUp ? OPEN_UP : OPEN_DOWN;

const CustomInput = forwardRef(({ value, onChange, onFocus }, ref) => (
<div className="display-flex" onFocus={onFocus}>
<TextInput
id={name}
disabled={disabled}
inputRef={ref}
onChange={onChange}
className="usa-date-picker__external-input"
aria-describedby={`${labelId} ${hintId}`}
value={value}
autoComplete="off"
/>
<button disabled={disabled} aria-hidden tabIndex={-1} aria-label="open calendar" type="button" className="usa-date-picker__button" />
</div>
));

CustomInput.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func,
onFocus: PropTypes.func,
};
const isOutsideRange = (date) => {
const isBefore = minDate && date.isBefore(minDate);
const isAfter = maxDate && date.isAfter(maxDate);

CustomInput.defaultProps = {
value: undefined,
onChange: undefined,
onFocus: undefined,
return isBefore || isAfter;
};

return (
<>
<Label id={labelId} htmlFor={name}>{label}</Label>
<div className="usa-hint" id={hintId}>mm/dd/yyyy</div>
<Controller
render={({ onChange, value }) => (
<ReactDatePicker
dateFormat="MM/dd/yyyy"
showTimeSelect={false}
todayButton="Today"
minDate={minDate}
maxDate={maxDate}
strictParsing
selected={value}
onChange={onChange}
customInput={<CustomInput />}
dropdownMode="select"
placeholderText="Click to select time"
shouldCloseOnSelect
/>
render={({ onChange, value, ref }) => (
<div className="display-flex smart-hub--date-picker-input">
<button onClick={() => { updateFocus(true); }} disabled={disabled} tabIndex={-1} aria-label="open calendar" type="button" className="usa-date-picker__button margin-top-0" />
<SingleDatePicker
id={name}
focused={isFocused}
date={value}
ref={ref}
isOutsideRange={isOutsideRange}
numberOfMonths={1}
openDirection={openDirection}
disabled={disabled}
onDateChange={onChange}
onFocusChange={({ focused }) => updateFocus(focused)}
/>
</div>
)}
control={control}
name={name}
disabled={disabled}
defaultValue={null}
rules={{
required: true,
required,
}}
/>
</>
Expand All @@ -95,15 +76,19 @@ DateInput.propTypes = {
control: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
minDate: PropTypes.instanceOf(Date),
maxDate: PropTypes.instanceOf(Date),
minDate: PropTypes.instanceOf(moment),
maxDate: PropTypes.instanceOf(moment),
openUp: PropTypes.bool,
disabled: PropTypes.bool,
required: PropTypes.bool,
};

DateInput.defaultProps = {
minDate: undefined,
maxDate: undefined,
disabled: false,
openUp: false,
required: true,
};

export default DateInput;
55 changes: 31 additions & 24 deletions frontend/src/components/__tests__/DatePicker.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,66 @@
import '@testing-library/jest-dom';
import React from 'react';
import {
render, screen, fireEvent, waitFor,
render, screen, fireEvent, waitFor, act,
} from '@testing-library/react';

import { useForm } from 'react-hook-form';
import DatePicker from '../DatePicker';

// react-dates when opening the calendar in these tests. For details see
// https://github.com/airbnb/react-dates/issues/1426#issuecomment-593420014
Object.defineProperty(window, 'getComputedStyle', {
value: () => ({
paddingLeft: 0,
paddingRight: 0,
paddingTop: 0,
paddingBottom: 0,
marginLeft: 0,
marginRight: 0,
marginTop: 0,
marginBottom: 0,
borderBottomWidth: 0,
borderTopWidth: 0,
borderRightWidth: 0,
borderLeftWidth: 0,
}),
});

describe('DatePicker', () => {
// eslint-disable-next-line react/prop-types
const RenderDatePicker = ({ minDate, maxDate, disabled }) => {
const RenderDatePicker = ({ disabled }) => {
const { control } = useForm();
return (
<form>
<DatePicker
control={control}
label="label"
name="picker"
minDate={minDate}
maxDate={maxDate}
disabled={disabled}
/>
</form>
);
};

it('disabled flag disables text input', () => {
render(<RenderDatePicker disabled />);
it('disabled flag disables text input', async () => {
await act(async () => render(<RenderDatePicker disabled />));
expect(screen.getByRole('textbox')).toBeDisabled();
});

it('accepts text input', async () => {
render(<RenderDatePicker />);
const textbox = screen.getByRole('textbox');
fireEvent.change(textbox, { target: { value: '01/01/2000' } });
waitFor(() => expect(screen.getByRole('textbox')).toHaveTextContent('01/01/2000'));
});

describe('maxDate', () => {
it('causes input after minDate to be discarded', async () => {
const date = new Date(2000, 1, 2);
render(<RenderDatePicker maxDate={date} />);
const textbox = screen.getByRole('textbox');
fireEvent.change(textbox, { target: { value: '01/03/1000' } });
waitFor(() => expect(screen.getByRole('textbox')).toHaveTextContent(''));
});
await waitFor(() => expect(screen.getByRole('textbox')).toHaveValue('01/01/2000'));
});

describe('minDate', () => {
it('causes input before minDate to be discarded', async () => {
const date = new Date(2000, 1, 2);
render(<RenderDatePicker minDate={date} />);
const textbox = screen.getByRole('textbox');
fireEvent.change(textbox, { target: { value: '01/01/1000' } });
waitFor(() => expect(screen.getByRole('textbox')).toHaveTextContent(''));
it('clicking the open button will open the calendar', async () => {
await act(async () => {
render(<RenderDatePicker />);
const openCalendar = screen.getByRole('button');
fireEvent.click(openCalendar);
});
const button = await waitFor(() => screen.getByLabelText('Move backward to switch to the previous month.'));
await waitFor(() => expect(button).toBeVisible());
});
});
6 changes: 2 additions & 4 deletions frontend/src/pages/ActivityReport/SectionThree.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,24 +97,22 @@ const PageThree = ({
<Grid col={6}>
<DatePicker
control={control}
watch={watch}
value={startDate}
maxDate={endDate}
name="start-date"
label="Start Date"
register={register}
openUp
/>
</Grid>
<Grid col={6}>
<DatePicker
control={control}
watch={watch}
value={endDate}
minDate={startDate}
disabled={!startDate}
name="end-date"
label="End Date"
register={register}
openUp
/>
</Grid>
<Grid col={5}>
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/pages/ActivityReport/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@ import React from 'react';
import {
render, screen, fireEvent, waitFor, within,
} from '@testing-library/react';
import moment from 'moment';

import ActivityReport from '../index';

const formData = () => ({
'activity-method': ['in-person'],
'activity-type': ['training'],
duration: '1',
'end-date': new Date(),
'end-date': moment(),
grantees: 'Grantee Name 1',
'number-of-participants': '1',
'participant-category': 'grantee',
participants: 'CEO / CFO / Executive',
reason: 'reason 1',
requester: 'grantee',
'resources-used': 'eclkcurl',
'start-date': new Date(),
'start-date': moment(),
topics: 'first',
});

Expand Down
1 change: 1 addition & 0 deletions frontend/src/setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';
import 'react-dates/initialize';
// See https://github.com/testing-library/dom-testing-library/releases/tag/v7.0.0
// 'MutationObserver shim removed'
import MutationObserver from '@sheerun/mutationobserver-shim';
Expand Down