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

Allow Year/month navigation #25

Closed
rodryquintero opened this issue Aug 19, 2016 · 71 comments
Closed

Allow Year/month navigation #25

rodryquintero opened this issue Aug 19, 2016 · 71 comments
Labels
feature request I want a new feature in react-dates!

Comments

@rodryquintero
Copy link

rodryquintero commented Aug 19, 2016

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

@ljharb
Copy link
Member

ljharb commented Aug 19, 2016

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

@ljharb ljharb added the feature request I want a new feature in react-dates! label Aug 19, 2016
@rodryquintero
Copy link
Author

rodryquintero commented Aug 19, 2016

How about a dropdown.

@supnate
Copy link

supnate commented Aug 23, 2016

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

@themre
Copy link

themre commented Aug 24, 2016

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

@madisvain
Copy link

madisvain commented Aug 25, 2016

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.

@agupta93
Copy link

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

@rodryquintero
Copy link
Author

rodryquintero commented Sep 13, 2016

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

@agupta93
Copy link

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

@rodryquintero
Copy link
Author

rodryquintero commented Sep 14, 2016

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

@agupta93
Copy link

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

@infacq
Copy link

infacq commented Jan 12, 2017

hi... is there already in implementation?

@englishtom
Copy link

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

@ljharb
Copy link
Member

ljharb commented Mar 11, 2017

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!

@infacq
Copy link

infacq commented Mar 12, 2017

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

@ljharb
Copy link
Member

ljharb commented Mar 12, 2017

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

@jtomaszewski
Copy link

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

@ljharb
Copy link
Member

ljharb commented Apr 23, 2017

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

@nmchaves
Copy link

nmchaves commented Apr 24, 2017

@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?

@ljharb
Copy link
Member

ljharb commented Apr 25, 2017

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.

@secretyouth
Copy link

secretyouth commented Jun 8, 2017

Something like this would be nice.

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

yearpicker-option

@jvanderz22
Copy link

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

@secretyouth
Copy link

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

@fufjvnvnf
Copy link

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

@GoGoCarl
Copy link
Contributor

GoGoCarl commented Apr 6, 2018

I just PR'd a solution based on what @jvanderz22 did last year, minus the UI: #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!

@AlexJeffcott
Copy link

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

kevinnio pushed a commit to kevinnio/react-dates that referenced this issue May 8, 2018
Based on 9dd24d5
from @jvanderz22

Swaps out the given select-based UI and allows user to
supply their own custom interface instead.

See react-dates#25
See react-dates#558
kevinnio pushed a commit to kevinnio/react-dates that referenced this issue May 22, 2018
Based on 9dd24d5
from @jvanderz22

Swaps out the given select-based UI and allows user to
supply their own custom interface instead.

See react-dates#25
See react-dates#558
@kevinnio
Copy link

kevinnio commented May 23, 2018

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.

majapw pushed a commit to GoGoCarl/react-dates that referenced this issue May 31, 2018
Based on 9dd24d5
from @jvanderz22

Swaps out the given select-based UI and allows user to
supply their own custom interface instead.

See react-dates#25
See react-dates#558
majapw pushed a commit to GoGoCarl/react-dates that referenced this issue Jun 5, 2018
Based on 9dd24d5
from @jvanderz22

Swaps out the given select-based UI and allows user to
supply their own custom interface instead.

See react-dates#25
See react-dates#558
majapw pushed a commit to GoGoCarl/react-dates that referenced this issue Jun 13, 2018
Based on 9dd24d5
from @jvanderz22

Swaps out the given select-based UI and allows user to
supply their own custom interface instead.

See react-dates#25
See react-dates#558
majapw pushed a commit to GoGoCarl/react-dates that referenced this issue Jun 15, 2018
Based on 9dd24d5
from @jvanderz22

Swaps out the given select-based UI and allows user to
supply their own custom interface instead.

See react-dates#25
See react-dates#558
majapw pushed a commit to GoGoCarl/react-dates that referenced this issue Jun 18, 2018
Based on 9dd24d5
from @jvanderz22

Swaps out the given select-based UI and allows user to
supply their own custom interface instead.

See react-dates#25
See react-dates#558
@ljharb
Copy link
Member

ljharb commented Jun 20, 2018

Closed by #1106.

@ghost
Copy link

ghost commented Nov 23, 2018

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

@yjviB
Copy link

yjviB commented Dec 6, 2018

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
:

@Cuse89
Copy link

Cuse89 commented Dec 31, 2018

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>

@gauravbrills
Copy link

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

Rockstar solution :) 🥇 @yjvesabalaj

@kunalkagalkar
Copy link

Hi ,
Is there any function to navigate month pragmatically?

@Kyngo
Copy link

Kyngo commented Jul 15, 2019

Link is down

@karthigb
Copy link

@secretyouth Seriously, thank you!

@dulhaniAshish
Copy link

dulhaniAshish commented Mar 22, 2020

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.

@maxgfr
Copy link

maxgfr commented Oct 5, 2020

@yjviB Thanks !!

@far-jafari
Copy link

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?

@GeorgeTaveras1231
Copy link

I followed the link in the post description and that took me to a fishy website. Just wanted to raise this for anyone else stumbling into this page.

This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request I want a new feature in react-dates!
Projects
None yet
Development

No branches or pull requests