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
78 changes: 59 additions & 19 deletions apps/site/src/demos/DateRangePickerDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,10 @@ const FilterIcon = () => (
)

const DateRangePickerDemo = () => {
// Main playground state
const [playgroundRange, setPlaygroundRange] = useState<DateRange>({
// Main playground state (undefined = no selection, tests Issue #1187)
const [playgroundRange, setPlaygroundRange] = useState<
DateRange | undefined
>({
startDate: new Date(),
endDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
})
Expand Down Expand Up @@ -756,7 +758,7 @@ const DateRangePickerDemo = () => {
</h3>

<div className="p-6 bg-white border border-gray-200 rounded-lg overflow-hidden">
<div className="overflow-hidden">
<div className="overflow-hidden space-y-3">
<DateRangePicker
value={playgroundRange}
onChange={handlePlaygroundRangeChange}
Expand Down Expand Up @@ -786,6 +788,32 @@ const DateRangePickerDemo = () => {
formatConfig={getFormatConfig()}
triggerConfig={getTriggerConfig()}
/>
<div className="flex gap-2 flex-wrap">
<button
type="button"
onClick={() =>
setPlaygroundRange(undefined)
}
className="px-3 py-1.5 text-sm font-medium rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
>
Clear (value = undefined)
</button>
<button
type="button"
onClick={() =>
setPlaygroundRange({
startDate: new Date(),
endDate: new Date(
Date.now() +
7 * 24 * 60 * 60 * 1000
),
})
}
className="px-3 py-1.5 text-sm font-medium rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
>
Set range (today + 7 days)
</button>
</div>
</div>
</div>

Expand All @@ -795,26 +823,38 @@ const DateRangePickerDemo = () => {
Current Selection
</h4>
<div className="text-sm text-gray-600 space-y-1">
<div>
<strong>Start:</strong>{' '}
{playgroundRange.startDate.toLocaleString()}
</div>
{playgroundRange.endDate && (
{playgroundRange ? (
<>
<div>
<strong>End:</strong>{' '}
{playgroundRange.endDate.toLocaleString()}
</div>
<div>
<strong>Duration:</strong>{' '}
{Math.ceil(
(playgroundRange.endDate.getTime() -
playgroundRange.startDate.getTime()) /
(1000 * 60 * 60 * 24)
)}{' '}
days
<strong>Start:</strong>{' '}
{playgroundRange.startDate.toLocaleString()}
</div>
{playgroundRange.endDate && (
<>
<div>
<strong>End:</strong>{' '}
{playgroundRange.endDate.toLocaleString()}
</div>
<div>
<strong>Duration:</strong>{' '}
{Math.ceil(
(playgroundRange.endDate.getTime() -
playgroundRange.startDate.getTime()) /
(1000 *
60 *
60 *
24)
)}{' '}
days
</div>
</>
)}
</>
) : (
<div>
<strong>Value:</strong> undefined
(placeholder shown)
</div>
)}
</div>
</div>
Expand Down
58 changes: 26 additions & 32 deletions packages/blend/lib/components/DateRangePicker/DateRangePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,6 @@ const DateInputsSection: React.FC<DateInputsSectionProps> = ({
value={startDate || ''}
onChange={onStartDateChange}
error={!startDateValidation.isValid}
errorMessage={
!startDateValidation.isValid
? startDateValidation.message
: undefined
}
size={TextInputSize.SMALL}
autoFocus={false}
aria-invalid={!startDateValidation.isValid}
Expand All @@ -165,11 +160,11 @@ const DateInputsSection: React.FC<DateInputsSectionProps> = ({
</Block>

{!isSingleDatePicker &&
selectedRange &&
(!allowSingleDateSelection ||
(allowSingleDateSelection &&
selectedRange.startDate.getTime() !==
selectedRange.endDate?.getTime())) && (
!selectedRange ||
!selectedRange.endDate ||
selectedRange.startDate.getTime() !==
selectedRange.endDate.getTime()) && (
<Block
display="flex"
gap={
Expand Down Expand Up @@ -218,11 +213,6 @@ const DateInputsSection: React.FC<DateInputsSectionProps> = ({
value={endDate || ''}
onChange={onEndDateChange}
error={!endDateValidation.isValid}
errorMessage={
!endDateValidation.isValid
? endDateValidation.message
: undefined
}
size={TextInputSize.SMALL}
autoFocus={false}
aria-invalid={
Expand Down Expand Up @@ -443,14 +433,9 @@ const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>(

const [startDateValidation, setStartDateValidation] =
useState<DateValidationResult>({ isValid: true, error: 'none' })
// Empty end date when no selection is neutral (no error styling); Apply stays disabled via selectedRange check
const [endDateValidation, setEndDateValidation] =
useState<DateValidationResult>({
isValid: isSingleDatePicker || !!selectedRange?.endDate,
error:
isSingleDatePicker || selectedRange?.endDate
? 'none'
: 'invalid-date',
})
useState<DateValidationResult>({ isValid: true, error: 'none' })

const today = getTodayInTimezone(timezone)

Expand Down Expand Up @@ -529,35 +514,44 @@ const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>(
: DateRangePreset.CUSTOM
)
setStartDate(
dateRangeObj &&
formatDate(dateRangeObj.startDate, dateFormat, timezone)
dateRangeObj
? formatDate(
dateRangeObj.startDate,
dateFormat,
timezone
)
: undefined
)
if (dateRangeObj && dateRangeObj.endDate) {
if (dateRangeObj?.endDate) {
setEndDate(
formatDate(dateRangeObj.endDate, dateFormat, timezone)
)
} else {
setEndDate(undefined)
}
setStartTime(
dateRangeObj &&
formatDate(dateRangeObj.startDate, 'HH:mm', timezone)
dateRangeObj
? formatDate(dateRangeObj.startDate, 'HH:mm', timezone)
: undefined
)
if (dateRangeObj && dateRangeObj.endDate) {
if (dateRangeObj?.endDate) {
setEndTime(
formatDate(dateRangeObj.endDate, 'HH:mm', timezone)
)
} else {
setEndTime(undefined)
}
setStartDateValidation({ isValid: true, error: 'none' })
setEndDateValidation({
isValid: true,
error: 'none',
})
// Empty end date is neutral (no red error); Apply stays disabled until both dates selected
setEndDateValidation({ isValid: true, error: 'none' })
},
[timezone, dateFormat]
[timezone, dateFormat, isSingleDatePicker]
)

useEffect(() => {
if (!value) {
lastExternalValueRef.current = null
resetValues(undefined)
return
Comment on lines 551 to 555
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The new value === undefined reset behavior is a regression-prone path (and fixes Issue #1187). There are existing DateRangePicker test suites; please add a regression test that renders with value={undefined} and asserts the trigger/input shows the placeholder and that today is not marked as selected by default.

Copilot uses AI. Check for mistakes.
}

Expand Down
14 changes: 5 additions & 9 deletions packages/blend/lib/components/DateRangePicker/TimeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,15 +278,11 @@ const TimeSelector = forwardRef<HTMLDivElement, TimeSelectorProps>(

const handleOpenChange = useCallback(
(open: boolean) => {
const isDateSelected =
isStart && selectedRange
? selectedRange.startDate
: selectedRange?.endDate
if (!isProcessingSelection && isDateSelected) {
if (!isProcessingSelection) {
setIsOpen(open)
}
},
[isProcessingSelection, selectedRange, isStart]
[isProcessingSelection]
)

const handleInputChange = useCallback(
Expand Down Expand Up @@ -402,9 +398,9 @@ const TimeSelector = forwardRef<HTMLDivElement, TimeSelectorProps>(
id={id}
type="text"
disabled={
isStart && selectedRange
? !selectedRange.startDate
: !selectedRange?.endDate
isStart
? false
: !!selectedRange && !selectedRange.endDate
}
Comment on lines 398 to 404
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

With the new disabled logic, both time inputs can become enabled even when no corresponding date is selected (e.g. selectedRange is undefined makes the end-time input enabled too). In that state, TimeSelector can accept/format a time, but the parent handlers bail out when selectedRange is missing, so the UI can show a time that isn't actually stored/applied (and Apply remains disabled). Consider restoring the previous gating (disable and prevent menu opening until the relevant date exists) or updating the parent to persist time independently; also ensure TimeSelector clears its internal inputValue when value becomes empty to avoid stale times after clearing.

Copilot uses AI. Check for mistakes.
value={inputValue}
onChange={handleInputChange}
Expand Down
13 changes: 8 additions & 5 deletions packages/blend/lib/components/DateRangePicker/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1829,7 +1829,7 @@ export const handleDateInputChange = (
value: string,
dateFormat: string,
currentRange: DateRange | undefined,
timeValue?: string,
_timeValue?: string,
isStartDate: boolean = true,
disableFutureDates: boolean = false,
disablePastDates: boolean = false,
Expand All @@ -1852,8 +1852,11 @@ export const handleDateInputChange = (

if (validation.isValid && isDateInputComplete(formattedValue, dateFormat)) {
const today = getTodayInTimezone(timezone)
const [day, month, year] = '18/02/2026'.split('/')
const date = new Date(+year, +month - 1, +day)
const dateForCheck = parseDate(formattedValue, dateFormat, 12, 0)
const date =
dateForCheck && isValidDate(dateForCheck)
? dateForCheck
: new Date()
const endDateTimeCheck =
Comment on lines +1855 to 1860
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

handleDateInputChange now sets date to new Date() when parseDate(...) fails. That can incorrectly treat an unparsable/invalid input as “today” and clamp hours/minutes for past/future restrictions. Prefer deriving endDateTimeCheck/startDateTimeCheck only when dateForCheck is non-null and valid (otherwise treat as not-today).

Copilot uses AI. Check for mistakes.
disableFutureDates && today && isDateToday(date, today)
const startDateTimeCheck =
Expand Down Expand Up @@ -1909,14 +1912,14 @@ export const handleDateInputChange = (
]

const parsedDate = parseDate(formattedValue, dateFormat, hour, minute)
if (timeValue && parsedDate !== null && isValidDate(parsedDate)) {
if (parsedDate !== null && isValidDate(parsedDate)) {
updatedRange = isStartDate
? currentRange
? { ...currentRange, startDate: parsedDate }
: { startDate: parsedDate }
: currentRange
? { ...currentRange, endDate: parsedDate }
: undefined
: { startDate: parsedDate, endDate: parsedDate }
Comment on lines 1918 to +1922
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

When changing the end-date input with no existing currentRange, this now creates a range { startDate: parsedDate, endDate: parsedDate }. This implicitly sets a start date the user didn’t choose (and can enable downstream logic based on selectedRange), leading to inconsistent UI state (start input still empty) and potentially committing an unintended range. Consider keeping updatedRange undefined in this case, or preventing end-date edits until a start date exists, or storing an “incomplete range” state separately instead of fabricating a start date.

Copilot uses AI. Check for mistakes.
}
}

Expand Down
Loading