Skip to content
5 changes: 5 additions & 0 deletions .changeset/hip-pants-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"flowbite-react": patch
---

feat(Datepicker): Implemented a filter function prop
47 changes: 46 additions & 1 deletion apps/storybook/src/Datepicker.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Meta, StoryFn } from "@storybook/react";
import type { DatepickerProps } from "flowbite-react";
import { Datepicker, getFirstDateInRange, WeekStart } from "flowbite-react";
import { Datepicker, getFirstDateInRange, Views, WeekStart } from "flowbite-react";
import { useEffect, useState } from "react";

export default {
Expand Down Expand Up @@ -147,3 +147,48 @@ EmptyDates.args = {
theme: {},
label: "No date selected",
};

export const FilterWeekdaysOnly = Template.bind({});
FilterWeekdaysOnly.args = {
open: true,
autoHide: false,
showClearButton: true,
showTodayButton: true,
defaultValue: undefined,
value: undefined,
minDate: undefined,
maxDate: undefined,
filterDate: (date: Date, view: Views) => {
if (view === Views.Days) {
const day = date.getDay();
return day >= 1 && day <= 5;
}
return true;
},
language: "en",
weekStart: WeekStart.Sunday,
theme: {},
label: "Filter: Weekdays only",
};

export const FilterEvenDatesOnly = Template.bind({});
FilterEvenDatesOnly.args = {
open: true,
autoHide: false,
showClearButton: true,
showTodayButton: true,
defaultValue: undefined,
value: undefined,
minDate: undefined,
maxDate: undefined,
filterDate: (date: Date, view: Views) => {
if (view === Views.Days) {
return date.getDate() % 2 === 0;
}
return true;
},
language: "en",
weekStart: WeekStart.Sunday,
theme: {},
label: "Filter: Even dates only",
};
8 changes: 8 additions & 0 deletions apps/web/content/docs/components/datepicker.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ The `labelTodayButton` and `labelClearButton` can also be used to update the tex

<Example name="datepicker.localization" />

## Filter dates

You can use the `filterDate` prop to filter out specific dates from the datepicker component. This is useful if you want to disable certain dates or only allow selection of weekdays, for example.

The `Views` enum can be used to determine the current view of the datepicker, such as `Days`, `Months`, `Years` or `Decades`.

<Example name="datepicker.filter" />

## Limit the date

By using the `minDate` and `maxDate` props you can limit the date range that the user can select.
Expand Down
45 changes: 45 additions & 0 deletions apps/web/examples/datepicker/datepicker.filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import { Datepicker, Views } from "flowbite-react";
import type { CodeData } from "~/components/code-demo";

const code = `
"use client";

import { Datepicker, Views } from "flowbite-react";

export function Component() {
const filterFn = (date: Date, view: Views) => {
if (view === Views.Days) {
const day = date.getDay();
return day >= 1 && day <= 5;
}
return true;
};

return <Datepicker filterDate={filterFn} />;
}
`;

export function Component() {
const filterFn = (date: Date, view: Views) => {
if (view === Views.Days) {
const day = date.getDay();
return day >= 1 && day <= 5;
}
return true;
};

return <Datepicker filterDate={filterFn} />;
}

export const filter: CodeData = {
type: "single",
code: {
fileName: "index",
language: "tsx",
code,
},
githubSlug: "datepicker/datepicker.filter.tsx",
component: <Component />,
};
1 change: 1 addition & 0 deletions apps/web/examples/datepicker/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { autoHide } from "./datepicker.autoHide";
export { inline } from "./datepicker.inline";
export { localization } from "./datepicker.localization";
export { filter } from "./datepicker.filter";
export { range } from "./datepicker.range";
export { root } from "./datepicker.root";
export { title } from "./datepicker.title";
Expand Down
20 changes: 19 additions & 1 deletion packages/ui/src/components/Datepicker/Datepicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useRef } from "react";
import { describe, expect, it, vi } from "vitest";
import type { DatepickerRef } from "./Datepicker";
import { Datepicker } from "./Datepicker";
import { getFormattedDate } from "./helpers";
import { getFormattedDate, Views } from "./helpers";

describe("Components / Datepicker", () => {
it("should display today's date by default", () => {
Expand Down Expand Up @@ -185,6 +185,24 @@ describe("Components / Datepicker", () => {
expect(outsideRange).toBeDisabled();
});

it("the filter function should allow to disable a certain date", async () => {
const tomorrow = new Date();
tomorrow.setDate(new Date().getDate() + 1);

const filter = (date: Date, view: Views) => {
if (view === Views.Days) {
return date.toDateString() !== tomorrow.toDateString();
}
return true;
};

render(<Datepicker filterDate={filter} />);

await userEvent.click(screen.getByRole("textbox"));

expect(screen.getByText(tomorrow.getDate())).toBeDisabled();
});

it("should focus the input when ref.current.focus is called", () => {
const {
result: { current: ref },
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/src/components/Datepicker/Datepicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export interface DatepickerProps
labelTodayButton?: string;
minDate?: Date;
maxDate?: Date;
filterDate?: (date: Date, view: Views) => boolean;
language?: string;
weekStart?: WeekStart;
onChange?: (date: Date | null) => void;
Expand Down Expand Up @@ -127,6 +128,7 @@ export const Datepicker = forwardRef<DatepickerRef, DatepickerProps>((props, ref
defaultValue,
minDate,
maxDate,
filterDate,
language = "en",
weekStart = WeekStart.Sunday,
className,
Expand Down Expand Up @@ -277,6 +279,7 @@ export const Datepicker = forwardRef<DatepickerRef, DatepickerProps>((props, ref
language,
minDate,
maxDate,
filterDate,
weekStart,
isOpen,
setIsOpen,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface DatepickerContextValue {
weekStart: WeekStart;
minDate?: Date;
maxDate?: Date;
filterDate?: (date: Date, view: Views) => boolean;
isOpen?: boolean;
setIsOpen: (isOpen: boolean) => void;
view: Views;
Expand Down
14 changes: 12 additions & 2 deletions packages/ui/src/components/Datepicker/Views/Days.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@

import { twMerge } from "../../../helpers/tailwind-merge";
import { useDatePickerContext } from "../DatepickerContext";
import { addDays, getFirstDayOfTheMonth, getFormattedDate, getWeekDays, isDateEqual, isDateInRange } from "../helpers";
import {
addDays,
getFirstDayOfTheMonth,
getFormattedDate,
getWeekDays,
isDateEqual,
isDateInRange,
Views,
} from "../helpers";

export interface DatepickerViewsDaysTheme {
header: {
Expand All @@ -25,6 +33,7 @@ export function DatepickerViewsDays() {
weekStart,
minDate,
maxDate,
filterDate,
viewDate,
selectedDate,
changeSelectedDate,
Expand All @@ -51,7 +60,8 @@ export function DatepickerViewsDays() {
const day = getFormattedDate(language, currentDate, { day: "numeric" });

const isSelected = selectedDate && isDateEqual(selectedDate, currentDate);
const isDisabled = !isDateInRange(currentDate, minDate, maxDate);
const isDisabled =
!isDateInRange(currentDate, minDate, maxDate) || (filterDate && !filterDate(currentDate, Views.Days));

return (
<button
Expand Down
15 changes: 13 additions & 2 deletions packages/ui/src/components/Datepicker/Views/Decades.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ export interface DatepickerViewsDecadesTheme {
}

export function DatepickerViewsDecades() {
const { theme: rootTheme, viewDate, selectedDate, minDate, maxDate, setViewDate, setView } = useDatePickerContext();
const {
theme: rootTheme,
viewDate,
selectedDate,
minDate,
maxDate,
filterDate,
setViewDate,
setView,
} = useDatePickerContext();

const theme = rootTheme.views.decades;
const first = startOfYearPeriod(viewDate, 100);
Expand All @@ -31,7 +40,9 @@ export function DatepickerViewsDecades() {
const lastDate = addYears(firstDate, 9);

const isSelected = selectedDate && isDateInDecade(selectedDate, year);
const isDisabled = !isDateInRange(firstDate, minDate, maxDate) && !isDateInRange(lastDate, minDate, maxDate);
const isDisabled =
(!isDateInRange(firstDate, minDate, maxDate) && !isDateInRange(lastDate, minDate, maxDate)) ||
(filterDate && !filterDate(newDate, Views.Decades));

return (
<button
Expand Down
4 changes: 3 additions & 1 deletion packages/ui/src/components/Datepicker/Views/Months.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function DatepickerViewsMonth() {
theme: rootTheme,
minDate,
maxDate,
filterDate,
selectedDate,
viewDate,
language,
Expand All @@ -39,7 +40,8 @@ export function DatepickerViewsMonth() {
const month = getFormattedDate(language, newDate, { month: "short" });

const isSelected = selectedDate && isDateEqual(selectedDate, newDate);
const isDisabled = !isDateInRange(newDate, minDate, maxDate);
const isDisabled =
!isDateInRange(newDate, minDate, maxDate) || (filterDate && !filterDate(newDate, Views.Months));

return (
<button
Expand Down
14 changes: 12 additions & 2 deletions packages/ui/src/components/Datepicker/Views/Years.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ export interface DatepickerViewsYearsTheme {
}

export function DatepickerViewsYears() {
const { theme: rootTheme, selectedDate, minDate, maxDate, viewDate, setViewDate, setView } = useDatePickerContext();
const {
theme: rootTheme,
selectedDate,
minDate,
maxDate,
filterDate,
viewDate,
setViewDate,
setView,
} = useDatePickerContext();

const theme = rootTheme.views.years;

Expand All @@ -29,7 +38,8 @@ export function DatepickerViewsYears() {
newDate.setFullYear(year);

const isSelected = selectedDate && isDateEqual(selectedDate, newDate);
const isDisabled = !isDateInRange(newDate, minDate, maxDate);
const isDisabled =
!isDateInRange(newDate, minDate, maxDate) || (filterDate && !filterDate(newDate, Views.Years));

return (
<button
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/Datepicker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ export { Datepicker } from "./Datepicker";
export type { DatepickerPopupTheme, DatepickerProps, DatepickerTheme } from "./Datepicker";
export { DatepickerContext, useDatePickerContext } from "./DatepickerContext";
export type { DatepickerContextValue } from "./DatepickerContext";
export { getFirstDateInRange, WeekStart } from "./helpers";
export { getFirstDateInRange, WeekStart, Views } from "./helpers";
export { datePickerTheme } from "./theme";
Loading