Skip to content

Commit

Permalink
Add support for tileAs prop
Browse files Browse the repository at this point in the history
Closes #876
  • Loading branch information
wojtekmaj committed Sep 25, 2023
1 parent f7af121 commit dbb3342
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 17 deletions.
3 changes: 3 additions & 0 deletions packages/react-calendar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ Displays a complete, interactive calendar.
| showNeighboringMonth | Whether days from previous or next month shall be rendered if the month doesn't start on the first day of the week or doesn't end on the last day of the week, respectively. | `true` | `false` |
| selectRange | Whether the user shall select two dates forming a range instead of just one. **Note**: This feature will make react-calendar return array with two dates regardless of returnValue setting. | `false` | `true` |
| showWeekNumbers | Whether week numbers shall be shown at the left of MonthView or not. | `false` | `true` |
| tileAs | Component used for rendering calendar tiles. | `"button"` | <ul><li>String: `"div"`</li><li>React element: `MyCustomTile`</li></ul> |
| tileClassName | Class name(s) that will be applied to a given calendar item (day on month view, month on year view and so on). | n/a | <ul><li>String: `"class1 class2"`</li><li>Array of strings: `["class1", "class2 class3"]`</li><li>Function: `({ activeStartDate, date, view }) => view === 'month' && date.getDay() === 3 ? 'wednesday' : null`</li></ul> |
| tileContent | Allows to render custom content within a given calendar item (day on month view, month on year view and so on). | n/a | <ul><li>String: `"Sample"`</li><li>React element: `<TileContent />`</li><li>Function: `({ activeStartDate, date, view }) => view === 'month' && date.getDay() === 0 ? <p>It's Sunday!</p> : null`</li></ul> |
| tileDisabled | Pass a function to determine if a certain day should be displayed as disabled. | n/a | <ul><li>Function: `({ activeStartDate, date, view }) => date.getDay() === 0`</li></ul> |
Expand All @@ -163,8 +164,10 @@ Displays a given month, year, decade and a century, respectively.
| maxDate | Maximum date that the user can select. Periods partially overlapped by maxDate will also be selectable, although react-calendar will ensure that no later date is selected. | n/a | Date: `new Date()` |
| minDate | Minimum date that the user can select. Periods partially overlapped by minDate will also be selectable, although react-calendar will ensure that no earlier date is selected. | n/a | Date: `new Date()` |
| onClick | Function called when the user clicks an item (day on month view, month on year view and so on). | n/a | `(value) => alert('New date is: ', value)` |
| tileAs | Component used for rendering calendar tiles. | `"button"` | <ul><li>String: `"div"`</li><li>React element: `MyCustomTile`</li></ul> |
| tileClassName | Class name(s) that will be applied to a given calendar item (day on month view, month on year view and so on). | n/a | <ul><li>String: `"class1 class2"`</li><li>Array of strings: `["class1", "class2 class3"]`</li><li>Function: `({ date, view }) => view === 'month' && date.getDay() === 3 ? 'wednesday' : null`</li></ul> |
| tileContent | Allows to render custom content within a given item (day on month view, month on year view and so on). **Note**: For tiles with custom content you might want to set fixed height of `react-calendar__tile` to ensure consistent layout. | n/a | `({ date, view }) => view === 'month' && date.getDay() === 0 ? <p>It's Sunday!</p> : null` |
| tileDisabled | Pass a function to determine if a certain day should be displayed as disabled. | n/a | <ul><li>Function: `({ activeStartDate, date, view }) => date.getDay() === 0`</li></ul> |
| value | Calendar value. Can be either one value or an array of two values. | n/a | <ul><li>Date: `new Date()`</li><li>An array of dates: `[new Date(2017, 0, 1), new Date(2017, 7, 1)]`</li><li>String: `2017-01-01`</li><li>An array of strings: `['2017-01-01', '2017-08-01']`</li></ul> |

## Useful links
Expand Down
28 changes: 18 additions & 10 deletions packages/react-calendar/src/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
LooseValue,
NavigationLabelFunc,
OnArgs,
OnClickEventType,
OnClickFunc,
OnClickWeekNumberFunc,
Range,
Expand Down Expand Up @@ -60,7 +61,7 @@ defaultMinDate.setFullYear(1, 0, 1);
defaultMinDate.setHours(0, 0, 0, 0);
const defaultMaxDate = new Date(8.64e15);

export type CalendarProps = {
export type CalendarProps<T extends React.ElementType> = {
activeStartDate?: Date;
allowPartialRange?: boolean;
calendarType?: CalendarType | DeprecatedCalendarType;
Expand Down Expand Up @@ -90,12 +91,12 @@ export type CalendarProps = {
nextAriaLabel?: string;
nextLabel?: React.ReactNode;
onActiveStartDateChange?: ({ action, activeStartDate, value, view }: OnArgs) => void;
onChange?: (value: Value, event: React.MouseEvent<HTMLButtonElement>) => void;
onClickDay?: OnClickFunc;
onClickDecade?: OnClickFunc;
onClickMonth?: OnClickFunc;
onChange?: (value: Value, event: OnClickEventType<T>) => void;
onClickDay?: OnClickFunc<T>;
onClickDecade?: OnClickFunc<T>;
onClickMonth?: OnClickFunc<T>;
onClickWeekNumber?: OnClickWeekNumberFunc;
onClickYear?: OnClickFunc;
onClickYear?: OnClickFunc<T>;
onDrillDown?: ({ action, activeStartDate, value, view }: OnArgs) => void;
onDrillUp?: ({ action, activeStartDate, value, view }: OnArgs) => void;
onViewChange?: ({ action, activeStartDate, value, view }: OnArgs) => void;
Expand All @@ -110,6 +111,7 @@ export type CalendarProps = {
showNavigation?: boolean;
showNeighboringMonth?: boolean;
showWeekNumbers?: boolean;
tileAs?: T;
tileClassName?: TileClassNameFunc | ClassName;
tileContent?: TileContentFunc | React.ReactNode;
tileDisabled?: TileDisabledFunc;
Expand Down Expand Up @@ -293,7 +295,10 @@ function areDatesEqual(date1?: Date | null, date2?: Date | null) {
return date1 instanceof Date && date2 instanceof Date && date1.getTime() === date2.getTime();
}

const Calendar = forwardRef(function Calendar(props: CalendarProps, ref) {
const Calendar = forwardRef(function Calendar<T extends React.ElementType = 'button'>(
props: CalendarProps<T>,
ref,
) {
const {
activeStartDate: activeStartDateProps,
allowPartialRange,
Expand Down Expand Up @@ -344,6 +349,7 @@ const Calendar = forwardRef(function Calendar(props: CalendarProps, ref) {
showNavigation = true,
showNeighboringMonth = true,
showWeekNumbers,
tileAs,
tileClassName,
tileContent,
tileDisabled,
Expand Down Expand Up @@ -457,7 +463,7 @@ const Calendar = forwardRef(function Calendar(props: CalendarProps, ref) {
);

const onClickTile = useCallback(
(value: Date, event: React.MouseEvent<HTMLButtonElement>) => {
(value: Date, event: OnClickEventType<T>) => {
const callback = (() => {
switch (view) {
case 'century':
Expand All @@ -479,7 +485,7 @@ const Calendar = forwardRef(function Calendar(props: CalendarProps, ref) {
);

const drillDown = useCallback(
(nextActiveStartDate: Date, event: React.MouseEvent<HTMLButtonElement>) => {
(nextActiveStartDate: Date, event: OnClickEventType<T>) => {
if (!drillDownAvailable) {
return;
}
Expand Down Expand Up @@ -573,7 +579,7 @@ const Calendar = forwardRef(function Calendar(props: CalendarProps, ref) {
]);

const onChange = useCallback(
(rawNextValue: Date, event: React.MouseEvent<HTMLButtonElement>) => {
(rawNextValue: Date, event: OnClickEventType<T>) => {
const previousValue = value;

onClickTile(rawNextValue, event);
Expand Down Expand Up @@ -712,6 +718,7 @@ const Calendar = forwardRef(function Calendar(props: CalendarProps, ref) {
minDate,
onClick,
onMouseOver: selectRange ? onMouseOver : undefined,
tileAs,
tileClassName,
tileContent,
tileDisabled,
Expand Down Expand Up @@ -871,6 +878,7 @@ Calendar.propTypes = {
showNavigation: PropTypes.bool,
showNeighboringMonth: PropTypes.bool,
showWeekNumbers: PropTypes.bool,
tileAs: PropTypes.oneOfType([PropTypes.func, PropTypes.oneOf(['button', 'div'] as const)]),
tileClassName: PropTypes.oneOfType([PropTypes.func, isClassName]),
tileContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
tileDisabled: PropTypes.func,
Expand Down
14 changes: 13 additions & 1 deletion packages/react-calendar/src/Tile.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,24 @@ describe('<Tile /> component', () => {
view: 'month',
} satisfies React.ComponentProps<typeof Tile>;

it('renders button properly', () => {
it('renders button properly by default', () => {
const { container } = render(<Tile {...defaultProps} />);

expect(container.querySelector('button')).toBeInTheDocument();
});

it('renders button given tileAs="button"', () => {
const { container } = render(<Tile {...defaultProps} tileAs="button" />);

expect(container.querySelector('button')).toBeInTheDocument();
});

it('renders div given tileAs="div"', () => {
const { container } = render(<Tile {...defaultProps} tileAs="div" />);

expect(container.querySelector('div')).toBeInTheDocument();
});

it('passes onClick to button', () => {
const onClick = vi.fn();

Expand Down
15 changes: 10 additions & 5 deletions packages/react-calendar/src/Tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import clsx from 'clsx';

import type {
ClassName,
OnClickEventType,
TileClassNameFunc,
TileContentFunc,
TileDisabledFunc,
View,
} from './shared/types.js';

type TileProps = {
type TileProps<T extends React.ElementType> = {
activeStartDate: Date;
children: React.ReactNode;
classes?: string[];
Expand All @@ -20,16 +21,17 @@ type TileProps = {
maxDateTransform: (date: Date) => Date;
minDate?: Date;
minDateTransform: (date: Date) => Date;
onClick?: (date: Date, event: React.MouseEvent<HTMLButtonElement>) => void;
onClick?: (date: Date, event: OnClickEventType<T>) => void;
onMouseOver?: (date: Date) => void;
style?: React.CSSProperties;
tileAs?: T;
tileClassName?: TileClassNameFunc | ClassName;
tileContent?: TileContentFunc | React.ReactNode;
tileDisabled?: TileDisabledFunc;
view: View;
};

export default function Tile(props: TileProps) {
export default function Tile<T extends React.ElementType = 'button'>(props: TileProps<T>) {
const {
activeStartDate,
children,
Expand All @@ -44,6 +46,7 @@ export default function Tile(props: TileProps) {
onClick,
onMouseOver,
style,
tileAs,
tileClassName: tileClassNameProps,
tileContent: tileContentProps,
tileDisabled,
Expand All @@ -62,8 +65,10 @@ export default function Tile(props: TileProps) {
return typeof tileContentProps === 'function' ? tileContentProps(args) : tileContentProps;
}, [activeStartDate, date, tileContentProps, view]);

const TileComponent = tileAs || 'button';

return (
<button
<TileComponent
className={clsx(classes, tileClassName)}
disabled={
(minDate && minDateTransform(minDate) > date) ||
Expand All @@ -78,6 +83,6 @@ export default function Tile(props: TileProps) {
>
{formatAbbr ? <abbr aria-label={formatAbbr(locale, date)}>{children}</abbr> : children}
{tileContent}
</button>
</TileComponent>
);
}
1 change: 1 addition & 0 deletions packages/react-calendar/src/shared/propTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export const tileProps = {
onClick: PropTypes.func,
onMouseOver: PropTypes.func,
style: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
tileAs: PropTypes.oneOfType([PropTypes.func, PropTypes.oneOf(['button', 'div'] as const)]),
tileClassName: PropTypes.oneOfType([PropTypes.func, isClassName]),
tileContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
tileDisabled: PropTypes.func,
Expand Down
8 changes: 7 additions & 1 deletion packages/react-calendar/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,14 @@ export type OnArgs = {
value: Value;
view: View;
};
export type OnClickType<T extends React.ElementType> = React.ComponentPropsWithoutRef<T>['onClick'];

export type OnClickFunc = (value: Date, event: React.MouseEvent<HTMLButtonElement>) => void;
export type OnClickEventType<T extends React.ElementType> = Parameters<OnClickType<T>>[1];

export type OnClickFunc<T extends React.ElementType> = (
value: Date,
event: OnClickEventType<T>,
) => void;

export type OnClickWeekNumberFunc = (
weekNumber: number,
Expand Down

0 comments on commit dbb3342

Please sign in to comment.