Repository: flatpickr/flatpickr
Pull request #2304 (2020-10-15 12:24:40)

Fix for picker showing and hiding again
### Summary
I am proposing a reversion of the merge #2127, which addressed issue #2126. This commit made flatpickr use 'click' events instead of 'mousedown' events in order to be compatible with [WCAG 2.1](https://www.w3.org/TR/WCAG21), specifically [2.5.2 Pointer Cancellation](https://www.w3.org/TR/WCAG21/#pointer-cancellation).

### Reasoning
This change has introduced some strange behaviour in flatpickr because of the way that 'click' works. The fact that flatpickr runs identical events for both focus and click events ensures that compatibility with WCAG 2.1 - 2.5.2 is not possible. Think of the following scenario:

**A user 'clicks' into a flatpickr input field:**
- The browser fires a Focus event - flatpickr runs self.open (therefore invalidating WCAG 2.1 compatibility as there is no way of cancelling this event)
- The browser fires a Mousedown event - the element under the pointer is 'self._input'. The target of the event is registered as such. Flatpickr does nothing, but is still drawing the calendar since self.open got called in the Focus event
- Flatpickr finishes drawing the calendar, and the div is visible underneath the pointer
- The user releases the pointing device,  triggering a 'mouseup' event. Because the user took ~50ms to release the 'click', the 'mouseup' target element is now the flatpickrcalendar div.
- The 'click' event is fired, but with the document as the target instead of the self._input, because the overall target of the combination of 'mousedown' and 'mouseup' was the document, not self._input. This means that self.open is not run a second time, but the documentClick method is run (because this is registered to run on a document.onClick).
- documentClick sees that the picker is open, figures that the 'click' event occured outside of the flatpickr element and forces the flatpickr to close.

TL;DR - If you click too slowly, and flatpickr draws under your mouse, the browser gets confused and thinks you're clicking somewhere else, closing flatpickr.


### Solution
The solution to this is to move back to using 'mousedown' events. This ensures the correct event target is being used every time. While this does mean that in theory flatpickr isn't compatible with WCAG 2.1 - 2.5.2, the reality is that this is already moot as the plugin does a draw on the Focus event, meaning that the actual behaviour of the plugin is as if it is using 'mousedown' anyway, and it doesn't allow for cancellation of that event (except in the exact case detailed above, which isn't desired behaviour).

This PR directly addresses issue #2286. I thought of making this more complicated by using some sort of latestMousedown Target buffer on the click event or something, but it all ends up in the same place; in my opinion moving back to 'mousedown' is the most elegant solution.

chmln: @edwardsteven good points, will definitely consider this. 

Some issues that came up after changing to `click` (e.g. the macOS bug) were totally unanticipated.

Patch:
diff --git a/src/index.ts b/src/index.ts
index 98a4880b..e10b3bdf 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -423,11 +423,11 @@ function FlatpickrInstance(
 
     if (window.ontouchstart !== undefined)
       bind(window.document, "touchstart", documentClick);
-    else bind(window.document, "click", documentClick);
+    else bind(window.document, "mousedown", documentClick);
     bind(window.document, "focus", documentClick, { capture: true });
 
     bind(self._input, "focus", self.open);
-    bind(self._input, "click", self.open);
+    bind(self._input, "mousedown", self.open);
 
     if (self.daysContainer !== undefined) {
       bind(self.monthNav, "click", onMonthNavClick);
@@ -1538,13 +1538,12 @@ function FlatpickrInstance(
         ) > 0)
     )
       return false;
-    if (self.config.enable.length === 0 && self.config.disable.length === 0)
-      return true;
+    if (!self.config.enable && self.config.disable.length === 0) return true;
 
     if (dateToCheck === undefined) return false;
 
-    const bool = self.config.enable.length > 0,
-      array = bool ? self.config.enable : self.config.disable;
+    const bool = !!self.config.enable,
+      array = self.config.enable ?? self.config.disable;
 
     for (let i = 0, d; i < array.length; i++) {
       d = array[i];
@@ -1561,7 +1560,7 @@ function FlatpickrInstance(
       )
         // disabled by date
         return bool;
-      else if (typeof d === "string" && dateToCheck !== undefined) {
+      else if (typeof d === "string") {
         // disabled by date string
         const parsed = self.parseDate(d, undefined, true);
         return parsed && parsed.getTime() === dateToCheck.getTime()
@@ -2072,7 +2071,7 @@ function FlatpickrInstance(
       !self.config.inline &&
       self.config.mode === "single" &&
       !self.config.disable.length &&
-      !self.config.enable.length &&
+      !self.config.enable &&
       !self.config.weekNumbers &&
       /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
         navigator.userAgent
diff --git a/src/l10n/ja.ts b/src/l10n/ja.ts
index 23fe489d..0cfadb50 100644
--- a/src/l10n/ja.ts
+++ b/src/l10n/ja.ts
@@ -56,6 +56,11 @@ export const Japanese: CustomLocale = {
   time_24hr: true,
   rangeSeparator: " から ",
   firstDayOfWeek: 1,
+  monthAriaLabel: '月',
+  amPM: ["午前", "午後"],
+  yearAriaLabel: "年",
+  hourAriaLabel: "時間",
+  minuteAriaLabel: "分",
 };
 
 fp.l10ns.ja = Japanese;
diff --git a/src/types/locale.ts b/src/types/locale.ts
index 86db651a..052a83b7 100644
--- a/src/types/locale.ts
+++ b/src/types/locale.ts
@@ -70,6 +70,7 @@ export type CustomLocale = {
   toggleTitle?: Locale["toggleTitle"];
   scrollTitle?: Locale["scrollTitle"];
   yearAriaLabel?: string;
+  monthAriaLabel?: string;
   hourAriaLabel?: string;
   minuteAriaLabel?: string;
   amPM?: Locale["amPM"];
diff --git a/src/types/options.ts b/src/types/options.ts
index 21c37129..258438cd 100644
--- a/src/types/options.ts
+++ b/src/types/options.ts
@@ -263,7 +263,7 @@ export type Options = Partial<BaseOptions>;
 
 export interface ParsedOptions {
   _disable: DateLimit<Date>[];
-  _enable: DateLimit<Date>[];
+  _enable?: DateLimit<Date>[];
   _maxDate?: Date;
   _maxTime?: Date;
   _minDate?: Date;
@@ -287,7 +287,7 @@ export interface ParsedOptions {
   defaultSeconds: number;
   disable: DateLimit<Date>[];
   disableMobile: boolean;
-  enable: DateLimit<Date>[];
+  enable?: DateLimit<Date>[];
   enableSeconds: boolean;
   enableTime: boolean;
   errorHandler: (err: Error) => void;
@@ -334,7 +334,6 @@ export interface ParsedOptions {
 
 export const defaults: ParsedOptions = {
   _disable: [],
-  _enable: [],
   allowInput: false,
   allowInvalidPreload: false,
   altFormat: "F j, Y",
@@ -354,7 +353,6 @@ export const defaults: ParsedOptions = {
   defaultSeconds: 0,
   disable: [],
   disableMobile: false,
-  enable: [],
   enableSeconds: false,
   enableTime: false,
   errorHandler: (err: Error) =>


Test patch:
diff --git a/__tests__/src/index.spec.ts b/__tests__/src/index.spec.ts
index fa1f9441..bf6b0adf 100644
--- a/__tests__/src/index.spec.ts
+++ b/__tests__/src/index.spec.ts
@@ -616,15 +616,15 @@ describe("flatpickr", () => {
 
       expect(fp.config.disable.indexOf(null as any)).toBe(-1);
 
-      expect((fp.config.enable[0] as DateRangeLimit).from instanceof Date).toBe(
-        true
-      );
-      expect((fp.config.enable[0] as DateRangeLimit).to instanceof Date).toBe(
+      expect(
+        (fp.config.enable?.[0] as DateRangeLimit).from instanceof Date
+      ).toBe(true);
+      expect((fp.config.enable?.[0] as DateRangeLimit).to instanceof Date).toBe(
         true
       );
-      expect(fp.config.enable[1] instanceof Date).toBe(true);
+      expect(fp.config.enable?.[1] instanceof Date).toBe(true);
 
-      expect(fp.config.enable.indexOf(null as any)).toBe(-1);
+      expect(fp.config.enable?.indexOf(null as any)).toBe(-1);
     });
 
     it("documentClick", () => {
@@ -636,7 +636,7 @@ describe("flatpickr", () => {
       fp._input.focus();
 
       expect(fp.isOpen).toBe(true);
-      simulate("click", window.document.body, { which: 1 }, CustomEvent);
+      simulate("mousedown", window.document.body, { which: 1 }, CustomEvent);
       fp._input.blur();
 
       expect(fp.isOpen).toBe(false);
@@ -653,7 +653,7 @@ describe("flatpickr", () => {
       expect(fp.selectedDates.length).toBe(1);
 
       fp.isOpen = true;
-      simulate("click", window.document.body, { which: 1 }, CustomEvent);
+      simulate("mousedown", window.document.body, { which: 1 }, CustomEvent);
       expect(fp.isOpen).toBe(false);
       expect(fp.selectedDates.length).toBe(0);
       expect(fp._input.value).toBe("");