#Application
View Compiled
html, body
  margin 0
  padding 0
  width 100%
  height 100%
  color #fff
  background linear-gradient(to left top, #333, #111)
  font-family "Lato", sans-serif
  
#Application
  width 100%
  height 100%
  display flex
  align-items center
  justify-content center

.DateInput
  .DateInput__help
    text-align center
    transition 0.2s ease-out all
    margin-top 2rem
  .DateInput__is-in
    opacity 1
  .DateInput__is-out
    opacity 0
  .DateInput__container
    width 100%
    .DateInput__separator
      display inline-block
      font-size 250%
      font-weight 100
      text-align center
    .DateInput__group
      width calc(100% / 6.5)
      display inline-block
      label
        display block
        text-align center
        color #fff
        margin-bottom 0.5rem
      input
        padding-top 0.5rem
        user-select none
        display block
        margin 0 auto
        width 100%
        text-align center
        font-size 250%
        border 0
        outline 0
        background transparent
        color #fff
        border-top 2px solid transparent
        font-weight 200
      input::selection
        background transparent
      input:focus
        border-top 2px solid #fff
        transition .4s ease-out all
View Compiled
const KEYS = {
  BACKSPACE: 8,
  
  END: 35,
  HOME: 36,
  
  LEFT: 37,
  UP: 38,
  RIGHT: 39,
  DOWN: 40,
  
  NUM_0: 48,
  NUM_1: 49,
  NUM_2: 50,
  NUM_3: 51,
  NUM_4: 52,
  NUM_5: 53,
  NUM_6: 54,
  NUM_7: 55,
  NUM_8: 56,
  NUM_9: 57,
  
  NUMPAD_0: 96,
  NUMPAD_1: 97,
  NUMPAD_2: 98,
  NUMPAD_3: 99,
  NUMPAD_4: 100,
  NUMPAD_5: 101,
  NUMPAD_6: 102,
  NUMPAD_7: 103,
  NUMPAD_8: 104,
  NUMPAD_9: 105
  
};

function getCharacter(keyCode) {
  switch(keyCode) {
    default: return ".";
    case KEYS.NUM_0: case KEYS.NUMPAD_0: return "0";
    case KEYS.NUM_1: case KEYS.NUMPAD_1: return "1";
    case KEYS.NUM_2: case KEYS.NUMPAD_2: return "2";
    case KEYS.NUM_3: case KEYS.NUMPAD_3: return "3";
    case KEYS.NUM_4: case KEYS.NUMPAD_4: return "4";
    case KEYS.NUM_5: case KEYS.NUMPAD_5: return "5";
    case KEYS.NUM_6: case KEYS.NUMPAD_6: return "6";
    case KEYS.NUM_7: case KEYS.NUMPAD_7: return "7";
    case KEYS.NUM_8: case KEYS.NUMPAD_8: return "8";
    case KEYS.NUM_9: case KEYS.NUMPAD_9: return "9";
  }
}

function getDestructuredDate(date) {
  const y = date.getFullYear();
  const m = date.getMonth();
  const d = date.getDate();
  const h = date.getHours();
  const i = date.getMinutes();
  const s = date.getSeconds();
  return { y, m, d, h, i, s };
}

function getLabels() {
  const y = /es(-[A-Z]{2})?/.test(navigator.language) ? "Año": "Year";
  const m = /es(-[A-Z]{2})?/.test(navigator.language) ? "Mes": "Month";
  const d = /es(-[A-Z]{2})?/.test(navigator.language) ? "Día": "Day";
  const h = /es(-[A-Z]{2})?/.test(navigator.language) ? "Hora": "Hour";
  const i = /es(-[A-Z]{2})?/.test(navigator.language) ? "Min.": "Min.";
  const s = /es(-[A-Z]{2})?/.test(navigator.language) ? "Seg.": "Sec.";
  return { y, m, d, h, i, s };
}

function setDate(date, y = date.getFullYear(), m = date.getMonth(), d = date.getDate(), h = date.getHours(), i = date.getMinutes(), s = date.getSeconds()) {
  return new Date(y, m, d, h, i, s);
}

function getDatePart(date, part) {
  switch(part) {
    default: throw new Error("Invalid part name");
    case "year": return date.getFullYear();
    case "month": return date.getMonth();
    case "day": return date.getDate();
    case "hours": return date.getHours();
    case "minutes": return date.getMinutes();
    case "seconds": return date.getSeconds();
  }
}

function setDatePart(date, part, value) {
  const y = (part === "year" ? value : date.getFullYear());
  const m = (part === "month" ? value : date.getMonth());
  const d = (part === "day" ? value : date.getDate());
  const h = (part === "hours" ? value : date.getHours()); 
  const i = (part === "minutes" ? value : date.getMinutes()); 
  const s = (part === "seconds" ? value : date.getSeconds());
  return new Date(y,m,d,h,i,s);
}

function addDate(date, ay, am, ad, ah, ai, as) {
  const { y, m, d, h, i, s } = getDestructuredDate(date);
  return new Date(y + ay,m + am,d + ad,h + ah,i + ai,s + as);
}

function incrementYear(date,q = 1) { return addDate(date,q,0,0,0,0,0); }
function decrementYear(date,q = 1) { return addDate(date,-q,0,0,0,0,0); }

function incrementMonth(date,q = 1) { return addDate(date,0,q,0,0,0,0); }
function decrementMonth(date,q = 1) { return addDate(date,0,-q,0,0,0,0); }

function incrementDay(date,q = 1) { return addDate(date,0,0,q,0,0,0); }
function decrementDay(date,q = 1) { return addDate(date,0,0,-q,0,0,0); }

function incrementHours(date,q = 1) { return addDate(date,0,0,0,q,0,0); }
function decrementHours(date,q = 1) { return addDate(date,0,0,0,-q,0,0); }

function incrementMinutes(date,q = 1) { return addDate(date,0,0,0,0,q,0); }
function decrementMinutes(date,q = 1) { return addDate(date,0,0,0,0,-q,0); }

function incrementSeconds(date,q = 1) { return addDate(date,0,0,0,0,0,q); }
function decrementSeconds(date,q = 1) { return addDate(date,0,0,0,0,0,-q); }

function incrementDate(date, part, q = 1) {
  switch(part) {
    default: throw new Error("Invalid part name");
    case "year": return incrementYear(date, q);
    case "month": return incrementMonth(date, q);
    case "day": return incrementDay(date, q);
    case "hours": return incrementHours(date, q);
    case "minutes": return incrementMinutes(date, q);
    case "seconds": return incrementSeconds(date, q);
  }
}

function decrementDate(date, part, q = 1) {
  switch(part) {
    default: throw new Error("Invalid part name");
    case "year": return decrementYear(date, q);
    case "month": return decrementMonth(date, q);
    case "day": return decrementDay(date, q);
    case "hours": return decrementHours(date, q);
    case "minutes": return decrementMinutes(date, q);
    case "seconds": return decrementSeconds(date, q);
  }
}

function modifyDate(date, part, increment = true) {
  if (increment) {
    return incrementDate(date, part);
  } else {
    return decrementDate(date, part);
  }
}

const DateInput = React.createClass({
  getDefaultProps() {
    return {
      helpTime: 5000,
      helpTexts: [
        "You can use ← and → keys to move between fields",
        "You can use ↓ and ↑ keys to change current value",
        "Press left mouse button over a number and drag up and down to change current value"
      ]
    };
  },
  
  getInitialState() {
    return {
      activeElement: null,
      date: new Date(),
      selectionRange: 0,
      helpIndex: 0,
      helpIsVisible: false,
      isDragging: false
    };
  },
  
  handleCopy(e) {
    e.preventDefault();
    const data = this.state.date.toLocaleString();
    const datetime = this.state.date.toISOString();
    e.clipboardData.setData("text/plain", data);
    e.clipboardData.setData("text/html", `<time datetime="${datetime}">${data}</time>`);
  },
  
  handlePaste(e) {
    const data = e.clipboardData.getData("text/plain");
    const newDate = Date.parse(data);
    if (isNaN(newDate)) {
      e.preventDefault();
    } else {
      this.setState({
        date: new Date(newDate)
      });
    }
  },
  
  focusPrevious() {
    switch (this.state.activeElement) {
      case this.refs.year: break;
      case this.refs.month: this.setState({ activeElement: this.refs.year, selectionRange: this.refs.year.value.length }); break;
      case this.refs.day: this.setState({ activeElement: this.refs.month, selectionRange: this.refs.month.value.length }); break;
      case this.refs.hours: this.setState({ activeElement: this.refs.day, selectionRange: this.refs.day.value.length }); break;
      case this.refs.minutes: this.setState({ activeElement: this.refs.hours, selectionRange: this.refs.hours.value.length }); break;
      case this.refs.seconds: this.setState({ activeElement: this.refs.minutes, selectionRange: this.refs.minutes.value.length }); break;
    }
  },
  
  focusNext() {
    switch (this.state.activeElement) {
      case this.refs.year: this.setState({ activeElement: this.refs.month, selectionRange: 0 }); break;
      case this.refs.month: this.setState({ activeElement: this.refs.day, selectionRange: 0 }); break;
      case this.refs.day: this.setState({ activeElement: this.refs.hours, selectionRange: 0 }); break;
      case this.refs.hours: this.setState({ activeElement: this.refs.minutes, selectionRange: 0 }); break;
      case this.refs.minutes: this.setState({ activeElement: this.refs.seconds, selectionRange: 0 }); break;
      case this.refs.seconds: break;
    }
  },
  
  focusFirst() {
    this.setState({ activeElement: this.refs.year, selectionRange: 0 });
  },
  
  focusLast() {
    this.setState({ activeElement: this.refs.seconds, selectionRange: 0 });
  },
  
  isYearActive() { return this.state.activeElement === this.refs.year; },
  isMonthActive() { return this.state.activeElement === this.refs.month; },
  isDayActive() { return this.state.activeElement === this.refs.day; },
  isHoursActive() { return this.state.activeElement === this.refs.hours; },
  isMinutesActive() { return this.state.activeElement === this.refs.minutes; },
  isSecondsActive() { return this.state.activeElement === this.refs.seconds; },
  
  handleKey(e) {
    if (e.keyCode === KEYS.DOWN || e.keyCode === KEYS.UP) {
      e.preventDefault();
      this.setState({ date: modifyDate(this.state.date, e.target.name, e.keyCode === KEYS.UP) });
    } else if (e.keyCode === KEYS.LEFT || e.keyCode === KEYS.RIGHT) {
      if (this.state.selectionRange === 0 && e.keyCode === KEYS.LEFT) {
        this.focusPrevious();
      } else if (this.state.selectionRange === e.target.value.length && e.keyCode === KEYS.RIGHT) {
        this.focusNext();
      } else {
        this.setState({
          selectionRange: e.target.selectionStart + (e.keyCode === KEYS.LEFT ? -1 : 1)
        });
      }
    } else if (e.keyCode === KEYS.HOME || e.keyCode === KEYS.END) {
      if (e.keyCode === KEYS.HOME) {
        this.focusFirst();
      } else if (e.keyCode === KEYS.END) {
        this.focusLast();
      }
    } else if (e.keyCode === KEYS.BACKSPACE) {
      if (this.state.selectionRange === 0) {
        this.focusPrevious();
      } else {
        this.setState({
          selectionRange: Math.max(this.state.selectionRange - 1, 0)
        });
      }
    } else {
      const character = getCharacter(e.keyCode);
      if (!/^[0-9]$/.test(character)) {
        return;
      }
      
      const value = e.target.value;
      const selectionStart = e.target.selectionStart;
      const selectionEnd = e.target.selectionEnd;
      let newValue = parseInt(value.substr(0,selectionStart) + character + value.substr(selectionStart + 1));
      if (this.state.activeElement === this.refs.year && String(newValue).length > 4
       || this.state.activeElement === this.refs.month && String(newValue).length > 2
       || this.state.activeElement === this.refs.day && String(newValue).length > 2
       || this.state.activeElement === this.refs.hours && String(newValue).length > 2
       || this.state.activeElement === this.refs.minutes && String(newValue).length > 2
       || this.state.activeElement === this.refs.seconds && String(newValue).length > 2) {
        return;
      }
      
      if (this.isMonthActive()) {
        if (newValue > 12) {
          if (this.state.selectionRange === 0) {
            newValue = 10;
          } else {
            newValue = 12;
          }
        }
        newValue--;
      }
      
      if (this.isDayActive()) {
        if (newValue > 31) {
          if (this.state.selectionRange === 0) {
            newValue = 30;
          } else {
            newValue = 31;
          }
        }
      }
      
      if (this.isHoursActive()) {
        if (newValue > 23) {
          if (this.state.selectionRange === 0) {
            newValue = 20;
          } else {
            newValue = 23;
          }
        }
      }
      
      if (this.isMinutesActive() 
       || this.isSecondsActive()) {
        if (newValue > 59) {
          if (this.state.selectionRange === 0) {
            newValue = 50;
          } else {
            newValue = 59;
          }
        }
      }

      let newSelectionRange = selectionStart + 1;
      if (this.state.activeElement === this.refs.year && this.state.selectionRange + 1 === 4
       || this.state.activeElement === this.refs.month && this.state.selectionRange + 1 === 2
       || this.state.activeElement === this.refs.day && this.state.selectionRange + 1 === 2
       || this.state.activeElement === this.refs.hours && this.state.selectionRange + 1 === 2
       || this.state.activeElement === this.refs.minutes && this.state.selectionRange + 1 === 2) {
        this.focusNext();
        newSelectionRange = 0;
      }
      
      this.setState({
        date: setDatePart(this.state.date, e.target.name, newValue),
        selectionRange: newSelectionRange
      });
    }
  },
  
  format(value) {
    let string = String(value);
    if (string.length === 1) {
      return "0" + string;
    } else {
      return string;
    }
  },
  
  delayed(fn) {
    if (this.timeout) {
      clearTimeout(this.timeout);
    }
    this.timeout = setTimeout(fn, 0);
  },
  
  hideHelp() {
    this.setState({ helpIsVisible: false });
    this.helpTimeout = setTimeout(this.changeHelp, 200);
  },
  
  showHelp() {
    this.setState({ helpIsVisible: true });
    this.helpTimeout = setTimeout(this.hideHelp, this.props.helpTime);
  },
   
  changeHelp() {
    this.setState({
      helpIndex: (this.state.helpIndex + 1) % this.props.helpTexts.length
    });
    this.helpTimeout = setTimeout(this.showHelp, 200);
  },
  
  componentDidMount() {
    this.hideHelp = this.hideHelp.bind(this);
    this.showHelp = this.showHelp.bind(this);
    this.changeHelp = this.changeHelp.bind(this);
    this.handleMouseUp = this.handleMouseUp.bind(this);
    this.handleMouseDown = this.handleMouseDown.bind(this);
    this.changeHelp();
  },
    
  componentWillUnmount() {
    clearTimeout(this.helpTimeout);
    this.helpTimeout = null;
  },
  
  componentDidUpdate(prevProps, prevState) {
    if (this.state.activeElement) {
      if (this.state.activeElement !== prevState.activeElement) {
        this.state.activeElement.focus();
      }
      this.delayed(() => {
        this.state.activeElement.setSelectionRange(this.state.selectionRange,this.state.selectionRange);
        this.timeout = null;
      });
    }
  },
  
  hasFocus() {
    return document.activeElement === this.refs.year 
        || document.activeElement === this.refs.month
        || document.activeElement === this.refs.day
        || document.activeElement === this.refs.hours
        || document.activeElement === this.refs.minutes
        || document.activeElement === this.refs.seconds;
  },
  
  handleBlur(e) {
    if (this.blurTimeout) {
      clearTimeout(this.blurTimeout);
      this.blurTimeout = null;
    }
    this.blurTimeout = setTimeout(() => {
      if (document.activeElement === null
      || (document.activeElement !== this.refs.year
       && document.activeElement !== this.refs.month
       && document.activeElement !== this.refs.day
       && document.activeElement !== this.refs.hours
       && document.activeElement !== this.refs.minutes
       && document.activeElement !== this.refs.seconds)) {
        this.setState({
          activeElement: null,
          selectionRange: 0
        });
      }
    }, 0);
  },
  
  handleMouseDown(e) {
    e.preventDefault();
    if (this.state.isDragging === false && e.button === 0) {
      this.setState({ 
        isDragging: true,
        activeElement: e.target,
        selectionRange: 0
      });
      
      this.current = this.start = e.clientY;

      document.addEventListener("mouseup", this.handleMouseUp);
      document.addEventListener("mouseleave", this.handleMouseUp);
      document.addEventListener("mousemove", this.handleMouseMove);
      window.requestAnimationFrame(this.handleFrame);
    }
  },
  
  handleMouseUp(e) {
    if (this.state.isDragging === true) {
      this.setState({ isDragging: false });

      document.removeEventListener("mouseup", this.handleMouseUp);
      document.removeEventListener("mouseleave", this.handleMouseUp);
      document.removeEventListener("mousemove", this.handleMouseMove);
    }
  },
  
  getDirection(value) {
    if (value > 0) {
      return 1;
    } else if (value < 0) {
      return -1;
    }
    return 0;
  },
  
  handleFrame(now) {
    if (this.now === undefined) {
      this.now = now;
    }
    const diff = Math.round((this.current - this.start) / (window.innerHeight * 0.01));
    const dir = this.getDirection(diff);
    const cadence = Math.max(50, Math.min(200, 500 - (Math.abs(diff) * 10)));
    if (dir !== 0) {
      if (now - this.now > cadence) {
        this.now = now;
        const value = parseInt(this.state.activeElement.value);
        const newValue = value - dir; // we need to invert direction.
        this.setState({
          date: setDatePart(this.state.date, this.state.activeElement.name, newValue),
        });
      }
    }
    
    if (this.state.isDragging) {
      window.requestAnimationFrame(this.handleFrame);
    }
  },
  
  handleMouseMove(e) {
    this.current = e.clientY;    
  },
  
  isHelpVisible() {
    return this.hasFocus() && this.state.helpIsVisible;
  },
  
  renderHelp() {
    const classes = "DateInput__help " + (this.isHelpVisible() ? "DateInput__is-in" : "DateInput__is-out");
    const help = this.props.helpTexts[this.state.helpIndex];
    return (
      <div className={classes}>{help}</div>
    );
  },
  
  render() {
    const labels = getLabels();
    const date = getDestructuredDate(this.state.date);
    const help = this.renderHelp();
    return (
      <div className="DateInput">
        <div className="DateInput__container">
          <div className="DateInput__group">
            <label for="year">{labels.y}</label>
            <input ref="year"
                   type="text"
                   name="year"
                   id="year"
                   onMouseDown={this.handleMouseDown}
                   onBlur={this.handleBlur}
                   onClick={this.handleClick}
                   onKeyDown={this.handleKey}
                   onPaste={this.handlePaste}
                   onCopy={this.handleCopy}
                   value={this.format(date.y)} />
          </div>
          <div className="DateInput__separator">
            /
          </div>
          <div className="DateInput__group">
            <label for="month">{labels.m}</label>
            <input ref="month"
                   type="text"
                   name="month"
                   id="month"
                   onMouseDown={this.handleMouseDown}
                   onBlur={this.handleBlur}
                   onClick={this.handleClick}
                   onKeyDown={this.handleKey}
                   onPaste={this.handlePaste}
                   onCopy={this.handleCopy}
                   value={this.format(date.m + 1)} />
          </div>
          <div className="DateInput__separator">
            /
          </div>
          <div className="DateInput__group">
            <label for="day">{labels.d}</label>
            <input ref="day"
                   type="text"
                   name="day"
                   id="day"
                   onMouseDown={this.handleMouseDown}
                   onBlur={this.handleBlur}
                   onClick={this.handleClick}
                   onKeyDown={this.handleKey}
                   onPaste={this.handlePaste}
                   onCopy={this.handleCopy}
                   value={this.format(date.d)} />
          </div>
          <div className="DateInput__group">
            <label for="hours">{labels.h}</label>
            <input ref="hours"
                   type="text"
                   name="hours"
                   id="hours"
                   onMouseDown={this.handleMouseDown}
                   onBlur={this.handleBlur}
                   onClick={this.handleClick}
                   onKeyDown={this.handleKey}
                   onPaste={this.handlePaste}
                   onCopy={this.handleCopy}
                   value={this.format(date.h)} />
          </div>
          <div className="DateInput__separator">
            :
          </div>
          <div className="DateInput__group">
            <label for="minutes">{labels.i}</label>
            <input ref="minutes"
                   type="text"
                   name="minutes"
                   id="minutes"
                   onMouseDown={this.handleMouseDown}
                   onBlur={this.handleBlur}
                   onClick={this.handleClick}
                   onKeyDown={this.handleKey}
                   onPaste={this.handlePaste}
                   onCopy={this.handleCopy}
                   value={this.format(date.i)} />
          </div>
          <div className="DateInput__separator">
            :
          </div>
          <div className="DateInput__group">
            <label for="seconds">{labels.s}</label>
            <input ref="seconds"
                   type="text"
                   name="seconds"
                   id="seconds"
                   onMouseDown={this.handleMouseDown}
                   onBlur={this.handleBlur}
                   onClick={this.handleClick}
                   onKeyDown={this.handleKey}
                   onPaste={this.handlePaste}
                   onCopy={this.handleCopy}
                   value={this.format(date.s)} />
          </div>
        </div>
        {help}
      </div>
    );
  }
});

ReactDOM.render(
  <DateInput />,
  document.querySelector("#Application")
);
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://fb.me/react-15.1.0.min.js
  2. https://fb.me/react-dom-15.1.0.min.js