Skip to content
19 changes: 19 additions & 0 deletions .changeset/bil-5134-add-allowlist-param-to-datepicker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@clickhouse/click-ui': minor
---

Adds optional `allowOnlyDatesList` prop to `DatePicker`, which enables the user to provide a predefined allowlist of dates that can be selected.
Comment thread
vickiwyang marked this conversation as resolved.

### How to use?

```tsx
<DatePicker
allowOnlyDatesList={[
new Date('2026-01-15'),
new Date('2026-01-20'),
new Date('2026-02-01'),
]}
onSelectDate={(date) => console.log('Selected:', date)}
/>
```
Only the dates in `allowOnlyDatesList` will be selectable. All other dates will be disabled.
13 changes: 12 additions & 1 deletion src/components/DatePicker/DatePicker.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Args } from '@storybook/react-vite';
import { DatePicker } from '@/components/DatePicker';
import { getNextNDatesForDatePickerAllowOnlyList } from './utils';

const defaultStory = {
args: {
allowOnlyDatesList: [],
onSelectDate: (date: Date) => {
console.log('Date selected: ', date);
},
Expand All @@ -26,6 +28,7 @@ const defaultStory = {
const date = args.date ? new Date(args.date) : undefined;
return (
<DatePicker
allowOnlyDatesList={args.allowOnlyDatesList}
date={date}
disabled={args.disabled}
futureDatesDisabled={args.futureDatesDisabled}
Expand All @@ -40,6 +43,14 @@ const defaultStory = {

export default defaultStory;

export const Playground = {
export const Default = {
...defaultStory,
};

export const DatePickerAllowOnlyNext30Days = {
...defaultStory,
args: {
...defaultStory.args,
allowOnlyDatesList: getNextNDatesForDatePickerAllowOnlyList(30),
},
};
43 changes: 43 additions & 0 deletions src/components/DatePicker/DatePicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,49 @@ describe('DatePicker', () => {

expect(handleSelectDate).not.toHaveBeenCalled();
});

it('disables selecting dates not in allowOnlyDatesList', async () => {
const date = new Date('07-04-2020');
const allowOnlyDatesList = [new Date('07-04-2020'), new Date('07-06-2020')];
const handleSelectDate = vi.fn();
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });

const { getByTestId, findByText } = renderCUI(
<DatePicker
allowOnlyDatesList={allowOnlyDatesList}
date={date}
onSelectDate={handleSelectDate}
/>
);

user.click(getByTestId('datepicker-input'));
user.click(await findByText('5'));

expect(handleSelectDate).not.toHaveBeenCalled();
});

it('disables selecting futures dates in allowOnlyDatesList when futureDatesDisabled', async () => {
// System time is July 5, 2020
const date = new Date('07-04-2020');
const allowOnlyDatesList = [new Date('07-04-2020'), new Date('07-06-2020')];
const handleSelectDate = vi.fn();
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });

const { getByTestId, findByText } = renderCUI(
<DatePicker
allowOnlyDatesList={allowOnlyDatesList}
date={date}
futureDatesDisabled
onSelectDate={handleSelectDate}
/>
);

user.click(getByTestId('datepicker-input'));
// July 6 is in allowOnlyDatesList but is a future date
user.click(await findByText('6'));

expect(handleSelectDate).not.toHaveBeenCalled();
});
});

describe('two phased date selection', () => {
Expand Down
11 changes: 10 additions & 1 deletion src/components/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const PopoverContent = styled(Popover.Content)`
`;

interface CalendarProps {
allowOnlyDatesList?: Array<Date>;
calendarBody: Body;
closeDatepicker: () => void;
futureDatesDisabled: boolean;
Expand All @@ -49,6 +50,7 @@ interface CalendarProps {
}

const Calendar = ({
allowOnlyDatesList,
calendarBody,
closeDatepicker,
futureDatesDisabled,
Expand Down Expand Up @@ -134,7 +136,12 @@ const Calendar = ({
{week.map(({ date, isCurrentMonth, key: dayKey, value: fullDate }) => {
const isSelected = selectedDate && isSameDate(selectedDate, fullDate);
const isPresent = isSameDate(today, fullDate);
const isDisabled = futureDatesDisabled ? fullDate > today : false;
const isNotAllowed =
allowOnlyDatesList &&
allowOnlyDatesList.length > 0 &&
!allowOnlyDatesList.some(d => isSameDate(d, fullDate));
const isFutureDisabled = futureDatesDisabled && fullDate > today;
const isDisabled = isNotAllowed || isFutureDisabled;
const currentIndex = dayIndex;
dayIndex++;

Expand Down Expand Up @@ -172,6 +179,7 @@ const Calendar = ({
};

export const DatePicker = ({
allowOnlyDatesList,
date,
disabled = false,
futureDatesDisabled = false,
Expand Down Expand Up @@ -276,6 +284,7 @@ export const DatePicker = ({
>
{body => (
<Calendar
allowOnlyDatesList={allowOnlyDatesList}
autoFocus={autoFocusCalendar}
calendarBody={body}
closeDatepicker={onCloseDatePicker}
Expand Down
1 change: 1 addition & 0 deletions src/components/DatePicker/DatePicker.types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface DatePickerProps {
allowOnlyDatesList?: Array<Date>;
date?: Date;
disabled?: boolean;
futureDatesDisabled?: boolean;
Expand Down
8 changes: 8 additions & 0 deletions src/components/DatePicker/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,14 @@ export const getPredefinedTimePeriodsForDateTimePicker = (): DateRangeListItem[]
return dateRangeList;
};

export const getNextNDatesForDatePickerAllowOnlyList = (numberOfDays: number): Date[] => {
const now = dayjs();

return Array.from({ length: numberOfDays }, (_, i) =>
now.add(i, 'day').startOf('day').toDate()
);
};

export const datesAreWithinMaxRange = (
startDate: Date,
endDate: Date,
Expand Down
Loading