Repository: Hacker0x01/react-datepicker
Pull request #4756 (2024-05-03 11:19:44)

[typescript-migration] year.jsx
---
name: Migrate year.jsx
---
 
## Description
**Linked issue**: https://github.com/Hacker0x01/react-datepicker/issues/4700
 
**Changes**
`year.jsx` was migrated to ts + added JSDocs
 
## Contribution checklist
- [x] I have followed the [contributing guidelines](https://github.com/Hacker0x01/react-datepicker/blob/main/CONTRIBUTING.md).
- [x] I have added sufficient test coverage for my changes.
- [x] I have formatted my code with Prettier and checked for linting issues with ESLint for code readability.



Patch:
diff --git a/src/date_utils.ts b/src/date_utils.ts
index 7f805b27..0481fc79 100644
--- a/src/date_utils.ts
+++ b/src/date_utils.ts
@@ -910,7 +910,8 @@ export function isQuarterDisabled(
   );
 }
 
-export function isYearInRange(year: number, start: Date, end: Date): boolean {
+export function isYearInRange(year: number, start?: Date, end?: Date): boolean {
+  if (!start || !end) return false;
   if (!isValidDate(start) || !isValidDate(end)) return false;
   const startYear = getYear(start);
   const endYear = getYear(end);
@@ -1504,7 +1505,9 @@ export function isDateBefore(date: Date, dateToCompare: Date): boolean {
  * @param event - The keyboard event.
  * @returns - Returns true if the space key was pressed down, false otherwise.
  */
-export function isSpaceKeyDown(event: KeyboardEvent): boolean {
+export function isSpaceKeyDown(
+  event: React.KeyboardEvent<HTMLDivElement>,
+): boolean {
   const SPACE_KEY = " ";
   return event.key === SPACE_KEY;
 }
diff --git a/src/year.jsx b/src/year.tsx
similarity index 56%
rename from src/year.jsx
rename to src/year.tsx
index 55b82efd..b0419ab8 100644
--- a/src/year.jsx
+++ b/src/year.tsx
@@ -1,83 +1,126 @@
-import React from "react";
-import PropTypes from "prop-types";
+import React, { Component } from "react";
 import { getYear, newDate } from "./date_utils";
 import * as utils from "./date_utils";
 import { clsx } from "clsx";
 
 const VERTICAL_NAVIGATION_OFFSET = 3;
 
-export default class Year extends React.Component {
-  static propTypes = {
-    clearSelectingDate: PropTypes.func,
-    date: PropTypes.instanceOf(Date),
-    disabledKeyboardNavigation: PropTypes.bool,
-    endDate: PropTypes.instanceOf(Date),
-    onDayClick: PropTypes.func,
-    preSelection: PropTypes.instanceOf(Date),
-    setPreSelection: PropTypes.func,
-    selected: PropTypes.object,
-    inline: PropTypes.bool,
-    maxDate: PropTypes.instanceOf(Date),
-    minDate: PropTypes.instanceOf(Date),
-    usePointerEvent: PropTypes.bool,
-    onYearMouseEnter: PropTypes.func.isRequired,
-    onYearMouseLeave: PropTypes.func.isRequired,
-    selectingDate: PropTypes.instanceOf(Date),
-    renderYearContent: PropTypes.func,
-    selectsEnd: PropTypes.bool,
-    selectsStart: PropTypes.bool,
-    selectsRange: PropTypes.bool,
-    startDate: PropTypes.instanceOf(Date),
-    excludeDates: PropTypes.arrayOf(
-      PropTypes.oneOfType([
-        PropTypes.instanceOf(Date),
-        PropTypes.shape({
-          date: PropTypes.instanceOf(Date).isRequired,
-          message: PropTypes.string,
-        }),
-      ]),
-    ),
-    includeDates: PropTypes.array,
-    filterDate: PropTypes.func,
-    yearItemNumber: PropTypes.number,
-    handleOnKeyDown: PropTypes.func,
-    yearClassName: PropTypes.func,
-  };
+interface YearProps {
+  clearSelectingDate?: () => void;
+  date?: Date;
+  disabledKeyboardNavigation?: boolean;
+  endDate?: Date;
+  onDayClick?: (
+    date: Date,
+    event:
+      | React.MouseEvent<HTMLDivElement>
+      | React.KeyboardEvent<HTMLDivElement>,
+  ) => void;
+  preSelection?: Date;
+  setPreSelection?: (date: Date) => void;
+  selected?: Date;
+  inline?: boolean;
+  maxDate?: Date;
+  minDate?: Date;
+  usePointerEvent?: boolean;
+  onYearMouseEnter: (
+    e: React.MouseEvent<HTMLDivElement, MouseEvent>,
+    year: number,
+  ) => void;
+  onYearMouseLeave: (
+    e: React.MouseEvent<HTMLDivElement, MouseEvent>,
+    year: number,
+  ) => void;
+  selectingDate?: Date;
+  renderYearContent?: (year: number) => React.ReactNode;
+  selectsEnd?: boolean;
+  selectsStart?: boolean;
+  selectsRange?: boolean;
+  startDate?: Date;
+  excludeDates?: Date[] | { date: Date; message?: string }[];
+  includeDates?: [];
+  filterDate?: (date: Date) => boolean;
+  yearItemNumber?: number;
+  handleOnKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
+  yearClassName?: (date: Date) => string;
+}
 
-  constructor(props) {
+/**
+ * `Year` is a component that represents a year in a date picker.
+ *
+ * @class
+ * @param {YearProps} props - The properties that define the `Year` component.
+ * @property {() => void} [props.clearSelectingDate] - Function to clear the selected date.
+ * @property {Date} [props.date] - The currently selected date.
+ * @property {boolean} [props.disabledKeyboardNavigation] - If true, keyboard navigation is disabled.
+ * @property {Date} [props.endDate] - The end date in a range selection.
+ * @property {(date: Date) => void} props.onDayClick - Function to handle day click events.
+ * @property {Date} props.preSelection - The date that is currently in focus.
+ * @property {(date: Date) => void} props.setPreSelection - Function to set the pre-selected date.
+ * @property {{ [key: string]: any }} props.selected - The selected date(s).
+ * @property {boolean} props.inline - If true, the date picker is displayed inline.
+ * @property {Date} props.maxDate - The maximum selectable date.
+ * @property {Date} props.minDate - The minimum selectable date.
+ * @property {boolean} props.usePointerEvent - If true, pointer events are used instead of mouse events.
+ * @property {(date: Date) => void} props.onYearMouseEnter - Function to handle mouse enter events on a year.
+ * @property {(date: Date) => void} props.onYearMouseLeave - Function to handle mouse leave events on a year.
+ */
+export default class Year extends Component<YearProps> {
+  constructor(props: YearProps) {
     super(props);
   }
 
   YEAR_REFS = [...Array(this.props.yearItemNumber)].map(() =>
-    React.createRef(),
+    React.createRef<HTMLDivElement>(),
   );
 
-  isDisabled = (date) => utils.isDayDisabled(date, this.props);
+  isDisabled = (date: Date) =>
+    utils.isDayDisabled(date, {
+      minDate: this.props.minDate,
+      maxDate: this.props.maxDate,
+      excludeDates: this.props.excludeDates,
+      includeDates: this.props.includeDates,
+      filterDate: this.props.filterDate,
+    });
 
-  isExcluded = (date) => utils.isDayExcluded(date, this.props);
+  isExcluded = (date: Date) =>
+    utils.isDayExcluded(date, {
+      excludeDates: this.props.excludeDates,
+    });
 
   selectingDate = () => this.props.selectingDate ?? this.props.preSelection;
 
-  updateFocusOnPaginate = (refIndex) => {
-    const waitForReRender = function () {
-      this.YEAR_REFS[refIndex].current.focus();
-    }.bind(this);
+  updateFocusOnPaginate = (refIndex: number) => {
+    const waitForReRender = () => {
+      this.YEAR_REFS[refIndex]?.current?.focus();
+    };
 
     window.requestAnimationFrame(waitForReRender);
   };
 
-  handleYearClick = (day, event) => {
+  handleYearClick = (
+    day: Date,
+    event:
+      | React.MouseEvent<HTMLDivElement>
+      | React.KeyboardEvent<HTMLDivElement>,
+  ) => {
     if (this.props.onDayClick) {
       this.props.onDayClick(day, event);
     }
   };
 
-  handleYearNavigation = (newYear, newDate) => {
+  handleYearNavigation = (newYear: number, newDate: Date) => {
     const { date, yearItemNumber } = this.props;
+    if (date === undefined || yearItemNumber === undefined) {
+      return;
+    }
+
     const { startPeriod } = utils.getYearsPeriod(date, yearItemNumber);
 
-    if (this.isDisabled(newDate) || this.isExcluded(newDate)) return;
-    this.props.setPreSelection(newDate);
+    if (this.isDisabled(newDate) || this.isExcluded(newDate)) {
+      return;
+    }
+    this.props.setPreSelection?.(newDate);
 
     if (newYear - startPeriod < 0) {
       this.updateFocusOnPaginate(yearItemNumber - (startPeriod - newYear));
@@ -85,27 +128,27 @@ export default class Year extends React.Component {
       this.updateFocusOnPaginate(
         Math.abs(yearItemNumber - (newYear - startPeriod)),
       );
-    } else this.YEAR_REFS[newYear - startPeriod].current.focus();
+    } else this.YEAR_REFS[newYear - startPeriod]?.current?.focus();
   };
 
-  isSameDay = (y, other) => utils.isSameDay(y, other);
+  isSameDay = (y: Date, other: Date) => utils.isSameDay(y, other);
 
-  isCurrentYear = (y) => y === getYear(newDate());
+  isCurrentYear = (y: number) => y === getYear(newDate());
 
-  isRangeStart = (y) =>
+  isRangeStart = (y: number) =>
     this.props.startDate &&
     this.props.endDate &&
     utils.isSameYear(utils.setYear(newDate(), y), this.props.startDate);
 
-  isRangeEnd = (y) =>
+  isRangeEnd = (y: number) =>
     this.props.startDate &&
     this.props.endDate &&
     utils.isSameYear(utils.setYear(newDate(), y), this.props.endDate);
 
-  isInRange = (y) =>
+  isInRange = (y: number) =>
     utils.isYearInRange(y, this.props.startDate, this.props.endDate);
 
-  isInSelectingRange = (y) => {
+  isInSelectingRange = (y: number) => {
     const { selectsStart, selectsEnd, selectsRange, startDate, endDate } =
       this.props;
 
@@ -127,7 +170,7 @@ export default class Year extends React.Component {
     return false;
   };
 
-  isSelectingRangeStart = (y) => {
+  isSelectingRangeStart = (y: number) => {
     if (!this.isInSelectingRange(y)) {
       return false;
     }
@@ -136,12 +179,12 @@ export default class Year extends React.Component {
     const _year = utils.setYear(newDate(), y);
 
     if (selectsStart) {
-      return utils.isSameYear(_year, this.selectingDate());
+      return utils.isSameYear(_year, this.selectingDate() ?? null);
     }
-    return utils.isSameYear(_year, startDate);
+    return utils.isSameYear(_year, startDate ?? null);
   };
 
-  isSelectingRangeEnd = (y) => {
+  isSelectingRangeEnd = (y: number) => {
     if (!this.isInSelectingRange(y)) {
       return false;
     }
@@ -150,12 +193,19 @@ export default class Year extends React.Component {
     const _year = utils.setYear(newDate(), y);
 
     if (selectsEnd || selectsRange) {
-      return utils.isSameYear(_year, this.selectingDate());
+      return utils.isSameYear(_year, this.selectingDate() ?? null);
     }
-    return utils.isSameYear(_year, endDate);
+    return utils.isSameYear(_year, endDate ?? null);
   };
 
-  isKeyboardSelected = (y) => {
+  isKeyboardSelected = (y: number) => {
+    if (
+      this.props.date === undefined ||
+      this.props.selected === undefined ||
+      this.props.preSelection === undefined
+    ) {
+      return;
+    }
     const date = utils.getStartOfYear(utils.setYear(this.props.date, y));
     return (
       !this.props.disabledKeyboardNavigation &&
@@ -165,12 +215,20 @@ export default class Year extends React.Component {
     );
   };
 
-  onYearClick = (e, y) => {
+  onYearClick = (
+    e:
+      | React.MouseEvent<HTMLDivElement, MouseEvent>
+      | React.KeyboardEvent<HTMLDivElement>,
+    y: number,
+  ) => {
     const { date } = this.props;
+    if (date === undefined) {
+      return;
+    }
     this.handleYearClick(utils.getStartOfYear(utils.setYear(date, y)), e);
   };
 
-  onYearKeyDown = (e, y) => {
+  onYearKeyDown = (e: React.KeyboardEvent<HTMLDivElement>, y: number) => {
     const { key } = e;
     const { date, yearItemNumber, handleOnKeyDown } = this.props;
 
@@ -182,22 +240,38 @@ export default class Year extends React.Component {
     if (!this.props.disabledKeyboardNavigation) {
       switch (key) {
         case "Enter":
+          if (this.props.selected === undefined) {
+            break;
+          }
           this.onYearClick(e, y);
-          this.props.setPreSelection(this.props.selected);
+          this.props.setPreSelection?.(this.props.selected);
           break;
         case "ArrowRight":
+          if (this.props.preSelection === undefined) {
+            break;
+          }
           this.handleYearNavigation(
             y + 1,
             utils.addYears(this.props.preSelection, 1),
           );
           break;
         case "ArrowLeft":
+          if (this.props.preSelection === undefined) {
+            break;
+          }
           this.handleYearNavigation(
             y - 1,
             utils.subYears(this.props.preSelection, 1),
           );
           break;
         case "ArrowUp": {
+          if (
+            date === undefined ||
+            yearItemNumber === undefined ||
+            this.props.preSelection === undefined
+          ) {
+            break;
+          }
           const { startPeriod } = utils.getYearsPeriod(date, yearItemNumber);
           let offset = VERTICAL_NAVIGATION_OFFSET;
           let newYear = y - offset;
@@ -221,6 +295,13 @@ export default class Year extends React.Component {
           break;
         }
         case "ArrowDown": {
+          if (
+            date === undefined ||
+            yearItemNumber === undefined ||
+            this.props.preSelection === undefined
+          ) {
+            break;
+          }
           const { endPeriod } = utils.getYearsPeriod(date, yearItemNumber);
           let offset = VERTICAL_NAVIGATION_OFFSET;
           let newYear = y + offset;
@@ -249,7 +330,7 @@ export default class Year extends React.Component {
     handleOnKeyDown && handleOnKeyDown(e);
   };
 
-  getYearClassNames = (y) => {
+  getYearClassNames = (y: number) => {
     const {
       date,
       minDate,
@@ -264,12 +345,27 @@ export default class Year extends React.Component {
     return clsx(
       "react-datepicker__year-text",
       `react-datepicker__year-${y}`,
-      yearClassName ? yearClassName(utils.setYear(date, y)) : undefined,
+      date ? yearClassName?.(utils.setYear(date, y)) : undefined,
       {
-        "react-datepicker__year-text--selected": y === getYear(selected),
+        "react-datepicker__year-text--selected": selected
+          ? y === getYear(selected)
+          : undefined,
         "react-datepicker__year-text--disabled":
           (minDate || maxDate || excludeDates || includeDates || filterDate) &&
-          utils.isYearDisabled(y, this.props),
+          utils.isYearDisabled(y, {
+            minDate: this.props.minDate,
+            maxDate: this.props.maxDate,
+            excludeDates: this.props.excludeDates?.reduce((acc, item) => {
+              if (item instanceof Date) {
+                acc.push(item);
+              } else {
+                acc.push(item.date);
+              }
+              return acc;
+            }, [] as Date[]),
+            includeDates: this.props.includeDates,
+            filterDate: this.props.filterDate,
+          }),
         "react-datepicker__year-text--keyboard-selected":
           this.isKeyboardSelected(y),
         "react-datepicker__year-text--range-start": this.isRangeStart(y),
@@ -286,8 +382,13 @@ export default class Year extends React.Component {
     );
   };
 
-  getYearTabIndex = (y) => {
-    if (this.props.disabledKeyboardNavigation) return "-1";
+  getYearTabIndex = (y: number) => {
+    if (
+      this.props.disabledKeyboardNavigation ||
+      this.props.preSelection === undefined
+    ) {
+      return "-1";
+    }
     const preSelected = utils.getYear(this.props.preSelection);
 
     return y === preSelected ? "0" : "-1";
@@ -302,7 +403,7 @@ export default class Year extends React.Component {
     });
   };
 
-  getYearContent = (y) => {
+  getYearContent = (y: number) => {
     return this.props.renderYearContent ? this.props.renderYearContent(y) : y;
   };
 
@@ -310,6 +411,9 @@ export default class Year extends React.Component {
     const yearsList = [];
     const { date, yearItemNumber, onYearMouseEnter, onYearMouseLeave } =
       this.props;
+    if (date === undefined) {
+      return null;
+    }
     const { startPeriod, endPeriod } = utils.getYearsPeriod(
       date,
       yearItemNumber,
@@ -330,7 +434,7 @@ export default class Year extends React.Component {
 
             this.onYearKeyDown(ev, y);
           }}
-          tabIndex={this.getYearTabIndex(y)}
+          tabIndex={Number(this.getYearTabIndex(y))}
           className={this.getYearClassNames(y)}
           onMouseEnter={
             !this.props.usePointerEvent


Test patch:
diff --git a/test/year_picker_test.test.js b/test/year_picker_test.test.js
index 2734b1e0..8d2bb6c3 100644
--- a/test/year_picker_test.test.js
+++ b/test/year_picker_test.test.js
@@ -1,6 +1,6 @@
 import React from "react";
 import DatePicker from "../src/index.jsx";
-import Year from "../src/year.jsx";
+import Year from "../src/year.tsx";
 import { render, fireEvent } from "@testing-library/react";
 import * as utils from "../src/date_utils.ts";
 import { getKey } from "./test_utils.js";