From f539048ae0acd8bdfb107201e06a599d3bd67b13 Mon Sep 17 00:00:00 2001 From: Jefferson Silva Date: Tue, 17 Dec 2024 22:59:37 -0300 Subject: [PATCH 01/15] 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 b6bd4bd9..174e60e7 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 c54d2b3a..c336182f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1788,6 +1788,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 @@ -8476,6 +8477,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" From 4afec461172e7780876298e7b51904e3bab9ca4b Mon Sep 17 00:00:00 2001 From: Jefferson Silva Date: Tue, 17 Dec 2024 23:18:22 -0300 Subject: [PATCH 02/15] feat: add tests for date range picker component --- .../__tests__/DateRangePicker.spec.tsx | 60 +++++++++++++++++++ .../src/components/DateRangePicker/index.tsx | 2 + 2 files changed, 62 insertions(+) create mode 100644 packages/react-material-ui/__tests__/DateRangePicker.spec.tsx diff --git a/packages/react-material-ui/__tests__/DateRangePicker.spec.tsx b/packages/react-material-ui/__tests__/DateRangePicker.spec.tsx new file mode 100644 index 00000000..3dc9c06d --- /dev/null +++ b/packages/react-material-ui/__tests__/DateRangePicker.spec.tsx @@ -0,0 +1,60 @@ +/** + * @jest-environment jsdom + */ + +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, fireEvent, getByRole } from '@testing-library/react'; +import { + LocalizationProvider, + MuiPickersAdapter, + MuiPickersAdapterContext, +} from '@mui/x-date-pickers'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import DateRangePicker from '../src/components/DateRangePicker'; + +describe('DateRangePicker Component', () => { + test('should render correctly', () => { + const { getByRole } = render(); + const field = getByRole('group'); + + expect(field).toBeInTheDocument(); + }); + + test('should render correctly with label', () => { + const { getByText, getByRole } = render( + , + ); + const field = getByRole('group'); + const legend = getByText('Date Range'); + + expect(field).toBeInTheDocument(); + expect(legend).toBeInTheDocument(); + }); + + test('should render correctly with label and display two inputs', () => { + const { getByText, getByRole, getByTestId } = render( + , + ); + const field = getByRole('group'); + const legend = getByText('Date Range'); + const startDateInput = getByTestId('start-date-input'); + const endDateInput = getByTestId('end-date-input'); + + expect(field).toBeInTheDocument(); + expect(legend).toBeInTheDocument(); + expect(startDateInput).toBeInTheDocument(); + expect(endDateInput).toBeInTheDocument(); + }); + + // test('should open popover on field click', () => { + // const { getByRole } = render(); + // const field = getByRole('group'); + + // expect(field).toBeInTheDocument(); + + // fireEvent.click(field); + + // expect(getByRole('presentation')).toBeInTheDocument(); + // }); +}); diff --git a/packages/react-material-ui/src/components/DateRangePicker/index.tsx b/packages/react-material-ui/src/components/DateRangePicker/index.tsx index e0127eb8..1f7301cd 100644 --- a/packages/react-material-ui/src/components/DateRangePicker/index.tsx +++ b/packages/react-material-ui/src/components/DateRangePicker/index.tsx @@ -262,12 +262,14 @@ const DateRangePicker = ({ ref={startDateInputRef} value={startDateInputValue} onChange={(event) => handleStartDateChange(event.target.value)} + data-testid="start-date-input" /> - handleEndDateChange(event.target.value)} + data-testid="end-date-input" /> From dbe5cd41f51d758d3f1b5c00d7a1c4664ba27ce7 Mon Sep 17 00:00:00 2001 From: Jefferson Silva Date: Wed, 18 Dec 2024 19:41:44 -0300 Subject: [PATCH 03/15] refactor: change calendar headers --- .../src/components/DateRangePicker/index.tsx | 155 +++++++++++++----- 1 file changed, 110 insertions(+), 45 deletions(-) diff --git a/packages/react-material-ui/src/components/DateRangePicker/index.tsx b/packages/react-material-ui/src/components/DateRangePicker/index.tsx index 1f7301cd..eff1772e 100644 --- a/packages/react-material-ui/src/components/DateRangePicker/index.tsx +++ b/packages/react-material-ui/src/components/DateRangePicker/index.tsx @@ -2,14 +2,23 @@ import React, { useState, useRef, FieldsetHTMLAttributes } from 'react'; import { Box, Popover, + Button, Typography, Grid, IconButton, SxProps, ClickAwayListener, FormHelperText, + Stack, } from '@mui/material'; -import { DateCalendar, PickersDay } from '@mui/x-date-pickers'; +import { alpha } from '@mui/material/styles'; +import { styled } from '@mui/material/styles'; +import { + DateCalendar, + PickersDay, + PickersDayProps, + PickersCalendarHeaderProps, +} from '@mui/x-date-pickers'; import { isWithinInterval, isSameDay, @@ -46,12 +55,52 @@ const hiddenButtonSx = { display: 'none', }; -const DateRangePicker = ({ - label, - sx, - error, - ...props -}: DateRangePickerProps) => { +const CustomCalendarHeaderRoot = styled('div')({ + display: 'flex', + justifyContent: 'space-between', + padding: '8px 16px', + alignItems: 'center', +}); + +function CustomStartCalendarHeader(props: PickersCalendarHeaderProps) { + const { currentMonth } = props; + + const selectPreviousMonth = () => null; + + return ( + + + + + + + + {format(currentMonth, 'MMMM YYYY')} + + + ); +} + +function CustomEndCalendarHeader(props: PickersCalendarHeaderProps) { + const { currentMonth } = props; + + const selectNextMonth = () => null; + + return ( + + + {format(currentMonth, 'MMMM YYYY')} + + + + + + + + ); +} + +const DateRangePicker = ({ label, error, ...props }: DateRangePickerProps) => { const fieldsetRef = useRef(null); const hiddenStartCalendarButtonRef = useRef(null); @@ -73,6 +122,7 @@ const DateRangePicker = ({ const [dateSelectionMode, setDateSelectionMode] = useState( DateSelectionMode.FROM, ); + const [errorMessage, setErrorMessage] = useState(''); const handleOpen = () => { startDateInputRef?.current?.focus(); @@ -102,6 +152,7 @@ const DateRangePicker = ({ end: endOfDay(hoveredDate), }); } + return false; }; @@ -112,13 +163,10 @@ const DateRangePicker = ({ setStartDateInputValue(date); setDateRange({ + ...dateRange, startDate: addDays(new Date(date), 1), - endDate: isAfterEndDate ? null : dateRange.endDate, }); - - if (isAfterEndDate) { - setEndDateInputValue(''); - } + setErrorMessage(isAfterEndDate ? 'Invalid range' : ''); }; const handleEndDateChange = (date: string) => { @@ -128,38 +176,26 @@ const DateRangePicker = ({ setEndDateInputValue(date); setDateRange({ - startDate: isBeforeStartDate ? null : dateRange.startDate, + ...dateRange, endDate: addDays(new Date(date), 1), }); - - if (isBeforeStartDate) { - setStartDateInputValue(''); - } + setErrorMessage(isBeforeStartDate ? 'Invalid range' : ''); }; 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 }; - } + const isAfterEndDate = prev.endDate && date && date > prev.endDate; + setErrorMessage(isAfterEndDate ? 'Invalid range' : ''); 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 }; - } + const isBeforeStartDate = + prev.startDate && date && date < prev.startDate; + setErrorMessage(isBeforeStartDate ? 'Invalid range' : ''); setEndDateInputValue(date ? format(date, 'yyyy-MM-dd') : ''); return { ...prev, endDate: date }; @@ -177,7 +213,7 @@ const DateRangePicker = ({ setHoveredDate(null); }; - const renderDay = (props: any) => { + const renderDay = (props: PickersDayProps) => { const isSelected = (dateRange.startDate && isSameDay(props.day, dateRange.startDate)) || (dateRange.endDate && isSameDay(props.day, dateRange.endDate)); @@ -212,13 +248,11 @@ const DateRangePicker = ({ }; const handleMonthChange = (direction: 'next' | 'previous') => { - if (direction === 'previous') { - hiddenStartCalendarButtonRef?.current?.click(); - } - - if (direction === 'next') { - hiddenEndCalendarButtonRef?.current?.click(); - } + const buttonRef = + direction === 'previous' + ? hiddenStartCalendarButtonRef + : hiddenEndCalendarButtonRef; + buttonRef?.current?.click(); }; return ( @@ -227,7 +261,12 @@ const DateRangePicker = ({ ref={fieldsetRef} component="fieldset" sx={{ - border: `1px solid ${error ? '#d32f2f' : 'rgba(0, 0, 0, 0.23);'}`, + border: (theme) => + `1px solid ${ + error || errorMessage + ? theme.palette.error.main + : alpha(theme.palette.common.black, 0.23) + }`, borderRadius: '4px', width: 'fit-content', height: '40px', @@ -245,7 +284,12 @@ const DateRangePicker = ({ lineHeight: 1, fontSize: '12px', padding: '2px 4px', - color: `${error ? '#d32f2f' : 'rgba(0, 0, 0, 0.6);'}`, + color: (theme) => + `${ + error || errorMessage + ? theme.palette.error.main + : alpha(theme.palette.common.black, 0.6) + }`, height: '14px', float: 'unset', position: 'absolute', @@ -264,7 +308,11 @@ const DateRangePicker = ({ onChange={(event) => handleStartDateChange(event.target.value)} data-testid="start-date-input" /> - - + alpha(theme.palette.common.black, 0.23) }} + > + - + - {error ? ( + {error || errorMessage ? ( theme.palette.error.main, }} > - {error} + {error || errorMessage} ) : null} @@ -307,6 +355,7 @@ const DateRangePicker = ({ // value={dateRange.startDate} onChange={handleDateSelection} slots={{ + calendarHeader: CustomStartCalendarHeader, day: (date) => renderDay(date), leftArrowIcon: () => ( renderDay(date), leftArrowIcon: () => ( + + + + From ea2720c2876409407df46eb0ea3dc49159f10dff Mon Sep 17 00:00:00 2001 From: Jefferson Silva Date: Thu, 19 Dec 2024 20:15:46 -0300 Subject: [PATCH 04/15] refactor: change calendar header components --- .../src/components/DateRangePicker/index.tsx | 97 ++++++------------- 1 file changed, 30 insertions(+), 67 deletions(-) diff --git a/packages/react-material-ui/src/components/DateRangePicker/index.tsx b/packages/react-material-ui/src/components/DateRangePicker/index.tsx index eff1772e..a959d760 100644 --- a/packages/react-material-ui/src/components/DateRangePicker/index.tsx +++ b/packages/react-material-ui/src/components/DateRangePicker/index.tsx @@ -27,6 +27,7 @@ import { endOfDay, addMonths, addDays, + subMonths, isBefore, isAfter, } from 'date-fns'; @@ -48,13 +49,9 @@ export type DateRangePickerProps = { label?: string; sx?: SxProps; error?: string; + onRangeUpdate?: (range: DateRange) => void; } & FieldsetHTMLAttributes; -const hiddenButtonSx = { - visibility: 'hidden', - display: 'none', -}; - const CustomCalendarHeaderRoot = styled('div')({ display: 'flex', justifyContent: 'space-between', @@ -62,10 +59,15 @@ const CustomCalendarHeaderRoot = styled('div')({ alignItems: 'center', }); -function CustomStartCalendarHeader(props: PickersCalendarHeaderProps) { - const { currentMonth } = props; +type CustomCalendarHeaderProps = { + onActionButtonClick?: () => void; +} & PickersCalendarHeaderProps; - const selectPreviousMonth = () => null; +function CustomStartCalendarHeader(props: CustomCalendarHeaderProps) { + const { currentMonth, onMonthChange } = props; + + const selectPreviousMonth = () => + onMonthChange(subMonths(currentMonth, 1), 'right'); return ( @@ -74,23 +76,20 @@ function CustomStartCalendarHeader(props: PickersCalendarHeaderProps) { - - {format(currentMonth, 'MMMM YYYY')} - + {format(currentMonth, 'MMMM yyyy')} ); } -function CustomEndCalendarHeader(props: PickersCalendarHeaderProps) { - const { currentMonth } = props; +function CustomEndCalendarHeader(props: CustomCalendarHeaderProps) { + const { currentMonth, onMonthChange } = props; - const selectNextMonth = () => null; + const selectNextMonth = () => + onMonthChange(addMonths(currentMonth, 1), 'left'); return ( - - {format(currentMonth, 'MMMM YYYY')} - + {format(currentMonth, 'MMMM yyyy')} @@ -100,14 +99,13 @@ function CustomEndCalendarHeader(props: PickersCalendarHeaderProps) { ); } -const DateRangePicker = ({ label, error, ...props }: DateRangePickerProps) => { +const CustomDateRangePicker = ({ + label, + error, + ...props +}: DateRangePickerProps) => { const fieldsetRef = useRef(null); - - const hiddenStartCalendarButtonRef = useRef(null); - const leftArrowButtonRef = useRef(null); - const hiddenEndCalendarButtonRef = useRef(null); - const rightArrowButtonRef = useRef(null); - + const startCalendarRef = useRef(null); const startDateInputRef = useRef(null); const endDateInputRef = useRef(null); @@ -247,14 +245,6 @@ const DateRangePicker = ({ label, error, ...props }: DateRangePickerProps) => { ); }; - const handleMonthChange = (direction: 'next' | 'previous') => { - const buttonRef = - direction === 'previous' - ? hiddenStartCalendarButtonRef - : hiddenEndCalendarButtonRef; - buttonRef?.current?.click(); - }; - return ( { - {/* From Calendar */} renderDay(date), - leftArrowIcon: () => ( - handleMonthChange('previous')} - > - - - ), - rightArrowIcon: () => ( - + calendarHeader: (props) => ( + ), + day: (date) => renderDay(date), }} referenceDate={dateRange.startDate || new Date()} minDate={ @@ -381,28 +358,14 @@ const DateRangePicker = ({ label, error, ...props }: DateRangePickerProps) => { /> - {/* To Calendar */} renderDay(date), - leftArrowIcon: () => ( - - ), - rightArrowIcon: () => ( - handleMonthChange('next')} - > - - + calendarHeader: (props) => ( + ), + day: (date) => renderDay(date), }} referenceDate={addMonths( dateRange.startDate || new Date(), @@ -438,4 +401,4 @@ const DateRangePicker = ({ label, error, ...props }: DateRangePickerProps) => { ); }; -export default DateRangePicker; +export default CustomDateRangePicker; From accae239864585442f5ab3f731e1fd38686a0e09 Mon Sep 17 00:00:00 2001 From: Jefferson Silva Date: Thu, 19 Dec 2024 20:21:07 -0300 Subject: [PATCH 05/15] chore: remove unnecessary type --- .../src/components/DateRangePicker/index.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/react-material-ui/src/components/DateRangePicker/index.tsx b/packages/react-material-ui/src/components/DateRangePicker/index.tsx index a959d760..eee47401 100644 --- a/packages/react-material-ui/src/components/DateRangePicker/index.tsx +++ b/packages/react-material-ui/src/components/DateRangePicker/index.tsx @@ -59,11 +59,7 @@ const CustomCalendarHeaderRoot = styled('div')({ alignItems: 'center', }); -type CustomCalendarHeaderProps = { - onActionButtonClick?: () => void; -} & PickersCalendarHeaderProps; - -function CustomStartCalendarHeader(props: CustomCalendarHeaderProps) { +function CustomStartCalendarHeader(props: PickersCalendarHeaderProps) { const { currentMonth, onMonthChange } = props; const selectPreviousMonth = () => @@ -81,7 +77,7 @@ function CustomStartCalendarHeader(props: CustomCalendarHeaderProps) { ); } -function CustomEndCalendarHeader(props: CustomCalendarHeaderProps) { +function CustomEndCalendarHeader(props: PickersCalendarHeaderProps) { const { currentMonth, onMonthChange } = props; const selectNextMonth = () => From aa854c76f99d64188ca1d9194df02da8a7e0b0fb Mon Sep 17 00:00:00 2001 From: Jefferson Silva Date: Thu, 19 Dec 2024 21:32:51 -0300 Subject: [PATCH 06/15] chore: remove comments --- .DS_Store | Bin 8196 -> 8196 bytes .../__tests__/DateRangePicker.spec.tsx | 11 ----------- 2 files changed, 11 deletions(-) diff --git a/.DS_Store b/.DS_Store index 067535171e71168e6bd48b5ee2608ae84ac87033..2bb92d3666996ed7a2bd01d0c29c8fb7e0d00448 100644 GIT binary patch delta 81 zcmZp1XmOa}&nUbxU^hRb@Ma!?L`LmAhJ1z;hE#@lhFmbKlpzPmEB4IEPfp6oPhwzT f5MW?n{0qdqn-c_8m^UAlv1gvxz_XcM;x9V@TgMd( delta 34 qcmZp1XmOa}&nUDpU^hRb&}JTiM8?fa1;v>+x9~JGZ)TVH%MJj&-3qJ# diff --git a/packages/react-material-ui/__tests__/DateRangePicker.spec.tsx b/packages/react-material-ui/__tests__/DateRangePicker.spec.tsx index 3dc9c06d..9c0e0f8c 100644 --- a/packages/react-material-ui/__tests__/DateRangePicker.spec.tsx +++ b/packages/react-material-ui/__tests__/DateRangePicker.spec.tsx @@ -46,15 +46,4 @@ describe('DateRangePicker Component', () => { expect(startDateInput).toBeInTheDocument(); expect(endDateInput).toBeInTheDocument(); }); - - // test('should open popover on field click', () => { - // const { getByRole } = render(); - // const field = getByRole('group'); - - // expect(field).toBeInTheDocument(); - - // fireEvent.click(field); - - // expect(getByRole('presentation')).toBeInTheDocument(); - // }); }); From 5b71c2ad8f33ca1f970874fa932af673b20fac15 Mon Sep 17 00:00:00 2001 From: Jefferson Silva Date: Fri, 20 Dec 2024 12:04:49 -0300 Subject: [PATCH 07/15] chore: remove auto generated files --- .DS_Store | Bin 8196 -> 0 bytes packages/.DS_Store | Bin 6148 -> 0 bytes packages/react-material-ui/.DS_Store | Bin 6148 -> 0 bytes packages/react-material-ui/src/.DS_Store | Bin 6148 -> 0 bytes .../react-material-ui/src/components/.DS_Store | Bin 10244 -> 0 bytes 5 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store delete mode 100644 packages/.DS_Store delete mode 100644 packages/react-material-ui/.DS_Store delete mode 100644 packages/react-material-ui/src/.DS_Store delete mode 100644 packages/react-material-ui/src/components/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 2bb92d3666996ed7a2bd01d0c29c8fb7e0d00448..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMO>fgc5S?vH>ohG2P^4avEOD(uC?5iGNz?XF2{<$e4uFCkn_%kLQJgeXQKg*W zKkyg0@=N$H91y&n-4&6NOC0nyPvbnksaOAru|QU1<)iYn&)l zbP|eALVFgvLJ{m8JeSQ$)D_y%Dqt0uRY2D6%c$E^3dqaX?|X6D_T$vYad}H^p;v!% z!;Gk`IU(cWN!4$#z8S1Fw(jElKtuz4BZw#gR(Wqi@3Gy%)w*Acgjbt&@%GJBRBm#;aVWAF?=BqMUJy+6u&I_}^7UL0kETJ49J zn_sxHxa2H3W#?`4Lpo{>yumPQc>ULM>qVNz{>j|)Uq<1mRax6klYti{VV?<65W?im zt0)Q5Q6n8DLB`ysI^dL?Qme8)9zUqnHr)GmcqX z>1SL6n$WKTT2vry=>AgYD@Ua<=6)D$zdTy+u3pjN{&= zz+gy^dBzgwW{M1pWS$zaSsHDb^ENSbC%^-xL&Zr-#Y~NQ*q{qtGUcs zEnK6Bi5!|@u7G(2Jws&oIcF_(o#K_i^7t;SA`X&yU?>G$rx@6@?|)>EZB~JcD=@E? zST6&2&;I^@aYuFqtAJJD(kdVpcA7g4oZiU~D)UTbt?i<}Lzhk3O%yT(1DE4KF2{kV de;8u!!c=nV8Yc>|2hDy6U>WRS75JwL`~ffjN5FNKmn{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 diff --git a/packages/react-material-ui/src/.DS_Store b/packages/react-material-ui/src/.DS_Store deleted file mode 100644 index b95fb77c0d7804ac79056c45966308542ac547f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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 From 35583590537670739a0575b1d9cbc8410628f534 Mon Sep 17 00:00:00 2001 From: Jefferson Silva Date: Fri, 20 Dec 2024 12:12:49 -0300 Subject: [PATCH 08/15] refactor: add styles file --- .../src/components/DateRangePicker/DateInput/Styles.ts | 4 ++-- .../src/components/DateRangePicker/Styles.ts | 8 ++++++++ .../src/components/DateRangePicker/index.tsx | 9 +-------- 3 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 packages/react-material-ui/src/components/DateRangePicker/Styles.ts diff --git a/packages/react-material-ui/src/components/DateRangePicker/DateInput/Styles.ts b/packages/react-material-ui/src/components/DateRangePicker/DateInput/Styles.ts index 6a427e04..ccb3fd1c 100644 --- a/packages/react-material-ui/src/components/DateRangePicker/DateInput/Styles.ts +++ b/packages/react-material-ui/src/components/DateRangePicker/DateInput/Styles.ts @@ -1,6 +1,6 @@ import { styled } from '@mui/material/styles'; -export const CustomInput = styled('input')(({}) => ({ +export const CustomInput = styled('input')({ border: 'none', textAlign: 'center', textTransform: 'uppercase', @@ -14,4 +14,4 @@ export const CustomInput = styled('input')(({}) => ({ '&::-moz-calendar-picker-indicator': { display: 'none', }, -})); +}); diff --git a/packages/react-material-ui/src/components/DateRangePicker/Styles.ts b/packages/react-material-ui/src/components/DateRangePicker/Styles.ts new file mode 100644 index 00000000..f17c20db --- /dev/null +++ b/packages/react-material-ui/src/components/DateRangePicker/Styles.ts @@ -0,0 +1,8 @@ +import { styled } from '@mui/material/styles'; + +export const CustomCalendarHeaderRoot = styled('div')({ + display: 'flex', + justifyContent: 'space-between', + padding: '8px 16px', + alignItems: 'center', +}); diff --git a/packages/react-material-ui/src/components/DateRangePicker/index.tsx b/packages/react-material-ui/src/components/DateRangePicker/index.tsx index eee47401..b5a83df7 100644 --- a/packages/react-material-ui/src/components/DateRangePicker/index.tsx +++ b/packages/react-material-ui/src/components/DateRangePicker/index.tsx @@ -12,7 +12,6 @@ import { Stack, } from '@mui/material'; import { alpha } from '@mui/material/styles'; -import { styled } from '@mui/material/styles'; import { DateCalendar, PickersDay, @@ -34,6 +33,7 @@ import { import ChevronLeft from '@mui/icons-material/ChevronLeft'; import ChevronRight from '@mui/icons-material/ChevronRight'; import DateInput from './DateInput'; +import { CustomCalendarHeaderRoot } from './Styles'; interface DateRange { startDate: Date | null; @@ -52,13 +52,6 @@ export type DateRangePickerProps = { onRangeUpdate?: (range: DateRange) => void; } & FieldsetHTMLAttributes; -const CustomCalendarHeaderRoot = styled('div')({ - display: 'flex', - justifyContent: 'space-between', - padding: '8px 16px', - alignItems: 'center', -}); - function CustomStartCalendarHeader(props: PickersCalendarHeaderProps) { const { currentMonth, onMonthChange } = props; From be7b0e873c4f36ec9f1222896d4a6d2ba7f25223 Mon Sep 17 00:00:00 2001 From: Jefferson Silva Date: Fri, 20 Dec 2024 13:35:47 -0300 Subject: [PATCH 09/15] refactor: adjust methods for changing month range --- .../src/components/DateRangePicker/index.tsx | 198 ++++++++++++------ 1 file changed, 139 insertions(+), 59 deletions(-) diff --git a/packages/react-material-ui/src/components/DateRangePicker/index.tsx b/packages/react-material-ui/src/components/DateRangePicker/index.tsx index b5a83df7..de4ab056 100644 --- a/packages/react-material-ui/src/components/DateRangePicker/index.tsx +++ b/packages/react-material-ui/src/components/DateRangePicker/index.tsx @@ -52,49 +52,15 @@ export type DateRangePickerProps = { onRangeUpdate?: (range: DateRange) => void; } & FieldsetHTMLAttributes; -function CustomStartCalendarHeader(props: PickersCalendarHeaderProps) { - const { currentMonth, onMonthChange } = props; - - const selectPreviousMonth = () => - onMonthChange(subMonths(currentMonth, 1), 'right'); - - return ( - - - - - - - {format(currentMonth, 'MMMM yyyy')} - - ); -} - -function CustomEndCalendarHeader(props: PickersCalendarHeaderProps) { - const { currentMonth, onMonthChange } = props; - - const selectNextMonth = () => - onMonthChange(addMonths(currentMonth, 1), 'left'); - - return ( - - {format(currentMonth, 'MMMM yyyy')} - - - - - - - ); -} - -const CustomDateRangePicker = ({ +const DateRangePicker = ({ label, error, + onRangeUpdate, ...props }: DateRangePickerProps) => { const fieldsetRef = useRef(null); - const startCalendarRef = useRef(null); + const previousMonthButtonRef = useRef(null); + const nextMonthButtonRef = useRef(null); const startDateInputRef = useRef(null); const endDateInputRef = useRef(null); @@ -154,6 +120,13 @@ const CustomDateRangePicker = ({ startDate: addDays(new Date(date), 1), }); setErrorMessage(isAfterEndDate ? 'Invalid range' : ''); + + if (onRangeUpdate) { + onRangeUpdate({ + ...dateRange, + startDate: addDays(new Date(date), 1), + }); + } }; const handleEndDateChange = (date: string) => { @@ -167,6 +140,13 @@ const CustomDateRangePicker = ({ endDate: addDays(new Date(date), 1), }); setErrorMessage(isBeforeStartDate ? 'Invalid range' : ''); + + if (onRangeUpdate) { + onRangeUpdate({ + ...dateRange, + startDate: addDays(new Date(date), 1), + }); + } }; const handleDateSelection = (date: Date | null) => { @@ -177,6 +157,10 @@ const CustomDateRangePicker = ({ setErrorMessage(isAfterEndDate ? 'Invalid range' : ''); setStartDateInputValue(date ? format(date, 'yyyy-MM-dd') : ''); + if (onRangeUpdate) { + onRangeUpdate({ ...prev, startDate: date }); + } + return { ...prev, startDate: date }; } else if (dateSelectionMode === DateSelectionMode.TO) { const isBeforeStartDate = @@ -185,6 +169,10 @@ const CustomDateRangePicker = ({ setErrorMessage(isBeforeStartDate ? 'Invalid range' : ''); setEndDateInputValue(date ? format(date, 'yyyy-MM-dd') : ''); + if (onRangeUpdate) { + onRangeUpdate({ ...prev, endDate: date }); + } + return { ...prev, endDate: date }; } @@ -200,6 +188,22 @@ const CustomDateRangePicker = ({ setHoveredDate(null); }; + const onClearButtonClick = () => { + setDateRange({ + startDate: null, + endDate: null, + }); + setStartDateInputValue(''); + setEndDateInputValue(''); + + if (onRangeUpdate) { + onRangeUpdate({ + startDate: null, + endDate: null, + }); + } + }; + const renderDay = (props: PickersDayProps) => { const isSelected = (dateRange.startDate && isSameDay(props.day, dateRange.startDate)) || @@ -323,19 +327,57 @@ const CustomDateRangePicker = ({ > - + Select Date Range ( - - ), + calendarHeader: ( + props: PickersCalendarHeaderProps, + ) => { + const { currentMonth, onMonthChange } = props; + + const selectPreviousMonth = () => { + onMonthChange(subMonths(currentMonth, 1), 'right'); + previousMonthButtonRef?.current?.click(); + }; + + const selectNextMonth = () => + onMonthChange(addMonths(currentMonth, 1), 'left'); + + return ( + + + + + + + + + {format(currentMonth, 'MMMM yyyy')} + + + ); + }, day: (date) => renderDay(date), }} referenceDate={dateRange.startDate || new Date()} @@ -344,6 +386,14 @@ const CustomDateRangePicker = ({ ? dateRange.startDate || undefined : undefined } + sx={{ + '.MuiDayCalendar-header': { + justifyContent: 'space-between', + }, + '.MuiDayCalendar-weekContainer': { + justifyContent: 'space-between', + }, + }} /> @@ -351,9 +401,42 @@ const CustomDateRangePicker = ({ ( - - ), + calendarHeader: ( + props: PickersCalendarHeaderProps, + ) => { + const { currentMonth, onMonthChange } = props; + + const selectNextMonth = () => { + onMonthChange(addMonths(currentMonth, 1), 'left'); + nextMonthButtonRef?.current?.click(); + }; + + const selectPreviousMonth = () => + onMonthChange(subMonths(currentMonth, 1), 'right'); + + return ( + + + {format(currentMonth, 'MMMM yyyy')} + + + + + + + + + ); + }, day: (date) => renderDay(date), }} referenceDate={addMonths( @@ -365,23 +448,20 @@ const CustomDateRangePicker = ({ ? dateRange.startDate || undefined : undefined } + sx={{ + '.MuiDayCalendar-header': { + justifyContent: 'space-between', + }, + '.MuiDayCalendar-weekContainer': { + justifyContent: 'space-between', + }, + }} /> - + @@ -390,4 +470,4 @@ const CustomDateRangePicker = ({ ); }; -export default CustomDateRangePicker; +export default DateRangePicker; From eac988193243bca7461683cda0d8a0c559337a08 Mon Sep 17 00:00:00 2001 From: Jefferson Silva Date: Fri, 20 Dec 2024 14:37:36 -0300 Subject: [PATCH 10/15] chore: remove comments --- .../react-material-ui/src/components/DateRangePicker/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react-material-ui/src/components/DateRangePicker/index.tsx b/packages/react-material-ui/src/components/DateRangePicker/index.tsx index de4ab056..f912577c 100644 --- a/packages/react-material-ui/src/components/DateRangePicker/index.tsx +++ b/packages/react-material-ui/src/components/DateRangePicker/index.tsx @@ -84,7 +84,7 @@ const DateRangePicker = ({ const handleClose = () => { setAnchorEl(null); - setHoveredDate(null); // Reset hover state + setHoveredDate(null); }; const open = Boolean(anchorEl); @@ -179,7 +179,6 @@ const DateRangePicker = ({ return prev; }); - // Toggle the selection mode after a date is selected setDateSelectionMode((prev) => prev === DateSelectionMode.FROM ? DateSelectionMode.TO From f0cbe89c322d2772c1f2a458724dc5ebd997688a Mon Sep 17 00:00:00 2001 From: Jefferson Silva Date: Fri, 20 Dec 2024 17:04:26 -0300 Subject: [PATCH 11/15] chore: rename style files --- .../src/components/DateRangePicker/DateInput/index.tsx | 2 +- .../react-material-ui/src/components/DateRangePicker/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-material-ui/src/components/DateRangePicker/DateInput/index.tsx b/packages/react-material-ui/src/components/DateRangePicker/DateInput/index.tsx index 8bda1b61..abd15d2e 100644 --- a/packages/react-material-ui/src/components/DateRangePicker/DateInput/index.tsx +++ b/packages/react-material-ui/src/components/DateRangePicker/DateInput/index.tsx @@ -1,5 +1,5 @@ import React, { InputHTMLAttributes, forwardRef } from 'react'; -import { CustomInput } from './Styles'; +import { CustomInput } from './styles'; type Props = InputHTMLAttributes; diff --git a/packages/react-material-ui/src/components/DateRangePicker/index.tsx b/packages/react-material-ui/src/components/DateRangePicker/index.tsx index f912577c..842ca427 100644 --- a/packages/react-material-ui/src/components/DateRangePicker/index.tsx +++ b/packages/react-material-ui/src/components/DateRangePicker/index.tsx @@ -33,7 +33,7 @@ import { import ChevronLeft from '@mui/icons-material/ChevronLeft'; import ChevronRight from '@mui/icons-material/ChevronRight'; import DateInput from './DateInput'; -import { CustomCalendarHeaderRoot } from './Styles'; +import { CustomCalendarHeaderRoot } from './styles'; interface DateRange { startDate: Date | null; From f61aeb946ad512d0de5034f85c8766906eb98785 Mon Sep 17 00:00:00 2001 From: Jefferson Silva Date: Mon, 23 Dec 2024 23:27:53 -0300 Subject: [PATCH 12/15] chore: add readme to date range picker --- .../src/components/DateRangePicker/README.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 packages/react-material-ui/src/components/DateRangePicker/README.md diff --git a/packages/react-material-ui/src/components/DateRangePicker/README.md b/packages/react-material-ui/src/components/DateRangePicker/README.md new file mode 100644 index 00000000..796d8454 --- /dev/null +++ b/packages/react-material-ui/src/components/DateRangePicker/README.md @@ -0,0 +1,29 @@ +# DateRangePicker + +The DateRangePicker is a custom set of inputs that deal with a range of dates. It is composed by a fieldset with two inputs with type date inside, each one handling half of the date range. + +## Example + +The following example describes the full composition that mounts the Filter component: + +```tsx +import { DateRangePicker } from '@concepta/react-material-ui'; + + setDateRangeOnState(range)} +/>; +``` + +## Props + +| Name | Type | Description | Optional | +| --- | --- | --- | --- | +| label | `string` | The label of the field, similar to MUI's `Input` | No +| error | `string` | Error message displayed on the bottom of the field | No +| sx | `object` | Custom styles to be applied to the fieldset container | No +| onRangeUpdate | `function` | Handler for updates in the date range. Returns an object containing `startDate` and `endDate` | No + +> The rest of the DateRangePicker props extend from [HTML `fieldset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset). From f0e92a9e8a4b72427b6e3f7527864ddb240b8f87 Mon Sep 17 00:00:00 2001 From: Jefferson Silva Date: Tue, 24 Dec 2024 15:44:35 -0300 Subject: [PATCH 13/15] feat: implement date range filter on crud module --- .../src/components/DateRangePicker/index.tsx | 39 +++++++++++++++++-- .../src/components/Filter/Filter.tsx | 26 ++++++++++++- .../components/submodules/Filter/index.tsx | 12 +++++- .../src/modules/crud/useCrudRoot.tsx | 6 ++- 4 files changed, 76 insertions(+), 7 deletions(-) diff --git a/packages/react-material-ui/src/components/DateRangePicker/index.tsx b/packages/react-material-ui/src/components/DateRangePicker/index.tsx index 842ca427..34394262 100644 --- a/packages/react-material-ui/src/components/DateRangePicker/index.tsx +++ b/packages/react-material-ui/src/components/DateRangePicker/index.tsx @@ -1,4 +1,9 @@ -import React, { useState, useRef, FieldsetHTMLAttributes } from 'react'; +import React, { + useState, + useRef, + useEffect, + FieldsetHTMLAttributes, +} from 'react'; import { Box, Popover, @@ -35,7 +40,7 @@ import ChevronRight from '@mui/icons-material/ChevronRight'; import DateInput from './DateInput'; import { CustomCalendarHeaderRoot } from './styles'; -interface DateRange { +export interface DateRange { startDate: Date | null; endDate: Date | null; } @@ -46,7 +51,9 @@ enum DateSelectionMode { } export type DateRangePickerProps = { + type?: string; label?: string; + value?: DateRange; sx?: SxProps; error?: string; onRangeUpdate?: (range: DateRange) => void; @@ -55,6 +62,7 @@ export type DateRangePickerProps = { const DateRangePicker = ({ label, error, + value, onRangeUpdate, ...props }: DateRangePickerProps) => { @@ -203,6 +211,30 @@ const DateRangePicker = ({ } }; + useEffect(() => { + if (value?.startDate && !startDateInputValue) { + setStartDateInputValue(format(value.startDate, 'yyyy-MM-dd')); + } + + if (value?.startDate && !dateRange.startDate) { + setDateRange({ + ...dateRange, + startDate: new Date(value.startDate), + }); + } + + if (value?.endDate && !endDateInputValue) { + setEndDateInputValue(format(value.endDate, 'yyyy-MM-dd')); + } + + if (value?.endDate && !dateRange.endDate) { + setDateRange({ + ...dateRange, + endDate: new Date(value.endDate), + }); + } + }, [value, dateRange]); + const renderDay = (props: PickersDayProps) => { const isSelected = (dateRange.startDate && isSameDay(props.day, dateRange.startDate)) || @@ -250,7 +282,6 @@ const DateRangePicker = ({ : alpha(theme.palette.common.black, 0.23) }`, borderRadius: '4px', - width: 'fit-content', height: '40px', fontSize: '1rem', padding: '8px', @@ -283,7 +314,7 @@ const DateRangePicker = ({ )} - + ; +/** + * Properties for the date range filter. + */ +type DateRangeFilter = { + type: 'dateRange'; + onChange?: (value: Date | null) => void; + onDebouncedSearchChange?: (value: Date) => void; +} & FilterCommon & + DateRangePickerProps; + /** * Properties for the autocomplete filter. */ @@ -110,6 +124,7 @@ type MultiSelectFilter = { export type FilterType = | TextFilter | DateFilter + | DateRangeFilter | AutocompleteFilter | SelectFilter | MultiSelectFilter; @@ -156,6 +171,15 @@ const renderComponent = (filter: FilterType) => { /> ); + case 'dateRange': + return ( + + ); + case 'select': return ( { const onFilterChange = ( id: string, - value: string | string[] | Date | null, + value: string | string[] | Date | null | DateRange, updateFilter?: boolean, reference?: FilterDetails['reference'], referenceValidationFn?: FilterDetails['referenceValidationFn'], @@ -265,6 +266,15 @@ const FilterSubmodule = (props: Props) => { onFilterChange(id, val, true, reference, referenceValidationFn), }; + case 'dateRange': + return { + ...commonFields, + type, + value: value as DateRange, + onRangeUpdate: (dateRange: DateRange) => + onFilterChange(id, dateRange, true), + }; + default: break; } diff --git a/packages/react-material-ui/src/modules/crud/useCrudRoot.tsx b/packages/react-material-ui/src/modules/crud/useCrudRoot.tsx index 212ee6f7..ea408e42 100644 --- a/packages/react-material-ui/src/modules/crud/useCrudRoot.tsx +++ b/packages/react-material-ui/src/modules/crud/useCrudRoot.tsx @@ -2,8 +2,12 @@ import { createContext, useContext } from 'react'; import { UseTableResult } from '../../components/Table/useTable'; import { Search, SimpleFilter } from '../../components/Table/types'; import { FilterDetails } from '../../components/submodules/Filter'; +import { DateRange } from '../../components/DateRangePicker'; -export type FilterValues = Record; +export type FilterValues = Record< + string, + string | string[] | Date | null | DateRange +>; export type CrudContextProps = { /** From f867bc04b14007dfc177c82d2fd03a960446f002 Mon Sep 17 00:00:00 2001 From: Jefferson Silva Date: Tue, 24 Dec 2024 16:06:46 -0300 Subject: [PATCH 14/15] feat: add test cases to date range picker --- .../__tests__/DateRangePicker.spec.tsx | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/react-material-ui/__tests__/DateRangePicker.spec.tsx b/packages/react-material-ui/__tests__/DateRangePicker.spec.tsx index 9c0e0f8c..e0d5db73 100644 --- a/packages/react-material-ui/__tests__/DateRangePicker.spec.tsx +++ b/packages/react-material-ui/__tests__/DateRangePicker.spec.tsx @@ -4,13 +4,7 @@ import '@testing-library/jest-dom'; import React from 'react'; -import { render, fireEvent, getByRole } from '@testing-library/react'; -import { - LocalizationProvider, - MuiPickersAdapter, - MuiPickersAdapterContext, -} from '@mui/x-date-pickers'; -import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import { render } from '@testing-library/react'; import DateRangePicker from '../src/components/DateRangePicker'; describe('DateRangePicker Component', () => { @@ -46,4 +40,21 @@ describe('DateRangePicker Component', () => { expect(startDateInput).toBeInTheDocument(); expect(endDateInput).toBeInTheDocument(); }); + + test('should set input values when prop is passed', () => { + const { getByTestId } = render( + , + ); + const startDateInput = getByTestId('start-date-input'); + const endDateInput = getByTestId('end-date-input'); + + expect(startDateInput).toHaveValue('2024-12-10'); + expect(endDateInput).toHaveValue('2025-01-08'); + }); }); From a3ec8dfe8f2e6ffff39b7b2f4b347c5d5afcc294 Mon Sep 17 00:00:00 2001 From: Jefferson Silva Date: Wed, 25 Dec 2024 17:38:18 -0300 Subject: [PATCH 15/15] chore: update crud module readme --- .../src/modules/crud/README.md | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/react-material-ui/src/modules/crud/README.md b/packages/react-material-ui/src/modules/crud/README.md index 60602ef3..2eae7bf5 100644 --- a/packages/react-material-ui/src/modules/crud/README.md +++ b/packages/react-material-ui/src/modules/crud/README.md @@ -134,6 +134,12 @@ To filter table items, the `filters` prop can be passed to the `tableProps` obje ], columns: 3, }, + { + id: 'range', + label: 'Date range', + type: 'dateRange', + columns: 4, + }, ], }} /> @@ -147,9 +153,37 @@ Each filter can have the following set of attributes: - `columns`: number of columns occupied by the input in a grid of 12 columns; - `size`: overall size of the input, small or medium; - `operator`: string that describes how much of the input value should match the data value; -- `type`: the type of the filter input, one of text, autocomplete or select; +- `type`: the type of the filter input, one of text, autocomplete, select, multiSelect, date and dateRange; - `options`: array of options displayed in the autocomplete or select inputs. +### Filter types + +#### text + +Simple text field, filtering data based on a single string. + +#### autocomplete + +Select field with the ability to search the items listed based on a text field. + +### select + +Standard select field, filtering data based on a selected option. + +### multiSelect + +Another approach to the Select field, with the ability to select multiple items. + +### date + +Standard date picker, filtering data based on a timestamp value. + +### dateRange + +Another approach to the date picker, with the ability to select start and end date through a single field. + +## External search + A callback can be passed to the module props if the current state of filters is needed after each input change. This callback is called `filterCallback` and is passed outside of the `tableProps`, as follows: ```jsx