Skip to content

Commit

Permalink
Merge pull request #59 from adhocteam/js-129-change-datepicker-lib
Browse files Browse the repository at this point in the history
Switch react-datepicker for react-dates
  • Loading branch information
jasalisbury committed Nov 6, 2020
2 parents c3ab233 + 34a5592 commit eafcba2
Show file tree
Hide file tree
Showing 10 changed files with 392 additions and 157 deletions.
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,17 +12,19 @@
"@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-idle-timer": "^4.4.2",
"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 @@ -15,6 +15,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 @@ -101,24 +101,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
Loading

0 comments on commit eafcba2

Please sign in to comment.