From 4d0bcf703ee48fbf0de31f0c9a7e38fc65355499 Mon Sep 17 00:00:00 2001 From: DrAcula27 Date: Wed, 8 Oct 2025 10:36:51 -0700 Subject: [PATCH 1/8] feat(issue-1868): modify DateSelector component to remove separator div and replace DateRanges with ReactDayPicker --- src/components/DateSelector/DateSelector.jsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/DateSelector/DateSelector.jsx b/src/components/DateSelector/DateSelector.jsx index a03f098af..8af9a3a78 100644 --- a/src/components/DateSelector/DateSelector.jsx +++ b/src/components/DateSelector/DateSelector.jsx @@ -12,7 +12,8 @@ import Typography from '@mui/material/Typography'; import ArrowToolTip from '@components/common/ArrowToolTip'; import options from './options'; import useStyles from './useStyles'; -import DateRanges from './DateRanges'; +import ReactDayPicker from '@components/common/ReactDayPicker'; +// import DateRanges from './DateRanges'; const dateFormat = 'YYYY-MM-DD'; @@ -71,14 +72,19 @@ function DateSelector({ range={range} onTogglePresets={closeOptionsOnDateToggle} /> -
+ {/*
*/}
- */} + From b8c734ecf13a5e1441e4ab9a820548482f837941 Mon Sep 17 00:00:00 2001 From: Danielle Date: Wed, 8 Oct 2025 10:51:28 -0700 Subject: [PATCH 2/8] feat(issue-1868): update calendar icon using provided svg --- src/components/common/DatePicker/DatePicker.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/common/DatePicker/DatePicker.jsx b/src/components/common/DatePicker/DatePicker.jsx index 5d3dc97af..5a3b135f9 100644 --- a/src/components/common/DatePicker/DatePicker.jsx +++ b/src/components/common/DatePicker/DatePicker.jsx @@ -4,7 +4,7 @@ import React, { import { connect } from 'react-redux'; import moment from 'moment'; import PropTypes from 'prop-types'; -import CalendarIcon from '@mui/icons-material/CalendarToday'; +// import CalendarIcon from '@mui/icons-material/CalendarToday'; import IconButton from '@mui/material/IconButton'; import makeStyles from '@mui/styles/makeStyles'; import useOutsideClick from '@hooks/useOutsideClick'; @@ -159,7 +159,10 @@ function DatePicker({ disableRipple size="large" > - + {/* */} + + +
{showCalendar ? ( From d0d9336396ba1bfa81c79623887569f301178ee2 Mon Sep 17 00:00:00 2001 From: Danielle Date: Wed, 29 Oct 2025 11:08:40 -0700 Subject: [PATCH 3/8] hide arrow, remove commented code --- src/components/DateSelector/DateSelector.jsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/DateSelector/DateSelector.jsx b/src/components/DateSelector/DateSelector.jsx index 8af9a3a78..88bb08508 100644 --- a/src/components/DateSelector/DateSelector.jsx +++ b/src/components/DateSelector/DateSelector.jsx @@ -13,7 +13,6 @@ import ArrowToolTip from '@components/common/ArrowToolTip'; import options from './options'; import useStyles from './useStyles'; import ReactDayPicker from '@components/common/ReactDayPicker'; -// import DateRanges from './DateRanges'; const dateFormat = 'YYYY-MM-DD'; @@ -65,22 +64,16 @@ function DateSelector({
- setExpandedMenu(!expandedMenu)} expanded={expandedMenu}> + setExpandedMenu(!expandedMenu)} expanded={expandedMenu} arrowHidden>
- {/*
*/}
- {/* */} Date: Wed, 29 Oct 2025 15:07:31 -0700 Subject: [PATCH 4/8] feat(issue-1868): implement date choosing functionality --- src/components/DateSelector/DateSelector.jsx | 77 +++++++- .../common/DatePicker/DatePicker.jsx | 166 +++++++----------- .../common/ReactDayPicker/ReactDayPicker.jsx | 66 +++++-- 3 files changed, 183 insertions(+), 126 deletions(-) diff --git a/src/components/DateSelector/DateSelector.jsx b/src/components/DateSelector/DateSelector.jsx index 88bb08508..197e63078 100644 --- a/src/components/DateSelector/DateSelector.jsx +++ b/src/components/DateSelector/DateSelector.jsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import moment from 'moment'; @@ -20,20 +20,55 @@ function DateSelector({ range, updateStartDate, updateEndDate, + startDate, + endDate, }) { const [expandedMenu, setExpandedMenu] = useState(false); + const [activeField, setActiveField] = useState(null); + const [initialStart, setInitialStart] = useState(null); + const [initialEnd, setInitialEnd] = useState(null); + const displayRef = useRef(null); + const collapseRef = useRef(null); const classes = useStyles(); + // Close on outside click and revert if the selection was not completed + useEffect(() => { + const handleDocClick = (e) => { + if (!expandedMenu) return; + if (displayRef.current && displayRef.current.contains(e.target)) return; + if (collapseRef.current && collapseRef.current.contains(e.target)) return; + + // clicked outside both display and collapse + const selectionCompleted = !range + ? (startDate && startDate !== initialStart) + : (startDate && endDate); + + if (!selectionCompleted) { + // revert to initial values captured when the collapse opened + updateStartDate(initialStart); + updateEndDate(initialEnd); + } + + setExpandedMenu(false); + setActiveField(null); + }; + + document.addEventListener('mousedown', handleDocClick); + return () => document.removeEventListener('mousedown', handleDocClick); + }, [expandedMenu, initialStart, initialEnd, startDate, endDate, range, updateStartDate, updateEndDate]); + const handleOptionSelect = optionDates => { const formattedStart = moment(optionDates[0]).format(dateFormat); const formattedEnd = moment(optionDates[1]).format(dateFormat); updateStartDate(formattedStart); updateEndDate(formattedEnd); setExpandedMenu(false); + setActiveField(null); }; const closeOptionsOnDateToggle = useCallback(() => { setExpandedMenu(false); + setActiveField(null); }, []); const { @@ -66,31 +101,55 @@ function DateSelector({ setExpandedMenu(!expandedMenu)} expanded={expandedMenu} arrowHidden> -
+
{ + // capture initial values for possible revert + setInitialStart(startDate); + setInitialEnd(endDate); + setActiveField(field); + setExpandedMenu(true); + }} + onCloseCollapse={() => { + setExpandedMenu(false); + setActiveField(null); + }} + activeField={activeField} + displayRef={displayRef} />
- +
+ { + setExpandedMenu(false); + setActiveField(null); + }} + /> +
); } +const mapStateToProps = state => ({ + startDate: state.filters.startDate, + endDate: state.filters.endDate, +}); + const mapDispatchToProps = dispatch => ({ updateStartDate: date => dispatch(reduxUpdateStartDate(date)), updateEndDate: date => dispatch(reduxUpdateEndDate(date)), }); -export default connect(null, mapDispatchToProps)(DateSelector); +export default connect(mapStateToProps, mapDispatchToProps)(DateSelector); DateSelector.propTypes = { range: PropTypes.bool, diff --git a/src/components/common/DatePicker/DatePicker.jsx b/src/components/common/DatePicker/DatePicker.jsx index 5a3b135f9..c0ac21118 100644 --- a/src/components/common/DatePicker/DatePicker.jsx +++ b/src/components/common/DatePicker/DatePicker.jsx @@ -4,10 +4,7 @@ import React, { import { connect } from 'react-redux'; import moment from 'moment'; import PropTypes from 'prop-types'; -// import CalendarIcon from '@mui/icons-material/CalendarToday'; -import IconButton from '@mui/material/IconButton'; import makeStyles from '@mui/styles/makeStyles'; -import useOutsideClick from '@hooks/useOutsideClick'; import ReactDayPicker from '@components/common/ReactDayPicker'; import { updateEndDate as reduxUpdateEndDate, @@ -25,7 +22,7 @@ const useStyles = makeStyles(theme => ({ backgroundColor: theme.palette.primary.dark, padding: 10, borderRadius: 5, - fontSize: '12px', + fontSize: '14px', color: theme.palette.text.secondaryLight, '& > div': { cursor: 'pointer', @@ -38,6 +35,11 @@ const useStyles = makeStyles(theme => ({ position: 'fixed', zIndex: 1, }, + datePicker: { + display: 'flex', + alignItems: 'center', + gap: '5px', + }, button: { padding: 0, color: theme.palette.text.dark, @@ -46,132 +48,88 @@ const useStyles = makeStyles(theme => ({ }, '& svg': { fontSize: 20, - fill: theme.palette.text.secondaryLight, + fill: theme.palette.text.textSecondaryDark, }, }, })); -const renderSelectedDays = (dates, classes, range) => { +// Modify renderSelectedDays() to populate the startDate and endDate fields (divs with the ids of 'startDate' and 'endDate'), rather than populating the spans. +/** + * renderSelectedDays + * - If fieldIndex is provided (0 = start, 1 = end) it returns a single element + * suitable for populating an individual field (shows the date or a placeholder). + * - If fieldIndex is not provided it returns the combined display (same as before): + * either "from - to", a single date, or the generic placeholder. + */ +const renderSelectedDays = (dates, classes, range, fieldIndex) => { const [from, to] = dates; const isFromSelected = Boolean(from); const isBothSelected = Boolean(from && to); - const selectedDaysElements = []; + // If caller asks for a specific field (start or end), return a single element + if (typeof fieldIndex === 'number') { + if (fieldIndex === 0) { + // Start field + if (isFromSelected) return {moment(from).format('L')}; + return Start Date; + } + if (fieldIndex === 1) { + // End field + if (to) return {moment(to).format('L')}; + return End Date; + } + } + // Backwards-compatible combined rendering if (isBothSelected) { - selectedDaysElements.push( + return [ {moment(from).format('L')}, - , {moment(to).format('L')}, - ); - return selectedDaysElements; + ]; } if (isFromSelected) { - selectedDaysElements.push( - - {' '} - {moment(from).format('L')} - {' '} - , - ); - return selectedDaysElements; + return [( + {moment(from).format('L')} + )]; } - selectedDaysElements.push( + return [( Select a date {' '} {range ? ' range' : ''} - , - ); - return selectedDaysElements; + + )]; }; function DatePicker({ - open, onTogglePresets, range, startDate, endDate, updateStartDate, updateEndDate, + // controlled by parent DateSelector + onOpenCollapse, onCloseCollapse, activeField, + range, startDate, endDate, updateStartDate, updateEndDate, displayRef, }) { - const [showCalendar, setShowCalendar] = useState(() => open); - const [initialStartDate, setInitialStartDate] = useState(startDate); - const [initialEndDate, setInitialEndDate] = useState(); const classes = useStyles(); - const ref = useRef(null); - - const closeCalendar = useCallback( - () => { - if (startDate && endDate){ - setShowCalendar(false); - } else if (startDate && !endDate){ - // The calendar was closed with an incomplete date range selection so we need to restart - // startDate and endDate to their initial values - updateStartDate(initialStartDate); - updateEndDate(initialEndDate); - setShowCalendar(false); - } else { - // This should never happen. Log a warning. - console.warn('Try to set a new date selection. Dates were in an invalid state. StartDate: ', startDate, " endDate: ", endDate); + const ref = displayRef || useRef(null); + + const handleFieldClick = (field) => { + // parent will open the SelectorBox collapse and tell ReactDayPicker which field is active + if (activeField === field) { + if (onCloseCollapse) onCloseCollapse(); + } else if (onOpenCollapse) { + onOpenCollapse(field); } - }, [startDate, endDate]); - useOutsideClick(ref, closeCalendar); - - const openCalendar = () => { - setInitialStartDate(startDate); - setInitialEndDate(endDate); - setShowCalendar(true); - } - - useEffect(() => { - setShowCalendar(false); - setInitialStartDate(startDate) - setInitialEndDate(endDate) - }, [open]); - - const getCoordinates = () => { - if (ref.current) { - const { left, top, height } = - ref.current.getClientRects()[0] ?? ref.current.getBoundingClientRect(); - const offsetFromSelectorDisplay = 2; - return { - left, - top: top + height + offsetFromSelectorDisplay, - }; - } - return {}; - }; - - const toggleCalendar = () => { - if (showCalendar) { - closeCalendar(); - } else { - openCalendar(); - } - if (onTogglePresets) onTogglePresets(); }; return ( -
-
- {renderSelectedDays([startDate, endDate], classes, range)} -
- - {/* */} +
+
handleFieldClick('start')} className={classes.datePicker}> - + - -
- {showCalendar ? ( - - ) : null} + {renderSelectedDays([startDate, endDate], classes, range, 0)} +
+ +
handleFieldClick('end')} style={{ cursor: 'pointer' }}> + {renderSelectedDays([startDate, endDate], classes, range, 1)}
); @@ -180,7 +138,10 @@ function DatePicker({ DatePicker.propTypes = { range: PropTypes.bool, open: PropTypes.bool, - onTogglePresets: PropTypes.func, + onOpenCollapse: PropTypes.func, + onCloseCollapse: PropTypes.func, + activeField: PropTypes.string, + displayRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), startDate: PropTypes.string, endDate: PropTypes.string, }; @@ -188,7 +149,10 @@ DatePicker.propTypes = { DatePicker.defaultProps = { open: false, range: false, - onTogglePresets: null, + onOpenCollapse: null, + onCloseCollapse: null, + activeField: null, + displayRef: null, startDate: null, endDate: null, }; diff --git a/src/components/common/ReactDayPicker/ReactDayPicker.jsx b/src/components/common/ReactDayPicker/ReactDayPicker.jsx index b1569fcd8..bba1e83e7 100644 --- a/src/components/common/ReactDayPicker/ReactDayPicker.jsx +++ b/src/components/common/ReactDayPicker/ReactDayPicker.jsx @@ -150,7 +150,7 @@ const useStyles = makeStyles(theme => ({ /** A wrapper around react-day-picker that selects a date range. */ function ReactDayPicker({ - range, updateStartDate, updateEndDate, startDate, endDate, + range, updateStartDate, updateEndDate, startDate, endDate, activeField, onSelectionComplete, }) { const classes = useStyles(); @@ -166,33 +166,59 @@ function ReactDayPicker({ }; const handleDayClick = day => { + const clicked = moment(day).format(INTERNAL_DATE_SPEC); + if (!range) { setFromDay(day); + if (onSelectionComplete) onSelectionComplete(); return; } - - // If both startDate and endDate were already selected. Start a new range selection. - if (startDate && endDate){ - setFromDay(day); - updateEndDate(null); - setEnteredTo(null); - // If startDate is selected and endDate is unselected, complete the range selection. - } else if (startDate && !endDate){ + + // When editing the end field + if (activeField === 'end') { + if (startDate) { + if (clicked < startDate) { + // clicked date is before current start -> make it the new start + setFromDay(day); + updateEndDate(null); + setEnteredTo(null); + return; // keep picker open for user to choose end + } + // clicked >= startDate -> set as end and complete + setToDay(day); + if (onSelectionComplete) onSelectionComplete(); + return; + } + // no startDate, treat clicked as start + setFromDay(day); + return; + } + + // Editing start (or default behavior) + if (startDate && endDate) { + // start a new range selection + setFromDay(day); + updateEndDate(null); + setEnteredTo(null); + return; + } + if (startDate && !endDate) { // If the user selects the startDate then chooses an endDate that precedes it, // swap the values of startDate and endDate - if (moment(day).format(INTERNAL_DATE_SPEC) < startDate) { + if (clicked < startDate) { const tempDate = startDate; setToDay(moment(tempDate).toDate()); setFromDay(day); updateEndDate(tempDate); setEnteredTo(moment(tempDate).toDate()); - } else { - setToDay(day); + return; } - } else { - // This should never happen. Log a warning. - console.warn('Try to set a new date selection. Dates were in an invalid state. StartDate: ', startDate, " endDate: ", endDate); - } + setToDay(day); + if (onSelectionComplete) onSelectionComplete(); + return; + } + // no start selected + setFromDay(day); }; const handleDayMouseEnter = day => { @@ -209,6 +235,13 @@ function ReactDayPicker({ const currentYear = today.getMonth(); const lastThreeMonths = new Date(currentYear, currentMonth - 3, today.getDate()); + // determine initial month to display based on which field the user is editing + const initialMonth = (activeField === 'start' && startDate) + ? moment(startDate).toDate() + : (activeField === 'end' && endDate) + ? moment(endDate).toDate() + : undefined; + return ( <> {/* */} @@ -216,6 +249,7 @@ function ReactDayPicker({ // className="Range" className={clsx(classes.root, range && classes.hasRange, !range && classes.noRange)} disabledDays={{ before: lastThreeMonths, after: today }} + month={initialMonth} numberOfMonths={1} selectedDays={[from, { from, to: enteredToDate }]} modifiers={{ start: from, end: enteredToDate }} From fae69e9ba9c507bb9974087c396c186b26d51787 Mon Sep 17 00:00:00 2001 From: Danielle Date: Wed, 29 Oct 2025 16:09:53 -0700 Subject: [PATCH 5/8] feat(issue-1868): add focus styles --- src/components/DateSelector/DateSelector.jsx | 2 +- .../common/DatePicker/DatePicker.jsx | 25 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/components/DateSelector/DateSelector.jsx b/src/components/DateSelector/DateSelector.jsx index 197e63078..22ba0bf64 100644 --- a/src/components/DateSelector/DateSelector.jsx +++ b/src/components/DateSelector/DateSelector.jsx @@ -101,7 +101,7 @@ function DateSelector({ setExpandedMenu(!expandedMenu)} expanded={expandedMenu} arrowHidden> -
+
{ diff --git a/src/components/common/DatePicker/DatePicker.jsx b/src/components/common/DatePicker/DatePicker.jsx index c0ac21118..6e95de963 100644 --- a/src/components/common/DatePicker/DatePicker.jsx +++ b/src/components/common/DatePicker/DatePicker.jsx @@ -15,17 +15,16 @@ import { const useStyles = makeStyles(theme => ({ selector: { display: 'flex', - justifyContent: 'space-between', + justifyContent: 'flex-start', alignItems: 'center', width: '100%', - maxWidth: 268, backgroundColor: theme.palette.primary.dark, - padding: 10, borderRadius: 5, fontSize: '14px', color: theme.palette.text.secondaryLight, '& > div': { cursor: 'pointer', + flex: '1 1 0%', }, }, placeholder: { @@ -39,6 +38,18 @@ const useStyles = makeStyles(theme => ({ display: 'flex', alignItems: 'center', gap: '5px', + border: `2px solid ${theme.palette.primary.dark}`, + padding: '8px 10px', + margin: 0, + boxSizing: 'border-box', + flex: '1 1 0%', + borderRadius: 5, + transition: 'border-color 150ms ease, box-shadow 150ms ease', + + '&:focus, &:focus-visible': { + borderColor: theme.palette.secondaryFocus || theme.palette.textFocus || '#87C8BC', + outline: 'none', + }, }, button: { padding: 0, @@ -120,15 +131,15 @@ function DatePicker({ }; return ( -
-
handleFieldClick('start')} className={classes.datePicker}> +
+
handleFieldClick('start')} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleFieldClick('start'); } }} className={classes.datePicker}> - + {renderSelectedDays([startDate, endDate], classes, range, 0)}
-
handleFieldClick('end')} style={{ cursor: 'pointer' }}> +
handleFieldClick('end')} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleFieldClick('end'); } }} className={classes.datePicker}> {renderSelectedDays([startDate, endDate], classes, range, 1)}
From e0949440f6b601cc3054c792ea66a68545c1fd38 Mon Sep 17 00:00:00 2001 From: Danielle Date: Wed, 29 Oct 2025 17:32:49 -0700 Subject: [PATCH 6/8] feat(issue-1868): update styles --- .../common/DatePicker/DatePicker.jsx | 2 +- .../common/ReactDayPicker/ReactDayPicker.jsx | 36 ++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/components/common/DatePicker/DatePicker.jsx b/src/components/common/DatePicker/DatePicker.jsx index 6e95de963..005f7fada 100644 --- a/src/components/common/DatePicker/DatePicker.jsx +++ b/src/components/common/DatePicker/DatePicker.jsx @@ -139,7 +139,7 @@ function DatePicker({ {renderSelectedDays([startDate, endDate], classes, range, 0)}
-
handleFieldClick('end')} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleFieldClick('end'); } }} className={classes.datePicker}> +
handleFieldClick('end')} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleFieldClick('end'); } }} className={classes.datePicker}> {renderSelectedDays([startDate, endDate], classes, range, 1)}
diff --git a/src/components/common/ReactDayPicker/ReactDayPicker.jsx b/src/components/common/ReactDayPicker/ReactDayPicker.jsx index bba1e83e7..8c713b7b8 100644 --- a/src/components/common/ReactDayPicker/ReactDayPicker.jsx +++ b/src/components/common/ReactDayPicker/ReactDayPicker.jsx @@ -10,7 +10,6 @@ import clsx from 'clsx'; import fonts from '@theme/fonts'; import colors from '@theme/colors'; import { INTERNAL_DATE_SPEC } from '../CONSTANTS'; -// import Styles from './Styles'; import WeekDay from './Weekday'; const useStyles = makeStyles(theme => ({ @@ -37,27 +36,23 @@ const useStyles = makeStyles(theme => ({ }, /* Selected range without start and end dates */ - '& .DayPicker-Day--selected:not(.DayPicker-Day--outside)': { - backgroundColor: `${theme.palette.selected.primary} !important`, + backgroundColor: `${theme.palette.primary.light} !important`, }, /* Disabled cell */ - '& .DayPicker-Day--disabled': { color: colors.textSecondaryDark, pointerEvents: 'none', }, /* Day cell hover */ - '& .DayPicker:not(.DayPicker--interactionDisabled), .DayPicker-Day:not(.DayPicker-Day--disabled):not(.DayPicker-Day--selected):not(.DayPicker-Day--outside):hover': { backgroundColor: `${theme.palette.selected.primary} !important`, borderRadius: '50% !important', }, /* General day cell */ - '& .DayPicker-Day': { borderRadius: '0 !important', display: 'block', // causing dates to run down in a single vertical column @@ -66,29 +61,49 @@ const useStyles = makeStyles(theme => ({ }, /* Today cell */ - '& .DayPicker-Day.DayPicker-Day--selected.DayPicker-Day--today, .DayPicker-Day--today': { color: theme.palette.primary.focus, }, /* Selected start and end days */ - '& .DayPicker-Day.DayPicker-Day--start.DayPicker-Day--selected, .DayPicker-Day.DayPicker-Day--end.DayPicker-Day--selected': { position: 'relative', zIndex: 1, }, + /* Selected start day */ + '& .DayPicker-Day.DayPicker-Day--start.DayPicker-Day--selected': { + color: '#0F181F !important', + '&::before': { + backgroundColor: `#87C8BC !important`, + background: `#87C8BC !important`, + border: 'none !important', + height: 'calc(100% + 0px) !important', + width: 'calc(100% + 0px) !important', + } + }, + + /* Selected end day */ + '& .DayPicker-Day.DayPicker-Day--end.DayPicker-Day--selected': { + '&::before': { + border: '1px solid #87C8BC !important', + height: 'calc(100% + 0px) !important', + width: 'calc(100% + 0px) !important', + } + }, + /* next and prev arrows */ '& .DayPicker-NavButton.DayPicker-NavButton': { top: 0, + filter: 'invert(44%) sepia(99%) saturate(1089%) hue-rotate(6deg) brightness(106%) contrast(96%)', }, '& .DayPicker-NavButton.DayPicker-NavButton--prev': { left: '1.5rem', + filter: 'invert(44%) sepia(99%) saturate(1089%) hue-rotate(6deg) brightness(106%) contrast(96%)', }, /* Rounded border with volume for selected start and end days of a range */ - '& .DayPicker-Day.DayPicker-Day--start.DayPicker-Day--selected:not(.DayPicker-Day--outside):before, .DayPicker-Day.DayPicker-Day--end.DayPicker-Day--selected:not(.DayPicker-Day--outside):before': { content: '""', position: 'absolute', @@ -105,7 +120,6 @@ const useStyles = makeStyles(theme => ({ /* Layout styling, Initial styling was table based. See docs: */ /* https://react-day-picker.js.org/examples/selected-range-enter */ - '& .DayPicker-Caption, .DayPicker-Weekdays, .DayPicker-WeekdaysRow, .DayPicker-Body': { display: 'block', width: '100%', @@ -244,9 +258,7 @@ function ReactDayPicker({ return ( <> - {/* */} Date: Wed, 29 Oct 2025 19:57:24 -0700 Subject: [PATCH 7/8] feat(issue-1868): fix stale date highlight, finalize styles --- src/components/DateSelector/DateSelector.jsx | 21 +++- .../common/DatePicker/DatePicker.jsx | 58 ++++++--- .../common/ReactDayPicker/ReactDayPicker.jsx | 116 +++++++++++++----- .../common/ReactDayPicker/Styles.jsx | 11 +- 4 files changed, 155 insertions(+), 51 deletions(-) diff --git a/src/components/DateSelector/DateSelector.jsx b/src/components/DateSelector/DateSelector.jsx index 22ba0bf64..5243f98cc 100644 --- a/src/components/DateSelector/DateSelector.jsx +++ b/src/components/DateSelector/DateSelector.jsx @@ -127,9 +127,24 @@ function DateSelector({ updateStartDate={updateStartDate} updateEndDate={updateEndDate} activeField={activeField} - onSelectionComplete={() => { - setExpandedMenu(false); - setActiveField(null); + onSelectionComplete={(selection) => { + // If ReactDayPicker provided the selection object, immediately + // ensure the redux store and our captured initial values match + // the selection. This avoids a race where the collapse closes + // before connected props reflect the new dates, which caused + // stale highlights when reopening the calendar. + if (selection && typeof selection.startDate !== 'undefined') { + if (selection.startDate !== startDate) updateStartDate(selection.startDate); + if (typeof selection.endDate !== 'undefined' && selection.endDate !== endDate) updateEndDate(selection.endDate); + setInitialStart(selection.startDate); + setInitialEnd(selection.endDate); + } + + // Defer closing so React/Redux can flush updates first. + Promise.resolve().then(() => { + setExpandedMenu(false); + setActiveField(null); + }); }} />
diff --git a/src/components/common/DatePicker/DatePicker.jsx b/src/components/common/DatePicker/DatePicker.jsx index 005f7fada..be8c0d61f 100644 --- a/src/components/common/DatePicker/DatePicker.jsx +++ b/src/components/common/DatePicker/DatePicker.jsx @@ -1,6 +1,7 @@ import React, { useRef, useState, useEffect, useCallback, } from 'react'; +import clsx from 'clsx'; import { connect } from 'react-redux'; import moment from 'moment'; import PropTypes from 'prop-types'; @@ -12,7 +13,15 @@ import { } from '@reducers/filters'; // TODO: Apply gaps (margin, padding) from theme -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles(theme => { + // local style constants to avoid repeating magic numbers + const BORDER_RADIUS = 5; + const GAP = '5px'; + const PADDING = '8px 10px'; + const BORDER_WIDTH = 2; + const ICON_SIZE = 20; + + return ({ selector: { display: 'flex', justifyContent: 'flex-start', @@ -22,7 +31,7 @@ const useStyles = makeStyles(theme => ({ borderRadius: 5, fontSize: '14px', color: theme.palette.text.secondaryLight, - '& > div': { + '& > *': { cursor: 'pointer', flex: '1 1 0%', }, @@ -37,13 +46,15 @@ const useStyles = makeStyles(theme => ({ datePicker: { display: 'flex', alignItems: 'center', - gap: '5px', - border: `2px solid ${theme.palette.primary.dark}`, - padding: '8px 10px', + gap: GAP, + backgroundColor: theme.palette.primary.dark, + border: `${BORDER_WIDTH}px solid ${theme.palette.primary.dark}`, + color: theme.palette.text.secondaryLight, + padding: PADDING, margin: 0, boxSizing: 'border-box', flex: '1 1 0%', - borderRadius: 5, + borderRadius: BORDER_RADIUS, transition: 'border-color 150ms ease, box-shadow 150ms ease', '&:focus, &:focus-visible': { @@ -51,6 +62,9 @@ const useStyles = makeStyles(theme => ({ outline: 'none', }, }, + active: { + borderColor: theme.palette.secondaryFocus || theme.palette.textFocus || '#87C8BC', + }, button: { padding: 0, color: theme.palette.text.dark, @@ -58,13 +72,13 @@ const useStyles = makeStyles(theme => ({ backgroundColor: theme.palette.primary.dark, }, '& svg': { - fontSize: 20, + fontSize: ICON_SIZE, fill: theme.palette.text.textSecondaryDark, }, }, -})); + }); +}); -// Modify renderSelectedDays() to populate the startDate and endDate fields (divs with the ids of 'startDate' and 'endDate'), rather than populating the spans. /** * renderSelectedDays * - If fieldIndex is provided (0 = start, 1 = end) it returns a single element @@ -132,16 +146,32 @@ function DatePicker({ return (
-
handleFieldClick('start')} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleFieldClick('start'); } }} className={classes.datePicker}> +
+ -
handleFieldClick('end')} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleFieldClick('end'); } }} className={classes.datePicker}> +
+
); } diff --git a/src/components/common/ReactDayPicker/ReactDayPicker.jsx b/src/components/common/ReactDayPicker/ReactDayPicker.jsx index 8c713b7b8..cb004d6ff 100644 --- a/src/components/common/ReactDayPicker/ReactDayPicker.jsx +++ b/src/components/common/ReactDayPicker/ReactDayPicker.jsx @@ -2,7 +2,7 @@ import 'react-day-picker/lib/style.css'; import moment from 'moment'; import PropTypes from 'prop-types'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import DayPicker from 'react-day-picker'; import { connect } from 'react-redux'; import makeStyles from '@mui/styles/makeStyles'; @@ -12,20 +12,34 @@ import colors from '@theme/colors'; import { INTERNAL_DATE_SPEC } from '../CONSTANTS'; import WeekDay from './Weekday'; +// style constants used within this file to reduce magic numbers +const STYLE = { + BORDER_RADIUS: '5px', + MIN_WIDTH: 277, // px + MONTH_MARGIN: '1em', + DAY_PADDING: '5px 8px', + DAY_MAX_WIDTH: 32, // px + NAV_LEFT: '1.5rem', + NAV_FILTER: 'invert(44%) sepia(99%) saturate(1089%) hue-rotate(6deg) brightness(106%) contrast(96%)', + SELECTED_BEFORE_EXTRA: '5px', + WEEK_MARGIN_BOTTOM: '8px', + WEEKDAY_FONT_SIZE: '12px', +}; + const useStyles = makeStyles(theme => ({ root: { fontFamily: fonts.family.roboto, background: theme.palette.primary.dark, - borderRadius: '5px', - minWidth: '297px', + borderRadius: STYLE.BORDER_RADIUS, + minWidth: `${STYLE.MIN_WIDTH}px`, '& .DayPicker-Months': { display: 'block', - width: '83%', + width: '100%', marginRight: 'auto', marginLeft: 'auto', }, - '& DayPicker-Month': { - margin: '0 11px', + '& .DayPicker-Month': { + margin: `${STYLE.MONTH_MARGIN} auto !important`, }, '& .DayPicker-Body': { fontSize: '0.9rem', @@ -57,7 +71,7 @@ const useStyles = makeStyles(theme => ({ borderRadius: '0 !important', display: 'block', // causing dates to run down in a single vertical column flexGrow: 1, - maxWidth: '32px', + maxWidth: `${STYLE.DAY_MAX_WIDTH}px`, }, /* Today cell */ @@ -95,12 +109,12 @@ const useStyles = makeStyles(theme => ({ /* next and prev arrows */ '& .DayPicker-NavButton.DayPicker-NavButton': { top: 0, - filter: 'invert(44%) sepia(99%) saturate(1089%) hue-rotate(6deg) brightness(106%) contrast(96%)', + filter: STYLE.NAV_FILTER, }, '& .DayPicker-NavButton.DayPicker-NavButton--prev': { - left: '1.5rem', - filter: 'invert(44%) sepia(99%) saturate(1089%) hue-rotate(6deg) brightness(106%) contrast(96%)', + left: STYLE.NAV_LEFT, + filter: STYLE.NAV_FILTER, }, /* Rounded border with volume for selected start and end days of a range */ @@ -108,8 +122,8 @@ const useStyles = makeStyles(theme => ({ content: '""', position: 'absolute', border: '2px solid white', - height: 'calc(100% + 5px)', - width: 'calc(100% + 5px)', + height: `calc(100% + ${STYLE.SELECTED_BEFORE_EXTRA})`, + width: `calc(100% + ${STYLE.SELECTED_BEFORE_EXTRA})`, borderRadius: '50%', top: '50%', left: '50%', @@ -126,11 +140,11 @@ const useStyles = makeStyles(theme => ({ }, '& .DayPicker-Week .DayPicker-Day': { - padding: '5px 8px', + padding: STYLE.DAY_PADDING, }, '& .DayPicker-Week': { - marginBottom: '8px', + marginBottom: STYLE.WEEK_MARGIN_BOTTOM, }, '& .DayPicker-Week, .DayPicker-WeekdaysRow': { @@ -141,7 +155,7 @@ const useStyles = makeStyles(theme => ({ '& .DayPicker-Weekday': { display: 'block', - fontSize: '12px', + fontSize: STYLE.WEEKDAY_FONT_SIZE, }, }, hasRange: { @@ -162,7 +176,7 @@ const useStyles = makeStyles(theme => ({ }, })); -/** A wrapper around react-day-picker that selects a date range. */ +/* A wrapper around react-day-picker that selects a date range. */ function ReactDayPicker({ range, updateStartDate, updateEndDate, startDate, endDate, activeField, onSelectionComplete, }) { @@ -170,6 +184,20 @@ function ReactDayPicker({ // enteredTo represents the day that the user is currently hovering over. const [enteredTo, setEnteredTo] = useState(endDate); + // pendingSelection holds a selection object { startDate, endDate } that + // reflects the user's most recent click before Redux-connected props update. + // This lets the calendar highlight the newly-selected day immediately. + const [pendingSelection, setPendingSelection] = useState(null); + + // Clear pendingSelection when the connected props catch up to it. + useEffect(() => { + if (!pendingSelection) return; + const pendingStart = pendingSelection.startDate || null; + const pendingEnd = typeof pendingSelection.endDate !== 'undefined' ? pendingSelection.endDate : null; + if (pendingStart === startDate && pendingEnd === endDate) { + setPendingSelection(null); + } + }, [startDate, endDate, pendingSelection]); const setFromDay = day => { updateStartDate(moment(day).format(INTERNAL_DATE_SPEC)); @@ -184,7 +212,7 @@ function ReactDayPicker({ if (!range) { setFromDay(day); - if (onSelectionComplete) onSelectionComplete(); + if (onSelectionComplete) Promise.resolve().then(() => onSelectionComplete()); return; } @@ -192,25 +220,35 @@ function ReactDayPicker({ if (activeField === 'end') { if (startDate) { if (clicked < startDate) { - // clicked date is before current start -> make it the new start + // reflect immediately in UI + setPendingSelection({ startDate: clicked, endDate: null }); setFromDay(day); updateEndDate(null); setEnteredTo(null); return; // keep picker open for user to choose end } - // clicked >= startDate -> set as end and complete - setToDay(day); - if (onSelectionComplete) onSelectionComplete(); + // clicked >= startDate -> set as end and complete + + // show immediately + setPendingSelection({ startDate: startDate, endDate: clicked }); + setToDay(day); + if (onSelectionComplete) Promise.resolve().then(() => onSelectionComplete({ startDate, endDate: clicked })); return; } - // no startDate, treat clicked as start - setFromDay(day); - return; + // no startDate, treat clicked as start + + // reflect immediately + const startStr = moment(day).format(INTERNAL_DATE_SPEC); + setPendingSelection({ startDate: startStr, endDate: null }); + setFromDay(day); + if (onSelectionComplete) Promise.resolve().then(() => onSelectionComplete({ startDate: startStr, endDate: null })); + return; } // Editing start (or default behavior) if (startDate && endDate) { // start a new range selection + setPendingSelection({ startDate: moment(day).format(INTERNAL_DATE_SPEC), endDate: null }); setFromDay(day); updateEndDate(null); setEnteredTo(null); @@ -220,18 +258,24 @@ function ReactDayPicker({ // If the user selects the startDate then chooses an endDate that precedes it, // swap the values of startDate and endDate if (clicked < startDate) { + const tempDate = startDate; + // reflect swap immediately + setPendingSelection({ startDate: clicked, endDate: tempDate }); setToDay(moment(tempDate).toDate()); setFromDay(day); updateEndDate(tempDate); setEnteredTo(moment(tempDate).toDate()); return; } - setToDay(day); - if (onSelectionComplete) onSelectionComplete(); + + setPendingSelection({ startDate: startDate, endDate: clicked }); + setToDay(day); + if (onSelectionComplete) Promise.resolve().then(() => onSelectionComplete({ startDate, endDate: clicked })); return; } // no start selected + setPendingSelection({ startDate: moment(day).format(INTERNAL_DATE_SPEC), endDate: null }); setFromDay(day); }; @@ -242,8 +286,24 @@ function ReactDayPicker({ } }; - const from = moment(startDate).toDate(); - const enteredToDate = moment(enteredTo).toDate(); + // Use pendingSelection when present so the calendar highlights the user's + // choice immediately even before Redux props propagate. + const effectiveStart = pendingSelection && pendingSelection.startDate ? pendingSelection.startDate : startDate; + // For effectiveEnd, prefer (in order): pendingSelection.endDate, redux endDate prop, + // then the hovered enteredTo (used when user has selected a start but not an end). + let effectiveEnd; + if (pendingSelection && typeof pendingSelection.endDate !== 'undefined') { + effectiveEnd = pendingSelection.endDate; + } else if (endDate) { + effectiveEnd = endDate; + } else { + effectiveEnd = enteredTo; + } + + const from = effectiveStart ? moment(effectiveStart).toDate() : undefined; + const enteredToDate = effectiveEnd ? moment(effectiveEnd).toDate() : undefined; + useEffect(() => { + }, [pendingSelection, effectiveStart, effectiveEnd, startDate, endDate, enteredTo]); const today = new Date(); const currentMonth = today.getFullYear(); const currentYear = today.getMonth(); diff --git a/src/components/common/ReactDayPicker/Styles.jsx b/src/components/common/ReactDayPicker/Styles.jsx index 91b8c3ab5..f7c2fa45a 100644 --- a/src/components/common/ReactDayPicker/Styles.jsx +++ b/src/components/common/ReactDayPicker/Styles.jsx @@ -2,8 +2,7 @@ import React from 'react'; import { useTheme } from '@mui/material'; import PropTypes from 'prop-types'; -// This component is currently unused and has been ported to MUI makeStyles in ReactDayPicker. -// Keeping around for reference only. +// This component is used in `src/components/common/ReactDayPicker.jsx` function Styles({ range }) { const theme = useTheme(); @@ -41,14 +40,14 @@ function Styles({ range }) { text-decoration: underline; } - /* Selected range without start and end dates */ + /* Selected range without start and end dates */ .Range .DayPicker-Day--selected:not(.DayPicker-Day--outside) { background-color: ${theme.palette.selected.primary} !important; } /* Disabled cell */ - + .Range .DayPicker-Day--disabled { color: #a8a8a8; } @@ -122,7 +121,7 @@ function Styles({ range }) { border-top-left-radius: 50% !important; border-bottom-left-radius: 50% !important; } - + ` : ` .Range .DayPicker-Day.DayPicker-Day--start.DayPicker-Day--selected{ @@ -133,7 +132,7 @@ function Styles({ range }) { /* Layout styling, Initial styling was table based. See docs: */ - /* https://react-day-picker.js.org/examples/selected-range-enter */ + /* https://react-day-picker.js.org/examples/selected-range-enter */ .Range .DayPicker-Caption, .Range .DayPicker-Weekdays, From c829999056e0308111224b14dd819c5cac0a1d88 Mon Sep 17 00:00:00 2001 From: Danielle Date: Wed, 29 Oct 2025 23:30:27 -0700 Subject: [PATCH 8/8] feat(issue-1868): autofocus on end date field when user selects start date --- src/components/DateSelector/DateSelector.jsx | 29 +++++++- .../common/DatePicker/DatePicker.jsx | 5 +- .../common/ReactDayPicker/ReactDayPicker.jsx | 74 +++++++++++++------ 3 files changed, 85 insertions(+), 23 deletions(-) diff --git a/src/components/DateSelector/DateSelector.jsx b/src/components/DateSelector/DateSelector.jsx index 5243f98cc..9930876dc 100644 --- a/src/components/DateSelector/DateSelector.jsx +++ b/src/components/DateSelector/DateSelector.jsx @@ -29,6 +29,7 @@ function DateSelector({ const [initialEnd, setInitialEnd] = useState(null); const displayRef = useRef(null); const collapseRef = useRef(null); + const endDateBtnRef = useRef(null); const classes = useStyles(); // Close on outside click and revert if the selection was not completed @@ -117,6 +118,7 @@ function DateSelector({ }} activeField={activeField} displayRef={displayRef} + endDateBtnRef={endDateBtnRef} />
@@ -140,7 +142,32 @@ function DateSelector({ setInitialEnd(selection.endDate); } - // Defer closing so React/Redux can flush updates first. + // If the selection contains only a start date (user picked Start + // and still needs to pick an End), keep the collapse open and + // move focus to the End field so it's clear the user should pick + // an end date next. + if (selection && selection.startDate && (typeof selection.endDate === 'undefined' || selection.endDate === null)) { + // ensure the store has the new startDate (done above) and + // mark the active field as 'end' + setActiveField('end'); + setExpandedMenu(true); + + const focusEndDate = () => { + const endBtn = endDateBtnRef.current || displayRef.current?.querySelector?.('#endDate'); + if (endBtn) { + endBtn.focus(); + } + } + + // move focus to the End button using requestAnimationFrame for reliable DOM updates + requestAnimationFrame(() => { + requestAnimationFrame(focusEndDate); + }); + return; + } + + // Otherwise (selection includes end or is complete) close the + // collapse after letting React/Redux flush updates. Promise.resolve().then(() => { setExpandedMenu(false); setActiveField(null); diff --git a/src/components/common/DatePicker/DatePicker.jsx b/src/components/common/DatePicker/DatePicker.jsx index be8c0d61f..b975eccb1 100644 --- a/src/components/common/DatePicker/DatePicker.jsx +++ b/src/components/common/DatePicker/DatePicker.jsx @@ -130,7 +130,7 @@ const renderSelectedDays = (dates, classes, range, fieldIndex) => { function DatePicker({ // controlled by parent DateSelector onOpenCollapse, onCloseCollapse, activeField, - range, startDate, endDate, updateStartDate, updateEndDate, displayRef, + range, startDate, endDate, updateStartDate, updateEndDate, displayRef, endDateBtnRef, }) { const classes = useStyles(); const ref = displayRef || useRef(null); @@ -162,6 +162,7 @@ function DatePicker({