How to onFocus and onBlur a React/Redux form field

2019-07-10 02:23发布

I've got this simple v6 redux-form with an input that renders a custom component that populates its value using react-day-picker.

https://github.com/gpbl/react-day-picker

I chose react-day-picker over others because it doesn't depend on moment.js and works with my current set up.

When I focus the field, I want the datepicker to pop up, but if I click anywhere that's not the datepicker, I want it to disappear.

Essentially, I want my React datepicker to work like the jQueryUI one in:

https://jqueryui.com/datepicker/

The three scenarios I end up with are:

  1. I click the field, the datepicker pops up, but will not disappear unless I select a date or click the field again (this is too rigid for our needs).

  2. I click the field, the datepicker pops up, but will disappear TOO quickly if I click anywhere, as the input field's onBlur gets called before it processes the click event for the datepicker to populate the field with the chosen date.

  3. I click the field, the datepicker pops up, gets auto-focused, blurs properly, except when I click anything that's not < body> or the datepicker.

I first tried to use a sibling empty div that wraps the whole page, so when I click the empty div, it'll toggle the datepicker properly. This worked OK with z-indexes and position: fixed until I changed the datepicker's month, which seems to re-render the datepicker and messed with the order of the clicking, which led to situation 2) again.

My most current attempt is to auto-focus the datepicker div when it pops up, so when I blur anything that's not the datepicker, it will toggle the datepicker. This worked in theory, except the datepicker is a component with many nested < div>'s inside it to control day, week, month, disabled days... so when I click a 'day', it registers a blur because I'm clicking the 'day' <div>, not the root 'datepicker' <div>, which is what was initially focused.

The solution to the above was to tweak 'datepicker' <div>'s onBlur such that it will only toggle the datepicker when document.activeElement is < body>, but that only works if I don't click another form field.

WizardFormPageOne.js:

function WizardFormPageOne({ handleSubmit }) {
  return (
    <form onSubmit={handleSubmit} className="col-xs-6">
      <h1>WizardFormPageOne</h1>

      <div className="card">
        <div className="card-block">
          <div className="form-group">
            <label htmlFor="first">Label 1</label>
            <Field type="text" name="first" component={DateInput} className="form-control" />
          </div>
          ...

export default reduxForm({
  form: 'wizardForm',
  destroyOnUnmount: false,
})(WizardFormPageOne);

DateInput.js:

import React from 'react';

import styles from './styles.css';
import DatePicker from '../DatePicker';

class DateInput extends React.Component { // eslint-disable-line react/prefer-stateless-function
  constructor(props) {
    super(props);

    this.state = {
      dateValue: new Date(),
      activeDateWidget: false,
    };
  }

  changeDate = (date) => {
    this.setState({
      dateValue: date,
    });
  }

  changeActiveDateWidget = (e) => {
    e.stopPropagation();
    this.setState({
      activeDateWidget: !this.state.activeDateWidget,
    });
  }

  render() {
    const { input, meta } = this.props;
    const { dateValue, activeDateWidget } = this.state;
    return (
      <div className={styles.dateInput}>
        <input
          {...input}
          className="form-control"
          type="text"
          value={dateValue}
          onClick={this.changeActiveDateWidget}
          // onBlur={this.changeActiveDateWidget}
        />

        {activeDateWidget ? (
          <div>
            <DatePicker
              changeActiveDateWidget={this.changeActiveDateWidget}
              changeDate={this.changeDate}
              dateValue={dateValue}
            />
          </div>
        ) : (
          <div></div>
        )}
      </div>
    );
  }
}

export default DateInput;

DatePicker.js:

import React from 'react';
import 'react-day-picker/lib/style.css';
import DayPicker, { DateUtils } from 'react-day-picker';

import styles from './styles.css';
import disabledDays from './disabledDays';


class DatePicker extends React.Component { // eslint-disable-line react/prefer-stateless-function
  constructor(props) {
    super(props);
    this.state = {
      selectedDay: new Date(),
    };
  }

  componentDidMount() {
    if (this._input) {
      this._input.focus();
    }
  }

  handleDayClick = (e, day, { disabled }) => {
    e.stopPropagation();
    if (disabled) {
      return;
    }
    this.setState({ selectedDay: day }, () => {
      this.props.changeDate(day);
      this.props.changeActiveDateWidget();
    });
  }

  focusThisComponent = (e) => {
    if (e) {
      this._input = e;
    }
  }

  focusIt = () => {
    console.log('focusing');
  }

  blurIt = () => {
    console.log('blurring');
    setTimeout(() => {
      if (document.activeElement == document.body) {
        this.props.changeActiveDateWidget();
      }
    }, 1);
  }

  render() {
    const { changeActiveDateWidget } = this.props;
    const { selectedDay } = this.state;
    return (
      <div
        className={styles.datePicker}
        ref={this.focusThisComponent}
        tabIndex="1"
        onFocus={this.focusIt}
        onBlur={this.blurIt}
      >
        <DayPicker
          id="THISTHING"
          initialMonth={selectedDay}
          disabledDays={disabledDays}
          selectedDays={(day) => DateUtils.isSameDay(selectedDay, day)}
          onDayClick={this.handleDayClick}
        />
      </div>
    );
  }
}

export default DatePicker;

Here's a screencast of the issue I'm having now:

http://screencast.com/t/kZuIwUzl

The datepicker toggles properly, except when clicking on another field, at which point it stops blurring/toggling properly. All my tinkering either led me make to one of the three scenarios listed above.

3条回答
一纸荒年 Trace。
2楼-- · 2019-07-10 02:54

I couldn't get Steffen's answer to work for my scenario, and the example in http://react-day-picker.js.org/examples/?overlay doesn't blur properly if you open the date picker, click the non-active parts of the date picker, then click outside the date picker. I may be nitpicking at this point, and his solution is probably far easier to implement, but here's what I did to solve it:

In DatePicker.js, set an empty array that will serve as a collection of valid < div>'s. When onBlur is triggered, invoke a recursive function that takes the root DatePicker < div>, parses all it's children, and add them to the empty array. After that, check document.activeElement to see if it's in the array. If not, then toggle the DatePicker widget, else, do nothing.

Note that the check for document.activeElement must be done one tick after the blur, or else activeElement will be < body>.

Related links:

Get the newly focussed element (if any) from the onBlur event.

Which HTML elements can receive focus?

/**
*
* DatePicker
*
*/

import React from 'react';
import 'react-day-picker/lib/style.css';
import DayPicker, { DateUtils } from 'react-day-picker';

import styles from './styles.css';
import disabledDays from './disabledDays';

class DatePicker extends React.Component { // eslint-disable-line react/prefer-stateless-function
  constructor(props) {
    super(props);
    this.state = {
      selectedDay: new Date(),
    };

    this.validElements = [];
  }

  componentDidMount() {
    // Once DatePicker successfully sets a ref, component will mount
    // and autofocus onto DatePicker's wrapper div.
    if (this.refComponent) {
      this.refComponent.focus();
    }
  }

  setRefComponent = (e) => {
    if (e) {
      this.refComponent = e;
    }
  }

  findDatePickerDOMNodes = (element) => {
    if (element.hasChildNodes()) {
      this.validElements.push(element);
      const children = element.childNodes;
      for (let i = 0; i < children.length; i++) {
        this.validElements.push(children[i]);
        this.findDatePickerDOMNodes(children[i]);
      }
      return;
    }
  }

  handleDayClick = (e, day, { disabled }) => {
    if (disabled) {
      return;
    }
    this.setState({ selectedDay: day }, () => {
      this.props.changeDate(day);
      this.props.changeActiveDateWidget();
    });
  }

  handleBlur = () => {
    // Since DatePicker's wrapper div has been autofocused on mount, all
    // that needs to be done is to blur on anything that's not the DatePicker.
    // DatePicker has many child divs that handle things like day, week, month...
    // invoke a recursive function to gather all children of root DatePicker div, then run a test against valid DatePicker elements. If test fails, then changeActiveDateWidget.
    setTimeout(() => {
      const rootDatePickerElement = document.getElementById('datePickerWidget');
      this.findDatePickerDOMNodes(rootDatePickerElement);
      if (!this.validElements.includes(document.activeElement)) {
        this.props.changeActiveDateWidget();
      }
    }, 1);
  }

  render() {
    const { selectedDay } = this.state;
    return (
      <div
        className={styles.datePicker}
        onBlur={this.handleBlur}
        // tabIndex necessary for element to be auto-focused.
        tabIndex="1"
        ref={this.setRefComponent}
      >
        <DayPicker
          initialMonth={selectedDay}
          disabledDays={disabledDays}
          selectedDays={(day) => DateUtils.isSameDay(selectedDay, day)}
          onDayClick={this.handleDayClick}
          id="datePickerWidget"
        />
      </div>
    );
  }
}

DatePicker.propTypes = {
  changeDate: React.PropTypes.func,
  changeActiveDateWidget: React.PropTypes.func,
};

export default DatePicker;

and in DateInput.js, clicking the input may cause the toggle to trigger twice, so i just set it to always toggle true if clicking the input:

render() {
    const { input, meta } = this.props;
    const { dateValue, activeDateWidget } = this.state;
    return (
      <div className={styles.dateInput}>
        <input
          {...input}
          className="form-control"
          type="text"
          value={dateValue}
          onClick={() => { this.setState({ activeDateWidget: true }); }}
        />
查看更多
啃猪蹄的小仙女
3楼-- · 2019-07-10 03:02

Bit old question but I couldn't find simple answer so here is what I did:

onBlur={(e) => { 
  var picker = document.querySelector(".DayPicker")
  if(!picker.contains(e.relatedTarget)){
    this.setState({showDayPicker: false}) 
  }
}}

I'm setting a flag that hides DayPicker if the blur doesn't come from clicking on the DayPicker, otherwise it keeps the DayPicker displayed

查看更多
乱世女痞
4楼-- · 2019-07-10 03:09

You can take this example http://react-day-picker.js.org/examples/?overlay and do some small modifications to make it redux-form v6 compatible. Instead of using local state you should use this.props.input.value provided by redux-form Field component inside your render function. Additionally in the handleInputChange event handler you have to call this.props.input.onChange(e.target.value) instead of this.setState({ value: e.target.value }) and in handleInputBlur event call this.props.input.onBlur(e.target.value). That's all you have to do to make it work as a redux-form Field component.

查看更多
登录 后发表回答