React-dates: Allow Year/month navigation

Created on 19 Aug 2016  Â·  70Comments  Â·  Source: airbnb/react-dates

This allows for a quick way to jump to another year or month without having to use the nav keys and go one month at a time.

Here is an example. Clicking on the year or month displays another dialog showing the years/months.

React DatePicker

feature request

Most helpful comment

+1 for year navigation. Birthday input is a case.

All 70 comments

While I love the idea of direct yearly navigation, having one modally thing pop up over another seems horrifying to me.

How about a dropdown.

+1 for year navigation. Birthday input is a case.

year/month navigation seems to me very important. I wouldn't have additional popup, I would replace the existing popup content with years/months.

I think that this is a very important feature also but the UX of this should not be a new pop up but rather open inside the same widget.

Something like this:
http://eonasdan.github.io/bootstrap-datetimepicker/

This makes it easy to use and navigate between months / years.

I had one doubt if we go by @madisvain suggestion which seems doable, we would need to create another component monthPicker which would render the month and year view right? @ljharb

Seems like it. Btw, sorry for not working on my PR. Have not found the time to do it.

@rodryquintero but we still need to finalize what kind of behaviour is desired right?

I vote for the menu option. Easier to implement and more practical.

@rodryquintero the example suggested by @madisvain seems doable and practical to me lets see what @ljharb and @majapw have to say about this

hi... is there already in implementation?

+1 here. As @supnate says- it's a blocking issue for DoB use case.

Please do not post "+1" comments - everywhere on Github, these add noise and do not add value. Please indicate your support with emojis on the original post.

I will delete all such comments now, and any further in the future. Thanks!

I think sometimes it's better to stick with html5 date component... just for this purpose

Sadly the HTML5 date component does not style consistently across browsers and the experience is subpar.

I could maybe make a PR for this. What would be the desired UI/UX for it? maybe some example from other datepickers?

@jtomaszewski I think that before a PR, that's the exact question we'd need to figure out.

@jtomaszewski @ljharb As a starting point, here's the Pikaday datepicker:

screen shot 2017-04-24 at 2 39 33 pm
and after clicking on April:
screen shot 2017-04-24 at 2 39 42 pm

Very simple year/month navigation. I think this approach would work well with react-dates. This way, we wouldn't need to alter the existing UI much. We'd just add a little space between month and year and insert the down arrows. Any thoughts or alternative UI/UX suggestions?

My only concern there is having to style <select> - a dropdown should look like a dropdown at all times, not just be a "down arrow" tacked on.

Something like this would be nice.

I would assume the left/right arrows would move through each section.

yearpicker-option

I needed to add this feature to use this on a project I'm working on, but for now have only had time to implement it with a basic <select> box. Would you accept a pull req with that functionality? It currently looks like this:

screen shot 2017-06-09 at 4 21 27 pm

I wasn't happy with how this was progressing. So I made my own react date picker with month & year functionality. No range selection yet though.

https://github.com/secretyouth/react-datez

Is there any progress on the change per year functionality? I desperately want to add it to my superset fork!

Hi @ljharb @majapw , is there any in-progress PR regarding Year Navigation?

Hi @sag1v, we don't really want to support something that we haven't explored from a design/UX side. So I've been partnering with a designer at Airbnb to get some insight and hope to have something built in the near future.

@majapw Thanks for the feedback.

I just want to point out that from a user perspective, the best experience I had for year/month selection was with https://uxsolutions.github.io/bootstrap-datepicker/, which does not implement it as drop-downs. One more option to consider :smile:

+1 for @jbblanchet 's recommendation, that is the best experience for being able to pick years and months.

For picking a date of birth this is incredibly useful and was surprised that react-dates did not offer something similar

I have created an implementation of year picker as followed:
screen shot 2017-08-16 at 12 50 46

Anyone interested? I can create a PR for this

@BenSwennen This is awesome! A small suggestion might be that users might be more comfortable if you put buttons with the same step direction on the same side.

Hi,
Is there any way to hide arrow buttons on DateRangePicker?
I'm enabling only last 30 days on Calendar so I dont need the navigation buttons.
Thanks

Hi @mrmuhammadali, your question isn't really related to this issue. I think it would be more helpful if you opened a new issue for this or scoured the current issues to find one that is related to your question.

Hello @majapw, anything new on this ?

If you have some insight from your UX/Design team, we can propose a Pull Request with the functionality, this is a blocker for us for a birth-date input case.

My 2 cent: the best is a select or a simple field, as arrows like @BenSwennen proposed are pretty, but on a usability point of view, for someone born in 1960, it will require 57 clicks.

@BenSwennen I really like your solution. I am also looking for an alternative to change years fast.
Please make a pull request for this.

@hanselsen I created one, but unfortunately it didn't seem to get a lot of traction, so I removed it and kept it specific to our needs. I also struggled to find the time to get the tests to succeed and to get it to work properly. You can always help me if you want! https://github.com/airbnb/react-dates/pull/672

@BenSwennen I switched to react-datepicker and styled it, because it has more options and styling is not limited.

@BenSwennen I'm also very interested in what you implemented. Any chance we could get you to re-send your PR? :)

@secretyouth Your suggestion looks really nice, but I'd be happy with a PR from @jvanderz22 since he has already implemented it.

@majapw You have any luck with the designer you were partnering with?

@majapw @ljharb The use case for this is birthdays, anniversaries, etc. It is extremely painful to use react-dates for historical dates. I just need a quick way for people to switch years, I don't mind having them tab through 12 months. Just no 45 years and then 12 months :P

I have some designs, but realistically, since this is not a priority for us, I wouldn't be able to work on this until winter break (late December). If someone wants to tackle an initial prototype with the new react-with-styles framework that handles transition/animations between year navigation and works with keyboard navigation, I'd be happy to work on top of that.

Here are some of the ideas my designer was floating around:
pasted image at 2017_08_07 02_47 pm

We could call it a MonthYearPicker or something and it could build on top of the DayPicker, but separate the logic out of year navigation.

Without this feature this library isn't usable for me. :/

Such a pity that a highly requested feature like this takes more than a year to be discussed.

Weird, it’s almost as if the thing that determines maintainer’s personal availability isn’t “people want a feature”

@ljharb Maybe you have been burned by open source in the past and that is how you justified such a rude response, but hopefully I can clarify the frustration people are having and move this conversation back to something much more productive than sarcasm.

Everyone is very thankful for the work airbnb has done on this project so far and a lot of us have built our applications to depend on it. The feature we are requesting is the ability to use this picker in a historical context (birth dates, past events, etc) which is currently impossible to do.

No one has asked the maintainer's to spend your own personal availability to implement this feature. There have been multiple PRs and examples of how this could be implemented. What we were told by @majapw is nothing would be accepted until the Airbnb graphic designers took a look at it. To us (as outsiders) this meant that we should wait for airbnb to do work since the PRs submitted so far haven't received acceptance and we were told that this would continue to be the case.

All we need from the maintainer's of the this project is direction on what you are willing to accept so that one of us can send the PR. We've offered many options and just need you to OK one or implement it the way you want if that is how you want to do it.

Ignoring the feature because its not relevant to airbnb isn't really a great way to run an open source project who has MANY people willing to contribute.

@sontek Thanks for this words! And in my opinion you are totally right 👍

@sontek i was responding to https://github.com/airbnb/react-dates/issues/25#issuecomment-347831965, which was an exceedingly rude comment, and i think the sarcasm (towards that person only) was quite deserved.

The reasons for delaying on this feature have been made clear; it’s not that Airbnb has to do the work, it’s that the design has to be approved by our designers first - we have an obligation to provide the best UX, not just a working one. We’re not “ignoring the feature”, but neither is anyone entitled to get a feature added on a freely given project.

If you want to send a PR, https://github.com/airbnb/react-dates/issues/25#issuecomment-343056092 asks for exactly that:

If someone wants to tackle an initial prototype with the new react-with-styles framework that handles transition/animations between year navigation and works with keyboard navigation, I'd be happy to work on top of that.

Thanks for contributing!

Maybe there is a stop gap approach that could be documented as well. There is the renderCalendarInfo, could there be a way to use that to add our own dropdowns that change the current visible year/month until this can be implemented appropriately??

@ljharb I believe You are overthinking my comment. I said it is a pity that it takes years for a discussion, and that's it. I apologize if someone finds it rude or sarcastic, didn't mean to.

what's the status of this issue?

@timgivois Are you asking us to summarize all the conversation above? Or do you have a specific question?

@sontek #25 comment asks for help to start an initial prototype. I want to know if someone is working on that, or we need to add a Pull Request needed label.

This feature is definitely needed. This date picker component will be perfect when we're getting this feature! 👍

Anyone find a good alternative that has this feature?

I actually just built it out using react-dates with one major caveat being it flashes (closes then opens) every time you change the Month / Year as you cannot change the calendar date by more than 1 month at a time.

Changing initialVisibleMonth or a way to change multiple months / years at a time is the only blocking bit that needs built out, in my opinion; see #376. Hacking lifecycles to get around the change 1 month at a time limitation is about 50% of my code on my datepicker component.

What I do:

  • Render two selects for Month and Year via the renderMonth function.
  • Have a getValidCalendar[Months|Years] which look at my minDate / maxDate and some other things to see if February 2020 is a valid month to be shown in the dropdowns.
  • onChange select => local state.calendarDate as well as a bunch of other stuff to unfocus then re-focus on the next render (super ugly hack, via component lifecycles).
  • Some additional logic to make it so when you change to the endDate it changes the selects as well as the calendar view.

screen shot 2018-02-01 at 3 19 29 pm

Happy to help, but gave resolving #376 a shot and couldn't / didn't have the bandwidth.

Is there a new implementation for the DayPicker to use it for Birth Dates?

@xloeza while waiting on official implementation I recommend blueprintjs datetime http://blueprintjs.com/docs/v1/#datetime/daterangeinput
http://blueprintjs.com/docs/v2/#datetime/daterangeinput

I just PR'd a solution based on what @jvanderz22 did last year, minus the UI: https://github.com/airbnb/react-dates/pull/1106

My hope is that this can be something we can use sooner than later while some "official" design get mulled over.

My proposal is to simply add a renderCaption prop that allows callers to build their own UI in the heading. It provides the current month and callbacks to set the month and year. The caller has to do the rest.

From there, any of the UIs above could potentially be built. Here's an a simple implementation of one based on standard HTML select drop-downs (code example of this in my PR):

simplereactdatescalendar

or, something more custom:

customreactdatescalendar

I believe this would unblock a bunch of folks, so hopefully this is considered for a future release.

Thanks to the maintainers for all the hard work and the community for the dialog!

Is there a timeline for a solution for those blocked by this issue?

While the nice folks at Airbnb take a look at this, I created an npm/yarn package that may help you if you're stuck waiting for this functionality. This is a temporary solution and it's not fully optimized, so please keep that in mind: https://www.npmjs.com/package/react-dates-custom-month-year-navigation.

Closed by #1106.

hi guys, i just want to select the year, or select months past a selectbox only five. or month. thanks to everyone

Hi guys, i think I managed to do it somehow, this is how i solved it:
I used the renderMonthElement property as its a function and which allows to return everything, I returned a list of years and months.

Datepicker Code:

<SingleDatePicker
                        date={this.props.date}
                        onDateChange={this.props.onSelect}
                    renderMonthElement={this.renderMonthElement}
                    />

Functions:

returnYears = () => {
        let years = []
        for(let i = moment().year() - 100; i <= moment().year(); i++) {
            years.push(<option value={i}>{i}</option>);
        }
        return years;
    }
renderMonthElement = ({ month, onMonthSelect, onYearSelect }) =>
    <div style={{ display: 'flex', justifyContent: 'center' }}>
        <div>
            <select
                value={month.month()}
                onChange={(e) => onMonthSelect(month, e.target.value)}
            >
                {moment.months().map((label, value) => (
                    <option value={value}>{label}</option>
                ))}
            </select>
        </div>
        <div>
            <select value={month.year()} onChange={(e) => onYearSelect(month, e.target.value)}>
                {this.returnYears()}
            </select>
        </div>
    </div>

Image:
screen shot 2018-12-06 at 11 29 08
:

yjvesabalaj thanks for the great solution. I had problems when selecting a new month as it wasn't updating the calendar month. To correct this is simple had to parseInt the e.target.value getting passed into onMonthSelect.
<select value={month.month()} onChange={(e) => { onMonthSelect(month, parseInt(e.target.value))} } > {moment.months().map((label, i) => ( <option value={i} key={i}>{label}</option> ))} </select>

returnYears = () => { let years = [] for(let i = moment().year() - 100; i <= moment().year(); i++) { years.push(

Rockstar solution :) 🥇 @yjvesabalaj

Hi ,
Is there any function to navigate month pragmatically?

Link is down

@secretyouth Seriously, thank you!

In case someone is struggling w/ disabling navigations to months that are outside range
I worked on top of @yjvesabalaj solution -

const months = moment.months();

const years = range(2000, 2050);

const returnYears = (yearOptions) => {
  return yearOptions.map((y, index) => <option key={`${y}_${index}`} value={y}>{y}</option>);
};

const renderMonthElement = ({ month, onMonthSelect, onYearSelect }, monthOptions, yearOptions) =>  {
  return (
    <div className={cx("month-picker")}>
      <div>
        <select
          className={cx("styled")}
          value={monthOptions.indexOf(months[month.month()])}
          onChange={(e) => {
            const selected = monthOptions[e.target.value];
            onMonthSelect(month, months.indexOf(selected));
          }}>
          {monthOptions.map((label, index) => (
              <option key={`${label}_${index}`} value={index}>{label}</option>
          ))}
        </select>
      </div>
      <div>
        <select
          className={cx("styled")}
          value={month.year()}
          onChange={(e) => onYearSelect(month, e.target.value)}>
          {returnYears(yearOptions)}
        </select>
      </div>
    </div>
  );
};

/* eslint-disable no-bitwise */
const withInRange = memoize((minDate, maxDate, isOutsideRange, date) => {
  let isInRange = true;
  if (minDate && moment.isMoment(minDate)) {
    isInRange &= (date.diff(minDate, "days", true) > 0);
  }

  if (maxDate && moment.isMoment(maxDate)) {
    isInRange &= (date.diff(maxDate, "days", true) < 0);
  }

  if (isOutsideRange && typeof isOutsideRange === "function") {
    isInRange &= (!isOutsideRange(date));
  }

  return isInRange;
});

const monthElementWrapper = (
  {minDate, maxDate, isOutsideRange},
  {month, onMonthSelect, onYearSelect, isVisible}) => {

  if (moment.isMoment(month) && isVisible) {
    const monthSelected = month.month();
    const yearOptions = years.filter((y) =>  {
      const iterator =  moment(`${y} ${monthSelected + 1}`, `YYYY MM`);
      const startOfIterator = iterator.clone().startOf("month");
      const endOfIterator = iterator.clone().endOf("month");
      return (withInRange(minDate, maxDate, isOutsideRange, startOfIterator) ||
              withInRange(minDate, maxDate, isOutsideRange, endOfIterator));
    });
    const selectedYear = month.year();

    const monthOptions = months.filter((m) => {
      const iterator = moment(`${selectedYear}  ${m}`, `YYYY MMMM`);
      const startOfIterator = iterator.clone().startOf("month");
      const endOfIterator = iterator.clone().endOf("month");
      return (withInRange(minDate, maxDate, isOutsideRange, startOfIterator)
              || withInRange(minDate, maxDate, isOutsideRange, endOfIterator));
    });
    const indexOfMonth = monthOptions.indexOf(months[month.month()]);
    const indexOfYear = yearOptions.indexOf(month.year());
    const hasNextMonth = indexOfMonth >= 0 && indexOfMonth < monthOptions.length - 1;
    const hasPrevMonth = indexOfMonth > 0 && indexOfMonth < monthOptions.length;
    const hasNextYear = indexOfYear >= 0 && indexOfYear < yearOptions.length - 1;
    const hasPrevYear = indexOfYear > 0 && indexOfYear < yearOptions.length;
    const passProps = { month, onMonthSelect, onYearSelect };
    return {
      renderEl: renderMonthElement(passProps, monthOptions, yearOptions),
      hasPrev: hasPrevMonth || hasPrevYear,
      hasNext: hasNextMonth || hasNextYear
    };
  }

  return null;
};

And then in my wrapper component over react-dates, based on value received in hasNext, I am passing navNext as () => null or null. Similarly for hasPrev.

PS - This solution is just a very rough idea and can be optimised in terms of calculations that are happening.

@yjviB Thanks !!

In case someone is struggling w/ disabling navigations to months that are outside range
I worked on top of @yjvesabalaj solution -

const months = moment.months();

const years = range(2000, 2050);

const returnYears = (yearOptions) => {
  return yearOptions.map((y, index) => <option key={`${y}_${index}`} value={y}>{y}</option>);
};

const renderMonthElement = ({ month, onMonthSelect, onYearSelect }, monthOptions, yearOptions) =>  {
  return (
    <div className={cx("month-picker")}>
      <div>
        <select
          className={cx("styled")}
          value={monthOptions.indexOf(months[month.month()])}
          onChange={(e) => {
            const selected = monthOptions[e.target.value];
            onMonthSelect(month, months.indexOf(selected));
          }}>
          {monthOptions.map((label, index) => (
              <option key={`${label}_${index}`} value={index}>{label}</option>
          ))}
        </select>
      </div>
      <div>
        <select
          className={cx("styled")}
          value={month.year()}
          onChange={(e) => onYearSelect(month, e.target.value)}>
          {returnYears(yearOptions)}
        </select>
      </div>
    </div>
  );
};

/* eslint-disable no-bitwise */
const withInRange = memoize((minDate, maxDate, isOutsideRange, date) => {
  let isInRange = true;
  if (minDate && moment.isMoment(minDate)) {
    isInRange &= (date.diff(minDate, "days", true) > 0);
  }

  if (maxDate && moment.isMoment(maxDate)) {
    isInRange &= (date.diff(maxDate, "days", true) < 0);
  }

  if (isOutsideRange && typeof isOutsideRange === "function") {
    isInRange &= (!isOutsideRange(date));
  }

  return isInRange;
});

const monthElementWrapper = (
  {minDate, maxDate, isOutsideRange},
  {month, onMonthSelect, onYearSelect, isVisible}) => {

  if (moment.isMoment(month) && isVisible) {
    const monthSelected = month.month();
    const yearOptions = years.filter((y) =>  {
      const iterator =  moment(`${y} ${monthSelected + 1}`, `YYYY MM`);
      const startOfIterator = iterator.clone().startOf("month");
      const endOfIterator = iterator.clone().endOf("month");
      return (withInRange(minDate, maxDate, isOutsideRange, startOfIterator) ||
              withInRange(minDate, maxDate, isOutsideRange, endOfIterator));
    });
    const selectedYear = month.year();

    const monthOptions = months.filter((m) => {
      const iterator = moment(`${selectedYear}  ${m}`, `YYYY MMMM`);
      const startOfIterator = iterator.clone().startOf("month");
      const endOfIterator = iterator.clone().endOf("month");
      return (withInRange(minDate, maxDate, isOutsideRange, startOfIterator)
              || withInRange(minDate, maxDate, isOutsideRange, endOfIterator));
    });
    const indexOfMonth = monthOptions.indexOf(months[month.month()]);
    const indexOfYear = yearOptions.indexOf(month.year());
    const hasNextMonth = indexOfMonth >= 0 && indexOfMonth < monthOptions.length - 1;
    const hasPrevMonth = indexOfMonth > 0 && indexOfMonth < monthOptions.length;
    const hasNextYear = indexOfYear >= 0 && indexOfYear < yearOptions.length - 1;
    const hasPrevYear = indexOfYear > 0 && indexOfYear < yearOptions.length;
    const passProps = { month, onMonthSelect, onYearSelect };
    return {
      renderEl: renderMonthElement(passProps, monthOptions, yearOptions),
      hasPrev: hasPrevMonth || hasPrevYear,
      hasNext: hasNextMonth || hasNextYear
    };
  }

  return null;
};

And then in my wrapper component over react-dates, based on value received in hasNext, I am passing navNext as () => null or null. Similarly for hasPrev.

PS - This solution is just a very rough idea and can be optimised in terms of calculations that are happening.

Could you create a js-fiddle to see how it works?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

swaritkohli picture swaritkohli  Â·  3Comments

augnustin picture augnustin  Â·  3Comments

ekohanyi picture ekohanyi  Â·  3Comments

aaronvanston picture aaronvanston  Â·  3Comments

AsasinCree picture AsasinCree  Â·  3Comments