Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
396 changes: 100 additions & 296 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 1 addition & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
"@types/json-schema": "^7.0.15",
"@types/react": "17.0.39",
"@types/react-csv": "^1.1.10",
"@types/react-dates": "^21.8.6",
"@types/react-dom": "17.0.13",
"@types/react-router-dom": "^5.3.3",
"@typescript-eslint/eslint-plugin": "8.3.0",
Expand Down Expand Up @@ -131,7 +130,7 @@
"qrcode.react": "^4.2.0",
"react-canvas-confetti": "^2.0.7",
"react-csv": "^2.2.2",
"react-dates": "^21.8.0",
"react-day-picker": "^9.11.2",
"react-draggable": "^4.4.5",
"react-international-phone": "^4.5.0",
"react-virtualized-sticky-tree": "^3.0.0-beta18",
Expand All @@ -140,10 +139,6 @@
},
"overrides": {
"cross-spawn": "^7.0.5",
"react-dates": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"vite-plugin-svgr": {
"vite": "6.3.5"
},
Expand Down
78 changes: 78 additions & 0 deletions src/Shared/Components/DatePicker/DateTimePicker.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
.date-time-picker {
--rdp-accent-color: var(--B500);
--rdp-accent-background-color: var(--N700);
--rdp-day_button-border-radius: 0;
--rdp-day_button-border: 2px solid transparent;
--rdp-selected-border: none;
--rdp-today-color: var(--rdp-accent-color);
--rdp-range_middle-background-color: var(--O500);
--rdp-range_middle-color: inherit;
--rdp-range_start-color: var(--N100);
--rdp-range_start-background: linear-gradient(var(--O200), transparent 50%, var(--rdp-range_middle-background-color) 50%);
--rdp-range_start-date-background-color: var(--rdp-accent-color);
--rdp-range_end-background: linear-gradient(var(--O200), var(--rdp-range_middle-background-color) 50%, transparent 50%);
--rdp-range_end-color: var(--N100);
--rdp-range_end-date-background-color: var(--rdp-accent-color);
--rdp-week_number-border-radius: 100%;
--rdp-week_number-border: 2px solid var(--R100);

.rdp-day {
border: 1px solid var(--border-primary);

&_button {
color: var(--N900);
font-size: 14px;
font-weight: 400;
line-height: 20px;
border-radius: 0;
width: 100%;
height: 100%;

&:hover {
background: var(--bg-hover);
}
}
}

.rdp-day.rdp-hidden {
border: none;
}

.rdp-day.rdp-selected {
.rdp-day_button {
background: var(--B100);
color: var(--B500);
font-size: 14px;
font-weight: 600;
line-height: 20px;
border: none;
border-radius: 0;
width: 100%;
height: 100%;
}
}

.rdp-day.rdp-disabled {
.rdp-day_button {
opacity: 0.5;
cursor: not-allowed;
}
}

.rdp-caption_label {
color: var(--N900);
font-size: 16px;
font-weight: 600;
}

.rdp-month_grid {
margin-top: 12px;
}

.rdp-weekday {
color: var(--N700);
font-size: 12px;
font-weight: 600;
line-height: 20px;
}
}
247 changes: 190 additions & 57 deletions src/Shared/Components/DatePicker/DateTimePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,43 @@
* limitations under the License.
*/

import { useEffect, useState } from 'react'
import { SingleDatePicker } from 'react-dates'
import CustomizableCalendarDay from 'react-dates/esm/components/CustomizableCalendarDay'
import { useMemo, useRef } from 'react'
import { DateRange, DayPicker, OnSelectHandler } from 'react-day-picker'
import { SelectInstance } from 'react-select'
import moment from 'moment'
import dayjs from 'dayjs'

import { ReactComponent as CalendarIcon } from '@Icons/ic-calendar.svg'
import { ReactComponent as ClockIcon } from '@Icons/ic-clock.svg'
import { ReactComponent as ICWarning } from '@Icons/ic-warning.svg'
import { DATE_TIME_FORMATS } from '@Common/Constants'
import { ComponentSizeType } from '@Shared/constants'
import { getUniqueId } from '@Shared/Helpers'

import 'react-dates/initialize'

import { DATE_TIME_FORMATS } from '../../../Common'
import { useFocusTrapControl } from '../DTFocusTrap'
import { SelectPicker } from '../SelectPicker'
import { customDayStyles, DATE_PICKER_IDS, DATE_PICKER_PLACEHOLDER } from './constants'
import { DateTimePickerProps } from './types'
import { Icon } from '../Icon'
import { Popover, usePopover } from '../Popover'
import { SelectPicker, SelectPickerOptionType } from '../SelectPicker'
import { DATE_PICKER_CUSTOM_COMPONENTS, DATE_PICKER_IDS, DATE_PICKER_PLACEHOLDER } from './constants'
import { DateTimePickerProps, UpdateDateRangeType } from './types'
import { DEFAULT_TIME_OPTIONS, getTimeValue, updateDate, updateTime } from './utils'

import './datePicker.scss'
import 'react-dates/lib/css/_datepicker.css'
import 'react-day-picker/style.css'
import './DateTimePicker.scss'

const isDateUpdateRange = (
isRange: boolean,
handler: DateTimePickerProps['onChange'],
): handler is UpdateDateRangeType => isRange

const getTodayDate = (): Date => {
const today = dayjs()
return today.toDate()
}

const DateTimePicker = ({
date: dateObject = new Date(),
date: dateObject = getTodayDate(),
dateRange = {
from: getTodayDate(),
to: getTodayDate(),
},
Comment on lines +50 to +53
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The default value for dateRange is set to { from: getTodayDate(), to: getTodayDate() }, which means both start and end dates default to today. This could be confusing for range pickers where typically the end date might be undefined initially. Consider using { from: getTodayDate(), to: undefined } or making the default value conditional based on whether it's a range picker.

Copilot uses AI. Check for mistakes.
onChange,
timePickerProps = {} as SelectInstance,
disabled,
Expand All @@ -47,40 +59,170 @@ const DateTimePicker = ({
required,
hideTimeSelect = false,
readOnly = false,
isTodayBlocked = false,
dataTestIdForTime = DATE_PICKER_IDS.TIME,
dataTestidForDate = DATE_PICKER_IDS.DATE,
openDirection = 'down',
error = '',
isRangePicker = false,
isTodayBlocked = false,
blockPreviousDates = true,
isOutsideRange,
rangeShortcutOptions,
}: DateTimePickerProps) => {
const calendarPopoverId = useRef<string>(getUniqueId())

const { open, overlayProps, popoverProps, triggerProps, scrollableRef } = usePopover({
id: `date-time-picker-popover-${calendarPopoverId.current}`,
alignment: 'end',
variant: 'overlay',
})

const parsedPopoverProps = useMemo<typeof popoverProps>(
() => ({
...popoverProps,
className: `${popoverProps.className} w-100 p-12 date-time-picker`,
style: {
...popoverProps.style,
maxWidth: 'none',
},
}),
[popoverProps],
)

const parsedOverlayProps = useMemo<typeof overlayProps>(
() => ({
...overlayProps,
initialFocus: false,
}),
[overlayProps],
)

const time = getTimeValue(dateObject)
const selectedTimeOption = DEFAULT_TIME_OPTIONS.find((option) => option.value === time) ?? DEFAULT_TIME_OPTIONS[0]
const [isFocused, setFocused] = useState(false)
const handleTimeChange = (option: SelectPickerOptionType<string>) => {
if (isDateUpdateRange(isRangePicker, onChange)) {
return
}
onChange(updateTime(dateObject, option.value).value)
}

const { disableFocusTrap, resumeFocusTrap } = useFocusTrapControl()
const handleDateRangeChange = (range: DateRange) => {
if (isDateUpdateRange(isRangePicker, onChange)) {
const fromDate = range.from ? range.from : new Date()
const toDate = range.to ? range.to : undefined

const handleFocusChange = ({ focused }) => {
setFocused(focused)
onChange({
from: fromDate,
to: toDate,
})
Comment on lines +107 to +115
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When range.from is falsy in range selection mode, the code defaults to new Date(). This means that if a user clears the start date, it will automatically default to today instead of allowing an empty/undefined state. Consider whether this is the intended behavior or if it should allow undefined for from.

Copilot uses AI. Check for mistakes.
}
}
const handleDateChange = (event) => {
onChange(updateDate(dateObject, event?.toDate()))

const handleRangeSelect: OnSelectHandler<DateRange> = (range) => {
handleDateRangeChange(range)
}

const handleTimeChange = (option) => {
onChange(updateTime(dateObject, option.value).value)
const getRangeUpdateHandler = (range: DateRange) => () => {
handleDateRangeChange(range)
}

const today = moment()
// Function to disable dates including today and all past dates
const isDayBlocked = (day) => isTodayBlocked && !day.isAfter(today)
const handleSingleDateSelect: OnSelectHandler<Date> = (date) => {
if (!isDateUpdateRange(isRangePicker, onChange)) {
const updatedDate = date ? updateDate(dateObject, date) : null
onChange(updatedDate)
}
}
Comment on lines +127 to +132
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handleSingleDateSelect function might pass null to onChange when no date is selected, but the UpdateSingleDateType type signature expects a non-nullable Date parameter. This could cause a type error. Consider updating the type to UpdateSingleDateType = (date: Date | null) => void or ensure that null dates are properly handled.

Copilot uses AI. Check for mistakes.

useEffect(() => {
if (isFocused) {
disableFocusTrap()
return
const getDisabledState = () => {
if (readOnly) {
return true
}
resumeFocusTrap()
}, [isFocused])

const today = getTodayDate()
today.setHours(0, 0, 0, 0)
Comment on lines +139 to +140
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getTodayDate() function is called and then mutated with setHours(0, 0, 0, 0). This mutates a fresh Date object on every call to getDisabledState(). Consider creating the normalized today date once using useMemo to avoid unnecessary object mutations on every render when getDisabledState is called.

Copilot uses AI. Check for mistakes.

const isOutsideRangeFn = isOutsideRange || (() => false)

if (isTodayBlocked) {
return (date: Date) => date <= today || isOutsideRangeFn(date)
}

if (blockPreviousDates) {
return (date: Date) => date < today || isOutsideRangeFn(date)
}

return isOutsideRangeFn
}

const renderDatePicker = () => {
if (isRangePicker) {
return (
<div className="flexbox">
{!!rangeShortcutOptions?.length && (
<div className="flexbox-col py-20 px-16 w-200">
{rangeShortcutOptions.map(({ label: optionLabel, value, onClick }) => (
<button
type="button"
key={optionLabel}
className="bg__hover cn-9 dc__tab-focus dc__align-left dc__transparent p-8 br-4"
onClick={onClick || getRangeUpdateHandler(value)}
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shortcut buttons in the range picker lack accessible labels. Each button should have an aria-label attribute to describe its purpose for screen reader users, especially since the buttons only contain text that might not be self-descriptive in all contexts.

Suggested change
onClick={onClick || getRangeUpdateHandler(value)}
onClick={onClick || getRangeUpdateHandler(value)}
aria-label={`Select range: ${optionLabel}`}

Copilot uses AI. Check for mistakes.
>
{optionLabel}
</button>
))}
</div>
)}

<DayPicker
mode="range"
navLayout="around"
selected={{
from: dateRange.from,
to: dateRange.to,
}}
onSelect={handleRangeSelect}
disabled={getDisabledState()}
components={DATE_PICKER_CUSTOM_COMPONENTS}
/>
</div>
)
}

return (
<DayPicker
mode="single"
navLayout="around"
selected={dateObject}
onSelect={handleSingleDateSelect}
disabled={getDisabledState()}
components={DATE_PICKER_CUSTOM_COMPONENTS}
/>
)
}

const renderInputLabel = () => {
if (isRangePicker) {
const fromDate = dateRange.from ? dayjs(dateRange.from).format(DATE_TIME_FORMATS.DD_MMM_YYYY) : ''
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] When dateRange.from is falsy in range picker mode, the fromDate will be an empty string, resulting in a label like - .... This could be visually awkward. Consider displaying a more user-friendly placeholder text like "Select start date - ..." when from is not set.

Suggested change
const fromDate = dateRange.from ? dayjs(dateRange.from).format(DATE_TIME_FORMATS.DD_MMM_YYYY) : ''
const fromDate = dateRange.from ? dayjs(dateRange.from).format(DATE_TIME_FORMATS.DD_MMM_YYYY) : 'Select start date'

Copilot uses AI. Check for mistakes.
const toDate = dateRange.to ? dayjs(dateRange.to).format(DATE_TIME_FORMATS.DD_MMM_YYYY) : '...'

return `${fromDate} - ${toDate}`
}

return dayjs(dateObject).format(DATE_TIME_FORMATS.DD_MMM_YYYY)
}

const triggerElement = (
<SelectPicker
inputId="date-picker-input"
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The label element uses htmlFor={id} but the actual input element inside SelectPicker uses inputId="date-picker-input" (a hardcoded value). This means the label won't properly associate with the input when clicked. Either use the id prop for the inputId or ensure they match for proper accessibility.

Copilot uses AI. Check for mistakes.
placeholder={DATE_PICKER_PLACEHOLDER.DATE}
menuIsOpen={false}
isSearchable={false}
value={{
label: renderInputLabel(),
value: '',
startIcon: <Icon name="ic-calendar" color={null} />,
}}
size={ComponentSizeType.large}
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The disabled prop is not passed to the date picker trigger element (SelectPicker). When the DateTimePicker is disabled, only the time picker reflects this state (line 257), but the date picker input appears enabled. Consider passing isDisabled={disabled} to the trigger SelectPicker to ensure consistent disabled state across both inputs.

Suggested change
size={ComponentSizeType.large}
size={ComponentSizeType.large}
isDisabled={disabled}

Copilot uses AI. Check for mistakes.
/>
)

return (
<div className="date-time-picker">
Expand All @@ -90,28 +232,19 @@ const DateTimePicker = ({
</label>
)}
<div className="flex left dc__gap-8">
<SingleDatePicker
id={id}
placeholder="Select date"
date={moment(dateObject)}
onDateChange={handleDateChange}
focused={isFocused}
onFocusChange={handleFocusChange}
numberOfMonths={1}
openDirection={openDirection}
renderCalendarDay={(props) => <CustomizableCalendarDay {...props} {...customDayStyles} />}
hideKeyboardShortcutsPanel
withFullScreenPortal={false}
orientation="horizontal"
readOnly={readOnly || false}
customInputIcon={<CalendarIcon className="icon-dim-20 scn-6" />}
inputIconPosition="after"
displayFormat={DATE_TIME_FORMATS.DD_MMM_YYYY}
data-testid={dataTestidForDate}
isDayBlocked={isDayBlocked}
disabled={disabled}
appendToBody
/>
<Popover
open={open}
overlayProps={parsedOverlayProps}
popoverProps={parsedPopoverProps}
triggerProps={triggerProps}
triggerElement={triggerElement}
buttonProps={null}
>
<div ref={scrollableRef} className="p-4">
{renderDatePicker()}
</div>
</Popover>

{!hideTimeSelect && (
<div className="dc__no-shrink">
<SelectPicker
Expand Down
Loading