Repository: gpbl/react-day-picker
Pull request #1876 (2023-08-11 15:05:42)

fix: range not selected when missing the `from` value
### Context
A user might want to filter all items before a specific date and modify the `selected` state outside `react-day-picker`. 
While this works perfectly when only `from` is set. But when only `to` is set, the calendar doesn't reflect this state.
The next time a user selects a date, the state is affected like when no values were set at all.
Discussion: https://github.com/gpbl/react-day-picker/discussions/1875

### Analysis
`selected` on a range picker accepts a DateRange where `from` is undefined but `to` is a Date. 
Since this works in one direction and the types allow this state, this component should reflect the other direction too.

### Solution

To prevent current behaviour changes, All of the logic additions only take effect when `to` is set and `from` is not.
From within this component, this is an unreachable state, but when the state is controlled externally, it is now reflected in the internal state.

All other code changes and restructuring were made to benefit typescript.
No test were changed, few were added for specific state.



Patch:
diff --git a/package.json b/package.json
index a3ec00650..f18ec356a 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
   "types": "dist/index.d.ts",
   "style": "dist/style.css",
   "scripts": {
-    "prepublish": "onpm build",
+    "prepublish": "pnpm build",
     "build": "rimraf dist && rollup -c",
     "build-watch": "rollup -c -w",
     "lint": "eslint .",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 91ffdfbff..ba8425b11 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -13,10 +13,10 @@ importers:
         version: 29.5.0
       '@rollup/plugin-commonjs':
         specifier: ^25.0.0
-        version: 25.0.0(rollup@3.20.7)
+        version: 25.0.4(rollup@3.20.7)
       '@rollup/plugin-node-resolve':
         specifier: ^15.0.2
-        version: 15.0.2(rollup@3.20.7)
+        version: 15.1.0(rollup@3.20.7)
       '@rollup/plugin-terser':
         specifier: ^0.4.1
         version: 0.4.3(rollup@3.20.7)
@@ -1982,7 +1982,7 @@ packages:
       serve-handler: 6.1.3
       shelljs: 0.8.5
       terser-webpack-plugin: 5.3.7(webpack@5.88.2)
-      tslib: 2.5.0
+      tslib: 2.6.1
       update-notifier: 5.1.0
       url-loader: 4.1.1(file-loader@6.2.0)(webpack@5.88.2)
       wait-on: 6.0.1
@@ -2014,14 +2014,14 @@ packages:
       cssnano-preset-advanced: 5.3.8(postcss@8.4.27)
       postcss: 8.4.27
       postcss-sort-media-queries: 4.2.1(postcss@8.4.27)
-      tslib: 2.5.0
+      tslib: 2.6.1
 
   /@docusaurus/logger@2.4.1:
     resolution: {integrity: sha512-5h5ysIIWYIDHyTVd8BjheZmQZmEgWDR54aQ1BX9pjFfpyzFo5puKXKYrYJXbjEHGyVhEzmB9UXwbxGfaZhOjcg==}
     engines: {node: '>=16.14'}
     dependencies:
       chalk: 4.1.2
-      tslib: 2.5.0
+      tslib: 2.6.1
 
   /@docusaurus/mdx-loader@2.4.1(@docusaurus/types@2.4.1)(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-4KhUhEavteIAmbBj7LVFnrVYDiU51H5YWW1zY6SmBSte/YLhDutztLTBE0PQl1Grux1jzUJeaSvAzHpTn6JJDQ==}
@@ -2044,7 +2044,7 @@ packages:
       react-dom: 18.2.0(react@18.2.0)
       remark-emoji: 2.2.0
       stringify-object: 3.3.0
-      tslib: 2.5.0
+      tslib: 2.6.1
       unified: 9.2.2
       unist-util-visit: 2.0.3
       url-loader: 4.1.1(file-loader@6.2.0)(webpack@5.88.2)
@@ -2100,7 +2100,7 @@ packages:
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
       reading-time: 1.5.0
-      tslib: 2.5.0
+      tslib: 2.6.1
       unist-util-visit: 2.0.3
       utility-types: 3.10.0
       webpack: 5.88.2
@@ -2141,7 +2141,7 @@ packages:
       lodash: 4.17.21
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      tslib: 2.5.0
+      tslib: 2.6.1
       utility-types: 3.10.0
       webpack: 5.88.2
     transitivePeerDependencies:
@@ -2174,7 +2174,7 @@ packages:
       fs-extra: 10.1.0
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      tslib: 2.5.0
+      tslib: 2.6.1
       webpack: 5.88.2
     transitivePeerDependencies:
       - '@parcel/css'
@@ -2205,7 +2205,7 @@ packages:
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
       react-json-view: 1.21.3(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
-      tslib: 2.5.0
+      tslib: 2.6.1
     transitivePeerDependencies:
       - '@parcel/css'
       - '@swc/core'
@@ -2236,7 +2236,7 @@ packages:
       '@docusaurus/utils-validation': 2.4.1(@docusaurus/types@2.4.1)
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      tslib: 2.5.0
+      tslib: 2.6.1
     transitivePeerDependencies:
       - '@parcel/css'
       - '@swc/core'
@@ -2265,7 +2265,7 @@ packages:
       '@docusaurus/utils-validation': 2.4.1(@docusaurus/types@2.4.1)
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      tslib: 2.5.0
+      tslib: 2.6.1
     transitivePeerDependencies:
       - '@parcel/css'
       - '@swc/core'
@@ -2294,7 +2294,7 @@ packages:
       '@docusaurus/utils-validation': 2.4.1(@docusaurus/types@2.4.1)
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      tslib: 2.5.0
+      tslib: 2.6.1
     transitivePeerDependencies:
       - '@parcel/css'
       - '@swc/core'
@@ -2328,7 +2328,7 @@ packages:
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
       sitemap: 7.1.1
-      tslib: 2.5.0
+      tslib: 2.6.1
     transitivePeerDependencies:
       - '@parcel/css'
       - '@swc/core'
@@ -2427,7 +2427,7 @@ packages:
       react-dom: 18.2.0(react@18.2.0)
       react-router-dom: 5.3.3(react@18.2.0)
       rtlcss: 3.5.0
-      tslib: 2.5.0
+      tslib: 2.6.1
       utility-types: 3.10.0
     transitivePeerDependencies:
       - '@parcel/css'
@@ -2467,7 +2467,7 @@ packages:
       prism-react-renderer: 1.3.5(react@18.2.0)
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      tslib: 2.5.0
+      tslib: 2.6.1
       use-sync-external-store: 1.2.0(react@18.2.0)
       utility-types: 3.10.0
     transitivePeerDependencies:
@@ -2509,7 +2509,7 @@ packages:
       lodash: 4.17.21
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      tslib: 2.5.0
+      tslib: 2.6.1
       utility-types: 3.10.0
     transitivePeerDependencies:
       - '@algolia/client-search'
@@ -2535,7 +2535,7 @@ packages:
     engines: {node: '>=16.14'}
     dependencies:
       fs-extra: 10.1.0
-      tslib: 2.5.0
+      tslib: 2.6.1
     dev: false
 
   /@docusaurus/types@2.4.1(react-dom@18.2.0)(react@18.2.0):
@@ -2570,7 +2570,7 @@ packages:
         optional: true
     dependencies:
       '@docusaurus/types': 2.4.1(react-dom@18.2.0)(react@18.2.0)
-      tslib: 2.5.0
+      tslib: 2.6.1
 
   /@docusaurus/utils-validation@2.4.1(@docusaurus/types@2.4.1):
     resolution: {integrity: sha512-unII3hlJlDwZ3w8U+pMO3Lx3RhI4YEbY3YNsQj4yzrkZzlpqZOLuAiZK2JyULnD+TKbceKU0WyWkQXtYbLNDFA==}
@@ -2580,7 +2580,7 @@ packages:
       '@docusaurus/utils': 2.4.1(@docusaurus/types@2.4.1)
       joi: 17.6.0
       js-yaml: 4.1.0
-      tslib: 2.5.0
+      tslib: 2.6.1
     transitivePeerDependencies:
       - '@docusaurus/types'
       - '@swc/core'
@@ -2612,7 +2612,7 @@ packages:
       micromatch: 4.0.5
       resolve-pathname: 3.0.0
       shelljs: 0.8.5
-      tslib: 2.5.0
+      tslib: 2.6.1
       url-loader: 4.1.1(file-loader@6.2.0)(webpack@5.88.2)
       webpack: 5.88.2
     transitivePeerDependencies:
@@ -3103,7 +3103,7 @@ packages:
       open: 8.4.0
       picocolors: 1.0.0
       tiny-glob: 0.2.9
-      tslib: 2.5.0
+      tslib: 2.6.1
     dev: true
 
   /@polka/url@1.0.0-next.21:
@@ -3131,8 +3131,8 @@ packages:
       react: 18.2.0
     dev: false
 
-  /@rollup/plugin-commonjs@25.0.0(rollup@3.20.7):
-    resolution: {integrity: sha512-hoho2Kay9TZrLu0bnDsTTCaj4Npa+THk9snajP/XDNb9a9mmjTjh52EQM9sKl3HD1LsnihX7js+eA2sd2uKAhw==}
+  /@rollup/plugin-commonjs@25.0.4(rollup@3.20.7):
+    resolution: {integrity: sha512-L92Vz9WUZXDnlQQl3EwbypJR4+DM2EbsO+/KOcEkP4Mc6Ct453EeDB2uH9lgRwj4w5yflgNpq9pHOiY8aoUXBQ==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
       rollup: ^2.68.0||^3.0.0
@@ -3149,8 +3149,8 @@ packages:
       rollup: 3.20.7
     dev: true
 
-  /@rollup/plugin-node-resolve@15.0.2(rollup@3.20.7):
-    resolution: {integrity: sha512-Y35fRGUjC3FaurG722uhUuG8YHOJRJQbI6/CkbRkdPotSpDj9NtIN85z1zrcyDcCQIW4qp5mgG72U+gJ0TAFEg==}
+  /@rollup/plugin-node-resolve@15.1.0(rollup@3.20.7):
+    resolution: {integrity: sha512-xeZHCgsiZ9pzYVgAo9580eCGqwh/XCEUM9q6iQfGNocjgkufHAqC3exA+45URvhiYV8sBF9RlBai650eNs7AsA==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
       rollup: ^2.78.0||^3.0.0
@@ -4783,7 +4783,7 @@ packages:
     resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
     dependencies:
       pascal-case: 3.1.2
-      tslib: 2.5.0
+      tslib: 2.6.1
 
   /camelcase-css@2.0.1:
     resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
@@ -5669,7 +5669,7 @@ packages:
     resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
     dependencies:
       no-case: 3.0.4
-      tslib: 2.5.0
+      tslib: 2.6.1
 
   /dot-prop@5.3.0:
     resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
@@ -8312,7 +8312,7 @@ packages:
   /lower-case@2.0.2:
     resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
     dependencies:
-      tslib: 2.5.0
+      tslib: 2.6.1
 
   /lowercase-keys@1.0.1:
     resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==}
@@ -8350,7 +8350,7 @@ packages:
     resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==}
     engines: {node: '>=12'}
     dependencies:
-      '@jridgewell/sourcemap-codec': 1.4.14
+      '@jridgewell/sourcemap-codec': 1.4.15
     dev: true
 
   /magic-string@0.30.0:
@@ -8602,7 +8602,7 @@ packages:
     resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
     dependencies:
       lower-case: 2.0.2
-      tslib: 2.5.0
+      tslib: 2.6.1
 
   /node-emoji@1.11.0:
     resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==}
@@ -8847,7 +8847,7 @@ packages:
     resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
     dependencies:
       dot-case: 3.0.4
-      tslib: 2.5.0
+      tslib: 2.6.1
 
   /parent-module@1.0.1:
     resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
@@ -8899,7 +8899,7 @@ packages:
     resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
     dependencies:
       no-case: 3.0.4
-      tslib: 2.5.0
+      tslib: 2.6.1
 
   /path-exists@3.0.0:
     resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
@@ -10147,7 +10147,7 @@ packages:
   /rxjs@7.5.4:
     resolution: {integrity: sha512-h5M3Hk78r6wAheJF0a5YahB1yRQKCsZ4MsGdZ5O9ETbVtjPcScGfrMmoOq7EBsCRzd4BDkvDJ7ogP8Sz5tTFiQ==}
     dependencies:
-      tslib: 2.5.0
+      tslib: 2.6.1
 
   /safe-buffer@5.1.2:
     resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
@@ -10722,7 +10722,7 @@ packages:
     engines: {node: ^14.18.0 || >=16.0.0}
     dependencies:
       '@pkgr/utils': 2.3.1
-      tslib: 2.5.0
+      tslib: 2.6.1
     dev: true
 
   /tabbable@6.1.1:
@@ -10972,6 +10972,10 @@ packages:
 
   /tslib@2.5.0:
     resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
+    dev: true
+
+  /tslib@2.6.1:
+    resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==}
 
   /tsutils@3.21.0(typescript@4.9.5):
     resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
diff --git a/src/contexts/Modifiers/utils/isDateInRange.ts b/src/contexts/Modifiers/utils/isDateInRange.ts
index 88f95a7ea..844df8e78 100644
--- a/src/contexts/Modifiers/utils/isDateInRange.ts
+++ b/src/contexts/Modifiers/utils/isDateInRange.ts
@@ -5,21 +5,21 @@ import { DateRange } from 'types/Matchers';
 /** Return `true` whether `date` is inside `range`. */
 export function isDateInRange(date: Date, range: DateRange): boolean {
   let { from, to } = range;
-  if (!from) {
-    return false;
+  if (from && to) {
+    const isRangeInverted = differenceInCalendarDays(to, from) < 0;
+    if (isRangeInverted) {
+      [from, to] = [to, from];
+    }
+    const isInRange =
+      differenceInCalendarDays(date, from) >= 0 &&
+      differenceInCalendarDays(to, date) >= 0;
+    return isInRange;
   }
-  if (!to && isSameDay(from, date)) {
-    return true;
+  if (to) {
+    return isSameDay(to, date);
   }
-  if (!to) {
-    return false;
+  if (from) {
+    return isSameDay(from, date);
   }
-  const isRangeInverted = differenceInCalendarDays(to, from) < 0;
-  if (isRangeInverted) {
-    [from, to] = [to, from];
-  }
-  const isInRange =
-    differenceInCalendarDays(date, from) >= 0 &&
-    differenceInCalendarDays(to, date) >= 0;
-  return isInRange;
+  return false;
 }
diff --git a/src/contexts/SelectRange/SelectRangeContext.tsx b/src/contexts/SelectRange/SelectRangeContext.tsx
index 7b9430b03..1a6e9d44e 100644
--- a/src/contexts/SelectRange/SelectRangeContext.tsx
+++ b/src/contexts/SelectRange/SelectRangeContext.tsx
@@ -119,6 +119,9 @@ export function SelectRangeProviderInternal({
         ];
       }
     }
+  } else if (selectedTo) {
+    modifiers.range_start = [selectedTo];
+    modifiers.range_end = [selectedTo];
   }
 
   if (min) {
@@ -134,6 +137,12 @@ export function SelectRangeProviderInternal({
         before: addDays(selectedFrom, min - 1)
       });
     }
+    if (!selectedFrom && selectedTo) {
+      modifiers.disabled.push({
+        after: subDays(selectedTo, min - 1),
+        before: addDays(selectedTo, min - 1)
+      });
+    }
   }
   if (max) {
     if (selectedFrom && !selectedTo) {
@@ -155,6 +164,14 @@ export function SelectRangeProviderInternal({
         after: addDays(selectedTo, offset)
       });
     }
+    if (!selectedFrom && selectedTo) {
+      modifiers.disabled.push({
+        before: addDays(selectedTo, -max + 1)
+      });
+      modifiers.disabled.push({
+        after: addDays(selectedTo, max - 1)
+      });
+    }
   }
 
   return (
diff --git a/src/contexts/SelectRange/utils/addToRange.ts b/src/contexts/SelectRange/utils/addToRange.ts
index 626b38934..4f9fc5a2f 100644
--- a/src/contexts/SelectRange/utils/addToRange.ts
+++ b/src/contexts/SelectRange/utils/addToRange.ts
@@ -13,29 +13,32 @@ export function addToRange(
   range?: DateRange
 ): DateRange | undefined {
   const { from, to } = range || {};
-  if (!from) {
-    return { from: day, to: undefined };
-  }
-  if (!to && isSameDay(from, day)) {
-    return { from: from, to: day };
-  }
-  if (!to && isBefore(day, from)) {
-    return { from: day, to: from };
-  }
-  if (!to) {
+  if (from && to) {
+    if (isSameDay(to, day) && isSameDay(from, day)) {
+      return undefined;
+    }
+    if (isSameDay(to, day)) {
+      return { from: to, to: undefined };
+    }
+    if (isSameDay(from, day)) {
+      return undefined;
+    }
+    if (isAfter(from, day)) {
+      return { from: day, to };
+    }
     return { from, to: day };
   }
-  if (isSameDay(to, day) && isSameDay(from, day)) {
-    return undefined;
-  }
-  if (isSameDay(to, day)) {
-    return { from: to, to: undefined };
-  }
-  if (isSameDay(from, day)) {
-    return undefined;
-  }
-  if (isAfter(from, day)) {
+  if (to) {
+    if (isAfter(day, to)) {
+      return { from: to, to: day };
+    }
     return { from: day, to };
   }
-  return { from, to: day };
+  if (from) {
+    if (isBefore(day, from)) {
+      return { from: day, to: from };
+    }
+    return { from, to: day };
+  }
+  return { from: day, to: undefined };
 }


Test patch:
diff --git a/src/contexts/SelectRange/SelectRangeContext.test.ts b/src/contexts/SelectRange/SelectRangeContext.test.ts
index f1926124c..007420821 100644
--- a/src/contexts/SelectRange/SelectRangeContext.test.ts
+++ b/src/contexts/SelectRange/SelectRangeContext.test.ts
@@ -91,6 +91,26 @@ describe('when only the "from" day is selected', () => {
   });
 });
 
+describe('when only the "to" day is selected', () => {
+  const selected = { from: undefined, to };
+  const dayPickerProps: DayPickerRangeProps = {
+    ...initialProps,
+    selected
+  };
+  test('should return the "range_start" modifiers with the "to" day', () => {
+    const result = renderHook(dayPickerProps);
+    expect(result.current.modifiers.range_start).toEqual([to]);
+  });
+  test('should return the "range_end" modifiers with the "to" day', () => {
+    const result = renderHook(dayPickerProps);
+    expect(result.current.modifiers.range_end).toEqual([to]);
+  });
+  test('should not return any "range_middle" modifiers', () => {
+    const result = renderHook(dayPickerProps);
+    expect(result.current.modifiers.range_middle).toEqual([]);
+  });
+});
+
 describe('when a complete range of days is selected', () => {
   const selected = { from, to };
   const dayPickerProps: DayPickerRangeProps = {