diff --git a/src/components/Input.tsx b/src/components/Input.tsx
index 38fc856e..2473f098 100644
--- a/src/components/Input.tsx
+++ b/src/components/Input.tsx
@@ -3,7 +3,13 @@ import React, { useCallback, useContext, useEffect, useRef } from "react";
import { BORDER_COLOR, DATE_FORMAT, RING_COLOR } from "../constants";
import DatepickerContext from "../contexts/DatepickerContext";
-import { dateIsValid, parseFormattedDate } from "../helpers";
+import {
+ checkClassName,
+ clearInvalidInput,
+ dateIsValid,
+ parseFormattedDate,
+ shortString
+} from "../helpers";
import ToggleButton from "./ToggleButton";
@@ -51,22 +57,77 @@ const Input: React.FC
= (e: Props) => {
return classNames.input(input);
}
- const border = BORDER_COLOR.focus[primaryColor as keyof typeof BORDER_COLOR.focus];
- const ring =
- RING_COLOR["second-focus"][primaryColor as keyof (typeof RING_COLOR)["second-focus"]];
+ const border = BORDER_COLOR.focus[primaryColor];
+ const ring = RING_COLOR["second-focus"][primaryColor];
const defaultInputClassName = `relative transition-all duration-300 py-2.5 pl-4 pr-14 w-full border-gray-300 dark:bg-slate-800 dark:text-white/80 dark:border-slate-600 rounded-lg tracking-wide font-light text-sm placeholder-gray-400 bg-white focus:ring disabled:opacity-40 disabled:cursor-not-allowed ${border} ${ring}`;
- return typeof inputClassName === "function"
- ? inputClassName(defaultInputClassName)
- : typeof inputClassName === "string" && inputClassName !== ""
- ? inputClassName
- : defaultInputClassName;
- }, [inputRef, classNames, primaryColor, inputClassName]);
+ return checkClassName(defaultInputClassName, inputClassName);
+ }, [classNames, inputClassName, primaryColor]);
+
+ /**
+ * automatically adds correct separator character to date input
+ */
+ const addSeparatorToDate = useCallback(
+ (inputValue: string, displayFormat: string) => {
+ // fallback separator; replaced by user defined separator;
+ const separators = ["-", "-"];
+ const separatorIndices: number[] = [];
+ let formattedInput = inputValue;
+
+ // note that we are not using locale to avoid redundancy;
+ // instead preferred locale is determined by displayFormat
+ const localeSeparators = displayFormat.match(/\W/g);
+ if (localeSeparators?.length) {
+ // replace fallbacks with localized separators
+ separators.splice(0, separators.length, ...localeSeparators);
+ }
+
+ // find indices of separators
+ // required to distinguish between i.a. YDM and DMY
+ let start = 0;
+ separators.forEach(localeSeparator => {
+ const idx = displayFormat.indexOf(localeSeparator, start);
+ if (idx !== -1) {
+ start = idx + 1;
+ separatorIndices.push(idx);
+ }
+ });
+
+ // adding separator after day and month
+ separatorIndices.forEach((separatorIndex, idx) => {
+ if (inputValue.length === separatorIndex) {
+ formattedInput = inputValue + separators[idx];
+ }
+ });
+
+ // add middle separator for range dates and format end date
+ if (!asSingle && inputValue.length >= displayFormat.length) {
+ // get startDate and add separator
+ let rangeDate = inputValue.substring(0, displayFormat.length);
+ rangeDate = rangeDate + " " + separator + " ";
+
+ // cut off everything startdate and separator including blank spaces
+ let endDate = inputValue.substring(displayFormat.length + 2 + separator.length);
+ if (endDate.length) {
+ separatorIndices.forEach((separatorIndex, idx) => {
+ if (endDate.length === separatorIndex) {
+ endDate = endDate + separators[idx];
+ }
+ });
+ rangeDate = rangeDate + endDate;
+ }
+ return rangeDate;
+ }
+
+ return formattedInput;
+ },
+ [asSingle, separator]
+ );
const handleInputChange = useCallback(
(e: React.ChangeEvent) => {
- const inputValue = e.target.value;
+ const inputValue = clearInvalidInput(e.target.value);
const dates = [];
@@ -112,13 +173,40 @@ const Input: React.FC = (e: Props) => {
else changeDayHover(dates[0]);
}
- changeInputText(e.target.value);
+ changeInputText(addSeparatorToDate(inputValue, displayFormat));
},
- [asSingle, displayFormat, separator, changeDatepickerValue, changeDayHover, changeInputText]
+ [
+ addSeparatorToDate,
+ asSingle,
+ changeDatepickerValue,
+ changeDayHover,
+ changeInputText,
+ displayFormat,
+ separator
+ ]
);
const handleInputKeyDown = useCallback(
(e: React.KeyboardEvent) => {
+ if (e.key === "Backspace") {
+ // stop propagation
+ e.preventDefault();
+
+ // force deletion of separators
+ const input = inputRef.current;
+ // necessary because the addSeparator function will overwrite regular deletion
+ if (input?.value.length) {
+ let lastChar = input.value[input.value.length - 1];
+ // cut off all non-numeric values
+ while (RegExp(/\D/).exec(lastChar)) {
+ const shortenedString = shortString(input.value, input.value.length - 1);
+ input.value = shortenedString;
+ lastChar = shortenedString[shortenedString.length - 1];
+ }
+ // cut off last numeric value
+ input.value = shortString(input.value, input.value.length - 1);
+ }
+ }
if (e.key === "Enter") {
const input = inputRef.current;
if (input) {
@@ -155,12 +243,8 @@ const Input: React.FC = (e: Props) => {
const defaultToggleClassName =
"absolute right-0 h-full px-3 text-gray-400 focus:outline-none disabled:opacity-40 disabled:cursor-not-allowed";
- return typeof toggleClassName === "function"
- ? toggleClassName(defaultToggleClassName)
- : typeof toggleClassName === "string" && toggleClassName !== ""
- ? toggleClassName
- : defaultToggleClassName;
- }, [toggleClassName, buttonRef, classNames]);
+ return checkClassName(defaultToggleClassName, toggleClassName);
+ }, [toggleClassName, classNames]);
// UseEffects && UseLayoutEffect
useEffect(() => {
@@ -222,7 +306,7 @@ const Input: React.FC = (e: Props) => {
const arrow = arrowContainer?.current;
function showCalendarContainer() {
- if (arrow && div && div.classList.contains("hidden")) {
+ if (arrow && div?.classList.contains("hidden")) {
div.classList.remove("hidden");
div.classList.add("block");
@@ -274,15 +358,13 @@ const Input: React.FC = (e: Props) => {
disabled={disabled}
readOnly={readOnly}
placeholder={
- placeholder
- ? placeholder
- : `${displayFormat}${asSingle ? "" : ` ${separator} ${displayFormat}`}`
+ placeholder ||
+ `${displayFormat}${asSingle ? "" : ` ${separator} ${displayFormat}`}`
}
value={inputText}
id={inputId}
name={inputName}
autoComplete="off"
- role="presentation"
onChange={handleInputChange}
onKeyDown={handleInputKeyDown}
/>
@@ -293,7 +375,7 @@ const Input: React.FC = (e: Props) => {
disabled={disabled}
className={getToggleClassName()}
>
- {renderToggleIcon(inputText == null || !inputText?.length)}
+ {renderToggleIcon(!inputText.length)}
>
);
diff --git a/src/components/utils.tsx b/src/components/utils.tsx
index 6bdb31a7..e61f13ed 100644
--- a/src/components/utils.tsx
+++ b/src/components/utils.tsx
@@ -119,11 +119,12 @@ export const DoubleChevronRightIcon: React.FC = ({ className = "w-6 h
};
// eslint-disable-next-line react/display-name,@typescript-eslint/ban-types
-export const Arrow = React.forwardRef((props, ref) => {
+export const Arrow = React.forwardRef((_props, ref) => {
return (
);
});
diff --git a/src/contexts/DatepickerContext.ts b/src/contexts/DatepickerContext.ts
index d42aadd8..cacdc9d3 100644
--- a/src/contexts/DatepickerContext.ts
+++ b/src/contexts/DatepickerContext.ts
@@ -3,14 +3,15 @@ import React, { createContext } from "react";
import { DATE_FORMAT, LANGUAGE, START_WEEK } from "../constants";
import {
+ ClassNamesTypeProp,
+ ClassType,
+ ColorKeys,
Configs,
- Period,
- DateValueType,
- DateType,
DateRangeType,
- ClassNamesTypeProp,
- PopoverDirectionType,
- ColorKeys
+ DateType,
+ DateValueType,
+ Period,
+ PopoverDirectionType
} from "../types";
interface DatepickerStore {
@@ -35,9 +36,9 @@ interface DatepickerStore {
i18n: string;
value: DateValueType;
disabled?: boolean;
- inputClassName?: ((className: string) => string) | string | null;
- containerClassName?: ((className: string) => string) | string | null;
- toggleClassName?: ((className: string) => string) | string | null;
+ inputClassName?: ClassType;
+ containerClassName?: ClassType;
+ toggleClassName?: ClassType;
toggleIcon?: (open: boolean) => React.ReactNode;
readOnly?: boolean;
startWeekOn?: string | null;
@@ -60,19 +61,19 @@ const DatepickerContext = createContext({
arrowContainer: null,
period: { start: null, end: null },
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
- changePeriod: period => {},
+ changePeriod: _period => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
hideDatepicker: () => {},
dayHover: null,
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
- changeDayHover: (day: string | null) => {},
+ changeDayHover: (_day: string | null) => {},
inputText: "",
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
- changeInputText: text => {},
+ changeInputText: _text => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
- updateFirstDate: date => {},
+ updateFirstDate: _date => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
- changeDatepickerValue: (value: DateValueType, e: HTMLInputElement | null | undefined) => {},
+ changeDatepickerValue: (_value: DateValueType, _e: HTMLInputElement | null | undefined) => {},
showFooter: false,
value: null,
i18n: LANGUAGE,
diff --git a/src/helpers/index.ts b/src/helpers/index.ts
index d59d919a..83abb85e 100644
--- a/src/helpers/index.ts
+++ b/src/helpers/index.ts
@@ -11,6 +11,33 @@ export function classNames(...classes: (false | null | undefined | string)[]) {
return classes.filter(Boolean).join(" ");
}
+/**
+ * detect and delete non-numeric user input
+ * @returns shortened string
+ */
+export const clearInvalidInput = (value: string): string => {
+ if (value[value.length - 1]?.match(/\D/g)) {
+ return shortString(value, value.length - 1);
+ }
+ return value;
+};
+
+/**
+ * checks and returns user-defined className or default className
+ */
+export const checkClassName = (
+ defaultToggleClassName: string,
+ toggleClassName?: string | ((className: string) => string) | null
+) => {
+ if (typeof toggleClassName === "function") {
+ return toggleClassName(defaultToggleClassName);
+ }
+ if (typeof toggleClassName === "string" && toggleClassName !== "") {
+ return toggleClassName;
+ }
+ return defaultToggleClassName;
+};
+
export function getTextColorByPrimaryColor(color: string) {
switch (color) {
case "blue":
diff --git a/src/index.tsx b/src/index.tsx
index 1a4ded83..d0b0828b 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,4 +1,3 @@
import Datepicker from "./components/Datepicker";
-
export * from "./types";
export default Datepicker;
diff --git a/src/types/index.ts b/src/types/index.ts
index b9e561cd..940f510c 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -44,6 +44,8 @@ export type DateRangeType = {
export type DateValueType = DateRangeType | null;
+export type ClassType = ((className: string) => string) | string | null;
+
export type ClassNamesTypeProp = {
container?: (p?: object | null | undefined) => string | undefined;
input?: (p?: object | null | undefined) => string | undefined;
@@ -67,10 +69,10 @@ export interface DatepickerType {
startFrom?: Date | null;
i18n?: string;
disabled?: boolean;
- classNames?: ClassNamesTypeProp | undefined;
- containerClassName?: ((className: string) => string) | string | null;
- inputClassName?: ((className: string) => string) | string | null;
- toggleClassName?: ((className: string) => string) | string | null;
+ classNames?: ClassNamesTypeProp;
+ containerClassName?: ClassType;
+ inputClassName?: ClassType;
+ toggleClassName?: ClassType;
toggleIcon?: (open: boolean) => React.ReactNode;
inputId?: string;
inputName?: string;
diff --git a/tsconfig.json b/tsconfig.json
index cf845ae2..fc9fbef9 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,7 +3,9 @@
"target": "esnext",
"lib": ["dom", "esnext"],
"module": "esnext",
- "jsx": "preserve",
+ "jsx": "react-jsx",
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"strict": true,
@@ -12,16 +14,24 @@
"esModuleInterop": true,
"baseUrl": "src/",
"declaration": true,
+ "declarationDir": "./dist",
"outDir": "./dist",
"inlineSources": true,
"sourceMap": true,
"rootDir": "src",
"allowJs": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
"skipLibCheck": true,
"noEmit": true,
"resolveJsonModule": true,
- "isolatedModules": true
+ "isolatedModules": true,
+ // "noUncheckedIndexedAccess": true // should be enabled
+ "incremental": true
},
"include": ["src/**/*"],
- "exclude": ["node_modules"]
+ "exclude": ["node_modules", "dist", "src/**/*.spec.ts", "src/**/*.test.ts", "src/**/__tests__"]
}