From b55d862db4d2f33cb597a1b5b04bf15fa61c49f2 Mon Sep 17 00:00:00 2001 From: sri Date: Tue, 3 Sep 2024 14:39:12 +0200 Subject: [PATCH 01/14] feat(Input): add function to selected and add separator --- src/components/Input.tsx | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 38fc856e..55532e52 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -64,6 +64,27 @@ const Input: React.FC = (e: Props) => { : defaultInputClassName; }, [inputRef, classNames, primaryColor, inputClassName]); + /** + * automatically adds correct separator character to date input + */ + const addSeparatorToDate = useCallback((inputValue: string, format: string) => { + // fallback separator; repleace by locale separator + let separator = "/"; + const localeSeparator = format.match(/\W/g); + if (localeSeparator?.length) { + separator = localeSeparator[0]; + } + + let formattedInput = inputValue; + // adding separator after day and month + // currently only supports DMY/MDY and not YMD/YDM + if (inputValue.length === 2 || inputValue.length === 5) { + formattedInput = inputValue + separator[0]; + } + + return formattedInput; + }, []); + const handleInputChange = useCallback( (e: React.ChangeEvent) => { const inputValue = e.target.value; @@ -112,7 +133,7 @@ const Input: React.FC = (e: Props) => { else changeDayHover(dates[0]); } - changeInputText(e.target.value); + changeInputText(addSeparatorToDate(inputValue, displayFormat)); }, [asSingle, displayFormat, separator, changeDatepickerValue, changeDayHover, changeInputText] ); From 8a6389333162a2862163f1353fde2d923fd63d4b Mon Sep 17 00:00:00 2001 From: sri Date: Tue, 3 Sep 2024 14:40:09 +0200 Subject: [PATCH 02/14] feat(Input): clear invalid input --- src/components/Input.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 55532e52..de04499b 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -85,9 +85,23 @@ const Input: React.FC = (e: Props) => { return formattedInput; }, []); + /** + * detect and delete non-numeric user input + */ + const clearInvalidInput = useCallback( + (value: string) => { + if (value && value[value.length - 1].match(/\D/g)) { + return value.slice(0, value.length - 1); + } + return value; + }, + + [] + ); + const handleInputChange = useCallback( (e: React.ChangeEvent) => { - const inputValue = e.target.value; + const inputValue = clearInvalidInput(e.target.value); const dates = []; From a73cab0e291321cfcfd3665e7c7018c1917a2b9e Mon Sep 17 00:00:00 2001 From: sri Date: Tue, 3 Sep 2024 16:50:35 +0200 Subject: [PATCH 03/14] feat(Input): detect different date formats identifies separator(s) and adds it at right indices; works with various date formats e.g. YMD and DMY --- src/components/Input.tsx | 54 +++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/src/components/Input.tsx b/src/components/Input.tsx index de04499b..4db1570b 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -1,4 +1,6 @@ import dayjs from "dayjs"; +import localizedFormat from "dayjs/plugin/localizedFormat"; +require("dayjs/locale/de"); import React, { useCallback, useContext, useEffect, useRef } from "react"; import { BORDER_COLOR, DATE_FORMAT, RING_COLOR } from "../constants"; @@ -7,6 +9,8 @@ import { dateIsValid, parseFormattedDate } from "../helpers"; import ToggleButton from "./ToggleButton"; +dayjs.extend(localizedFormat); + type Props = { setContextRef?: (ref: React.RefObject) => void; }; @@ -67,20 +71,37 @@ const Input: React.FC = (e: Props) => { /** * automatically adds correct separator character to date input */ - const addSeparatorToDate = useCallback((inputValue: string, format: string) => { - // fallback separator; repleace by locale separator - let separator = "/"; - const localeSeparator = format.match(/\W/g); - if (localeSeparator?.length) { - separator = localeSeparator[0]; + 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); } - let formattedInput = inputValue; + // 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 - // currently only supports DMY/MDY and not YMD/YDM - if (inputValue.length === 2 || inputValue.length === 5) { - formattedInput = inputValue + separator[0]; - } + separatorIndices.forEach((separatorIndex, idx) => { + if (inputValue.length === separatorIndex) { + formattedInput = inputValue + separators[idx]; + } + }); return formattedInput; }, []); @@ -149,7 +170,16 @@ const Input: React.FC = (e: Props) => { changeInputText(addSeparatorToDate(inputValue, displayFormat)); }, - [asSingle, displayFormat, separator, changeDatepickerValue, changeDayHover, changeInputText] + [ + addSeparatorToDate, + asSingle, + changeDatepickerValue, + changeDayHover, + changeInputText, + clearInvalidInput, + displayFormat, + separator + ] ); const handleInputKeyDown = useCallback( From 865cf54840d2c867f448968d85f0b908f8f5e785 Mon Sep 17 00:00:00 2001 From: sri Date: Wed, 4 Sep 2024 14:01:48 +0200 Subject: [PATCH 04/14] feat(Input): enable auto formatting for range dates --- src/components/Input.tsx | 86 ++++++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 4db1570b..59a033e2 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -1,6 +1,4 @@ import dayjs from "dayjs"; -import localizedFormat from "dayjs/plugin/localizedFormat"; -require("dayjs/locale/de"); import React, { useCallback, useContext, useEffect, useRef } from "react"; import { BORDER_COLOR, DATE_FORMAT, RING_COLOR } from "../constants"; @@ -9,8 +7,6 @@ import { dateIsValid, parseFormattedDate } from "../helpers"; import ToggleButton from "./ToggleButton"; -dayjs.extend(localizedFormat); - type Props = { setContextRef?: (ref: React.RefObject) => void; }; @@ -71,40 +67,62 @@ const Input: React.FC = (e: Props) => { /** * 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); + 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); } - }); - // adding separator after day and month - separatorIndices.forEach((separatorIndex, idx) => { - if (inputValue.length === separatorIndex) { - formattedInput = inputValue + separators[idx]; + // 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; - }, []); + return formattedInput; + }, + [asSingle, separator] + ); /** * detect and delete non-numeric user input From dd80b8c8a8dbc5968dafb66267c0c23571a0d337 Mon Sep 17 00:00:00 2001 From: sri Date: Wed, 4 Sep 2024 14:42:38 +0200 Subject: [PATCH 05/14] fix(Input): fix deletion of input add separator function interfered with deletion and re-inserted deleted separators; currently only deletes single char, even if multiple are selected --- src/components/Input.tsx | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 59a033e2..c1dc1715 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -3,7 +3,7 @@ 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 { dateIsValid, parseFormattedDate, shortString } from "../helpers"; import ToggleButton from "./ToggleButton"; @@ -202,6 +202,25 @@ const Input: React.FC = (e: Props) => { 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) { + let lastChar = input.value[input.value.length - 1]; + // cut off all non-numeric values + while (lastChar?.match(/\D/)) { + 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) { From 3960f46890f514fd076778c59b151474953dd7d8 Mon Sep 17 00:00:00 2001 From: sri Date: Wed, 4 Sep 2024 14:53:09 +0200 Subject: [PATCH 06/14] refactor(Input): removing ternaries, adding named helper funcs --- src/components/Input.tsx | 49 ++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/src/components/Input.tsx b/src/components/Input.tsx index c1dc1715..7adc8a8d 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -43,6 +43,22 @@ const Input: React.FC = (e: Props) => { const buttonRef = useRef(null); const inputRef = useRef(null); + const checkClassName = useCallback( + ( + defaultToggleClassName: string, + toggleClassName?: string | ((className: string) => string) | null + ) => { + if (typeof toggleClassName === "function") { + return toggleClassName(defaultToggleClassName); + } + if (typeof toggleClassName === "string" && toggleClassName !== "") { + return toggleClassName; + } + return defaultToggleClassName; + }, + [] + ); + // Functions const getClassName = useCallback(() => { const input = inputRef.current; @@ -51,18 +67,13 @@ 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); + }, [checkClassName, classNames, inputClassName, primaryColor]); /** * automatically adds correct separator character to date input @@ -129,7 +140,7 @@ const Input: React.FC = (e: Props) => { */ const clearInvalidInput = useCallback( (value: string) => { - if (value && value[value.length - 1].match(/\D/g)) { + if (value[value?.length - 1].match(/\D/g)) { return value.slice(0, value.length - 1); } return value; @@ -257,12 +268,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, checkClassName, classNames]); // UseEffects && UseLayoutEffect useEffect(() => { @@ -324,7 +331,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"); @@ -376,15 +383,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} /> @@ -395,7 +400,7 @@ const Input: React.FC = (e: Props) => { disabled={disabled} className={getToggleClassName()} > - {renderToggleIcon(inputText == null || !inputText?.length)} + {renderToggleIcon(!inputText.length)} ); From b50736699939313183946a8a5aac91c58baf7084 Mon Sep 17 00:00:00 2001 From: sri Date: Wed, 4 Sep 2024 15:26:53 +0200 Subject: [PATCH 07/14] test(clearInvalidInput): install jest, add config, extract clearInvalidInput and move to helper folder, add test for helper function --- jest.config.js | 8 ++++ package.json | 4 ++ src/__tests__/clearInvalidInput.test.ts | 53 +++++++++++++++++++++++++ src/components/Input.tsx | 17 +------- 4 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 jest.config.js create mode 100644 src/__tests__/clearInvalidInput.test.ts diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..f56dfa44 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,8 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} **/ + +module.exports = { + testEnvironment: "node", + transform: { + "^.+.tsx?$": ["ts-jest", {}] + } +}; diff --git a/package.json b/package.json index d3d19f7d..da98815b 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "build": "npm run code-style && npm run clean && rollup -c", "pub": "npm run build && npm publish", "dev": "next dev -p 8888", + "test": "jest", "prepare": "husky install" }, "repository": { @@ -42,6 +43,7 @@ "react": "^17.0.2 || ^18.2.0" }, "devDependencies": { + "@jest/globals": "^29.7.0", "@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^11.0.0", @@ -60,6 +62,7 @@ "eslint-plugin-react": "^7.31.11", "eslint-plugin-react-hooks": "^4.6.0", "husky": "^8.0.0", + "jest": "^29.7.0", "lint-staged": "^13.2.3", "next": "^13.1.1", "postcss": "^8.4.19", @@ -68,6 +71,7 @@ "react-dom": "^18.2.0", "rollup": "^2.77.2", "tailwindcss": "^3.2.4", + "ts-jest": "^29.2.5", "tslib": "^2.4.0", "typescript": "^4.8.4" }, diff --git a/src/__tests__/clearInvalidInput.test.ts b/src/__tests__/clearInvalidInput.test.ts new file mode 100644 index 00000000..b862f5ca --- /dev/null +++ b/src/__tests__/clearInvalidInput.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, test } from "@jest/globals"; + +import { clearInvalidInput } from "../helpers"; + +describe("clearInvalidInput", () => { + test("should return the same string if the last character is numeric", () => { + const input = "1234"; + const result = clearInvalidInput(input); + expect(result).toBe("1234"); + }); + + test("should remove the last character if it is non-numeric", () => { + const input = "1234a"; + const result = clearInvalidInput(input); + expect(result).toBe("1234"); + }); + + test("should return an empty string if the input is a single non-numeric character", () => { + const input = "a"; + const result = clearInvalidInput(input); + expect(result).toBe(""); + }); + + test("should return an empty string if the input is empty", () => { + const input = ""; + const result = clearInvalidInput(input); + expect(result).toBe(""); + }); + + test("should handle multiple non-numeric characters at the end by removing only the last one", () => { + const input = "1234abc"; + const result = clearInvalidInput(input); + expect(result).toBe("1234ab"); + }); + + test("should return the same string if all characters are numeric", () => { + const input = "9876543210"; + const result = clearInvalidInput(input); + expect(result).toBe("9876543210"); + }); + + test("should correctly handle a string with special characters at the end", () => { + const input = "1234#"; + const result = clearInvalidInput(input); + expect(result).toBe("1234"); + }); + + test("should correctly handle a string with spaces at the end", () => { + const input = "1234 "; + const result = clearInvalidInput(input); + expect(result).toBe("1234"); + }); +}); diff --git a/src/components/Input.tsx b/src/components/Input.tsx index c1dc1715..07813515 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -3,7 +3,7 @@ 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, shortString } from "../helpers"; +import { clearInvalidInput, dateIsValid, parseFormattedDate, shortString } from "../helpers"; import ToggleButton from "./ToggleButton"; @@ -124,20 +124,6 @@ const Input: React.FC = (e: Props) => { [asSingle, separator] ); - /** - * detect and delete non-numeric user input - */ - const clearInvalidInput = useCallback( - (value: string) => { - if (value && value[value.length - 1].match(/\D/g)) { - return value.slice(0, value.length - 1); - } - return value; - }, - - [] - ); - const handleInputChange = useCallback( (e: React.ChangeEvent) => { const inputValue = clearInvalidInput(e.target.value); @@ -194,7 +180,6 @@ const Input: React.FC = (e: Props) => { changeDatepickerValue, changeDayHover, changeInputText, - clearInvalidInput, displayFormat, separator ] From 36888a7bdbe5af63d874a0ad65dc4927fc04d64b Mon Sep 17 00:00:00 2001 From: sri Date: Wed, 4 Sep 2024 15:27:25 +0200 Subject: [PATCH 08/14] refactor(helpers): move clearInvalidInput to helpers --- src/helpers/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/helpers/index.ts b/src/helpers/index.ts index d59d919a..a0479dda 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -11,6 +11,17 @@ 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[value.length - 1].match(/\D/g)) { + return shortString(value, value.length - 1); + } + return value; +}; + export function getTextColorByPrimaryColor(color: string) { switch (color) { case "blue": From 7d9e098bcf04ace015ca6721ed054142dd120b1a Mon Sep 17 00:00:00 2001 From: sri Date: Wed, 4 Sep 2024 15:30:59 +0200 Subject: [PATCH 09/14] refactor(index.js): remove console log --- pages/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pages/index.js b/pages/index.js index 492aa50c..90299a33 100644 --- a/pages/index.js +++ b/pages/index.js @@ -32,10 +32,8 @@ export default function Playground() { const [startFrom, setStartFrom] = useState("2023-03-01"); const [startWeekOn, setStartWeekOn] = useState(""); - const handleChange = (value, e) => { + const handleChange = value => { setValue(value); - console.log(e); - console.log("value", value); }; return (
From 8a7473725a6369833ade9e0d9f5680464f7c7a71 Mon Sep 17 00:00:00 2001 From: sri Date: Wed, 4 Sep 2024 16:17:05 +0200 Subject: [PATCH 10/14] refactor(Input): extract helper function; add test checkClassName function added to avoid duplicate code, adding test to verify accuracy --- src/__tests__/checkClassName.test.ts | 34 ++++++++++++++++++++++++++++ src/components/Input.tsx | 30 ++++++++---------------- src/helpers/index.ts | 18 ++++++++++++++- 3 files changed, 61 insertions(+), 21 deletions(-) create mode 100644 src/__tests__/checkClassName.test.ts diff --git a/src/__tests__/checkClassName.test.ts b/src/__tests__/checkClassName.test.ts new file mode 100644 index 00000000..e471feec --- /dev/null +++ b/src/__tests__/checkClassName.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test, jest } from "@jest/globals"; + +import { checkClassName } from "../helpers"; + +describe("checkClassName", () => { + const defaultClassName = "default-class"; + + test("should return result of toggleClassName function when it is a function", () => { + const toggleFunction = jest.fn(() => "custom-class"); + const result = checkClassName(defaultClassName, toggleFunction); + expect(result).toBe("custom-class"); + expect(toggleFunction).toHaveBeenCalledWith(defaultClassName); + }); + + test("should return toggleClassName when it is a non-empty string", () => { + const result = checkClassName(defaultClassName, "custom-class"); + expect(result).toBe("custom-class"); + }); + + test("should return defaultToggleClassName when toggleClassName is an empty string", () => { + const result = checkClassName(defaultClassName, ""); + expect(result).toBe(defaultClassName); + }); + + test("should return defaultToggleClassName when toggleClassName is null", () => { + const result = checkClassName(defaultClassName, null); + expect(result).toBe(defaultClassName); + }); + + test("should return defaultToggleClassName when toggleClassName is undefined", () => { + const result = checkClassName(defaultClassName, undefined); + expect(result).toBe(defaultClassName); + }); +}); diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 251d4b82..909c94b0 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 { clearInvalidInput, dateIsValid, parseFormattedDate, shortString } from "../helpers"; +import { + checkClassName, + clearInvalidInput, + dateIsValid, + parseFormattedDate, + shortString +} from "../helpers"; import ToggleButton from "./ToggleButton"; @@ -43,22 +49,6 @@ const Input: React.FC = (e: Props) => { const buttonRef = useRef(null); const inputRef = useRef(null); - const checkClassName = useCallback( - ( - defaultToggleClassName: string, - toggleClassName?: string | ((className: string) => string) | null - ) => { - if (typeof toggleClassName === "function") { - return toggleClassName(defaultToggleClassName); - } - if (typeof toggleClassName === "string" && toggleClassName !== "") { - return toggleClassName; - } - return defaultToggleClassName; - }, - [] - ); - // Functions const getClassName = useCallback(() => { const input = inputRef.current; @@ -73,7 +63,7 @@ const Input: React.FC = (e: Props) => { 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 checkClassName(defaultInputClassName, inputClassName); - }, [checkClassName, classNames, inputClassName, primaryColor]); + }, [classNames, inputClassName, primaryColor]); /** * automatically adds correct separator character to date input @@ -208,7 +198,7 @@ const Input: React.FC = (e: Props) => { if (input) { let lastChar = input.value[input.value.length - 1]; // cut off all non-numeric values - while (lastChar?.match(/\D/)) { + while (RegExp(/\D/).exec(lastChar)) { const shortenedString = shortString(input.value, input.value.length - 1); input.value = shortenedString; lastChar = shortenedString[shortenedString.length - 1]; @@ -254,7 +244,7 @@ const Input: React.FC = (e: Props) => { "absolute right-0 h-full px-3 text-gray-400 focus:outline-none disabled:opacity-40 disabled:cursor-not-allowed"; return checkClassName(defaultToggleClassName, toggleClassName); - }, [toggleClassName, checkClassName, classNames]); + }, [toggleClassName, classNames]); // UseEffects && UseLayoutEffect useEffect(() => { diff --git a/src/helpers/index.ts b/src/helpers/index.ts index a0479dda..83abb85e 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -16,12 +16,28 @@ export function classNames(...classes: (false | null | undefined | string)[]) { * @returns shortened string */ export const clearInvalidInput = (value: string): string => { - if (value && value[value.length - 1].match(/\D/g)) { + 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": From be2960b509f8e772002d2df26c46426412fda190 Mon Sep 17 00:00:00 2001 From: sri Date: Wed, 4 Sep 2024 16:24:16 +0200 Subject: [PATCH 11/14] refactor(types): add ClassName type avoiding duplicate code and improve readability --- src/contexts/DatepickerContext.ts | 19 ++++++++++--------- src/types/index.ts | 10 ++++++---- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/contexts/DatepickerContext.ts b/src/contexts/DatepickerContext.ts index d42aadd8..46174244 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; 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; From 1462fa61c78e7ba602387714c4794a31dc30b14e Mon Sep 17 00:00:00 2001 From: sri Date: Thu, 5 Sep 2024 13:25:54 +0200 Subject: [PATCH 12/14] fix(Input): check for value length before deletion prevents bug when pressing backspace in empty input --- src/components/Input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 909c94b0..2473f098 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -195,7 +195,7 @@ const Input: React.FC = (e: Props) => { // force deletion of separators const input = inputRef.current; // necessary because the addSeparator function will overwrite regular deletion - if (input) { + if (input?.value.length) { let lastChar = input.value[input.value.length - 1]; // cut off all non-numeric values while (RegExp(/\D/).exec(lastChar)) { From 1b7c64b8ef105f06b516edc3d5278f08fdfd79a3 Mon Sep 17 00:00:00 2001 From: sri Date: Thu, 5 Sep 2024 13:26:27 +0200 Subject: [PATCH 13/14] build(package): update deps --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index da98815b..1dfc3533 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,8 @@ "eslint-plugin-react-hooks": "^4.6.0", "husky": "^8.0.0", "jest": "^29.7.0", - "lint-staged": "^13.2.3", - "next": "^13.1.1", + "lint-staged": "^15.2.10", + "next": "^14.1.2", "postcss": "^8.4.19", "prettier": "^2.8.0", "react": "^18.2.0", From d820f3bf257c0b5ae52dcf0bd5a6993974073a1d Mon Sep 17 00:00:00 2001 From: sri Date: Thu, 5 Sep 2024 13:31:45 +0200 Subject: [PATCH 14/14] 1.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1dfc3533..83481c8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sciendis/react-tailwindcss-datepicker", - "version": "1.6.8", + "version": "1.7.0", "description": " Modern date range picker component for React using Tailwind and dayjs.", "main": "dist/index.cjs.js", "module": "dist/index.esm.js",