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

Fix browser issues with blur/focus by adding timeouts #715

Merged
merged 11 commits into from
May 6, 2018
13 changes: 9 additions & 4 deletions docs/src/code-samples/examples/input-custom-overlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import PropTypes from 'prop-types';
import DayPickerInput from 'react-day-picker/DayPickerInput';
import 'react-day-picker/lib/style.css';

function CustomOverlay({ classNames, selectedDay, children }) {
function CustomOverlay({ classNames, selectedDay, children, ...props }) {
return (
<div className={classNames.overlayWrapper} style={{ marginLeft: -100 }}>
<div
className={classNames.overlayWrapper}
style={{ marginLeft: -100 }}
{...props}
>
<div className={classNames.overlay}>
<h3>Hello day picker!</h3>
<button onClick={() => console.log('clicked!')}>button</button>
<p>
{selectedDay
? `You picked: ${selectedDay.toLocaleDateString()}`
Expand All @@ -22,8 +27,8 @@ function CustomOverlay({ classNames, selectedDay, children }) {

CustomOverlay.propTypes = {
classNames: PropTypes.object.isRequired,
selectedDay: PropTypes.oneOfType([Date]),
children: PropTypes.number.isRequired,
selectedDay: PropTypes.instanceOf(Date),
children: PropTypes.node.isRequired,
};

export default function Example() {
Expand Down
171 changes: 92 additions & 79 deletions src/DayPickerInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';

import DayPicker from './DayPicker';
import { isIE } from './Helpers';
import { isSameMonth, isDate } from './DateUtils';
import { getModifiersForDay } from './ModifiersUtils';
import { ESC, TAB } from './keys';
Expand All @@ -11,7 +10,36 @@ import { ESC, TAB } from './keys';
export const HIDE_TIMEOUT = 100;

/**
* The default function used to format a Date to String, passed to the `format` prop.
* The default component used as Overlay.
*
* @param {Object} props
*/
export function OverlayComponent({
input,
selectedDay,
month,
children,
classNames,
...props
}) {
return (
<div className={classNames.overlayWrapper} {...props}>
<div className={classNames.overlay}>{children}</div>
</div>
);
}

OverlayComponent.propTypes = {
input: PropTypes.any,
selectedDay: PropTypes.any,
month: PropTypes.instanceOf(Date),
children: PropTypes.node,
classNames: PropTypes.object,
};

/**
* The default function used to format a Date to String, passed to the `format`
* prop.
* @param {Date} d
* @return {String}
*/
Expand All @@ -26,7 +54,8 @@ export function defaultFormat(d) {
}

/**
* The default function used to parse a String as Date, passed to the `parse` prop.
* The default function used to parse a String as Date, passed to the `parse`
* prop.
* @param {String} str
* @return {Date}
*/
Expand Down Expand Up @@ -105,11 +134,7 @@ export default class DayPickerInput extends React.Component {
keepFocus: true,
component: 'input',
inputProps: {},
overlayComponent: ({ children, classNames }) => (
<div className={classNames.overlayWrapper}>
<div className={classNames.overlay}>{children}</div>
</div>
),
overlayComponent: OverlayComponent,
classNames: {
container: 'DayPickerInput',
overlayWrapper: 'DayPickerInput-OverlayWrapper',
Expand Down Expand Up @@ -175,8 +200,9 @@ export default class DayPickerInput extends React.Component {
componentWillUnmount() {
clearTimeout(this.clickTimeout);
clearTimeout(this.hideTimeout);
clearTimeout(this.ieInputFocusTimeout);
clearTimeout(this.ieInputBlurTimeout);
clearTimeout(this.inputFocusTimeout);
clearTimeout(this.inputBlurTimeout);
clearTimeout(this.overlayBlurTimeout);
}

getInitialMonthFromProps(props) {
Expand Down Expand Up @@ -217,15 +243,14 @@ export default class DayPickerInput extends React.Component {

input = null;
daypicker = null;
overlayNode = null;
clickTimeout = null;
hideTimeout = null;
ieInputFocusTimeout = null;
ieInputBlutTimeout = null;
inputBlurTimeout = null;
inputFocusTimeout = null;

/**
* Update the component's state and fire the `onDayChange` event
* passing the day's modifiers to it.
* Update the component's state and fire the `onDayChange` event passing the
* day's modifiers to it.
*
* @param {Date} day - Will be used for changing the month
* @param {String} value - Input field value
Expand Down Expand Up @@ -265,13 +290,13 @@ export default class DayPickerInput extends React.Component {
showDayPicker() {
const { parseDate, format, dayPickerProps } = this.props;
const { value, showOverlay } = this.state;
let month;
if (showOverlay === false) {
// Reset the current displayed month when showing the overlay
month = value
? parseDate(value, format, dayPickerProps.locale) // Use the month in the input field
: this.getInitialMonthFromProps(this.props); // Restore the month from the props
if (showOverlay) {
return;
}
// Reset the current displayed month when showing the overlay
const month = value
? parseDate(value, format, dayPickerProps.locale) // Use the month in the input field
: this.getInitialMonthFromProps(this.props); // Restore the month from the props
this.setState({
showOverlay: true,
month: month || this.state.month,
Expand All @@ -284,15 +309,10 @@ export default class DayPickerInput extends React.Component {
* @memberof DayPickerInput
*/
hideDayPicker() {
this.setState({ showOverlay: false });
}

showOverlayBasedOnTargetNode(node) {
if (this.overlayNode && this.overlayNode.contains(node)) {
this.showDayPicker();
} else {
this.hideDayPicker();
if (this.state.showOverlay === false) {
return;
}
this.setState({ showOverlay: false });
}

hideAfterDayClick() {
Expand All @@ -312,22 +332,29 @@ export default class DayPickerInput extends React.Component {

handleInputFocus(e) {
this.showDayPicker();
// Set `overlayHasFocus` after a timeout so the overlay can be hidden when
// the input is blurred
this.inputFocusTimeout = setTimeout(() => {
this.overlayHasFocus = false;
}, 2);
if (this.props.inputProps.onFocus) {
e.persist();
this.props.inputProps.onFocus(e);
}
}

// When the input is blurred, the overlay should disappear. However the input
// is blurred also when the user interacts with the overlay (e.g. the overlay
// get the focus by clicking it). In these cases, the overlay should not be
// hidden. There are different approaches to avoid hiding the overlay when
// this happens, but the only cross-browser hack we’ve found is to set all
// these timeouts in code before changing `overlayHasFocus`.
handleInputBlur(e) {
const target = e.relatedTarget;
if (!isIE()) {
this.showOverlayBasedOnTargetNode(target);
} else {
this.ieInputBlurTimeout = setTimeout(
() => this.showOverlayBasedOnTargetNode(target),
HIDE_TIMEOUT
);
}
this.inputBlurTimeout = setTimeout(() => {
if (!this.overlayHasFocus) {
this.hideDayPicker();
}
}, 1);
if (this.props.inputProps.onBlur) {
e.persist();
this.props.inputProps.onBlur(e);
Expand All @@ -339,30 +366,16 @@ export default class DayPickerInput extends React.Component {
return;
}
e.preventDefault();
if (!isIE()) {
this.input.focus();
} else {
// Fix behavior in Internet Explorer
// See https://github.com/gpbl/react-day-picker/pull/691
this.ieInputFocusTimeout = setTimeout(() => {
this.input.focus();
// Reset the hide timeout for reasons
// TODO: add a comment specifying why we need this
if (this.hideTimeout) {
this.hideTimeout = setTimeout(() => {
this.hideDayPicker();
this.hideTimeout = null;
}, HIDE_TIMEOUT);
}
}, HIDE_TIMEOUT);
}
this.input.focus();
this.overlayHasFocus = true;
}

handleOverlayBlur(e) {
this.setState({
showOverlay:
this.overlayNode && this.overlayNode.contains(e.relatedTarget),
});
handleOverlayBlur() {
// We need to set a timeout otherwise IE11 will hide the overlay when
// focusing it
this.overlayBlurTimeout = setTimeout(() => {
this.overlayHasFocus = false;
}, 3);
}

handleInputChange(e) {
Expand Down Expand Up @@ -395,6 +408,8 @@ export default class DayPickerInput extends React.Component {
handleInputKeyDown(e) {
if (e.keyCode === TAB) {
this.hideDayPicker();
} else {
this.showDayPicker();
}
if (this.props.inputProps.onKeyDown) {
e.persist();
Expand All @@ -403,9 +418,10 @@ export default class DayPickerInput extends React.Component {
}

handleInputKeyUp(e) {
// Hide the overlay if the ESC key is pressed
if (e.keyCode === ESC) {
this.hideDayPicker();
} else {
this.showDayPicker();
}
if (this.props.inputProps.onKeyUp) {
e.persist();
Expand Down Expand Up @@ -498,28 +514,25 @@ export default class DayPickerInput extends React.Component {
}
const Overlay = this.props.overlayComponent;
return (
<span
<Overlay
classNames={classNames}
month={this.state.month}
selectedDay={selectedDay}
input={this.input}
tabIndex={0} // tabIndex is necessary to catch focus/blur events on Safari
onFocus={this.handleOverlayFocus}
ref={el => (this.overlayNode = el)}
onBlur={this.handleOverlayBlur}
>
<Overlay
classNames={classNames}
<DayPicker
ref={el => (this.daypicker = el)}
onTodayButtonClick={onTodayButtonClick}
{...dayPickerProps}
month={this.state.month}
selectedDay={selectedDay}
input={this.input}
>
<DayPicker
ref={el => (this.daypicker = el)}
onTodayButtonClick={onTodayButtonClick}
{...dayPickerProps}
month={this.state.month}
selectedDays={selectedDay}
onDayClick={this.handleDayClick}
onMonthChange={this.handleMonthChange}
/>
</Overlay>
</span>
selectedDays={selectedDay}
onDayClick={this.handleDayClick}
onMonthChange={this.handleMonthChange}
/>
</Overlay>
);
}

Expand Down
5 changes: 0 additions & 5 deletions src/Helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,3 @@ export function nodeListToArray(nodeList) {
export function hasOwnProp(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}

export function isIE() {
// eslint-disable-next-line no-undef
return /* @cc_on!@ */ false || !!document.documentMode;
}
30 changes: 18 additions & 12 deletions test/daypickerinput/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,29 @@ describe('DayPickerInput', () => {
expect(document.activeElement).not.toEqual(instance.input);
});
});
describe('overlayblur', () => {
it('should set overlayHasFocus to false', done => {
const wrapper = mount(<DayPickerInput showOverlay keepFocus />);
wrapper.find('.DayPickerInput-Overlay').simulate('focus');
wrapper.find('.DayPickerInput-Overlay').simulate('blur');
setTimeout(() => {
wrapper.update();
expect(wrapper.instance().overlayHasFocus).toBe(false);
done();
}, 100);
});
});

describe('blur', () => {
it('should hide the overlay when the input is blurred', () => {
it('should hide the overlay when the input is blurred', done => {
const wrapper = mount(<DayPickerInput value="12/15/2017" />);
wrapper.find('input').simulate('focus');
wrapper.find('input').simulate('blur');
expect(wrapper.find('.DayPicker')).toHaveLength(0);
setTimeout(() => {
wrapper.update();
expect(wrapper.find('.DayPicker')).toHaveLength(0);
done();
}, 100);
});
it('should call `onBlur` event handler', () => {
const onBlur = jest.fn();
Expand All @@ -79,16 +95,6 @@ describe('DayPickerInput', () => {
expect(onBlur).toHaveBeenCalledTimes(1);
});
});

describe('overlayblur', () => {
it('should hide the overlay', () => {
const wrapper = mount(<DayPickerInput showOverlay keepFocus />);
wrapper.find('.DayPickerInput-Overlay').simulate('focus');
wrapper.find('.DayPickerInput-Overlay').simulate('blur');
expect(wrapper.find('.DayPicker')).toHaveLength(0);
});
});

describe('change', () => {
it('should call `onChange` event handler', () => {
const onChange = jest.fn();
Expand Down
Loading