Repository: flatpickr/flatpickr (ts)

Issue:
monthSelect plugin: prev/next year buttons do not honor minDate/maxDate settings
### How to Reproduce

(Note that this bug was concealed by #2277. In order to reproduce it, the fiddle uses a version of flatpickr compiled against [6dc91a5](https://github.com/rlue/flatpickr/commit/6dc91a5c29912ef6013f58698e0c41de000f20d5).)

> https://jsfiddle.net/ryanlue/x6vod3gw/1/

* Click the text field to open flatpickr.
* Click the previous year button multiple times. (The calendar will walk back beyond the minDate setting. All months below the minDate will be disabled, but the prev year button is not.)

A PR is forthcoming.

### Your Environment

* flatpickr: https://github.com/rlue/flatpickr/commit/6dc91a5c29912ef6013f58698e0c41de000f20d5
* Browser: Firefox 81.0b9
* OS: Debian bullseye

Pull request description:
Fix monthSelect plugin's minDate/maxDate handling
Fixes #2279.

This commit fixes a multi-layered bug
in which the prev/next year buttons in monthSelect
do not honor the `minDate` / `maxDate` config options.

That is, the months (cells) outside of the given bounds are disabled,
but the user is free to navigate to any year (page)
regardless of minDate/maxDate settings.

There were two problems at play:

1. First, the listeners on the prev/next year buttons had to be fixed.
   Originally, they blindly incremented/decremented the given year;
   now, they reuse an existing `changeYear()` function,
   which performs this check (among other things).

       - fp.currentYear--;
       + fp.changeYear(fp.currentYear - 1);

2. That mostly fixed the problem, but left one small wrinkle:
   the prev. year button was still visible on the screen
   when reaching the minYear bound.
   Clicking on it wouldn't do anything,
   but it was on-screen when it shouldn't have been.

   (To be perfectly honest,
   this problem and its fix still don't entirely make sense to me,
   but it's working for now, so bear with me...)
 
   This quirk was caused by the fact that
   parts of flatpickr are hardcoded for date selection
   and don't adapt cleanly to month selection--suppose the following:
 
   * minDate is 03/01/2019
   * the selected date is 09/01/2019
 
   In date select mode, paging backward would take you to 08/2019 (ok).
   In month select mode, paging backward would take you to 2018 (bad).
 
   To fix this problem, monthSelect has to use separate variables
   for user selection and calendar state:
 
   a. `current{Month,Year}` -> calendar state
   b. `selectedDates` -> user selection
 
   This commit artificially sets currentMonth to 0
   so that `updateNavigationCurrentMonth()` doesn't think there are
   valid earlier "months" to page back to.
   This is where it gets confusing: in theory,
   this _should_ also allow you to page forward past maxDate (bad),
   but that's not what I'm seeing in practice. ¯\_(ツ)_/¯

[0]: https://github.com/flatpickr/flatpickr/issues/2279

Patch:
diff --git a/src/plugins/monthSelect/index.ts b/src/plugins/monthSelect/index.ts
index d5bd402d..aab54d72 100644
--- a/src/plugins/monthSelect/index.ts
+++ b/src/plugins/monthSelect/index.ts
@@ -50,7 +50,7 @@ function monthSelectPlugin(pluginConfig?: Partial<Config>): Plugin {
         e.preventDefault();
         e.stopPropagation();
 
-        fp.currentYear--;
+        fp.changeYear(fp.currentYear - 1);
         selectYear();
       });
 
@@ -58,7 +58,7 @@ function monthSelectPlugin(pluginConfig?: Partial<Config>): Plugin {
         e.preventDefault();
         e.stopPropagation();
 
-        fp.currentYear++;
+        fp.changeYear(fp.currentYear + 1);
         selectYear();
       });
     }
@@ -110,8 +110,9 @@ function monthSelectPlugin(pluginConfig?: Partial<Config>): Plugin {
         currentlySelected[index].classList.remove("selected");
       }
 
+      const targetMonth = (fp.selectedDates[0] || new Date()).getMonth();
       const month = fp.rContainer.querySelector(
-        `.flatpickr-monthSelect-month:nth-child(${fp.currentMonth + 1})`
+        `.flatpickr-monthSelect-month:nth-child(${targetMonth + 1})`
       );
 
       if (month) {
@@ -131,7 +132,6 @@ function monthSelectPlugin(pluginConfig?: Partial<Config>): Plugin {
           selectedDate = fp.config.maxDate;
         }
         fp.currentYear = selectedDate.getFullYear();
-        fp.currentMonth = selectedDate.getMonth();
       }
 
       fp.currentYearElement.value = String(fp.currentYear);
@@ -171,7 +171,6 @@ function monthSelectPlugin(pluginConfig?: Partial<Config>): Plugin {
     function setMonth(date: Date) {
       const selectedDate = new Date(date);
       selectedDate.setFullYear(fp.currentYear);
-      fp.currentMonth = selectedDate.getMonth();
 
       fp.setDate(selectedDate, true);
 
@@ -241,6 +240,9 @@ function monthSelectPlugin(pluginConfig?: Partial<Config>): Plugin {
       onValueUpdate: setCurrentlySelected,
       onKeyDown,
       onReady: [
+        () => {
+          fp.currentMonth = 0;
+        },
         clearUnnecessaryDOMElements,
         addListeners,
         addMonths,


Test patch:
diff --git a/__tests__/src/plugins/monthSelect/index.spec.ts b/__tests__/src/plugins/monthSelect/index.spec.ts
index 1f017e58..f9232cf6 100644
--- a/__tests__/src/plugins/monthSelect/index.spec.ts
+++ b/__tests__/src/plugins/monthSelect/index.spec.ts
@@ -99,6 +99,28 @@ describe("monthSelect", () => {
 
         expect(fp.currentYearElement.value).toEqual(`${initYear + 1}`);
       });
+
+      it("should honor minDate / maxDate options (#2279)", () => {
+        const lastYear = new Date().getFullYear() - 1;
+        const nextYear = new Date().getFullYear() + 1;
+
+        const fp = createInstance({
+          plugins: [monthSelectPlugin()],
+          minDate: `${lastYear}-03-20`,
+          maxDate: `${nextYear}-03-20`,
+        }) as Instance;
+
+        const prevButton = fp.monthNav.querySelector(".flatpickr-prev-month")!;
+        prevButton.dispatchEvent(new MouseEvent("click"));
+
+        expect(prevButton.classList).toContain("flatpickr-disabled");
+
+        const nextButton = fp.monthNav.querySelector(".flatpickr-next-month")!;
+        nextButton.dispatchEvent(new MouseEvent("click"));
+        nextButton.dispatchEvent(new MouseEvent("click"));
+
+        expect(nextButton.classList).toContain("flatpickr-disabled");
+      });
     });
   });
 });