Skip to content

Commit

Permalink
Merge pull request gpbl#715 from gpbl/gpbl/fix-focus-blur-issues
Browse files Browse the repository at this point in the history
Fix browser issues with blur/focus by adding timeouts
  • Loading branch information
gpbl authored May 6, 2018
2 parents 11f4c41 + 14db92d commit f4842f1
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 101 deletions.
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

0 comments on commit f4842f1

Please sign in to comment.