Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(calendar): add custom cell content to Calendar #3554

Open
wants to merge 28 commits into
base: canary
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a24b443
feat(calendar): add custom cell content
1amageek Jun 24, 2024
487be5e
refactor(calendar-cell): add useRef hook to improve performance
1amageek Jun 24, 2024
7a0fa5c
refactor: improve CalendarCell by destructuring props and adding type…
1amageek Jun 25, 2024
a449568
test(calendar): add test for custom cell content in Calendar and Rang…
1amageek Jun 25, 2024
0218de1
chore: add changeset for custom cell content feature in Calendar and …
1amageek Jun 25, 2024
4d40ddd
refactor: use shared template component for custom cell content in ca…
1amageek Jun 29, 2024
c0f2b34
feat: add calendar width and custom cell templates for calendar and r…
1amageek Jul 25, 2024
448ab55
feat(calendar): add renderCellContent prop for custom cell content
1amageek Jul 25, 2024
9445d2b
feat(calendar): move renderCellContent to context
1amageek Nov 22, 2024
930d4af
feat(calendar): add cell components and context management
1amageek Nov 23, 2024
9fc7840
feat(calendar): implement custom cell content rendering
1amageek Nov 23, 2024
b327e40
feat(docs): add custom cell content examples for calendar components
1amageek Nov 23, 2024
b2fcf12
fix(theme): correct typo in calendar cellBody height class
1amageek Nov 23, 2024
39dd82c
Merge branch 'canary' into canary
1amageek Nov 23, 2024
0f22c26
feat(calendar): add day of week calculation to CalendarCell component
1amageek Nov 23, 2024
2ae85a9
refactor(calendar): improve calendar cell components and remove unuse…
1amageek Nov 23, 2024
01b3ba2
feat(calendar): update components to use CalendarCellHeader
1amageek Nov 23, 2024
874af0b
refactor(theme): update gridBodyRow styles for better spacing
1amageek Nov 23, 2024
e814634
refactor(calendar): integrate CalendarCellHeader into calendar cell c…
1amageek Nov 23, 2024
24ccded
feat(calendar): add support for custom cell content in calendar compo…
1amageek Nov 23, 2024
1cd1d21
feat(calendar): add role and aria-label to birthday events
1amageek Nov 27, 2024
8902936
feat(calendar): add calendar cell components and context support
1amageek Dec 7, 2024
84a85d7
feat(calendar): add calendar cell components and context support
1amageek Dec 7, 2024
deb9dfc
ci(changesets): version packages
jrgarciadev Dec 7, 2024
7dd0840
Merge pull request #1 from 1amageek/changeset-release/canary
1amageek Dec 7, 2024
60892c4
Add support for custom cell content in Calendar components
1amageek Dec 7, 2024
5e2da7c
Merge branch 'canary' into canary
1amageek Dec 7, 2024
215eb51
Merge branch 'canary' of https://github.com/nextui-org/nextui into ca…
1amageek Dec 12, 2024
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
5 changes: 5 additions & 0 deletions .changeset/popular-seals-appear.md
Original file line number Diff line number Diff line change
@@ -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.
46 changes: 46 additions & 0 deletions apps/docs/content/components/calendar/custom-cell-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const App = `import {Calendar, CalendarCellContent, CalendarCellButton, CalendarCellBody} from "@nextui-org/react";

export default function App() {
return (
<Calendar>
<CalendarCellContent>
<CalendarCellButton />
<CalendarCellBody>
<div className="flex flex-col w-full gap-0.5 justify-center items-center p-0.5">
<span className="bg-red-600 h-1 w-full rounded-full" />
<span className="bg-green-600 h-1 w-full rounded-full" />
<span className="bg-yellow-600 h-1 w-full rounded-full" />
</div>
1amageek marked this conversation as resolved.
Show resolved Hide resolved
</CalendarCellBody>
</CalendarCellContent>
</Calendar>
);
}`;
1amageek marked this conversation as resolved.
Show resolved Hide resolved

const AppTs = `import {Calendar, CalendarCellContent, CalendarCellHeader, CalendarCellBody} from "@nextui-org/react";

export default function App() {
return (
<Calendar>
<CalendarCellContent>
<CalendarCellHeader />
<CalendarCellBody>
<div className="flex flex-col w-full gap-0.5 justify-center items-center p-0.5">
<span className="bg-red-600 h-1 w-full rounded-full" />
<span className="bg-green-600 h-1 w-full rounded-full" />
<span className="bg-yellow-600 h-1 w-full rounded-full" />
</div>
</CalendarCellBody>
</CalendarCellContent>
</Calendar>
);
}`;
1amageek marked this conversation as resolved.
Show resolved Hide resolved

const react = {
"/App.jsx": App,
"/App.tsx": AppTs,
};

export default {
...react,
};
2 changes: 2 additions & 0 deletions apps/docs/content/components/calendar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -28,4 +29,5 @@ export const calendarContent = {
visibleMonths,
pageBehaviour,
presets,
customCellContent,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const App = `import {RangeCalendar, CalendarCellContent, CalendarCellButton, CalendarCellBody} from "@nextui-org/react";

export default function App() {
return (
<RangeCalendar>
<CalendarCellContent>
<CalendarCellButton />
<CalendarCellBody>
<div className="flex flex-col w-full gap-0.5 justify-center items-center p-0.5">
<span className="bg-red-600 h-1 w-full rounded-full" />
<span className="bg-green-600 h-1 w-full rounded-full" />
<span className="bg-yellow-600 h-1 w-full rounded-full" />
</div>
</CalendarCellBody>
</CalendarCellContent>
</RangeCalendar>
);
}`;

const AppTs = `import {RangeCalendar, CalendarCellContent, CalendarCellHeader, CalendarCellBody} from "@nextui-org/react";

export default function App() {
return (
<RangeCalendar>
<CalendarCellContent>
<CalendarCellHeader />
<CalendarCellBody>
<div className="flex flex-col w-full gap-0.5 justify-center items-center p-0.5">
<span className="bg-red-600 h-1 w-full rounded-full" />
<span className="bg-green-600 h-1 w-full rounded-full" />
<span className="bg-yellow-600 h-1 w-full rounded-full" />
</div>
</CalendarCellBody>
</CalendarCellContent>
</RangeCalendar>
);
}`;

const react = {
"/App.jsx": App,
"/App.tsx": AppTs,
};

export default {
...react,
};
2 changes: 2 additions & 0 deletions apps/docs/content/components/range-calendar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -29,5 +30,6 @@ export const rangeCalendarContent = {
visibleMonths,
pageBehaviour,
presets,
customCellContent,
withMonthAndYearPicker,
};
18 changes: 18 additions & 0 deletions apps/docs/content/docs/components/calendar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,20 @@ Here's the example to customize `topContent` and `bottomContent` to have some pr

<CodeDemo title="Presets" files={calendarContent.presets} />

### Custom Cell Content

The Calendar component supports customizing the cell content in two ways:

<CodeDemo title="Custom Cell" files={calendarContent.customCellContent} />

Comment on lines +132 to +137
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Documentation needs examples for custom cell content usage

The introduction of custom cell content feature needs more detailed examples showing:

  • Basic usage with simple custom content
  • Complex scenarios with conditional rendering
  • Integration with other UI components

Consider adding example code blocks like this:

// Basic custom content
<Calendar
  renderCellContent={(date) => (
    <div>
      <span>{date.day}</span>
      {isHoliday(date) && <HolidayIcon />}
    </div>
  )}
/>

// Complex conditional rendering
<Calendar
  renderCellContent={(date) => (
    <CalendarCellContent>
      <CalendarCellHeader>{date.day}</CalendarCellHeader>
      <CalendarCellBody>
        {events[date.toISOString()]?.map(event => (
          <EventIndicator key={event.id} type={event.type} />
        ))}
      </CalendarCellBody>
    </CalendarCellContent>
  )}
/>

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.
Expand All @@ -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. `<tr>`).
- **cell**: The date grid cell element (e.g. `<td>`).
- **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.
Comment on lines +163 to +166
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add prop documentation for new slots

The new slots need detailed documentation about their props and usage patterns.

Add prop documentation for each slot:

 - **cellContent**: The wrapper for custom cell content.
+- **cellContent**: The wrapper for custom cell content. Props:
+  - `children`: ReactNode - The content to render
+  - `isSelected`: boolean - Whether the cell is selected
+  - `isDisabled`: boolean - Whether the cell is disabled
+  - Additional aria and data attributes for accessibility
+
 - **cellHeaderWrapper**: The wrapper for the cell header content.
+- **cellHeaderWrapper**: The wrapper for the cell header content. Props:
+  - `children`: ReactNode - The header content
+  - Additional styling and layout props
+
 - **cellHeader**: The header element within the cell that handles selection.
+- **cellHeader**: The header element within the cell that handles selection. Props:
+  - `children`: ReactNode - The header content
+  - `onSelect`: () => void - Selection handler
+  - Additional interaction props
+
 - **cellBody**: The container for additional cell content.
+- **cellBody**: The container for additional cell content. Props:
+  - `children`: ReactNode - The additional content
+  - Additional styling and layout props
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **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.
- **cellContent**: The wrapper for custom cell content. Props:
- `children`: ReactNode - The content to render
- `isSelected`: boolean - Whether the cell is selected
- `isDisabled`: boolean - Whether the cell is disabled
- Additional aria and data attributes for accessibility
- **cellHeaderWrapper**: The wrapper for the cell header content. Props:
- `children`: ReactNode - The header content
- Additional styling and layout props
- **cellHeader**: The header element within the cell that handles selection. Props:
- `children`: ReactNode - The header content
- `onSelect`: () => void - Selection handler
- Additional interaction props
- **cellBody**: The container for additional cell content. Props:
- `children`: ReactNode - The additional content
- Additional styling and layout props

- **pickerWrapper**: The wrapper for the picker
- **pickerMonthList**: The month list picker.
- **pickerYearList**: The year list picker.
Expand Down
74 changes: 72 additions & 2 deletions apps/docs/content/docs/components/range-calendar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ A range calendar consists of a grouping element containing one or more date grid

---


<CarbonAd/>

## Installation
Expand All @@ -32,8 +33,8 @@ A range calendar consists of a grouping element containing one or more date grid

<ImportTabs
commands={{
main: 'import {RangeCalendar} from "@nextui-org/react";',
individual: 'import {RangeCalendar} from "@nextui-org/calendar";',
main: 'import {RangeCalendar, CalendarCellContent, CalendarCellButton, CalendarCellBody} from "@nextui-org/react";',
individual: 'import {RangeCalendar, CalendarCellContent, CalendarCellButton, CalendarCellBody} from "@nextui-org/calendar";',
}}
/>

Expand All @@ -45,6 +46,56 @@ Date values are provided using objects in the [@internationalized/date](https://

<CodeDemo title="Usage" files={rangeCalendarContent.usage} />

### Custom Cell Content

The Calendar component supports customizing the cell content in two ways:

1. Using a render function:
```tsx
<RangeCalendar>
{(date, cellState) => (
<CalendarCellContent>
<CalendarCellButton>
<span
className={
getDayOfWeek(date, locale) === 0
? "text-red-500"
: "text-default-500"
}
>
{date.day}
</span>
</CalendarCellButton>
</CalendarCellContent>
)}
</RangeCalendar>
```

2. Using component composition:
```tsx
<RangeCalendar>
<CalendarCellContent>
<CalendarCellButton />
<CalendarCellBody>
<div className="flex flex-col w-full gap-0.5 justify-center items-center p-0.5">
<span className="bg-red-600 h-1 w-full rounded-full" />
<span className="bg-green-600 h-1 w-full rounded-full" />
<span className="bg-yellow-600 h-1 w-full rounded-full" />
</div>
</CalendarCellBody>
</CalendarCellContent>
</RangeCalendar>
```

The calendar provides three components for cell customization:
- `CalendarCellContent`: Wrapper component for the entire cell content
- `CalendarCellButton`: Interactive button element that handles selection
- `CalendarCellBody`: Container for additional content below the button

These components inherit all calendar states (selected, disabled, etc.) and maintain proper accessibility.

<CodeDemo title="Custom Cell Content" files={rangeCalendarContent.customCellContent} />

### Disabled

The `isDisabled` boolean prop makes the Calendar disabled. Cells cannot be focused or selected.
Expand Down Expand Up @@ -137,6 +188,21 @@ Here's the example to customize `topContent` and `bottomContent` to have some pr

<CodeDemo title="Presets" files={rangeCalendarContent.presets} />

### Custom Cell Content

The Calendar component supports customizing the cell content in two ways:

<CodeDemo title="Custom Cell" files={rangeCalendarContent.customCellContent} />

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.
Expand All @@ -154,6 +220,10 @@ Here's the example to customize `topContent` and `bottomContent` to have some pr
- **gridBodyRow**: The date grid body row element (e.g. `<tr>`).
- **cell**: The date grid cell element (e.g. `<td>`).
- **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.
1amageek marked this conversation as resolved.
Show resolved Hide resolved
- **pickerWrapper**: The wrapper for the picker
- **pickerMonthList**: The month list picker.
- **pickerYearList**: The year list picker.
Expand Down
21 changes: 21 additions & 0 deletions packages/components/calendar/__tests__/calendar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -464,4 +464,25 @@ describe("Calendar", () => {
expect(year).toHaveAttribute("data-value", "2567");
});
});

describe("Custom cell content", () => {
1amageek marked this conversation as resolved.
Show resolved Hide resolved
it("should render custom content in the calendar cells", () => {
const wrapper = render(
<Calendar defaultValue={new CalendarDate(2024, 3, 31)}>
{(date) => (
<div>
{date.day}
<span>*</span>
</div>
)}
</Calendar>,
);

const gridCells = wrapper.getAllByRole("gridcell");
const customContentCell = gridCells.find((cell) => cell.textContent === "31*");

expect(customContentCell).not.toBeNull();
expect(customContentCell).toHaveTextContent("31*");
});
});
});
26 changes: 26 additions & 0 deletions packages/components/calendar/__tests__/range-calendar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<RangeCalendar
defaultValue={{start: new CalendarDate(2024, 6, 25), end: new CalendarDate(2024, 6, 26)}}
>
{(date) => (
<div>
{date.day}
<span>*</span>
</div>
)}
</RangeCalendar>,
);

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*");
});
});
});
29 changes: 29 additions & 0 deletions packages/components/calendar/src/calendar-cell-body.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement, CalendarCellBodyProps>(
({children, ...props}, ref) => {
const {slots, classNames} = useCalendarContext();
const bodyProps = {
...props,
ref: ref,
className: slots?.cellBody({class: classNames?.cellBody}),
"data-slot": "cell-body",
};

return <div {...bodyProps}>{children}</div>;
},
);

CalendarCellBody.displayName = "NextUI.CalendarCellBody";

export default CalendarCellBody;
Original file line number Diff line number Diff line change
@@ -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<CalendarCellContentDefaultProps> = ({date}) => {
return (
<CalendarCellContent>
<CalendarCellHeader>{date.day}</CalendarCellHeader>
</CalendarCellContent>
);
};

CalendarCellContentDefault.displayName = "NextUI.CalendarCellContentDefault";
Loading