diff --git a/example/index.tsx b/example/index.tsx index 6ae63ff..eeae802 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -48,9 +48,10 @@ import { subDays, addDays, startOfDay, format } from 'date-fns'; type FirstDayOfWeek = DatepickerConfigs['firstDayOfWeek']; const offsets: FirstDayOfWeek[] = [0, 1, 2, 3, 4, 5, 6]; +const demoDate = new Date(); + const App = () => { const { colorMode, toggleColorMode } = useColorMode(); - const demoDate = new Date(); const [date, setDate] = useState(demoDate); const [selectedDates, setSelectedDates] = useState([ new Date(), diff --git a/package-lock.json b/package-lock.json index b6304ab..85f8394 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.3.0", "license": "MIT", "dependencies": { + "date-fns": "^2.23.0", + "dayzed": "^3.2.2", "react-focus-lock": "^2.9.5" }, "devDependencies": { @@ -20,8 +22,6 @@ "@tsconfig/recommended": "^1.0.1", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", - "date-fns": "^2.23.0", - "dayzed": "^3.2.2", "dts-cli": "^1.6.3", "framer-motion": "^4.1.17", "husky": "^7.0.4", @@ -6531,7 +6531,6 @@ "version": "2.25.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.25.0.tgz", "integrity": "sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w==", - "dev": true, "engines": { "node": ">=0.11" }, @@ -6544,7 +6543,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/dayzed/-/dayzed-3.2.2.tgz", "integrity": "sha512-BzNoj+6+er5DPQ0F82Yh1U8MU/jyOo7R+jIlK9FsSjfk4ZIISalygzMcsXTGTFedm5UfTddQnELMVBQUXZDv9Q==", - "dev": true, "dependencies": { "@babel/runtime": "^7.6.2", "date-fns": "^2.0.0" @@ -7576,9 +7574,9 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, "engines": { "node": ">=10" @@ -20213,14 +20211,12 @@ "date-fns": { "version": "2.25.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.25.0.tgz", - "integrity": "sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w==", - "dev": true + "integrity": "sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w==" }, "dayzed": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/dayzed/-/dayzed-3.2.2.tgz", "integrity": "sha512-BzNoj+6+er5DPQ0F82Yh1U8MU/jyOo7R+jIlK9FsSjfk4ZIISalygzMcsXTGTFedm5UfTddQnELMVBQUXZDv9Q==", - "dev": true, "requires": { "@babel/runtime": "^7.6.2", "date-fns": "^2.0.0" @@ -21144,9 +21140,9 @@ } }, "eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, "requires": {} }, diff --git a/package.json b/package.json index 8437469..6f7571c 100644 --- a/package.json +++ b/package.json @@ -75,8 +75,8 @@ "typescript": "^4.4.4" }, "dependencies": { - "react-focus-lock": "^2.9.5", "date-fns": "^2.23.0", - "dayzed": "^3.2.2" + "dayzed": "^3.2.2", + "react-focus-lock": "^2.9.5" } } diff --git a/src/single.tsx b/src/single.tsx index 8c0e927..95fc1cb 100644 --- a/src/single.tsx +++ b/src/single.tsx @@ -1,4 +1,10 @@ -import React, { useEffect, useState } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { Button, ButtonProps, @@ -45,10 +51,7 @@ interface SingleProps extends DatepickerProps { export type VariantProps = | { - propsConfigs?: PropsConfigs; - } - | { - triggerVariant: 'default'; + triggerVariant?: 'default'; propsConfigs?: PropsConfigs; } | { @@ -70,49 +73,41 @@ const DefaultConfigs: Required = { monthsToDisplay: 1, }; -const defaultProps = { - defaultIsOpen: false, - closeOnSelect: true, - triggerVariant: 'default' as const, -}; - -export const SingleDatepicker: React.FC = (props) => { - const mergedProps = { ...defaultProps, ...props }; - const { - date: selectedDate, - name, - disabled, - onDateChange, - id, - minDate, - maxDate, - configs, - usePortal, - portalRef, - disabledDates, - defaultIsOpen, - triggerVariant, - propsConfigs, - closeOnSelect, - children, - } = mergedProps; - +export const SingleDatepicker: React.FC = ({ + date: selectedDate, + name, + disabled, + onDateChange, + id, + minDate, + maxDate, + configs, + usePortal, + portalRef, + disabledDates, + defaultIsOpen = false, + closeOnSelect = true, + children, + ...restProps +}) => { const [dateInView, setDateInView] = useState(selectedDate); const [offset, setOffset] = useState(0); + const internalUpdate = useRef(false); const { onOpen, onClose, isOpen } = useDisclosure({ defaultIsOpen }); const Icon = - mergedProps.triggerVariant === 'input' && mergedProps.triggerIcon ? ( - mergedProps.triggerIcon - ) : ( - - ); + restProps.triggerVariant === 'input' + ? restProps?.triggerIcon ?? + : null; - const datepickerConfigs = { - ...DefaultConfigs, - ...configs, - }; + const datepickerConfigs = useMemo( + () => ({ + ...DefaultConfigs, + ...configs, + }), + [configs] + ); const [tempInput, setInputVal] = useState( selectedDate ? format(selectedDate, datepickerConfigs.dateFormat) : '' @@ -125,37 +120,54 @@ export const SingleDatepicker: React.FC = (props) => { }; // dayzed utils - const handleOnDateSelected: OnDateSelected = ({ selectable, date }) => { - if (!selectable) return; - if (date instanceof Date && !isNaN(date.getTime())) { - onDateChange(date); - if (closeOnSelect) onClose(); - return; - } - }; + const handleOnDateSelected: OnDateSelected = useCallback( + ({ selectable, date }) => { + if (!selectable) return; + if (date instanceof Date && !isNaN(date.getTime())) { + internalUpdate.current = true; + onDateChange(date); + setInputVal(date ? format(date, datepickerConfigs.dateFormat) : ''); + if (closeOnSelect) onClose(); + return; + } + }, + [closeOnSelect, datepickerConfigs.dateFormat, onClose, onDateChange] + ); - const handleInputChange = (event: React.ChangeEvent) => { - setInputVal(event.target.value); - const newDate = parse( - event.target.value, - datepickerConfigs.dateFormat, - new Date() - ); - if (!(newDate instanceof Date && !isNaN(newDate.getTime()))) { - return; - } - const isDisabled = disabledDates?.has(startOfDay(newDate).getTime()); - if (isDisabled) return; - onDateChange(newDate); - }; + const handleInputChange = useCallback( + (event: React.ChangeEvent) => { + internalUpdate.current = true; + setInputVal(event.target.value); + const newDate = parse( + event.target.value, + datepickerConfigs.dateFormat, + new Date() + ); + if (!(newDate instanceof Date && !isNaN(newDate.getTime()))) { + return; + } + const isDisabled = disabledDates?.has(startOfDay(newDate).getTime()); + if (isDisabled) return; + onDateChange(newDate); + setDateInView(newDate); + }, + [datepickerConfigs.dateFormat, disabledDates, onDateChange] + ); const PopoverContentWrapper = usePortal ? Portal : React.Fragment; useEffect(() => { - if (selectedDate) { - setInputVal(format(selectedDate, datepickerConfigs.dateFormat)); + if (internalUpdate.current) { + internalUpdate.current = false; + return; } - }, [selectedDate, datepickerConfigs.dateFormat]); + setInputVal( + typeof selectedDate !== 'undefined' + ? format(selectedDate, datepickerConfigs.dateFormat) + : '' + ); + setDateInView(selectedDate); + }, [datepickerConfigs.dateFormat, selectedDate]); return ( = (props) => { onClose={onPopoverClose} isLazy > - {!children && triggerVariant === 'default' ? ( + {!children && (restProps.triggerVariant ?? 'default') === 'default' ? ( ) : null} - {!children && triggerVariant === 'input' ? ( + {!children && restProps.triggerVariant === 'input' ? ( = (props) => { value={tempInput} onChange={handleInputChange} paddingRight={'2.5rem'} - {...propsConfigs?.inputProps} + {...restProps.propsConfigs?.inputProps} /> @@ -217,7 +229,7 @@ export const SingleDatepicker: React.FC = (props) => { type="button" disabled={disabled} padding={'8px'} - {...propsConfigs?.triggerIconBtnProps} + {...restProps.propsConfigs?.triggerIconBtnProps} > {Icon} @@ -230,9 +242,11 @@ export const SingleDatepicker: React.FC = (props) => { > - + = (props) => { firstDayOfWeek: datepickerConfigs.firstDayOfWeek, }} configs={datepickerConfigs} - propsConfigs={propsConfigs} + propsConfigs={restProps.propsConfigs} disabledDates={disabledDates} />