From 38b41a3f1359f61a508e762e53b68f3604c18bde Mon Sep 17 00:00:00 2001 From: Jefferson Silva Date: Tue, 17 Dec 2024 22:59:37 -0300 Subject: [PATCH] feat: create date range picker field --- .DS_Store | Bin 0 -> 8196 bytes packages/.DS_Store | Bin 0 -> 6148 bytes packages/react-material-ui/.DS_Store | Bin 0 -> 6148 bytes packages/react-material-ui/package.json | 1 + packages/react-material-ui/src/.DS_Store | Bin 0 -> 6148 bytes .../src/components/.DS_Store | Bin 0 -> 10244 bytes .../DateRangePicker/DateInput/Styles.ts | 17 + .../DateRangePicker/DateInput/index.tsx | 10 + .../src/components/DateRangePicker/index.tsx | 374 ++++++++++++++++++ packages/react-material-ui/src/index.ts | 5 + yarn.lock | 8 + 11 files changed, 415 insertions(+) create mode 100644 .DS_Store create mode 100644 packages/.DS_Store create mode 100644 packages/react-material-ui/.DS_Store create mode 100644 packages/react-material-ui/src/.DS_Store create mode 100644 packages/react-material-ui/src/components/.DS_Store create mode 100644 packages/react-material-ui/src/components/DateRangePicker/DateInput/Styles.ts create mode 100644 packages/react-material-ui/src/components/DateRangePicker/DateInput/index.tsx create mode 100644 packages/react-material-ui/src/components/DateRangePicker/index.tsx diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..067535171e71168e6bd48b5ee2608ae84ac87033 GIT binary patch literal 8196 zcmeHMO>fgc5S?v9>ohG2RgrQ*vc$Ctp?nC$B~9ByCE(B?H~=bkY+6&tuHvMjsw(9S z|AD{2m0!Yt;RJ7IH^tdFy+M)Km3H6SJ8vg`JN9}UA`-1ZaF?h?L=LLNY6VR}k>j#X zl{tN86)3d-0GQM(jRS=$p<0jq#jz$#!BunPPO3SiHc#hJ11t5Mrp1*`)9r2_K# z;G#-wY8)x#M+X{C0e~f(=7K)*0Oq(Fn;J(7nTk18_8>G>=oCXJI>udT4s2>1DO7Y4 zicUg%7CJ)_>>WIp%}F#B+SV#y6_{2)=I%?#+fxe2%jfUAaoX|Y)W^8Ir8ZIPKe?bs zRMDJ}@noF!8?3JeYmKeD_&yL(AKwTfN`Q4fo6vh4AK>T%Ws~!WTgE(9*o2f7b8#(+ z!T)UVm*6|39=v6mprLVEz8e09^_ zSl=%>eI0{mC_+-%OYQw(cC6R$n|~ZfS-;WvF6QP-*A^C?MW^DtZ6Bt?cHip{vX<9- zEstKLY3z^tp8qlmhx^sFtu*O-Q4;o;5CtJj-n@#EARV^SK@w!lZL0yN?3DMbwX?Ga zjmElr|Ix?&jw&@wKUzY-dAwA|9iyUJqA|;aXW?0Jz$y?t* zL}W%R2UzRQtX1b|$UCOQ6MSX`Vi?kalWC|MI&4HYb126tCMBjy}P$Dj literal 0 HcmV?d00001 diff --git a/packages/.DS_Store b/packages/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..fd4103b2ee7fc93db29fb292121ed025f753aa83 GIT binary patch literal 6148 zcmeHKO>fjN5FNKmn{9;@)FSnQ6sgx1mO?8~FX>VaTuD_9fR?0LwGpYk$|l{zYNed% zkLk6)guerCY*&!oR)B;MT6v`L8-L8$eva*!h(xzapAz{*u%@G=h*v^?73FFw%7eyO&UF;@jWNWOBU(jSa~$H)pGQbW!-1X2bu!4a zVcF??6$^{4drQmSvbXAejLy_3D&w*mcH`nb=Z=)hlN&oujEp3ZB2%nQR5)mpg1f+nY?<-owo&`)_-n z^K+@bFr_$Fajt5)Z}A2$Vf+SH7f~T|B~Q_sBl!(>B{=2PJ=H8{SZg`OE%);C8YAmu zZWyrNmbW@LGjbDg3^)e<76W`f1W?A%Vr@_#9Z2*O0N6sY7TElAz&?S+&|+;6H4tH3 zfyPzXCx$TYC=YDA&|+=SxRbDt4`F*2_JtyJ?}#7h<|IOcE_Dnz2AT{knykCa|}2J=86H)I*bmxn3BC)XC}vYZ2%>dBtMordYeb*RPDDTt;Z+t>F;f`>6~$qMJ0P zkYYMRdJ0~TK7uQ$IL;fquS-0aPUr}|4e2?sN9U9@8RE_oqm6zi6fZ@6xjHJEm1FbP z-=cAzl~uq0U9{SrYuC4&Eoa+#8+?$npbD#LIS41Ox%5KHJi5$>(aS8Ijk>p<$)XCg zBAqBgmZS)I^C~M6IUC4nk(7!X=my7i+);OTK7Z8j?|Bao_7{8J{K-D&A3RtdJB|D*Ny|0c1$EkeWOI literal 0 HcmV?d00001 diff --git a/packages/react-material-ui/package.json b/packages/react-material-ui/package.json index da1e8dd3..b849f1db 100644 --- a/packages/react-material-ui/package.json +++ b/packages/react-material-ui/package.json @@ -40,6 +40,7 @@ "@rjsf/mui": "^5.0.0-beta.13", "@rjsf/utils": "^5.0.0-beta.13", "@rjsf/validator-ajv6": "^5.0.0-beta.13", + "date-fns": "^4.1.0", "lodash": "^4.17.21" }, "devDependencies": { diff --git a/packages/react-material-ui/src/.DS_Store b/packages/react-material-ui/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..b95fb77c0d7804ac79056c45966308542ac547f0 GIT binary patch literal 6148 zcmeHK&2G~`5S~qwW|ImjhgRwZ$r9J9q~%AgdNH9KxMBncK*6qEYSr~du|q&nB%k3O zcm=LJ3Gc!QzS-TP{PkM3)s8g#jdx~t*PrcKFA<5}DBdIL5Rrs3*4t=S2>V%=q-J|s zK%vJ_lv4sNsaT439gYFVz)fR-zuh*SkfIA3p#A^-zRc7~l&J`QE)LJ2yMML7x*{hU z7XzE~D;i{JQFgn(MQyF#SZ~Uv+>jrFGc^v%upAY=aQKd|UMrPFmv$JvNt1EEv-w=* zWtis4P$#5uf{^!b(>zw=o*LzGp>qS{kS*EjceW>!C*AIj_xNCcy5mg__t}2*bUJOx ztp^W}UiHtji(Gxvt0#earRAQ*OZX0BB_E%IVV!W5?0ylTJ}0d zic8`%v-O&Z{Ex|Z=N+1B2t}ke3vLBq5zfc z>NPZ%6#ky;Oj)Sjd7**psyd-4@eW=na8hk;ed`tQ3d}0NXZJd7&;WlO=lsrFX#@4^ zzoiIy4R6L&QbB#xBd{_`@HwGJl%mZjHf{j8hxbko?Clty%2{vfwu5bAOhfeAqZs~% z@O(rOjcgC4?-=Cf~f}3A_^YH2ZVRn=kZ=eT?suD9T>(A#cug<^B^0+(9 zvm$3@xtBWDBZZb3*6zU3kE6-dkFi5wC%k4hR-(zo27)aRvoU&}Q10M)q$a*DF_1}g zyM1#EnbLI>2ZuZNN8svYqWzk%<0gj?Hv9ElO1e(5yfoI z2rC(Z#XdN8lbJ}uPF;9mD8_T2z*EVJ+5NLl7Iu;LFmj1D8_6nJ$QqCdy}m4}EL`IZ zH&;pMVlj~1eDMl+1-t@Y0k42p;ImZVik?;2g#BUu|NlSBx%)|Z1-t@U0ae@TZguc{ zc7@{#!*}f=>hGvr7&p~cC}`w59#*d7@qw@7zd~hfi!B#Cw<|l<7H?2K`9A~v`5%W< Nk=~#G|EK5we*nmD5%T~5 literal 0 HcmV?d00001 diff --git a/packages/react-material-ui/src/components/DateRangePicker/DateInput/Styles.ts b/packages/react-material-ui/src/components/DateRangePicker/DateInput/Styles.ts new file mode 100644 index 00000000..6a427e04 --- /dev/null +++ b/packages/react-material-ui/src/components/DateRangePicker/DateInput/Styles.ts @@ -0,0 +1,17 @@ +import { styled } from '@mui/material/styles'; + +export const CustomInput = styled('input')(({}) => ({ + border: 'none', + textAlign: 'center', + textTransform: 'uppercase', + outline: 'none', + fontFamily: 'inherit', + fontSize: '1rem', + width: '112px', + '&::-webkit-calendar-picker-indicator': { + display: 'none', + }, + '&::-moz-calendar-picker-indicator': { + display: 'none', + }, +})); diff --git a/packages/react-material-ui/src/components/DateRangePicker/DateInput/index.tsx b/packages/react-material-ui/src/components/DateRangePicker/DateInput/index.tsx new file mode 100644 index 00000000..8bda1b61 --- /dev/null +++ b/packages/react-material-ui/src/components/DateRangePicker/DateInput/index.tsx @@ -0,0 +1,10 @@ +import React, { InputHTMLAttributes, forwardRef } from 'react'; +import { CustomInput } from './Styles'; + +type Props = InputHTMLAttributes; + +const DateInput = forwardRef((props: Props, ref: any) => { + return ; +}); + +export default DateInput; diff --git a/packages/react-material-ui/src/components/DateRangePicker/index.tsx b/packages/react-material-ui/src/components/DateRangePicker/index.tsx new file mode 100644 index 00000000..e0127eb8 --- /dev/null +++ b/packages/react-material-ui/src/components/DateRangePicker/index.tsx @@ -0,0 +1,374 @@ +import React, { useState, useRef, FieldsetHTMLAttributes } from 'react'; +import { + Box, + Popover, + Typography, + Grid, + IconButton, + SxProps, + ClickAwayListener, + FormHelperText, +} from '@mui/material'; +import { DateCalendar, PickersDay } from '@mui/x-date-pickers'; +import { + isWithinInterval, + isSameDay, + format, + startOfDay, + endOfDay, + addMonths, + addDays, + isBefore, + isAfter, +} from 'date-fns'; +import ChevronLeft from '@mui/icons-material/ChevronLeft'; +import ChevronRight from '@mui/icons-material/ChevronRight'; +import DateInput from './DateInput'; + +interface DateRange { + startDate: Date | null; + endDate: Date | null; +} + +enum DateSelectionMode { + FROM = 'from', + TO = 'to', +} + +export type DateRangePickerProps = { + label?: string; + sx?: SxProps; + error?: string; +} & FieldsetHTMLAttributes; + +const hiddenButtonSx = { + visibility: 'hidden', + display: 'none', +}; + +const DateRangePicker = ({ + label, + sx, + error, + ...props +}: DateRangePickerProps) => { + const fieldsetRef = useRef(null); + + const hiddenStartCalendarButtonRef = useRef(null); + const leftArrowButtonRef = useRef(null); + const hiddenEndCalendarButtonRef = useRef(null); + const rightArrowButtonRef = useRef(null); + + const startDateInputRef = useRef(null); + const endDateInputRef = useRef(null); + + const [anchorEl, setAnchorEl] = useState(null); + const [dateRange, setDateRange] = useState({ + startDate: null, + endDate: null, + }); + const [startDateInputValue, setStartDateInputValue] = useState(''); + const [endDateInputValue, setEndDateInputValue] = useState(''); + const [hoveredDate, setHoveredDate] = useState(null); + const [dateSelectionMode, setDateSelectionMode] = useState( + DateSelectionMode.FROM, + ); + + const handleOpen = () => { + startDateInputRef?.current?.focus(); + setAnchorEl(fieldsetRef.current); + }; + + const handleClose = () => { + setAnchorEl(null); + setHoveredDate(null); // Reset hover state + }; + + const open = Boolean(anchorEl); + + const isDateInRange = (date: Date) => { + const { startDate, endDate } = dateRange; + + if (startDate && endDate) { + return isWithinInterval(date, { + start: startOfDay(startDate), + end: endOfDay(endDate), + }); + } + + if (startDate && hoveredDate) { + return isWithinInterval(date, { + start: startOfDay(startDate), + end: endOfDay(hoveredDate), + }); + } + return false; + }; + + const handleStartDateChange = (date: string) => { + const isAfterEndDate = + dateRange.endDate && + isAfter(addDays(new Date(date), 1), dateRange.endDate); + + setStartDateInputValue(date); + setDateRange({ + startDate: addDays(new Date(date), 1), + endDate: isAfterEndDate ? null : dateRange.endDate, + }); + + if (isAfterEndDate) { + setEndDateInputValue(''); + } + }; + + const handleEndDateChange = (date: string) => { + const isBeforeStartDate = + dateRange.startDate && + isBefore(addDays(new Date(date), 1), dateRange.startDate); + + setEndDateInputValue(date); + setDateRange({ + startDate: isBeforeStartDate ? null : dateRange.startDate, + endDate: addDays(new Date(date), 1), + }); + + if (isBeforeStartDate) { + setStartDateInputValue(''); + } + }; + + const handleDateSelection = (date: Date | null) => { + setDateRange((prev) => { + if (dateSelectionMode === DateSelectionMode.FROM) { + if (prev.endDate && date && date > prev.endDate) { + // If "from" date is after the current "to" date, clear "to" date + setStartDateInputValue(format(date, 'yyyy-MM-dd')); + setEndDateInputValue(''); + + return { startDate: date, endDate: null }; + } + + setStartDateInputValue(date ? format(date, 'yyyy-MM-dd') : ''); + + return { ...prev, startDate: date }; + } else if (dateSelectionMode === DateSelectionMode.TO) { + if (prev.startDate && date && date < prev.startDate) { + // If "to" date is before the "from" date, reset selection to "from" + setEndDateInputValue(format(date, 'yyyy-MM-dd')); + setStartDateInputValue(''); + + return { startDate: null, endDate: date }; + } + + setEndDateInputValue(date ? format(date, 'yyyy-MM-dd') : ''); + + return { ...prev, endDate: date }; + } + + return prev; + }); + + // Toggle the selection mode after a date is selected + setDateSelectionMode((prev) => + prev === DateSelectionMode.FROM + ? DateSelectionMode.TO + : DateSelectionMode.FROM, + ); + setHoveredDate(null); + }; + + const renderDay = (props: any) => { + const isSelected = + (dateRange.startDate && isSameDay(props.day, dateRange.startDate)) || + (dateRange.endDate && isSameDay(props.day, dateRange.endDate)); + const isInRange = isDateInRange(props.day); + + return ( + { + if (dateRange.startDate && !dateRange.endDate) { + setHoveredDate(props.day); + } + }} + /> + ); + }; + + const handleMonthChange = (direction: 'next' | 'previous') => { + if (direction === 'previous') { + hiddenStartCalendarButtonRef?.current?.click(); + } + + if (direction === 'next') { + hiddenEndCalendarButtonRef?.current?.click(); + } + }; + + return ( + + {label && ( + + {label} + + )} + + + handleStartDateChange(event.target.value)} + /> + - + handleEndDateChange(event.target.value)} + /> + + + {error ? ( + + {error} + + ) : null} + + + + + + Select Date Range + + + + {/* From Calendar */} + + renderDay(date), + leftArrowIcon: () => ( + handleMonthChange('previous')} + > + + + ), + rightArrowIcon: () => ( + + ), + }} + referenceDate={dateRange.startDate || new Date()} + minDate={ + dateSelectionMode === DateSelectionMode.TO + ? dateRange.startDate || undefined + : undefined + } + /> + + + {/* To Calendar */} + + renderDay(date), + leftArrowIcon: () => ( + + ), + rightArrowIcon: () => ( + handleMonthChange('next')} + > + + + ), + }} + referenceDate={addMonths( + dateRange.startDate || new Date(), + 1, + )} + minDate={ + dateSelectionMode === DateSelectionMode.TO + ? dateRange.startDate || undefined + : undefined + } + /> + + + + + + + ); +}; + +export default DateRangePicker; diff --git a/packages/react-material-ui/src/index.ts b/packages/react-material-ui/src/index.ts index 1d6b27c2..8bd7a67c 100644 --- a/packages/react-material-ui/src/index.ts +++ b/packages/react-material-ui/src/index.ts @@ -89,3 +89,8 @@ export { default as OtpInput } from './components/OtpInput'; export { default as Breadcrumbs } from './components/Breadcrumbs'; export { FormLabel, FormLabelProps } from './components/FormLabel'; + +export { + default as DateRangePicker, + DateRangePickerProps, +} from './components/DateRangePicker'; diff --git a/yarn.lock b/yarn.lock index 81f50241..7c6afccf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1734,6 +1734,7 @@ __metadata: "@types/lodash": "npm:^4.14.198" "@types/react": "npm:^18.2.0" "@types/react-dom": "npm:^18.2.0" + date-fns: "npm:^4.1.0" lodash: "npm:^4.17.21" peerDependencies: "@concepta/react-auth-provider": ^2.0.0-alpha.10 @@ -8321,6 +8322,13 @@ __metadata: languageName: node linkType: hard +"date-fns@npm:^4.1.0": + version: 4.1.0 + resolution: "date-fns@npm:4.1.0" + checksum: 10c0/b79ff32830e6b7faa009590af6ae0fb8c3fd9ffad46d930548fbb5acf473773b4712ae887e156ba91a7b3dc30591ce0f517d69fd83bd9c38650fdc03b4e0bac8 + languageName: node + linkType: hard + "dateformat@npm:^3.0.0": version: 3.0.3 resolution: "dateformat@npm:3.0.3"