diff --git a/.changeset/popular-seals-appear.md b/.changeset/popular-seals-appear.md
new file mode 100644
index 0000000000..0869051cd3
--- /dev/null
+++ b/.changeset/popular-seals-appear.md
@@ -0,0 +1,5 @@
+---
+"@nextui-org/calendar": minor
+---
+
+Added the `renderCellContent` prop to the `Calendar` and `RangeCalendar` components, allowing developers to specify custom content for each calendar cell. Updated the existing tests and added new test cases to cover the custom cell content functionality.
diff --git a/apps/docs/content/components/calendar/custom-cell-content.raw.jsx b/apps/docs/content/components/calendar/custom-cell-content.raw.jsx
new file mode 100644
index 0000000000..d03423a70b
--- /dev/null
+++ b/apps/docs/content/components/calendar/custom-cell-content.raw.jsx
@@ -0,0 +1,50 @@
+import React from "react";
+import {
+ Calendar,
+ CalendarCellContent,
+ CalendarCellHeader,
+ CalendarCellBody,
+} from "@nextui-org/react";
+
+export default function App() {
+ return (
+
+ {(date) => (
+
+
+
+
+ {date.day % 7 === 0 && (
+
+ MTG
+
+ )}
+ {date.day % 5 === 0 && (
+
+ MTG
+
+ )}
+ {date.day % 3 === 0 && (
+
+ MTG
+
+ )}
+
+
+
+ )}
+
+ );
+}
diff --git a/apps/docs/content/components/calendar/custom-cell-content.tsx b/apps/docs/content/components/calendar/custom-cell-content.tsx
new file mode 100644
index 0000000000..4d15cd37c5
--- /dev/null
+++ b/apps/docs/content/components/calendar/custom-cell-content.tsx
@@ -0,0 +1,9 @@
+import App from "./custom-cell-content.raw.jsx?raw";
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/components/calendar/index.ts b/apps/docs/content/components/calendar/index.ts
index b0f687a6f5..c328c77c82 100644
--- a/apps/docs/content/components/calendar/index.ts
+++ b/apps/docs/content/components/calendar/index.ts
@@ -12,6 +12,7 @@ import internationalCalendars from "./international-calendars";
import visibleMonths from "./visible-months";
import pageBehaviour from "./page-behaviour";
import presets from "./presets";
+import customCellContent from "./custom-cell-content";
export const calendarContent = {
usage,
@@ -28,4 +29,5 @@ export const calendarContent = {
visibleMonths,
pageBehaviour,
presets,
+ customCellContent,
};
diff --git a/apps/docs/content/components/range-calendar/custom-cell-content.raw.jsx b/apps/docs/content/components/range-calendar/custom-cell-content.raw.jsx
new file mode 100644
index 0000000000..d9acec7a7e
--- /dev/null
+++ b/apps/docs/content/components/range-calendar/custom-cell-content.raw.jsx
@@ -0,0 +1,49 @@
+import {
+ RangeCalendar,
+ CalendarCellContent,
+ CalendarCellHeader,
+ CalendarCellBody,
+} from "@nextui-org/react";
+
+export default function App() {
+ return (
+
+ {(date) => (
+
+
+
+
+ {date.day % 7 === 0 && (
+
+ MTG
+
+ )}
+ {date.day % 5 === 0 && (
+
+ MTG
+
+ )}
+ {date.day % 3 === 0 && (
+
+ MTG
+
+ )}
+
+
+
+ )}
+
+ );
+}
diff --git a/apps/docs/content/components/range-calendar/custom-cell-content.tsx b/apps/docs/content/components/range-calendar/custom-cell-content.tsx
new file mode 100644
index 0000000000..4d15cd37c5
--- /dev/null
+++ b/apps/docs/content/components/range-calendar/custom-cell-content.tsx
@@ -0,0 +1,9 @@
+import App from "./custom-cell-content.raw.jsx?raw";
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/components/range-calendar/index.ts b/apps/docs/content/components/range-calendar/index.ts
index 37396dac6d..27856017ab 100644
--- a/apps/docs/content/components/range-calendar/index.ts
+++ b/apps/docs/content/components/range-calendar/index.ts
@@ -12,6 +12,7 @@ import internationalCalendars from "./international-calendars";
import visibleMonths from "./visible-months";
import pageBehaviour from "./page-behaviour";
import presets from "./presets";
+import customCellContent from "./custom-cell-content";
import withMonthAndYearPicker from "./with-month-and-year-picker";
export const rangeCalendarContent = {
@@ -29,5 +30,6 @@ export const rangeCalendarContent = {
visibleMonths,
pageBehaviour,
presets,
+ customCellContent,
withMonthAndYearPicker,
};
diff --git a/apps/docs/content/docs/components/calendar.mdx b/apps/docs/content/docs/components/calendar.mdx
index dd7513bf8f..46502c3924 100644
--- a/apps/docs/content/docs/components/calendar.mdx
+++ b/apps/docs/content/docs/components/calendar.mdx
@@ -129,6 +129,20 @@ Here's the example to customize `topContent` and `bottomContent` to have some pr
+### Custom Cell Content
+
+The Calendar component supports customizing the cell content in two ways:
+
+
+
+The Calendar provides three components for cell customization:
+
+- `CalendarCellContent`: The wrapper component for the cell content
+- `CalendarCellHeader`: The interactive header element that handles selection
+- `CalendarCellBody`: Additional content container below the button
+
+These components inherit all calendar states (selected, disabled, etc.) and maintain proper accessibility.
+
## Slots
- **base**: Calendar wrapper, it handles alignment, placement, and general appearance.
@@ -146,6 +160,10 @@ Here's the example to customize `topContent` and `bottomContent` to have some pr
- **gridBodyRow**: The date grid body row element (e.g. `
`).
- **cell**: The date grid cell element (e.g. ``).
- **cellButton**: The button element within the cell.
+- **cellContent**: The wrapper for custom cell content.
+- **cellHeaderWrapper**: The wrapper for the cell header content.
+- **cellHeader**: The header element within the cell that handles selection.
+- **cellBody**: The container for additional cell content.
- **pickerWrapper**: The wrapper for the picker
- **pickerMonthList**: The month list picker.
- **pickerYearList**: The year list picker.
diff --git a/packages/components/calendar/__tests__/calendar.test.tsx b/packages/components/calendar/__tests__/calendar.test.tsx
index f7ee51aff9..5e62bb7d40 100644
--- a/packages/components/calendar/__tests__/calendar.test.tsx
+++ b/packages/components/calendar/__tests__/calendar.test.tsx
@@ -464,4 +464,25 @@ describe("Calendar", () => {
expect(year).toHaveAttribute("data-value", "2567");
});
});
+
+ describe("Custom cell content", () => {
+ it("should render custom content in the calendar cells", () => {
+ const wrapper = render(
+
+ {(date) => (
+
+ {date.day}
+ *
+
+ )}
+ ,
+ );
+
+ const gridCells = wrapper.getAllByRole("gridcell");
+ const customContentCell = gridCells.find((cell) => cell.textContent === "31*");
+
+ expect(customContentCell).not.toBeNull();
+ expect(customContentCell).toHaveTextContent("31*");
+ });
+ });
});
diff --git a/packages/components/calendar/__tests__/range-calendar.test.tsx b/packages/components/calendar/__tests__/range-calendar.test.tsx
index c1ac49944e..532acc4dc7 100644
--- a/packages/components/calendar/__tests__/range-calendar.test.tsx
+++ b/packages/components/calendar/__tests__/range-calendar.test.tsx
@@ -748,4 +748,30 @@ describe("RangeCalendar", () => {
expect(end).toEqual(new CalendarDate(2019, 6, 25));
});
});
+
+ describe("Custom cell content", () => {
+ it("should render custom content in the range calendar cells", () => {
+ const wrapper = render(
+
+ {(date) => (
+
+ {date.day}
+ *
+
+ )}
+ ,
+ );
+
+ const gridCells = wrapper.getAllByRole("gridcell");
+ const customContentCellA = gridCells.find((cell) => cell.textContent === "25*");
+ const customContentCellB = gridCells.find((cell) => cell.textContent === "26*");
+
+ expect(customContentCellA).not.toBeNull();
+ expect(customContentCellA).toHaveTextContent("25*");
+ expect(customContentCellB).not.toBeNull();
+ expect(customContentCellB).toHaveTextContent("26*");
+ });
+ });
});
diff --git a/packages/components/calendar/src/calendar-cell-body.tsx b/packages/components/calendar/src/calendar-cell-body.tsx
new file mode 100644
index 0000000000..55c734fdf9
--- /dev/null
+++ b/packages/components/calendar/src/calendar-cell-body.tsx
@@ -0,0 +1,29 @@
+import type {HTMLNextUIProps} from "@nextui-org/system";
+
+import React from "react";
+
+import {useCalendarContext} from "./calendar-context";
+
+interface Props extends HTMLNextUIProps<"div"> {
+ children: React.ReactNode;
+}
+
+export type CalendarCellBodyProps = Props;
+
+export const CalendarCellBody = React.forwardRef(
+ ({children, ...props}, ref) => {
+ const {slots, classNames} = useCalendarContext();
+ const bodyProps = {
+ ...props,
+ ref: ref,
+ className: slots?.cellBody({class: classNames?.cellBody}),
+ "data-slot": "cell-body",
+ };
+
+ return {children} ;
+ },
+);
+
+CalendarCellBody.displayName = "NextUI.CalendarCellBody";
+
+export default CalendarCellBody;
diff --git a/packages/components/calendar/src/calendar-cell-content-default.tsx b/packages/components/calendar/src/calendar-cell-content-default.tsx
new file mode 100644
index 0000000000..b0cfecd32d
--- /dev/null
+++ b/packages/components/calendar/src/calendar-cell-content-default.tsx
@@ -0,0 +1,19 @@
+import React from "react";
+import {CalendarDate} from "@internationalized/date";
+
+import {CalendarCellContent} from "./calendar-cell-content";
+import {CalendarCellHeader} from "./calendar-cell-header";
+
+export interface CalendarCellContentDefaultProps {
+ date: CalendarDate;
+}
+
+export const CalendarCellContentDefault: React.FC = ({date}) => {
+ return (
+
+ {date.day}
+
+ );
+};
+
+CalendarCellContentDefault.displayName = "NextUI.CalendarCellContentDefault";
diff --git a/packages/components/calendar/src/calendar-cell-content.tsx b/packages/components/calendar/src/calendar-cell-content.tsx
new file mode 100644
index 0000000000..948a84cbcf
--- /dev/null
+++ b/packages/components/calendar/src/calendar-cell-content.tsx
@@ -0,0 +1,25 @@
+import type {HTMLNextUIProps} from "@nextui-org/system";
+
+import {useCalendarContext} from "./calendar-context";
+
+export interface CalendarCellContentProps extends HTMLNextUIProps<"div"> {
+ children: React.ReactNode;
+}
+
+export const CalendarCellContent = ({children, ...props}: CalendarCellContentProps) => {
+ const {slots, classNames} = useCalendarContext();
+
+ return (
+
+ {children}
+
+ );
+};
+
+CalendarCellContent.displayName = "NextUI.CalendarCellContent";
+
+export default CalendarCellContent;
diff --git a/packages/components/calendar/src/calendar-cell-context.tsx b/packages/components/calendar/src/calendar-cell-context.tsx
new file mode 100644
index 0000000000..6b4ed84475
--- /dev/null
+++ b/packages/components/calendar/src/calendar-cell-context.tsx
@@ -0,0 +1,45 @@
+import type {CalendarState, RangeCalendarState} from "@react-stately/calendar";
+import type {CalendarDate} from "@internationalized/date";
+import type {DOMAttributes} from "@react-types/shared";
+
+import {createContext} from "@nextui-org/react-utils";
+
+export interface CalendarCellContextType {
+ // Core date and state
+ date: CalendarDate;
+ state: CalendarState | RangeCalendarState;
+ buttonProps: DOMAttributes;
+ formattedDate: string;
+
+ // Selection states
+ isSelected: boolean;
+ isRangeSelection: boolean;
+ isRangeStart: boolean;
+ isRangeEnd: boolean;
+ isSelectionStart: boolean;
+ isSelectionEnd: boolean;
+
+ // Interaction states
+ isDisabled: boolean;
+ isPressable: boolean;
+ isPressed: boolean;
+ isFocused: boolean;
+ isFocusVisible: boolean;
+ isHovered: boolean;
+
+ // Validation states
+ isInvalid: boolean;
+ isUnavailable: boolean;
+
+ // Display states
+ isOutsideMonth: boolean;
+ isToday: boolean;
+ isReadOnly: boolean;
+}
+
+export const [CalendarCellProvider, useCalendarCell] = createContext({
+ name: "CalendarCellContext",
+ strict: true,
+ errorMessage:
+ "useCalendarCell: `context` is undefined. Seems you forgot to wrap component within the CalendarCellProvider",
+});
diff --git a/packages/components/calendar/src/calendar-cell-header.tsx b/packages/components/calendar/src/calendar-cell-header.tsx
new file mode 100644
index 0000000000..6caebfc27f
--- /dev/null
+++ b/packages/components/calendar/src/calendar-cell-header.tsx
@@ -0,0 +1,72 @@
+import type {HTMLNextUIProps} from "@nextui-org/system";
+
+import {dataAttr} from "@nextui-org/shared-utils";
+
+import {useCalendarCell} from "./calendar-cell-context";
+import {useCalendarContext} from "./calendar-context";
+
+export interface CalendarCellHeaderProps extends HTMLNextUIProps<"div"> {
+ children?: React.ReactNode;
+}
+
+export const CalendarCellHeader = ({children}: CalendarCellHeaderProps) => {
+ const {slots, classNames} = useCalendarContext();
+ const {
+ date,
+ state,
+ isSelected,
+ isDisabled,
+ isInvalid,
+ isOutsideMonth,
+ isToday,
+ isUnavailable,
+ isRangeSelection,
+ isRangeStart,
+ isRangeEnd,
+ isSelectionStart,
+ isSelectionEnd,
+ isFocused,
+ isFocusVisible,
+ isHovered,
+ isPressed,
+ } = useCalendarCell();
+
+ return (
+
+
+ {children ? children : date.day}
+
+
+ );
+};
+
+CalendarCellHeader.displayName = "NextUI.CalendarCellHeader";
+
+export default CalendarCellHeader;
diff --git a/packages/components/calendar/src/calendar-cell.tsx b/packages/components/calendar/src/calendar-cell.tsx
index b2e742c588..0a426b1c99 100644
--- a/packages/components/calendar/src/calendar-cell.tsx
+++ b/packages/components/calendar/src/calendar-cell.tsx
@@ -9,7 +9,10 @@ import {useLocale} from "@react-aria/i18n";
import {useFocusRing} from "@react-aria/focus";
import {useHover} from "@react-aria/interactions";
import {useRef} from "react";
-import {dataAttr} from "@nextui-org/shared-utils";
+
+import {CalendarCellContextType, CalendarCellProvider} from "./calendar-cell-context";
+import {CalendarCellContentDefault} from "./calendar-cell-content-default";
+import {useCalendarContext} from "./calendar-context";
export interface CalendarCellProps extends HTMLNextUIProps<"td">, AriaCalendarCellProps {
state: CalendarState | RangeCalendarState;
@@ -23,6 +26,7 @@ export function CalendarCell(originalProps: CalendarCellProps) {
const {state, slots, isPickerVisible, currentMonth, classNames, ...props} = originalProps;
const ref = useRef(null);
+ const {cellContent} = useCalendarContext();
const {
cellProps,
@@ -67,29 +71,51 @@ export function CalendarCell(originalProps: CalendarCellProps) {
isDisabled: isDisabled || isUnavailable || state.isReadOnly,
});
+ const cellContextValue: CalendarCellContextType = {
+ // Core date and state
+ date: props.date,
+ state,
+ buttonProps,
+ formattedDate,
+
+ // Selection states
+ isSelected,
+ isRangeSelection: isSelected && "highlightedRange" in state,
+ isRangeStart,
+ isRangeEnd,
+ isSelectionStart,
+ isSelectionEnd,
+
+ // Interaction states
+ isDisabled,
+ isPressable: !isDisabled && !isUnavailable && !state.isReadOnly,
+ isPressed,
+ isFocused,
+ isFocusVisible,
+ isHovered,
+
+ // Validation states
+ isInvalid,
+ isUnavailable,
+
+ // Display states
+ isOutsideMonth: !isSameMonth(props.date, currentMonth),
+ isToday: isToday(props.date, state.timeZone),
+ isReadOnly: state.isReadOnly,
+ };
+
return (
- {formattedDate}
+
+ {typeof cellContent === "function"
+ ? cellContent(props.date)
+ : cellContent ?? }
+
|
);
diff --git a/packages/components/calendar/src/calendar.tsx b/packages/components/calendar/src/calendar.tsx
index 30891d3c71..ce5ef2e516 100644
--- a/packages/components/calendar/src/calendar.tsx
+++ b/packages/components/calendar/src/calendar.tsx
@@ -1,4 +1,4 @@
-import type {DateValue} from "@internationalized/date";
+import type {CalendarDate, DateValue} from "@internationalized/date";
import type {ForwardedRef, ReactElement} from "react";
import {forwardRef} from "@nextui-org/system";
@@ -7,7 +7,10 @@ import {UseCalendarProps, useCalendar} from "./use-calendar";
import {CalendarProvider} from "./calendar-context";
import {CalendarBase} from "./calendar-base";
-interface Props extends Omit, "isHeaderWrapperExpanded"> {}
+interface Props
+ extends Omit, "isHeaderWrapperExpanded" | "children"> {
+ children?: ((date: CalendarDate) => React.ReactNode) | React.ReactNode;
+}
export type CalendarProps = Props;
@@ -15,7 +18,12 @@ const Calendar = forwardRef(function Calendar(
props: CalendarProps,
ref: ForwardedRef,
) {
- const {context, getBaseCalendarProps} = useCalendar({...props, ref});
+ const {children, ...otherProps} = props;
+ const {context, getBaseCalendarProps} = useCalendar({
+ ...otherProps,
+ ref,
+ cellContent: children,
+ });
return (
diff --git a/packages/components/calendar/src/index.ts b/packages/components/calendar/src/index.ts
index f34634627c..a74bebb668 100644
--- a/packages/components/calendar/src/index.ts
+++ b/packages/components/calendar/src/index.ts
@@ -1,5 +1,8 @@
import Calendar from "./calendar";
import RangeCalendar from "./range-calendar";
+import CalendarCellContent from "./calendar-cell-content";
+import CalendarCellHeader from "./calendar-cell-header";
+import CalendarCellBody from "./calendar-cell-body";
// export types
export type {CalendarProps} from "./calendar";
@@ -16,4 +19,4 @@ export {useRangeCalendar} from "./use-range-calendar";
export {CalendarProvider, useCalendarContext} from "./calendar-context";
// export component
-export {Calendar, RangeCalendar};
+export {Calendar, RangeCalendar, CalendarCellContent, CalendarCellHeader, CalendarCellBody};
diff --git a/packages/components/calendar/src/range-calendar.tsx b/packages/components/calendar/src/range-calendar.tsx
index 913f2604c8..eda6ba24fb 100644
--- a/packages/components/calendar/src/range-calendar.tsx
+++ b/packages/components/calendar/src/range-calendar.tsx
@@ -1,4 +1,4 @@
-import type {DateValue} from "@internationalized/date";
+import type {CalendarDate, DateValue} from "@internationalized/date";
import type {ForwardedRef, ReactElement} from "react";
import {forwardRef} from "@nextui-org/system";
@@ -10,8 +10,10 @@ import {CalendarBase} from "./calendar-base";
interface Props
extends Omit<
UseRangeCalendarProps,
- "isHeaderExpanded" | "onHeaderExpandedChange" | "isHeaderWrapperExpanded"
- > {}
+ "isHeaderExpanded" | "onHeaderExpandedChange" | "isHeaderWrapperExpanded" | "children"
+ > {
+ children?: ((date: CalendarDate) => React.ReactNode) | React.ReactNode;
+}
export type RangeCalendarProps = Props;
@@ -19,7 +21,12 @@ const RangeCalendar = forwardRef(function RangeCalendar(
props: RangeCalendarProps,
ref: ForwardedRef,
) {
- const {context, getBaseCalendarProps} = useRangeCalendar({...props, ref});
+ const {children, ...otherProps} = props;
+ const {context, getBaseCalendarProps} = useRangeCalendar({
+ ...otherProps,
+ ref,
+ cellContent: children,
+ });
return (
diff --git a/packages/components/calendar/src/use-calendar-base.ts b/packages/components/calendar/src/use-calendar-base.ts
index e87be7c489..7cfaaefc2e 100644
--- a/packages/components/calendar/src/use-calendar-base.ts
+++ b/packages/components/calendar/src/use-calendar-base.ts
@@ -81,6 +81,12 @@ interface Props extends NextUIBaseProps {
* @returns void
*/
onHeaderExpandedChange?: (isExpanded: boolean) => void;
+ /**
+ * Function to custom render the content of the calendar cell
+ * @param date The date to render
+ * @returns ReactNode
+ */
+ cellContent?: ((date: CalendarDate) => React.ReactNode) | React.ReactNode;
/**
* This function helps to reduce the bundle size by providing a custom calendar system.
*
@@ -181,6 +187,7 @@ export type ContextType = {
setIsHeaderExpanded?: (isExpanded: boolean) => void;
classNames?: SlotsToClasses;
disableAnimation?: boolean;
+ cellContent?: ((date: CalendarDate) => React.ReactNode) | React.ReactNode;
};
export function useCalendarBase(originalProps: UseCalendarBasePropsComplete) {
@@ -200,7 +207,6 @@ export function useCalendarBase(originalProps: UseCalendarBasePropsComplete) {
const {
ref,
as,
- children,
className,
topContent,
bottomContent,
@@ -315,7 +321,6 @@ export function useCalendarBase(originalProps: UseCalendarBasePropsComplete) {
return {
Component,
- children,
domRef,
slots,
locale,
diff --git a/packages/components/calendar/src/use-calendar.ts b/packages/components/calendar/src/use-calendar.ts
index def46e9730..bd4ae7e864 100644
--- a/packages/components/calendar/src/use-calendar.ts
+++ b/packages/components/calendar/src/use-calendar.ts
@@ -18,12 +18,12 @@ export type UseCalendarProps = UseCalendarBaseProps & AriaC
export function useCalendar({
buttonPickerProps: buttonPickerPropsProp,
className,
+ cellContent,
...originalProps
}: UseCalendarProps) {
const {
Component,
slots,
- children,
domRef,
locale,
minValue,
@@ -107,6 +107,7 @@ export function useCalendar({
classNames,
showMonthAndYearPickers,
disableAnimation,
+ cellContent,
}),
[
state,
@@ -118,12 +119,12 @@ export function useCalendar({
visibleMonths,
disableAnimation,
showMonthAndYearPickers,
+ cellContent,
],
);
return {
Component,
- children,
domRef,
context,
state,
diff --git a/packages/components/calendar/src/use-range-calendar.ts b/packages/components/calendar/src/use-range-calendar.ts
index 08d9c321e4..635f0054a7 100644
--- a/packages/components/calendar/src/use-range-calendar.ts
+++ b/packages/components/calendar/src/use-range-calendar.ts
@@ -16,7 +16,7 @@ import {CalendarBaseProps} from "./calendar-base";
type NextUIBaseProps = Omit<
HTMLNextUIProps<"div">,
- keyof AriaRangeCalendarProps
+ keyof AriaRangeCalendarProps | "children"
>;
interface Props extends UseCalendarBaseProps, NextUIBaseProps {}
@@ -26,12 +26,12 @@ export type UseRangeCalendarProps = Props & AriaRangeCal
export function useRangeCalendar({
buttonPickerProps: buttonPickerPropsProp,
className,
+ cellContent,
...originalProps
}: UseRangeCalendarProps) {
const {
Component,
slots,
- children,
domRef,
locale,
showHelper,
@@ -115,6 +115,7 @@ export function useRangeCalendar({
showMonthAndYearPickers,
classNames,
disableAnimation,
+ cellContent,
}),
[
state,
@@ -125,13 +126,13 @@ export function useRangeCalendar({
setIsHeaderExpanded,
visibleMonths,
disableAnimation,
+ cellContent,
showMonthAndYearPickers,
],
);
return {
Component,
- children,
domRef,
context,
state,
diff --git a/packages/components/calendar/stories/calendar.stories.tsx b/packages/components/calendar/stories/calendar.stories.tsx
index cf8f410c76..64ffff94fb 100644
--- a/packages/components/calendar/stories/calendar.stories.tsx
+++ b/packages/components/calendar/stories/calendar.stories.tsx
@@ -8,6 +8,7 @@ import {
isWeekend,
startOfWeek,
startOfMonth,
+ getDayOfWeek,
} from "@internationalized/date";
import {I18nProvider, useLocale} from "@react-aria/i18n";
import {Button, ButtonGroup} from "@nextui-org/button";
@@ -15,7 +16,14 @@ import {Radio, RadioGroup} from "@nextui-org/radio";
import {cn} from "@nextui-org/theme";
import {NextUIProvider} from "@nextui-org/system";
-import {Calendar, CalendarProps, DateValue} from "../src";
+import {
+ Calendar,
+ CalendarProps,
+ DateValue,
+ CalendarCellContent,
+ CalendarCellHeader,
+ CalendarCellBody,
+} from "../src";
export default {
title: "Components/Calendar",
@@ -262,6 +270,72 @@ const CalendarWidthTemplate = (args: CalendarProps) => {
);
};
+const CustomCellTemplate = (args: CalendarProps) => {
+ const {locale} = useLocale();
+
+ return (
+
+
+
+ {(date) => (
+
+
+
+
+ {date.day % 7 === 0 && (
+
+ MTG
+
+ )}
+ {date.day % 5 === 0 && (
+
+ MTG
+
+ )}
+ {date.day % 3 === 0 && (
+
+ MTG
+
+ )}
+
+
+
+ )}
+
+
+
+
+ {(date) => {
+ const dayOfWeek = getDayOfWeek(date, locale);
+ const style =
+ dayOfWeek === 0 ? "text-red-500" : dayOfWeek === 6 ? "text-blue-500" : "inherit";
+
+ return (
+
+
+ {date.day}
+
+
+ );
+ }}
+
+
+
+ );
+};
+
const ReducedMotionTemplate = (args: CalendarProps) => {
return (
@@ -406,6 +480,13 @@ export const CalendarWidth = {
},
};
+export const CustomCellContent = {
+ render: CustomCellTemplate,
+ args: {
+ ...defaultProps,
+ },
+};
+
export const ReducedMotion = {
render: ReducedMotionTemplate,
args: {
diff --git a/packages/components/calendar/stories/range-calendar.stories.tsx b/packages/components/calendar/stories/range-calendar.stories.tsx
index 44f3398531..6e08f69b64 100644
--- a/packages/components/calendar/stories/range-calendar.stories.tsx
+++ b/packages/components/calendar/stories/range-calendar.stories.tsx
@@ -12,13 +12,20 @@ import {
startOfWeek,
endOfMonth,
endOfWeek,
+ getDayOfWeek,
} from "@internationalized/date";
import {I18nProvider, useLocale} from "@react-aria/i18n";
import {Button, ButtonGroup} from "@nextui-org/button";
import {Radio, RadioGroup} from "@nextui-org/radio";
import {cn} from "@nextui-org/theme";
-import {RangeCalendar, RangeCalendarProps} from "../src";
+import {
+ RangeCalendar,
+ RangeCalendarProps,
+ CalendarCellContent,
+ CalendarCellHeader,
+ CalendarCellBody,
+} from "../src";
export default {
title: "Components/RangeCalendar",
@@ -75,7 +82,11 @@ const ControlledTemplate = (args: RangeCalendarProps) => {
{
+ if (newValue) {
+ setValue(newValue);
+ }
+ }}
{...args}
color="secondary"
/>
@@ -156,7 +167,11 @@ const InvalidDatesTemplate = (args: RangeCalendarProps) => {
errorMessage={isInvalid ? "Stay dates cannot fall on weekends" : undefined}
isInvalid={isInvalid}
value={date}
- onChange={setDate}
+ onChange={(newValue) => {
+ if (newValue) {
+ setDate(newValue);
+ }
+ }}
/>
);
};
@@ -278,7 +293,11 @@ const PresetsTemplate = (args: RangeCalendarProps) => {
}
value={value}
- onChange={setValue}
+ onChange={(newValue) => {
+ if (newValue) {
+ setValue(newValue);
+ }
+ }}
onFocusChange={setFocusedValue}
{...args}
/>
@@ -286,6 +305,72 @@ const PresetsTemplate = (args: RangeCalendarProps) => {
);
};
+const CustomCellTemplate = (args: RangeCalendarProps) => {
+ const {locale} = useLocale();
+
+ return (
+
+
+
+ {(date) => (
+
+
+
+
+ {date.day % 7 === 0 && (
+
+ MTG
+
+ )}
+ {date.day % 5 === 0 && (
+
+ MTG
+
+ )}
+ {date.day % 3 === 0 && (
+
+ MTG
+
+ )}
+
+
+
+ )}
+
+
+
+
+ {(date) => {
+ const dayOfWeek = getDayOfWeek(date, locale);
+ const style =
+ dayOfWeek === 0 ? "text-red-500" : dayOfWeek === 6 ? "text-blue-500" : "inherit";
+
+ return (
+
+
+ {date.day}
+
+
+ );
+ }}
+
+
+
+ );
+};
+
export const Default = {
render: Template,
args: {
@@ -414,3 +499,10 @@ export const Presets = {
...defaultProps,
},
};
+
+export const CustomCellContent = {
+ render: CustomCellTemplate,
+ args: {
+ ...defaultProps,
+ },
+};
diff --git a/packages/core/theme/src/components/calendar.ts b/packages/core/theme/src/components/calendar.ts
index 224002b373..7b8064b9fb 100644
--- a/packages/core/theme/src/components/calendar.ts
+++ b/packages/core/theme/src/components/calendar.ts
@@ -28,12 +28,15 @@ const calendar = tv({
grid: "w-full border-collapse z-0",
gridHeader: "bg-content1 shadow-[0px_20px_20px_0px_rgb(0_0_0/0.05)]",
gridHeaderRow: "px-4 pb-2 flex justify-center text-default-400",
- gridHeaderCell: "flex w-8 justify-center items-center font-medium text-small",
- gridBody: "",
- gridBodyRow: "flex justify-center items-center first:mt-2",
- cell: "py-0.5 px-0",
- cellButton: [
- "w-8 h-8 flex items-center text-foreground justify-center rounded-full",
+ gridHeaderCell: "flex w-full justify-center items-center font-medium text-small",
+ gridBody: "w-full h-full",
+ gridBodyRow: "flex h-full px-4 justify-start items-start first:mt-2",
+ cell: "py-0.5 px-0 w-full h-full",
+ cellButton: ["relative w-full h-full justify-start", ...dataFocusVisibleClasses],
+ cellContent: "flex flex-col w-full gap-0.5 justify-start items-center ",
+ cellHeaderWrapper: ["flex w-full h-full justify-center items-center"],
+ cellHeader: [
+ "w-8 h-8 flex items-center text-foreground justify-center rounded-full shrink-0",
"box-border appearance-none select-none whitespace-nowrap font-normal",
"subpixel-antialiased overflow-hidden tap-highlight-transparent",
"data-[disabled=true]:text-default-300",
@@ -45,6 +48,7 @@ const calendar = tv({
"data-[unavailable=true]:line-through",
...dataFocusVisibleClasses,
],
+ cellBody: "w-full h-full",
pickerWrapper:
"absolute inset-x-0 top-0 flex w-full h-[var(--picker-height)] justify-center opacity-0 pointer-events-none",
pickerMonthList: "items-start",
@@ -71,18 +75,22 @@ const calendar = tv({
// @internal
isRange: {
true: {
- cellButton: [
+ cellHeaderWrapper: [
// base
"relative",
"overflow-visible",
// before pseudo element
"before:content-[''] before:absolute before:inset-0 before:z-[-1] before:rounded-none",
+ "after:content-[''] after:absolute after:inset-0 after:z-[-1] after:rounded-none",
// hide before pseudo element when the selected cell is outside the month
"data-[outside-month=true]:before:hidden",
"data-[selected=true]:data-[range-selection=true]:data-[outside-month=true]:bg-transparent",
"data-[selected=true]:data-[range-selection=true]:data-[outside-month=true]:text-default-300",
+ "data-[outside-month=true]:after:hidden",
+ "data-[selected=true]:data-[range-selection=true]:data-[outside-month=true]:bg-transparent",
+ "data-[selected=true]:data-[range-selection=true]:data-[outside-month=true]:text-default-300",
// middle
// "data-[selected=true]:data-[range-selection=true]:bg-transparent",
@@ -90,10 +98,26 @@ const calendar = tv({
// start (pseudo)
"data-[range-start=true]:before:rounded-l-full",
"data-[selection-start=true]:before:rounded-l-full",
+ "data-[range-start=true]:before:w-8",
+ "data-[selection-start=true]:before:w-8",
+ "data-[range-start=true]:before:justify-self-center",
+ "data-[selection-start=true]:before:justify-self-center",
+ "data-[range-start=true]:after:w-1/2",
+ "data-[selection-start=true]:after:w-1/2",
+ "data-[range-start=true]:after:justify-self-end",
+ "data-[selection-start=true]:after:justify-self-end",
// end (pseudo)
"data-[range-end=true]:before:rounded-r-full",
"data-[selection-end=true]:before:rounded-r-full",
+ "data-[range-end=true]:before:w-8",
+ "data-[selection-end=true]:before:w-8",
+ "data-[range-end=true]:before:justify-self-center",
+ "data-[selection-end=true]:before:justify-self-center",
+ "data-[range-end=true]:after:w-1/2",
+ "data-[selection-end=true]:after:w-1/2",
+ "data-[range-end=true]:after:justify-self-start",
+ "data-[selection-end=true]:after:justify-self-start",
// start (selected)
"data-[selected=true]:data-[selection-start=true]:data-[range-selection=true]:rounded-full",
@@ -106,7 +130,7 @@ const calendar = tv({
},
hideDisabledDates: {
true: {
- cellButton: "data-[disabled=true]:data-[outside-month=true]:opacity-0",
+ cellHeader: "data-[disabled=true]:data-[outside-month=true]:opacity-0",
},
false: {},
},
@@ -130,20 +154,20 @@ const calendar = tv({
},
showShadow: {
true: {
- cellButton: "data-[selected=true]:shadow-md",
+ cellHeader: "data-[selected=true]:shadow-md",
},
false: {
- cellButton: "shadow-none data-[selected=true]:shadow-none",
+ cellHeader: "shadow-none data-[selected=true]:shadow-none",
},
},
disableAnimation: {
true: {
- cellButton: "transition-none",
+ cellHeader: "transition-none",
},
false: {
headerWrapper: ["[&_.chevron-icon]:transition-transform", "after:transition-height"],
grid: "transition-opacity",
- cellButton: ["origin-center transition-[transform,background-color,color] !duration-150"],
+ cellHeader: ["origin-center transition-[transform,background-color,color] !duration-150"],
pickerWrapper: "transition-opacity !duration-250",
pickerItem: "transition-opacity",
},
@@ -161,7 +185,7 @@ const calendar = tv({
isRange: false,
color: "foreground",
class: {
- cellButton: [
+ cellHeader: [
"data-[hover=true]:bg-default-200",
"data-[selected=true]:bg-foreground",
"data-[selected=true]:text-background",
@@ -176,7 +200,7 @@ const calendar = tv({
isRange: false,
color: "primary",
class: {
- cellButton: [
+ cellHeader: [
"data-[selected=true]:bg-primary",
"data-[selected=true]:text-primary-foreground",
"data-[hover=true]:bg-primary-50",
@@ -190,7 +214,7 @@ const calendar = tv({
isRange: false,
color: "secondary",
class: {
- cellButton: [
+ cellHeader: [
"data-[selected=true]:bg-secondary",
"data-[selected=true]:text-secondary-foreground",
"data-[hover=true]:bg-secondary-50",
@@ -204,7 +228,7 @@ const calendar = tv({
isRange: false,
color: "success",
class: {
- cellButton: [
+ cellHeader: [
"data-[selected=true]:bg-success",
"data-[selected=true]:text-success-foreground",
"data-[hover=true]:bg-success-100",
@@ -222,7 +246,7 @@ const calendar = tv({
isRange: false,
color: "warning",
class: {
- cellButton: [
+ cellHeader: [
"data-[selected=true]:bg-warning",
"data-[selected=true]:text-warning-foreground",
"data-[hover=true]:bg-warning-100",
@@ -240,7 +264,7 @@ const calendar = tv({
isRange: false,
color: "danger",
class: {
- cellButton: [
+ cellHeader: [
"data-[selected=true]:bg-danger",
"data-[selected=true]:text-danger-foreground",
"data-[hover=true]:bg-danger-100",
@@ -259,11 +283,13 @@ const calendar = tv({
isRange: true,
color: "foreground",
class: {
- cellButton: [
+ cellHeaderWrapper: [
// middle
"data-[selected=true]:data-[range-selection=true]:before:bg-foreground/10",
"data-[selected=true]:data-[range-selection=true]:text-foreground",
-
+ "data-[selected=true]:data-[range-selection=true]:after:bg-foreground/10",
+ ],
+ cellHeader: [
// start (selected)
"data-[selected=true]:data-[selection-start=true]:data-[range-selection=true]:bg-foreground",
"data-[selected=true]:data-[selection-start=true]:data-[range-selection=true]:text-background",
@@ -278,11 +304,13 @@ const calendar = tv({
isRange: true,
color: "primary",
class: {
- cellButton: [
+ cellHeaderWrapper: [
// middle
"data-[selected=true]:data-[range-selection=true]:before:bg-primary-50",
"data-[selected=true]:data-[range-selection=true]:text-primary",
-
+ "data-[selected=true]:data-[range-selection=true]:after:bg-primary-50",
+ ],
+ cellHeader: [
// start (selected)
"data-[selected=true]:data-[selection-start=true]:data-[range-selection=true]:bg-primary",
"data-[selected=true]:data-[selection-start=true]:data-[range-selection=true]:text-primary-foreground",
@@ -297,11 +325,13 @@ const calendar = tv({
isRange: true,
color: "secondary",
class: {
- cellButton: [
+ cellHeaderWrapper: [
// middle
"data-[selected=true]:data-[range-selection=true]:before:bg-secondary-50",
"data-[selected=true]:data-[range-selection=true]:text-secondary",
-
+ "data-[selected=true]:data-[range-selection=true]:after:bg-secondary-50",
+ ],
+ cellHeader: [
// start (selected)
"data-[selected=true]:data-[selection-start=true]:data-[range-selection=true]:bg-secondary",
"data-[selected=true]:data-[selection-start=true]:data-[range-selection=true]:text-secondary-foreground",
@@ -316,13 +346,16 @@ const calendar = tv({
isRange: true,
color: "success",
class: {
- cellButton: [
+ cellHeaderWrapper: [
// middle
"data-[selected=true]:data-[range-selection=true]:before:bg-success-100",
"data-[selected=true]:data-[range-selection=true]:text-success-600",
"dark:data-[selected=true]:data-[range-selection=true]:before:bg-success-50",
"dark:data-[selected=true]:data-[range-selection=true]:text-success-500",
-
+ "data-[selected=true]:data-[range-selection=true]:after:bg-success-100",
+ "dark:data-[selected=true]:data-[range-selection=true]:after:bg-success-50",
+ ],
+ cellHeader: [
// start (selected)
"data-[selected=true]:data-[selection-start=true]:data-[range-selection=true]:bg-success",
"data-[selected=true]:data-[selection-start=true]:data-[range-selection=true]:text-success-foreground",
@@ -339,12 +372,15 @@ const calendar = tv({
isRange: true,
color: "warning",
class: {
- cellButton: [
+ cellHeaderWrapper: [
// middle
"data-[selected=true]:data-[range-selection=true]:before:bg-warning-100",
"dark:data-[selected=true]:data-[range-selection=true]:before:bg-warning-50",
"data-[selected=true]:data-[range-selection=true]:text-warning-500",
-
+ "data-[selected=true]:data-[range-selection=true]:after:bg-warning-100",
+ "dark:data-[selected=true]:data-[range-selection=true]:after:bg-warning-50",
+ ],
+ cellHeader: [
// start (selected)
"data-[selected=true]:data-[selection-start=true]:data-[range-selection=true]:bg-warning",
"data-[selected=true]:data-[selection-start=true]:data-[range-selection=true]:text-warning-foreground",
@@ -359,11 +395,13 @@ const calendar = tv({
isRange: true,
color: "danger",
class: {
- cellButton: [
+ cellHeaderWrapper: [
// middle
"data-[selected=true]:data-[range-selection=true]:before:bg-danger-50",
"data-[selected=true]:data-[range-selection=true]:text-danger-500",
-
+ "data-[selected=true]:data-[range-selection=true]:after:bg-danger-50",
+ ],
+ cellHeader: [
// start (selected)
"data-[selected=true]:data-[selection-start=true]:data-[range-selection=true]:bg-danger",
"data-[selected=true]:data-[selection-start=true]:data-[range-selection=true]:text-danger-foreground",
@@ -379,42 +417,42 @@ const calendar = tv({
showShadow: true,
color: "foreground",
class: {
- cellButton: "data-[selected=true]:shadow-foreground/40",
+ cellHeader: "data-[selected=true]:shadow-foreground/40",
},
},
{
showShadow: true,
color: "primary",
class: {
- cellButton: "data-[selected=true]:shadow-primary/40",
+ cellHeader: "data-[selected=true]:shadow-primary/40",
},
},
{
showShadow: true,
color: "secondary",
class: {
- cellButton: "data-[selected=true]:shadow-secondary/40",
+ cellHeader: "data-[selected=true]:shadow-secondary/40",
},
},
{
showShadow: true,
color: "success",
class: {
- cellButton: "data-[selected=true]:shadow-success/40",
+ cellHeader: "data-[selected=true]:shadow-success/40",
},
},
{
showShadow: true,
color: "warning",
class: {
- cellButton: "data-[selected=true]:shadow-warning/40",
+ cellHeader: "data-[selected=true]:shadow-warning/40",
},
},
{
showShadow: true,
color: "danger",
class: {
- cellButton: "data-[selected=true]:shadow-danger/40",
+ cellHeader: "data-[selected=true]:shadow-danger/40",
},
},
// showShadow & isRange
@@ -422,7 +460,7 @@ const calendar = tv({
showShadow: true,
isRange: true,
class: {
- cellButton: [
+ cellHeader: [
// remove shadow from middle
"data-[selected=true]:shadow-none",
// add shadow to start (selected)
|