Skip to content

Commit

Permalink
Merge pull request #6 from knightburton/feat/v3-body
Browse files Browse the repository at this point in the history
feat: v3 - restructure the body to work with slots
  • Loading branch information
knightburton authored Oct 9, 2023
2 parents 8d28867 + 545a000 commit c91ec9d
Show file tree
Hide file tree
Showing 15 changed files with 339 additions and 320 deletions.
47 changes: 16 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const App = () => (
weekStartsOn={1}
start={new Date(2021, 1, 1)}
end={new Date(2021, 6, 31)}
slotProps={{ bodyCell: { onClick: (event, data) => console.log(event, data) } }}
/>
);

Expand All @@ -55,11 +56,7 @@ For more detailed example check the [example](./example) directory.
| numberOfRowsFirstRender | number | `8` | Number of weeks to render below the visible weeks on the first render. |
| numberOfRowsPreRender | number | `4` | Number of weeks to render below the visible weeks. Tweaking this can help reduce flickering during scrolling on certain browers/devices. |
| startRenderOnCurrentWeek | boolean | `false` | Wether the render of weeks should start at the current week or the start od the given calendar interval. |
| 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`. |
| 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. |
| slots | [Slots](#slots) | `undefined` | The components used for each slot inside. |
| slotProps | [Slots](#slot-props) | `undefined` | The extra props for the slot components. You can override the existing props or add new ones. |
Expand Down Expand Up @@ -91,44 +88,32 @@ For more detailed example check the [example](./example) directory.
```ts
type Slots = {
root?: React.ElementType;
header?: {
root?: React.ElementType;
cell?: React.ElementType;
cellContent?: React.ElementType;
};
header?: React.ElementType;
headerCell?: React.ElementType;
headerCellContent?: React.ElementType;
body?: React.ElementType;
bodyRow?: React.ElementType;
bodyCell?: React.ElementType;
bodyCellContent?: React.ElementType;
};
```

#### Slot Props
```ts
type SlotProps = {
root?: Partial<React.ComponentPropsWithRef<'div'>>;
header?: {
root?: Partial<React.ComponentPropsWithRef<'ul'>> & { disabled?: boolean };
cell?: Partial<React.ComponentPropsWithRef<'li'>>;
cellContent?: Partial<React.ComponentPropsWithRef<'div'>>;
header?: Partial<React.ComponentPropsWithRef<'ul'>> & { disabled?: boolean };
headerCell?: Partial<React.ComponentPropsWithRef<'li'>>;
headerCellContent?: Partial<React.ComponentPropsWithRef<'div'>>;
body?: Partial<React.ComponentPropsWithRef<'div'>>;
bodyRow?: Partial<React.ComponentPropsWithRef<'ul'>>;
bodyCell?: Omit<Partial<React.ComponentPropsWithRef<'li'>>, 'onClick'> & {
onClick: (event: React.MouseEventHandler<HTMLLIElement>, data: BodyCellType) => void;
};
bodyCellContent?: RPartial<React.ComponentPropsWithRef<'div'>>;
};
```

#### Body Container Props
| Prop name | Type | Description |
| --- | --- | --- |
| children | `React.ReactNode` | All the underlaying elements that needs to be rendered to show the body content. |
| className | `string` | Merged classnames that includes the default one and the given one from `bodyContainerClassName` prop. |

#### Body Cell Content Props
| Prop name | Type | Description |
| --- | --- | --- |
| data | [Cell Data](#cell-data) | The actual date data structure that should be rendered in the cell. |
| className | `string` | Merged classnames that includes the default one and the given one from `bodyCellContentClassName` prop. |

#### Empty Props
| Prop name | Type | Description |
| --- | --- | --- |
| emptyLabel | `string` | The actual message that should be visible when there is no valid date range. |
| className | `string` | Merged classnames that includes the default one and the given one from `emptyClassName` prop. |

### Development
Local development is broken into two parts (ideally using two terminal tabs).

Expand Down
66 changes: 34 additions & 32 deletions __tests__/components/Body.test.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,79 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import Body, { BodyProps } from '../../src/components/Body';
import Body, { BodyPrivateProps } from '../../src/components/Body';
import { mockAllIsIntersecting } from '../testUtils';

const mockBodyProps: BodyProps = {
const mockBodyProps: BodyPrivateProps = {
startDate: new Date(2023, 1, 16, 0, 0, 0, 0),
numberOfWeeks: 3,
renderRow: numberOfWeek => <p key={numberOfWeek}>{`${numberOfWeek}. week`}</p>,
containerComponent: ({ children, className }) => <div className={className}>{children}</div>,
containerClassName: 'test-body',
numberOfTodayWeek: 1,
startRenderOnCurrentWeek: false,
locale: 'en-EN',
numberOfRowsPreRender: 3,
updateVisibilityMatrix: jest.fn(),
visibilityMatrix: { 0: false, 1: true, 2: false, 3: false },
slots: {
row: React.forwardRef<HTMLParagraphElement>((_, ref): JSX.Element => <p ref={ref}>week</p>),
},
};
const defaultInlineSnapshot = `
<div
class="body"
id="test"
>
<p>
0. week
week
</p>
<p>
1. week
week
</p>
<p>
2. week
week
</p>
<p>
3. week
week
</p>
</div>
`;
const customInlineSnapshot = `
<div
class="body test-body"
class="body"
>
<p>
0. week
week
</p>
<p>
1. week
week
</p>
<p>
2. week
week
</p>
<p>
3. week
week
</p>
</div>
`;

describe('Body', () => {
test('shows the default Body with a 3 weeks date range', () => {
const { asFragment } = render(<Body startDate={mockBodyProps.startDate} numberOfWeeks={mockBodyProps.numberOfWeeks} renderRow={mockBodyProps.renderRow} />);
const firstWeekText = screen.getByText('0. week');
const thirdWeekText = screen.getByText('3. week');
const { asFragment } = render(<Body {...mockBodyProps} slotProps={{ root: { id: 'test' } }} />);
const weeks = screen.getAllByText('week');

mockAllIsIntersecting(true);

expect(firstWeekText).toBeTruthy();
expect(thirdWeekText).toBeTruthy();
expect(weeks).toBeTruthy();
expect(weeks).toHaveLength(4);
expect(asFragment().firstChild).toMatchInlineSnapshot(defaultInlineSnapshot);
});

test('shows the default Body with a all props changed', () => {
const { asFragment } = render(
<Body
startDate={mockBodyProps.startDate}
numberOfWeeks={mockBodyProps.numberOfWeeks}
renderRow={mockBodyProps.renderRow}
containerComponent={mockBodyProps.containerComponent}
containerClassName={mockBodyProps.containerClassName}
/>,
);
const firstWeekText = screen.getByText('0. week');
const thirdWeekText = screen.getByText('3. week');
const { asFragment } = render(<Body {...mockBodyProps} />);
const weeks = screen.getAllByText('week');

mockAllIsIntersecting(true);

expect(firstWeekText).toBeTruthy();
expect(thirdWeekText).toBeTruthy();
expect(weeks).toBeTruthy();
expect(weeks).toHaveLength(4);
expect(asFragment().firstChild).toMatchInlineSnapshot(customInlineSnapshot);
});
});
36 changes: 16 additions & 20 deletions __tests__/components/BodyCell.test.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import * as React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import BodyCell, { BodyCellProps } from '../../src/components/BodyCell';
import BodyCell, { BodyCellPrivateProps } from '../../src/components/BodyCell';
import { mockBodyCellAttributes } from '../testUtils';

const mockBodyCellProps: BodyCellProps = {
const mockBodyCellProps: BodyCellPrivateProps = {
data: mockBodyCellAttributes(),
locale: 'hu-HU',
onClick: jest.fn(),
contentComponent: ({ data, className }) => <p className={className}>{data.day}</p>,
className: 'test-body-cell',
contentClassName: 'test-body-cell-content',
slots: {
content: ({ data, className }) => <p className={className}>{data.day}</p>,
},
slotProps: {
root: { className: 'test-body-cell', onClick: jest.fn() },
content: { className: 'test-body-cell-content' },
},
};
const defaultInlineSnapshot = `
<li
class="body__cell"
role="presentation"
>
<span
<div
class="body__cell__content"
>
18
</span>
</div>
</li>
`;
const customInlineSnapshot = `
Expand Down Expand Up @@ -51,16 +54,9 @@ describe('BodyCell', () => {
expect(asFragment().firstChild).toMatchInlineSnapshot(defaultInlineSnapshot);
});

test('shows the BodyCell with override components and classNames', () => {
test('shows the BodyCell with override slots', () => {
const { asFragment } = render(
<BodyCell
data={mockBodyCellProps.data}
locale={mockBodyCellProps.locale}
onClick={mockBodyCellProps.onClick}
contentComponent={mockBodyCellProps.contentComponent}
contentClassName={mockBodyCellProps.contentClassName}
className={mockBodyCellProps.className}
/>,
<BodyCell data={mockBodyCellProps.data} locale={mockBodyCellProps.locale} slots={mockBodyCellProps.slots} slotProps={mockBodyCellProps.slotProps} />,
);
const dayText = screen.getByText('18');
const listItems = screen.getAllByRole('presentation');
Expand All @@ -75,14 +71,14 @@ describe('BodyCell', () => {
const listItem = screen.getByRole('presentation');

fireEvent.click(listItem);
expect(mockBodyCellProps.onClick).not.toHaveBeenCalled();
expect(mockBodyCellProps.slotProps?.root?.onClick).not.toHaveBeenCalled();
});

test('fire presentation item on click with handler', () => {
render(<BodyCell data={mockBodyCellProps.data} onClick={mockBodyCellProps.onClick} />);
render(<BodyCell data={mockBodyCellProps.data} slotProps={mockBodyCellProps.slotProps} />);
const listItem = screen.getByRole('presentation');

fireEvent.click(listItem);
expect(mockBodyCellProps.onClick).toHaveBeenCalledWith(mockBodyCellProps.data);
expect(mockBodyCellProps.slotProps?.root?.onClick).toHaveBeenCalled();
});
});
19 changes: 8 additions & 11 deletions __tests__/components/BodyCellContent.test.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import BodyCellContent, { BodyCellContentProps } from '../../src/components/BodyCellContent';
import BodyCellContent from '../../src/components/BodyCellContent';
import { mockBodyCellAttributes } from '../testUtils';

const mockComponent = ({ data, className }: BodyCellContentProps): JSX.Element => <p className={className}>{data.day}</p>;
const mockClassName = 'test-body-cell-content';
const defaultInlineSnapshot = `
<span
class="body__cell__content"
>
<div>
18
</span>
</div>
`;
const customerInlineSnapshot = `
<p
class="body__cell__content ${mockClassName}"
<div
class="${mockClassName}"
>
18
</p>
</div>
`;

describe('BodyCellContent', () => {
Expand All @@ -36,8 +33,8 @@ describe('BodyCellContent', () => {
expect(text).toBeTruthy();
});

test('shows the BodyCellContent with custom component and className', () => {
const { asFragment } = render(<BodyCellContent data={mockBodyCellAttributes()} component={mockComponent} className={mockClassName} />);
test('shows the BodyCellContent with custom className', () => {
const { asFragment } = render(<BodyCellContent data={mockBodyCellAttributes()} className={mockClassName} />);
const text = screen.getByText('18');

expect(text).toBeTruthy();
Expand Down
48 changes: 0 additions & 48 deletions __tests__/components/BodyContainer.test.tsx

This file was deleted.

Loading

0 comments on commit c91ec9d

Please sign in to comment.