Skip to content

Commit

Permalink
Merge pull request #3 from knightburton/feat/v3-container
Browse files Browse the repository at this point in the history
feat: v3 - container refactor to use slots
  • Loading branch information
knightburton authored Oct 6, 2023
2 parents 936a8ad + d2f2670 commit 5d06ad6
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 61 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"newline-per-chained-call": ["error", { "ignoreChainWithDepth": 3 }],
"import/no-extraneous-dependencies": ["off"],
"react/prop-types": ["off"],
"react/require-default-props": ["off"]
"react/require-default-props": ["off"],
"react/jsx-props-no-spreading": ["off"]
}
}
31 changes: 15 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const App = () => (
weekStartsOn={1}
start={new Date(2021, 1, 1)}
end={new Date(2021, 6, 31)}
height={700}
/>
);

Expand All @@ -59,22 +58,13 @@ For more detailed example check the [example](./example) directory.
| onCellClick | function | `undefined` | Function called when the user clicks a cell. It returns a [Cell Data](#cell-data). |
| showHeader | boolean | `true` | Whether the whole header shall be shown or not. |
| weekStartsOn | number | `0` | The index of the day that the week should starts on. Can be `0`, `1`, `2`, `3`, `4`, `5` or `6`. |
| containerComponent | ComponentType<[ContainerProps](#container-props)> | `undefined` | React component that should be rendered as the main container component. For the passed props check the [ContainerProps](#container-props), please. |
| headerContainerComponent | ComponentType<[Header ContainerProps](#header-container-props)> | `undefined` | React component that should be rendered as the header container component. For the passed props check the [Header ContainerProps](#header-container-props), please.|
| headerCellContentComponent | ComponentType<[HeaderCellContentProps](#header-cell-content-props)> | `undefined` | React component that should be rendered as the header cell content component. For the passed props check the [HeaderCellContentProps](#header-cell-content-props), please. |
| bodyContainerComponent | ComponentType<[BodyContainerProps](#body-container-props)> | `undefined` | React component that should be rendered as the main body container component. For the passed props check the [BodyContainerProps](#body-container-props), please. |
| bodyCellContentComponent | ComponentType<[BodyCellContentProps](#body-cell-content-props)> | `undefined` | React component that should be rendered as the cell content component. For the passed props check the [BodyCellContentProps](#body-cell-content-props), please. |
| emptyComponent | ComponentType<[EmptyProps](#empty-props)> | `undefined` | React component that should be rendered as the empty date range message component. For the passed props check the [EmptyProps](#empty-props), please. |
| containerClassName | string | `''` | Classname(s) that should applied to the calendar container element. |
| headerContainerClassName | string | `''` | Classname(s) that should applied to the header container element. |
| headerRowClassName | string | `''` | Classname(s) that should applied to the header row (list) element. |
| headerCellClassName | string | `''` | Classname(s) that should applied to the header cell (list item) element. |
| headerCellContentClassName | string | `''` | Classname(s) that should applied to the header cell content element. |
| bodyContainerClassName | string | `''` | Classname(s) that should applied to the body container element. |
| bodyRowClassName | string | `''` | Classname(s) that should applied to the body row (list) element. |
| bodyCellClassName | string | `''` | Classname(s) that should applied to the cell (list item) element. |
| bodyCellContentClassName | string | `''` | Classname(s) that should applied to the cell content element. |
| emptyClassName | string | `''` | Classname(s) that should applied to the empty date range message element. |
| slots | [Slots](#slots) | `undefined` | The components used for each slot inside. |
| slotProps | [Slots](#slots) | `undefined` | The extra props for the slot components. You can override the existing props or add new ones. |

#### Cell Data
| Prop name | Type | Description |
Expand All @@ -99,12 +89,21 @@ For more detailed example check the [example](./example) directory.
| long | `string` | Day of the week formatted with the provided `locale` prop as `long` weekday. |
| narrow | `string` | Day of the week formatted with the provided `locale` prop as `narrow` weekday. |

#### Container Props
#### Slots
| Slot | Type | Description |
| --- | --- | --- |
| container | [ContainerSlots](#container-slots) | React components that should be rendered as the main container component. |

#### Container Slots
| Slot | Type | Description |
| --- | --- | --- |
| root | React.ElementType<[ContainerRootProps](#container-root-props)> | All the underlying elements that needs to be rendered to show the calendar content. |

#### Container Root Props
| Prop name | Type | Description |
| --- | --- | --- |
| children | `React.ReactNode` | All the underlaying elements that needs to be rendered to show the calendar content. |
| className | `string` | Merged classnames that includes the default one and the given one from `containerClassName` prop. |
| height | `number` or `'100%'` or `'auto'` | The same height prop that is given for the main component as a prop. |
| children | `React.ReactNode` | All the underlying elements that needs to be rendered to show the calendar content. |
| className | `string` | Merged classnames that includes the default one and the given one from `slotProps.container.root.className` prop. |

#### Header Container Props
| Prop name | Type | Description |
Expand Down
16 changes: 8 additions & 8 deletions __tests__/components/Container.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import Container, { ContainerProps } from '../../src/components/Container';

const mockText = 'Mock Children';
const mockChildren = <span>{mockText}</span>;
const mockComponent = ({ className, children, height }: ContainerProps) => (
<p className={className} style={{ height }}>
const mockComponent = ({ className, children, style }: ContainerProps) => (
<p className={className} style={style}>
{children}
</p>
);
Expand Down Expand Up @@ -56,25 +56,25 @@ describe('Container', () => {
expect(asFragment().firstChild).toMatchInlineSnapshot(defaultInlineSnapshot);
});

test('shows the default Container with a simple dom element as children and height prop', () => {
const { asFragment } = render(<Container height={100}>{mockChildren}</Container>);
test('shows the default Container with a simple dom element as children and style slot props on root', () => {
const { asFragment } = render(<Container slotProps={{ root: { style: { height: 100 } } }}>{mockChildren}</Container>);
const text = screen.getByText(mockText);

expect(text).toBeTruthy();
expect(asFragment().firstChild).toMatchInlineSnapshot(heightInlineSnapshot);
});

test('shows the given Container with a simple dom element as children', () => {
const { asFragment } = render(<Container component={mockComponent}>{mockChildren}</Container>);
test('shows the given Container with a simple component as root slot', () => {
const { asFragment } = render(<Container slots={{ root: mockComponent }}>{mockChildren}</Container>);
const text = screen.getByText(mockText);

expect(text).toBeTruthy();
expect(asFragment().firstChild).toMatchInlineSnapshot(componentInlineSnapshot);
});

test('shows the given Container and custom classname with a simple dom element as children', () => {
test('shows the given Container with a root slot and custom className slot props and a simple dom element as children', () => {
const { asFragment } = render(
<Container component={mockComponent} className="test-calendar">
<Container slots={{ root: mockComponent }} slotProps={{ root: { className: 'test-calendar' } }}>
{mockChildren}
</Container>,
);
Expand Down
21 changes: 13 additions & 8 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import IntervalCalendar from '@knightburton/react-interval-calendar';
import IntervalCalendar, { ContainerProps } from '@knightburton/react-interval-calendar';
import './App.css';

const getLabel = (data: { isFirstDayOfYear: boolean; isFirstDayOfMonth: boolean; day: string; date: Date }) => {
Expand All @@ -7,22 +7,27 @@ const getLabel = (data: { isFirstDayOfYear: boolean; isFirstDayOfMonth: boolean;
return data.day;
};

const Container = ({ children, className, height }: ContainerProps) => (
<div className={className} style={{ height }}>
{children}
</div>
);

const App = () => (
<div className="wrapper">
<IntervalCalendar
weekStartsOn={1}
start={new Date(2023, 0, 1)}
end={new Date(2023, 11, 31)}
height={700}
onCellClick={cell => console.log(cell)}
containerClassName="container"
headerContainerClassName="headerContainer"
headerCellClassName="headerCell"
headerCellContentClassName="headerCellContent"
headerCellContentComponent={({ data, className }) => <span className={className}>{data.long}</span>}
bodyRowClassName="bodyRow"
bodyCellClassName="bodyCell"
bodyCellContentComponent={({ data }) => <span className={`${data.isMonthEven ? 'evenMonth' : ''} ${data.isToday ? 'today' : ''}`}>{getLabel(data)}</span>}
slots={{
container: Container,
}}
slotProps={{
container: { height: 700 },
}}
/>
</div>
);
Expand Down
39 changes: 17 additions & 22 deletions src/components/Container.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,27 @@
import React, { memo, useMemo } from 'react';
import React, { memo } from 'react';
import classnames from '../utils/classnames';
import styles from './styles.less';
import { SlotComponentProps } from '../types';

export interface ContainerProps {
children?: React.ReactNode;
className?: string;
height?: number | '100%' | 'auto';
}
export type ContainerProps = SlotComponentProps<'div', Record<string, unknown>>;

export interface ContainerPrivateProps extends ContainerProps {
component?: React.ComponentType<ContainerProps>;
}
export type ContainerPrivateProps = {
slots?: {
root?: React.ElementType;
};
slotProps?: {
root: ContainerProps;
};
children?: React.ReactNode;
};

const Container = memo(
({ children, component: Component, className, height }: ContainerPrivateProps): JSX.Element => {
const classes = useMemo(() => classnames(styles.calendar, className), [className]);
({ children, slots, slotProps }: ContainerPrivateProps): JSX.Element => {
const containerClassName = classnames(styles.calendar, slotProps?.root?.className);
const containerProps = { ...(slotProps?.root || {}), className: containerClassName };
const ContainerSlot = slots?.root || 'div';

if (Component)
return (
<Component className={classes} height={height}>
{children}
</Component>
);
return (
<div className={classes} style={{ height }}>
{children}
</div>
);
return <ContainerSlot {...containerProps}>{children}</ContainerSlot>;
},
);

Expand Down
22 changes: 16 additions & 6 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useCallback, useMemo, useState } from 'react';
import { getCalendarBaseAttributes } from './helpers';
import { CalendarTuple, VisibilityMatrix, WeekdayIndex, BodyCellType } from './types';
import Container, { ContainerProps } from './components/Container';
import Container, { ContainerProps, ContainerPrivateProps } from './components/Container';
import Header from './components/Header';
import { HeaderContainerProps } from './components/HeaderContainer';
import { HeaderCellContentProps } from './components/HeaderCellContent';
Expand All @@ -12,8 +12,9 @@ import BodyCell from './components/BodyCell';
import { BodyCellContentProps } from './components/BodyCellContent';
import Empty, { EmptyProps } from './components/Empty';

export type { ContainerProps } from './components/Container';

type ClassNames = {
container?: string;
headerContainer?: string;
headerRow?: string;
headerCell?: string;
Expand All @@ -25,11 +26,18 @@ type ClassNames = {
empty?: string;
};

type Slots = {
container?: ContainerPrivateProps['slots'];
};

type SlotProps = {
container?: ContainerPrivateProps['slotProps'];
};

export type IntervalCalendarProps = {
start?: Date;
end?: Date;
emptyLabel?: string;
height?: number | '100%' | 'auto';
locale?: string;
numberOfRowsFirstRender?: number;
numberOfRowsPreRender?: number;
Expand All @@ -43,27 +51,29 @@ export type IntervalCalendarProps = {
bodyContainerComponent?: React.ComponentType<BodyContainerProps>;
bodyCellContentComponent?: React.ComponentType<BodyCellContentProps>;
emptyComponent?: React.ComponentType<EmptyProps>;
slots?: Slots;
slotProps?: SlotProps;
classNames?: ClassNames;
};

const IntervalCalendar = ({
start = undefined,
end = undefined,
emptyLabel = '',
height = 500,
locale = 'default',
numberOfRowsFirstRender = 8,
numberOfRowsPreRender = 4,
startRenderOnCurrentWeek = false,
onCellClick = undefined,
showHeader = true,
weekStartsOn = 0,
containerComponent,
headerContainerComponent,
headerCellContentComponent,
bodyContainerComponent,
bodyCellContentComponent,
emptyComponent,
slots,
slotProps,
classNames,
}: IntervalCalendarProps): JSX.Element => {
const [startDate, , numberOfWeeks, numberOfTodayWeek] = useMemo<CalendarTuple>(() => getCalendarBaseAttributes(start, end, weekStartsOn), [start, end, weekStartsOn]);
Expand All @@ -85,7 +95,7 @@ const IntervalCalendar = ({
);

return (
<Container height={height} component={containerComponent} className={classNames?.container}>
<Container slots={slots?.container} slotProps={slotProps?.container}>
<Header
weekStartsOn={weekStartsOn}
locale={locale}
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ export type ClassNamesObject = Record<string, unknown>;
export type ClassNamesArray = Array<Argument>;
export type Argument = ClassNamesValue | ClassNamesObject | ClassNamesArray;
export type ClassNames = Argument;
export type SlotComponentProps<SlotComponent extends React.ElementType, Overrides> = Partial<React.ComponentPropsWithRef<SlotComponent>> & Overrides;

0 comments on commit 5d06ad6

Please sign in to comment.