Skip to content

Commit f22af1c

Browse files
author
Austin Green
committed
Update interaction strategy with focus logic
1 parent 093e33d commit f22af1c

File tree

8 files changed

+404
-28
lines changed

8 files changed

+404
-28
lines changed

packages/datepickers/src/DatepickerRange/DatepickerRange.spec.tsx

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,15 +314,58 @@ describe('DatepickerRange', () => {
314314
expect(monthDisplays[1]).toHaveTextContent('March 2019');
315315
});
316316

317+
it('resets preview date if start input is focused while outside of visible range', () => {
318+
const { getAllByTestId, getByTestId } = render(
319+
<Example startValue={DEFAULT_START_VALUE} endValue={DEFAULT_END_VALUE} />
320+
);
321+
322+
const previousPaddle = getAllByTestId('previous-month')[0];
323+
324+
fireEvent.click(previousPaddle);
325+
fireEvent.click(previousPaddle);
326+
fireEvent.click(previousPaddle);
327+
fireEvent.click(previousPaddle);
328+
329+
const monthDisplays = getAllByTestId('month-display');
330+
331+
expect(monthDisplays[0]).toHaveTextContent('October 2018');
332+
expect(monthDisplays[1]).toHaveTextContent('November 2018');
333+
334+
fireEvent.focus(getByTestId('start'));
335+
336+
expect(monthDisplays[0]).toHaveTextContent('February 2019');
337+
expect(monthDisplays[1]).toHaveTextContent('March 2019');
338+
});
339+
340+
it('resets preview date if end input is focused while outside of visible range', () => {
341+
const { getAllByTestId, getByTestId } = render(
342+
<Example startValue={DEFAULT_START_VALUE} endValue={DEFAULT_END_VALUE} />
343+
);
344+
345+
const nextPaddle = getAllByTestId('next-month')[1];
346+
347+
fireEvent.click(nextPaddle);
348+
fireEvent.click(nextPaddle);
349+
fireEvent.click(nextPaddle);
350+
fireEvent.click(nextPaddle);
351+
352+
const monthDisplays = getAllByTestId('month-display');
353+
354+
expect(monthDisplays[0]).toHaveTextContent('June 2019');
355+
expect(monthDisplays[1]).toHaveTextContent('July 2019');
356+
357+
fireEvent.focus(getByTestId('end'));
358+
359+
expect(monthDisplays[0]).toHaveTextContent('March 2019');
360+
expect(monthDisplays[1]).toHaveTextContent('April 2019');
361+
});
362+
317363
it('renders small styling correctly', () => {
318364
const { getAllByTestId, rerender } = render(<Example small />);
319-
320365
const calendarWrappers = getAllByTestId('calendar-wrapper');
321366

322367
expect(calendarWrappers[0]).toHaveStyleRule('padding', '16px');
323-
324368
rerender(<Example />);
325-
326369
expect(calendarWrappers[0]).toHaveStyleRule('padding', '20px');
327370
});
328371
});
@@ -450,6 +493,86 @@ describe('DatepickerRange', () => {
450493

451494
expect(onChangeSpy).not.toHaveBeenCalled();
452495
});
496+
497+
it('updates valid start value when start input is focused', () => {
498+
const { getAllByTestId, getByTestId } = render(
499+
<Example
500+
startValue={DEFAULT_START_VALUE}
501+
endValue={DEFAULT_END_VALUE}
502+
onChange={onChangeSpy}
503+
/>
504+
);
505+
506+
const monthDisplays = getAllByTestId('calendar-wrapper');
507+
508+
fireEvent.focus(getByTestId('start'));
509+
fireEvent.click(globalGetAllByTestId(monthDisplays[0], 'day')[12]);
510+
511+
expect(onChangeSpy).toHaveBeenCalledWith({
512+
startValue: new Date(2019, 1, 8),
513+
endValue: new Date(2019, 2, 5)
514+
});
515+
});
516+
517+
it('updates invalid start value when start input is focused', () => {
518+
const { getAllByTestId, getByTestId } = render(
519+
<Example
520+
startValue={DEFAULT_START_VALUE}
521+
endValue={DEFAULT_END_VALUE}
522+
onChange={onChangeSpy}
523+
/>
524+
);
525+
526+
const monthDisplays = getAllByTestId('calendar-wrapper');
527+
528+
fireEvent.focus(getByTestId('start'));
529+
fireEvent.click(globalGetAllByTestId(monthDisplays[1], 'day')[12]);
530+
531+
expect(onChangeSpy).toHaveBeenCalledWith({
532+
startValue: new Date(2019, 2, 8),
533+
endValue: undefined
534+
});
535+
});
536+
537+
it('updates valid end value when end input is focused', () => {
538+
const { getAllByTestId, getByTestId } = render(
539+
<Example
540+
startValue={DEFAULT_START_VALUE}
541+
endValue={DEFAULT_END_VALUE}
542+
onChange={onChangeSpy}
543+
/>
544+
);
545+
546+
const monthDisplays = getAllByTestId('calendar-wrapper');
547+
548+
fireEvent.focus(getByTestId('end'));
549+
fireEvent.click(globalGetAllByTestId(monthDisplays[1], 'day')[12]);
550+
551+
expect(onChangeSpy).toHaveBeenCalledWith({
552+
startValue: new Date(2019, 1, 5),
553+
endValue: new Date(2019, 2, 8)
554+
});
555+
});
556+
557+
it('updates invalid end value when end input is focused', () => {
558+
const { getAllByTestId, getByTestId } = render(
559+
<Example
560+
startValue={DEFAULT_START_VALUE}
561+
endValue={DEFAULT_END_VALUE}
562+
onChange={onChangeSpy}
563+
/>
564+
);
565+
566+
const monthDisplays = getAllByTestId('calendar-wrapper');
567+
568+
fireEvent.focus(getByTestId('end'));
569+
fireEvent.click(globalGetAllByTestId(monthDisplays[0], 'day')[8]);
570+
571+
expect(onChangeSpy).toHaveBeenCalledWith({
572+
startValue: new Date(2019, 1, 4),
573+
endValue: undefined
574+
});
575+
});
453576
});
454577

455578
describe('customParseDate()', () => {

packages/datepickers/src/DatepickerRange/DatepickerRange.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const DatepickerRange = (props: PropsWithChildren<IDatepickerRangeProps>) => {
7171

7272
const [state, dispatch] = useReducer(reducer, retrieveInitialState(props));
7373
const previousStartValue = useRef(props.startValue);
74+
const previousEndValue = useRef(props.endValue);
7475
const startInputRef = useRef<HTMLInputElement>();
7576
const endInputRef = useRef<HTMLInputElement>();
7677

@@ -80,7 +81,7 @@ const DatepickerRange = (props: PropsWithChildren<IDatepickerRangeProps>) => {
8081
value: props.startValue
8182
});
8283

83-
if (previousStartValue.current !== props.startValue) {
84+
if (previousStartValue.current !== props.startValue && props.startValue !== undefined) {
8485
endInputRef.current && endInputRef.current.focus();
8586
}
8687

@@ -92,6 +93,12 @@ const DatepickerRange = (props: PropsWithChildren<IDatepickerRangeProps>) => {
9293
type: 'CONTROLLED_END_VALUE_CHANGE',
9394
value: props.endValue
9495
});
96+
97+
if (previousEndValue.current !== props.endValue && props.endValue !== undefined) {
98+
startInputRef.current && startInputRef.current.focus();
99+
}
100+
101+
previousEndValue.current = props.endValue;
95102
}, [props.endValue]);
96103

97104
return (

packages/datepickers/src/DatepickerRange/components/End.spec.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import React from 'react';
99
import { render, fireEvent } from 'garden-test-utils';
1010
import mockDate from 'mockdate';
11+
import { KEY_CODES } from '@zendeskgarden/container-utilities';
1112
import DatepickerRange, { IDatepickerRangeProps } from '../DatepickerRange';
1213

1314
const DEFAULT_START_VALUE = new Date(2019, 1, 5);
@@ -113,6 +114,25 @@ describe('DatepickerRange', () => {
113114
});
114115
});
115116

117+
it('calls onChange with provided date if ENTER key is used', () => {
118+
const { getByTestId } = render(
119+
<Example
120+
startValue={DEFAULT_START_VALUE}
121+
endValue={DEFAULT_END_VALUE}
122+
onChange={onChangeSpy}
123+
/>
124+
);
125+
const endInput = getByTestId('end');
126+
127+
fireEvent.change(endInput, { target: { value: 'January 4th, 2019' } });
128+
fireEvent.keyDown(endInput, { keyCode: KEY_CODES.ENTER });
129+
130+
expect(onChangeSpy).toHaveBeenCalledWith({
131+
startValue: DEFAULT_START_VALUE,
132+
endValue: new Date(2019, 0, 4)
133+
});
134+
});
135+
116136
it('does not call onChange with provided date if invalid', () => {
117137
const { getByTestId } = render(
118138
<Example
@@ -168,5 +188,45 @@ describe('DatepickerRange', () => {
168188

169189
expect(onBlurSpy).toHaveBeenCalled();
170190
});
191+
192+
it('calls onFocus prop if provided', () => {
193+
const onFocusSpy = jest.fn();
194+
195+
const { getByTestId } = render(
196+
<DatepickerRange>
197+
<DatepickerRange.Start>
198+
<input data-test-id="start" />
199+
</DatepickerRange.Start>
200+
<DatepickerRange.End>
201+
<input data-test-id="end" onFocus={onFocusSpy} />
202+
</DatepickerRange.End>
203+
<DatepickerRange.Calendar />
204+
</DatepickerRange>
205+
);
206+
207+
fireEvent.focus(getByTestId('end'));
208+
209+
expect(onFocusSpy).toHaveBeenCalled();
210+
});
211+
212+
it('calls onKeyDown prop if provided', () => {
213+
const onKeyDownSpy = jest.fn();
214+
215+
const { getByTestId } = render(
216+
<DatepickerRange>
217+
<DatepickerRange.Start>
218+
<input data-test-id="start" />
219+
</DatepickerRange.Start>
220+
<DatepickerRange.End>
221+
<input data-test-id="end" onKeyDown={onKeyDownSpy} />
222+
</DatepickerRange.End>
223+
<DatepickerRange.Calendar />
224+
</DatepickerRange>
225+
);
226+
227+
fireEvent.keyDown(getByTestId('end'));
228+
229+
expect(onKeyDownSpy).toHaveBeenCalled();
230+
});
171231
});
172232
});

packages/datepickers/src/DatepickerRange/components/End.tsx

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,59 @@
55
* found at http://www.apache.org/licenses/LICENSE-2.0.
66
*/
77

8-
import React, { PropsWithChildren } from 'react';
8+
import React, { PropsWithChildren, HTMLProps, useCallback } from 'react';
9+
import { KEY_CODES } from '@zendeskgarden/container-utilities';
910
import useDatepickerRangeContext from '../utils/useDatepickerRangeContext';
1011

11-
const End = (props: PropsWithChildren<any>) => {
12+
const End = (props: PropsWithChildren<HTMLProps<HTMLInputElement>>) => {
1213
const { state, dispatch, endInputRef } = useDatepickerRangeContext();
1314

14-
return React.cloneElement(React.Children.only(props.children as any), {
15-
value: state.endInputValue,
16-
ref: endInputRef,
17-
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
15+
const onChangeCallback = useCallback(
16+
(e: React.ChangeEvent<HTMLInputElement>) => {
1817
dispatch({ type: 'END_INPUT_ONCHANGE', value: e.target.value });
1918

20-
props.children.props.onChange && props.children.props.onChange(e);
19+
(props.children as any).props.onChange && (props.children as any).props.onChange(e);
20+
},
21+
[dispatch, props.children]
22+
);
23+
24+
const onFocusCallback = useCallback(
25+
(e: React.FocusEvent<HTMLInputElement>) => {
26+
dispatch({ type: 'END_FOCUS' });
27+
28+
(props.children as any).props.onFocus && (props.children as any).props.onFocus(e);
2129
},
22-
onBlur: (e: React.FocusEvent<HTMLInputElement>) => {
30+
[dispatch, props.children]
31+
);
32+
33+
const onKeydownCallback = useCallback(
34+
(e: React.KeyboardEvent<HTMLInputElement>) => {
35+
if (e.keyCode === KEY_CODES.ENTER) {
36+
dispatch({ type: 'END_BLUR' });
37+
e.preventDefault();
38+
}
39+
40+
(props.children as any).props.onKeyDown && (props.children as any).props.onKeyDown(e);
41+
},
42+
[dispatch, props.children]
43+
);
44+
45+
const onBlurCallback = useCallback(
46+
(e: React.FocusEvent<HTMLInputElement>) => {
2347
dispatch({ type: 'END_BLUR' });
2448

25-
props.children.props.onBlur && props.children.props.onBlur(e);
26-
}
49+
(props.children as any).props.onBlur && (props.children as any).props.onBlur(e);
50+
},
51+
[dispatch, props.children]
52+
);
53+
54+
return React.cloneElement(React.Children.only(props.children as any), {
55+
value: state.endInputValue,
56+
ref: endInputRef,
57+
onChange: onChangeCallback,
58+
onFocus: onFocusCallback,
59+
onKeyDown: onKeydownCallback,
60+
onBlur: onBlurCallback
2761
});
2862
};
2963

0 commit comments

Comments
 (0)