diff --git a/.eslintrc.js b/.eslintrc.js index ed2dc449..a0f32522 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,6 @@ module.exports = { parser: '@typescript-eslint/parser', - extends: ['turbo', 'prettier'], + extends: ['plugin:react-hooks/recommended', 'turbo', 'prettier'], plugins: ['sort-imports-es6-autofix', '@typescript-eslint'], rules: { 'react/jsx-key': 'off', diff --git a/apps/schedulely-docs/docs/Usage/Actions.md b/apps/schedulely-docs/docs/Usage/Actions.md index 0b7d8e32..34a8f7e8 100644 --- a/apps/schedulely-docs/docs/Usage/Actions.md +++ b/apps/schedulely-docs/docs/Usage/Actions.md @@ -4,8 +4,9 @@ description: Functions used for interacting with Schedulely --- While Schedulely expects developers to implement their own components to achieve their desired goals, we have provided clear interfaces for how actions should be handled. -The ActionProvider is used under the hood to take in functions as arguments, memoize them, and pass them in to the calendar components. This makes state management within -Schedulely simple, and ensures we are re-rendering the bare minimum. +The ActionProvider is used under the hood to take in functions as arguments and pass them in to the calendar components. This makes state management within +Schedulely simple, and ensures we are re-rendering the bare minimum. If you are creating custom calendar components, these actions are available to you on each components +respective interface, and can be implemented(or not implemented) however you choose. **All actions return `() => null` unless explicitly overridden.** diff --git a/package-lock.json b/package-lock.json index f24a0c7e..abeacd32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "eslint-config-prettier": "^8.3.0", "eslint-config-turbo": "latest", "eslint-plugin-react": "7.31.8", + "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-sort-imports-es6-autofix": "^0.6.0", "husky": "^8.0.1", "prettier": "latest", @@ -19246,10 +19247,6 @@ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, - "node_modules/tsconfig": { - "resolved": "packages/tsconfig", - "link": true - }, "node_modules/tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", @@ -20998,7 +20995,7 @@ }, "packages/Schedulely": { "name": "schedulely", - "version": "0.1.15", + "version": "0.2.0", "license": "Apache-2.0", "dependencies": { "@ladle/react": "^2.4.5", @@ -21053,7 +21050,8 @@ "license": "0BSD" }, "packages/tsconfig": { - "version": "0.0.0" + "version": "0.0.0", + "extraneous": true } }, "dependencies": { @@ -35105,9 +35103,6 @@ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, - "tsconfig": { - "version": "file:packages/tsconfig" - }, "tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", diff --git a/package.json b/package.json index 7b804355..1d0b4954 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,10 @@ "eslint": "^8.24.0", "eslint-config-next": "^12.0.8", "eslint-config-prettier": "^8.3.0", + "eslint-config-turbo": "latest", "eslint-plugin-react": "7.31.8", + "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-sort-imports-es6-autofix": "^0.6.0", - "eslint-config-turbo": "latest", "husky": "^8.0.1", "prettier": "latest", "turbo": "1.6.2", diff --git a/packages/Schedulely/__stories__/Schedulely.stories.tsx b/packages/Schedulely/__stories__/Schedulely.stories.tsx index 9a217ef3..8312044f 100644 --- a/packages/Schedulely/__stories__/Schedulely.stories.tsx +++ b/packages/Schedulely/__stories__/Schedulely.stories.tsx @@ -23,6 +23,11 @@ export const NoEvents = () => { const props: SchedulelyProps = { events: [], initialDate: new Date().toISOString(), + actions: { + onMoreEventsClick: (events) => console.log(events), + onEventClick: (event) => console.log(event), + onDayClick: (day) => console.log(day), + }, }; return ( @@ -39,6 +44,11 @@ export const DefaultTheme = () => { const props: SchedulelyProps = { events: storyEvents, initialDate: new Date().toISOString(), + actions: { + onMoreEventsClick: (events) => console.log(events), + onEventClick: (event) => console.log(event), + onDayClick: (day) => console.log(day), + }, }; return ( false); let mockEventOnClickHandler = jest.fn(() => {}); let mockSetParentContainerRef = jest.fn((eventId: string) => {}); -let mockSetRefFromKey = jest.fn((eventId: string) => {}); -let mockIsEventVisible = jest.fn((eventId: string) => true); jest.mock('@/hooks', () => ({ useComponents: jest.fn(() => ({ @@ -65,8 +63,6 @@ jest.mock('@/hooks', () => ({ })), useEventIntersection: jest.fn(() => ({ setParentContainerRef: mockSetParentContainerRef, - setRefFromKey: mockSetRefFromKey, - isEventVisible: mockIsEventVisible, })), })); @@ -80,11 +76,6 @@ describe('EventWeekLayout', () => { ); }); - afterEach(() => { - mockIsEventVisible.mockClear(); - mockSetRefFromKey.mockClear(); - }); - describe.each(events.map((x) => x.summary))('event %s', (value) => { let eventDomObject: HTMLElement; @@ -125,22 +116,6 @@ describe('EventWeekLayout', () => { describe('useEventIntersection', () => { it('setParentContainerRef is called on the parent', () => expect(mockSetParentContainerRef).toHaveBeenCalledTimes(1)); - - it('isEventVisible is called', () => - expect(mockIsEventVisible).toHaveBeenCalledTimes(events.length)); - - it('isEventVisible is called with each eventId', () => - expect(mockIsEventVisible.mock.calls.flat().sort()).toEqual( - events.map((x) => x.id).sort() - )); - - it('setRefFromKey is called', () => - expect(mockSetRefFromKey).toHaveBeenCalledTimes(events.length)); - - it('setRefFromKey is called with each eventId', () => - expect(mockSetRefFromKey.mock.calls.flat().sort()).toEqual( - events.map((x) => x.id).sort() - )); }); describe('getGridEndIndex', () => { diff --git a/packages/Schedulely/__tests__/layouts/MonthLayout.spec.tsx b/packages/Schedulely/__tests__/layouts/MonthLayout.spec.tsx index 893fdd82..b0bf3479 100644 --- a/packages/Schedulely/__tests__/layouts/MonthLayout.spec.tsx +++ b/packages/Schedulely/__tests__/layouts/MonthLayout.spec.tsx @@ -1,211 +1,211 @@ -import { EventLayoutProps, MonthLayout, WeekLayoutProps } from '@/layouts'; -import { InternalCalendarEvent, InternalEventWeek } from '@/types'; -import { ReactNode } from 'react'; -import { RenderResult, render } from '@testing-library/react'; - -const mockCalendarWithEvents = [ - { - weekStart: new Date(2022, 8, 25), - weekEnd: new Date(2022, 9, 1), - daysInWeek: [ - new Date(2022, 8, 25), - new Date(2022, 8, 26), - new Date(2022, 8, 27), - new Date(2022, 8, 28), - new Date(2022, 8, 29), - new Date(2022, 8, 30), - new Date(2022, 9, 1), - ], - events: [] as InternalCalendarEvent[], - eventsOnDays: {}, - }, - { - weekStart: new Date(2022, 9, 2), - weekEnd: new Date(2022, 9, 8), - daysInWeek: [ - new Date(2022, 9, 2), - new Date(2022, 9, 3), - new Date(2022, 9, 4), - new Date(2022, 9, 5), - new Date(2022, 9, 6), - new Date(2022, 9, 7), - new Date(2022, 9, 8), - ], - events: [] as InternalCalendarEvent[], - eventsOnDays: {}, - }, - { - weekStart: new Date(2022, 9, 9), - weekEnd: new Date(2022, 9, 15), - daysInWeek: [ - new Date(2022, 9, 9), - new Date(2022, 9, 10), - new Date(2022, 9, 11), - new Date(2022, 9, 12), - new Date(2022, 9, 13), - new Date(2022, 9, 14), - new Date(2022, 9, 15), - ], - events: [] as InternalCalendarEvent[], - eventsOnDays: {}, - }, - { - weekStart: new Date(2022, 9, 16), - weekEnd: new Date(2022, 9, 22), - daysInWeek: [ - new Date(2022, 9, 16), - new Date(2022, 9, 17), - new Date(2022, 9, 18), - new Date(2022, 9, 19), - new Date(2022, 9, 20), - new Date(2022, 9, 21), - new Date(2022, 9, 22), - ], - events: [] as InternalCalendarEvent[], - eventsOnDays: {}, - }, - { - weekStart: new Date(2022, 9, 23), - weekEnd: new Date(2022, 9, 29), - daysInWeek: [ - new Date(2022, 9, 23), - new Date(2022, 9, 24), - new Date(2022, 9, 25), - new Date(2022, 9, 26), - new Date(2022, 9, 27), - new Date(2022, 9, 28), - new Date(2022, 9, 29), - ], - events: [] as InternalCalendarEvent[], - eventsOnDays: {}, - }, - { - weekStart: new Date(2022, 9, 30), - weekEnd: new Date(2022, 10, 5), - daysInWeek: [ - new Date(2022, 9, 30), - new Date(2022, 9, 31), - new Date(2022, 1, 1), - new Date(2022, 1, 2), - new Date(2022, 1, 3), - new Date(2022, 1, 4), - new Date(2022, 1, 5), - ], - events: [] as InternalCalendarEvent[], - eventsOnDays: {}, - }, -] as InternalEventWeek[]; - -const mockUseKeyboardControls = jest.fn(() => null); - -jest.mock('@/hooks', () => ({ - useCalendar: jest.fn(() => ({ - calendarWithEvents: mockCalendarWithEvents, - })), - useKeyboardControls: jest.fn(() => mockUseKeyboardControls()), -})); - -const mockEventIntersectionProviderPropsCheck = jest.fn(); -jest.mock('@/providers', () => ({ - EventIntersectionProvider: jest.fn( - ({ - children, - events, - }: { - children: ReactNode; - events: InternalCalendarEvent[]; - }) => { - mockEventIntersectionProviderPropsCheck(events); - return
{children}
; - } - ), - HighlightProvider: jest.fn(({ children }: { children: ReactNode }) => ( -
{children}
- )), -})); - -const mockEventWeekPropsCheck = jest.fn(); -jest.mock('@/layouts/eventWeekLayout', () => ({ - EventWeekLayout: jest.fn(({ events, daysInweek }: EventLayoutProps) => { - mockEventWeekPropsCheck(events, daysInweek); - return
; - }), -})); - -const mockWeekLayoutPropsCheck = jest.fn(); -jest.mock('@/layouts/weekLayout', () => ({ - WeekLayout: jest.fn(({ dates }: WeekLayoutProps) => { - mockWeekLayoutPropsCheck(dates); - return
; - }), -})); - -describe('MonthLayout', () => { - let testObject: RenderResult; - - beforeEach(() => { - testObject = render(); - }); - - afterEach(() => { - mockUseKeyboardControls.mockClear(); - }); - - describe.each(mockCalendarWithEvents.map((row, i) => ({ ...row, index: i })))( - 'Week $index', - (week) => { - describe('EventWeekLayout', () => { - it('receives array of days', () => { - expect(mockEventWeekPropsCheck.mock.calls[week.index][1]).toEqual( - mockCalendarWithEvents[week.index].daysInWeek - ); - }); - - it('receives array of events', () => { - expect(mockEventWeekPropsCheck.mock.calls[week.index][0]).toEqual( - mockCalendarWithEvents[week.index].events - ); - }); - }); - - describe('WeekLayout', () => { - it('receives array of days', () => { - expect(mockWeekLayoutPropsCheck.mock.calls[week.index][0]).toEqual( - mockCalendarWithEvents[week.index].daysInWeek - ); - }); - }); - - describe('EventIntersectionProvider', () => { - it('receives array of days', () => { - expect( - mockEventIntersectionProviderPropsCheck.mock.calls[week.index][0] - ).toEqual(mockCalendarWithEvents[week.index].events); - }); - }); - } - ); - - it('initializes keyboard controls', () => - expect(mockUseKeyboardControls).toHaveBeenCalledTimes(1)); - - it('renders one highlight provider', () => - expect(testObject.getAllByTestId('highlight-provider-mock').length).toEqual( - 1 - )); - - it('renders the correct number of intersection providers', () => - expect( - testObject.queryAllByTestId('intersection-provider-mock').length - ).toEqual(mockCalendarWithEvents.length)); - - it('renders the correct number of EventWeekLayout', () => - expect( - testObject.queryAllByTestId('event-week-layout-mock').length - ).toEqual(mockCalendarWithEvents.length)); - - it('renders the correct number of WeekLayout', () => - expect(testObject.queryAllByTestId('week-layout-mock').length).toEqual( - mockCalendarWithEvents.length - )); -}); +import { EventLayoutProps, MonthLayout, WeekLayoutProps } from '@/layouts'; +import { InternalCalendarEvent, InternalEventWeek } from '@/types'; +import { ReactNode } from 'react'; +import { RenderResult, render } from '@testing-library/react'; + +const mockCalendarWithEvents = [ + { + weekStart: new Date(2022, 8, 25), + weekEnd: new Date(2022, 9, 1), + daysInWeek: [ + new Date(2022, 8, 25), + new Date(2022, 8, 26), + new Date(2022, 8, 27), + new Date(2022, 8, 28), + new Date(2022, 8, 29), + new Date(2022, 8, 30), + new Date(2022, 9, 1), + ], + events: [] as InternalCalendarEvent[], + eventsOnDays: {}, + }, + { + weekStart: new Date(2022, 9, 2), + weekEnd: new Date(2022, 9, 8), + daysInWeek: [ + new Date(2022, 9, 2), + new Date(2022, 9, 3), + new Date(2022, 9, 4), + new Date(2022, 9, 5), + new Date(2022, 9, 6), + new Date(2022, 9, 7), + new Date(2022, 9, 8), + ], + events: [] as InternalCalendarEvent[], + eventsOnDays: {}, + }, + { + weekStart: new Date(2022, 9, 9), + weekEnd: new Date(2022, 9, 15), + daysInWeek: [ + new Date(2022, 9, 9), + new Date(2022, 9, 10), + new Date(2022, 9, 11), + new Date(2022, 9, 12), + new Date(2022, 9, 13), + new Date(2022, 9, 14), + new Date(2022, 9, 15), + ], + events: [] as InternalCalendarEvent[], + eventsOnDays: {}, + }, + { + weekStart: new Date(2022, 9, 16), + weekEnd: new Date(2022, 9, 22), + daysInWeek: [ + new Date(2022, 9, 16), + new Date(2022, 9, 17), + new Date(2022, 9, 18), + new Date(2022, 9, 19), + new Date(2022, 9, 20), + new Date(2022, 9, 21), + new Date(2022, 9, 22), + ], + events: [] as InternalCalendarEvent[], + eventsOnDays: {}, + }, + { + weekStart: new Date(2022, 9, 23), + weekEnd: new Date(2022, 9, 29), + daysInWeek: [ + new Date(2022, 9, 23), + new Date(2022, 9, 24), + new Date(2022, 9, 25), + new Date(2022, 9, 26), + new Date(2022, 9, 27), + new Date(2022, 9, 28), + new Date(2022, 9, 29), + ], + events: [] as InternalCalendarEvent[], + eventsOnDays: {}, + }, + { + weekStart: new Date(2022, 9, 30), + weekEnd: new Date(2022, 10, 5), + daysInWeek: [ + new Date(2022, 9, 30), + new Date(2022, 9, 31), + new Date(2022, 1, 1), + new Date(2022, 1, 2), + new Date(2022, 1, 3), + new Date(2022, 1, 4), + new Date(2022, 1, 5), + ], + events: [] as InternalCalendarEvent[], + eventsOnDays: {}, + }, +] as (InternalEventWeek & { weekStart: Date; weekEnd: Date })[]; + +const mockUseKeyboardControls = jest.fn(() => null); + +jest.mock('@/hooks', () => ({ + useCalendar: jest.fn(() => ({ + calendarWithEvents: mockCalendarWithEvents, + })), + useKeyboardControls: jest.fn(() => mockUseKeyboardControls()), +})); + +const mockEventIntersectionProviderPropsCheck = jest.fn(); +jest.mock('@/providers', () => ({ + EventIntersectionProvider: jest.fn( + ({ + children, + events, + }: { + children: ReactNode; + events: InternalCalendarEvent[]; + }) => { + mockEventIntersectionProviderPropsCheck(events); + return
{children}
; + } + ), + HighlightProvider: jest.fn(({ children }: { children: ReactNode }) => ( +
{children}
+ )), +})); + +const mockEventWeekPropsCheck = jest.fn(); +jest.mock('@/layouts/eventWeekLayout', () => ({ + EventWeekLayout: jest.fn(({ events, daysInweek }: EventLayoutProps) => { + mockEventWeekPropsCheck(events, daysInweek); + return
; + }), +})); + +const mockWeekLayoutPropsCheck = jest.fn(); +jest.mock('@/layouts/weekLayout', () => ({ + WeekLayout: jest.fn(({ dates }: WeekLayoutProps) => { + mockWeekLayoutPropsCheck(dates); + return
; + }), +})); + +describe('MonthLayout', () => { + let testObject: RenderResult; + + beforeEach(() => { + testObject = render(); + }); + + afterEach(() => { + mockUseKeyboardControls.mockClear(); + }); + + describe.each(mockCalendarWithEvents.map((row, i) => ({ ...row, index: i })))( + 'Week $index', + (week) => { + describe('EventWeekLayout', () => { + it('receives array of days', () => { + expect(mockEventWeekPropsCheck.mock.calls[week.index][1]).toEqual( + mockCalendarWithEvents[week.index].daysInWeek + ); + }); + + it('receives array of events', () => { + expect(mockEventWeekPropsCheck.mock.calls[week.index][0]).toEqual( + mockCalendarWithEvents[week.index].events + ); + }); + }); + + describe('WeekLayout', () => { + it('receives array of days', () => { + expect(mockWeekLayoutPropsCheck.mock.calls[week.index][0]).toEqual( + mockCalendarWithEvents[week.index].daysInWeek + ); + }); + }); + + describe('EventIntersectionProvider', () => { + it('receives array of days', () => { + expect( + mockEventIntersectionProviderPropsCheck.mock.calls[week.index][0] + ).toEqual(mockCalendarWithEvents[week.index].events); + }); + }); + } + ); + + it('initializes keyboard controls', () => + expect(mockUseKeyboardControls).toHaveBeenCalledTimes(1)); + + it('renders one highlight provider', () => + expect(testObject.getAllByTestId('highlight-provider-mock').length).toEqual( + 1 + )); + + it('renders the correct number of intersection providers', () => + expect( + testObject.queryAllByTestId('intersection-provider-mock').length + ).toEqual(mockCalendarWithEvents.length)); + + it('renders the correct number of EventWeekLayout', () => + expect( + testObject.queryAllByTestId('event-week-layout-mock').length + ).toEqual(mockCalendarWithEvents.length)); + + it('renders the correct number of WeekLayout', () => + expect(testObject.queryAllByTestId('week-layout-mock').length).toEqual( + mockCalendarWithEvents.length + )); +}); diff --git a/packages/Schedulely/package.json b/packages/Schedulely/package.json index 0adbebea..03ab562c 100644 --- a/packages/Schedulely/package.json +++ b/packages/Schedulely/package.json @@ -1,6 +1,6 @@ { "name": "schedulely", - "version": "0.2.0", + "version": "0.2.1", "keywords": [ "schedule", "calendar", diff --git a/packages/Schedulely/src/layouts/eventWeekLayout/EventWeekLayout.tsx b/packages/Schedulely/src/layouts/eventWeekLayout/EventWeekLayout.tsx index 3928371c..8f042a4a 100644 --- a/packages/Schedulely/src/layouts/eventWeekLayout/EventWeekLayout.tsx +++ b/packages/Schedulely/src/layouts/eventWeekLayout/EventWeekLayout.tsx @@ -28,8 +28,7 @@ export const EventWeekLayout = ({ events, daysInweek }: EventLayoutProps) => { const { eventComponent: EventComponent } = useComponents(); const { setHighlight, clearHighlight, isHighlighted } = useEventHighlight(); const { onEventClick } = useActions(); - const { setParentContainerRef, setRefFromKey, isEventVisible } = - useEventIntersection(); + const { setParentContainerRef } = useEventIntersection(); return (
@@ -43,11 +42,9 @@ export const EventWeekLayout = ({ events, daysInweek }: EventLayoutProps) => { style={{ gridColumnStart: getGridStartIndex(event.start, daysInweek[0]), gridColumnEnd: getGridEndIndex(event.end, daysInweek[6]), - visibility: isEventVisible(event.id) ? 'visible' : 'hidden', }} onMouseOver={() => setHighlight(event.id)} onMouseLeave={clearHighlight} - ref={setRefFromKey(event.id)} > ) => { - const onEventClick = useCallback(actions?.onEventClick || (() => null), [ - actions?.onEventClick, - ]); + const defaultAction = () => null; - const onMoreEventsClick = useCallback( - actions?.onMoreEventsClick || (() => null), - [actions?.onMoreEventsClick] - ); - - const onDayClick = useCallback(actions?.onDayClick || (() => null), [ - actions?.onDayClick, - ]); - - const onMonthChangeClick = useCallback( - actions?.onMonthChangeClick || (() => null), - [actions?.onMonthChangeClick] - ); + const onEventClick = actions?.onEventClick || defaultAction; + const onMoreEventsClick = actions?.onMoreEventsClick || defaultAction; + const onDayClick = actions?.onDayClick || defaultAction; + const onMonthChangeClick = actions?.onMonthChangeClick || defaultAction; const context: ActionContextState = { onEventClick, diff --git a/packages/Schedulely/src/providers/BreakPointProvider.tsx b/packages/Schedulely/src/providers/BreakPointProvider.tsx index ae7e9761..ff7cac12 100644 --- a/packages/Schedulely/src/providers/BreakPointProvider.tsx +++ b/packages/Schedulely/src/providers/BreakPointProvider.tsx @@ -1,5 +1,12 @@ import { BreakpointContextState, ComponentSize } from '@/types'; -import { ReactNode, createContext, useEffect, useState } from 'react'; +import { + ReactNode, + createContext, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; export const BreakpointContext = createContext( null @@ -18,28 +25,30 @@ export const BreakpointProvider = ({ containerRef: React.MutableRefObject; children: ReactNode; }) => { - const breakpoints = { small: 500, large: 800 }; const [breakpoint, setBreakpoint] = useState(); + const resizeObserver = useRef(); - useEffect(() => { - const observer = new ResizeObserver((entries) => { - const { width } = entries[0].contentRect; + const onResize: ResizeObserverCallback = useCallback((entries) => { + const breakpoints = { small: 500, large: 800 }; + const { width } = entries[0].contentRect; + + if (width <= breakpoints.small) setBreakpoint('small'); + if (width > breakpoints.small && width < breakpoints.large) + setBreakpoint('medium'); + if (width >= breakpoints.large) setBreakpoint('large'); + }, []); - if (width <= breakpoints.small) setBreakpoint('small'); - if (width > breakpoints.small && width < breakpoints.large) - setBreakpoint('medium'); - if (width >= breakpoints.large) setBreakpoint('large'); - }); + useEffect(() => { + resizeObserver.current = new ResizeObserver(onResize); + }, [onResize]); - if (observer && containerRef?.current) { - observer.observe(containerRef?.current); + useEffect(() => { + if (containerRef?.current) { + resizeObserver.current!.observe(containerRef?.current); } return () => { - if (observer && containerRef?.current) { - observer.unobserve(containerRef?.current); - } - observer.disconnect(); + resizeObserver.current!.disconnect(); }; }, [containerRef]); diff --git a/packages/Schedulely/src/providers/CalendarProvider.tsx b/packages/Schedulely/src/providers/CalendarProvider.tsx index a0ea6a26..34afdc1e 100644 --- a/packages/Schedulely/src/providers/CalendarProvider.tsx +++ b/packages/Schedulely/src/providers/CalendarProvider.tsx @@ -47,16 +47,16 @@ export const CalendarProvider = ({ let format: 'long' | 'short' = 'long'; if (breakpoint === 'small') format = 'short'; return dateAdapter.getMonthName(currentDate, format); - }, [currentDate, breakpoint]); + }, [currentDate, breakpoint, dateAdapter]); const currentYear = useMemo( () => dateAdapter.getYear(currentDate), - [currentDate] + [currentDate, dateAdapter] ); const isCurrentMonth = useMemo( () => dateAdapter.isCurrentMonth(currentDate), - [currentDate] + [currentDate, dateAdapter] ); // Does this need memo? @@ -65,7 +65,7 @@ export const CalendarProvider = ({ if (breakpoint === 'medium') format = 'short'; if (breakpoint === 'small') format = 'narrow'; return dateAdapter.getDaysOfWeek(format); - }, [breakpoint]); + }, [breakpoint, dateAdapter]); const calendarView = useMemo( () => dateAdapter.getCalendarView(currentDate), @@ -107,9 +107,16 @@ export const CalendarProvider = ({ () => calendarView.map((week) => ({ daysInWeek: week, - events: events.filter((event) => - dateAdapter.isEventInWeek(event.start, event.end, week) - ), + events: events + .filter((event) => + dateAdapter.isEventInWeek(event.start, event.end, week) + ) + .sort( + (x, y) => + x.end.valueOf() - + x.start.valueOf() - + (y.end.valueOf() - y.end.valueOf()) + ), eventsOnDays: week.map((day) => ({ date: day, events: events.filter( diff --git a/packages/Schedulely/src/providers/EventIntersectionProvider.tsx b/packages/Schedulely/src/providers/EventIntersectionProvider.tsx index b1fe9248..c58660a1 100644 --- a/packages/Schedulely/src/providers/EventIntersectionProvider.tsx +++ b/packages/Schedulely/src/providers/EventIntersectionProvider.tsx @@ -1,5 +1,12 @@ import { EventIntersectionState, InternalCalendarEvent } from '@/types'; -import { ReactNode, createContext, useEffect, useState } from 'react'; +import { + ReactNode, + createContext, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import { useCalendar } from '@/hooks/useCalendar'; export const EventIntersectionContext = @@ -23,10 +30,6 @@ export const EventIntersectionProvider = ({ dateAdapter: { isDateBetween }, } = useCalendar(); - const [childContainerRefs, setChildContainerRefs] = useState< - Record - >({}); - const [parentContainerRef, setParentContainerRef] = useState(null); @@ -34,53 +37,75 @@ export const EventIntersectionProvider = ({ Record >({}); - const setRefFromKey = (key: string) => (element: HTMLElement | null) => - setChildContainerRefs((current) => { - current[key] = element; - return current; - }); + const observerRef = useRef(); - const getEventsOnDate = (date: Date) => - Object.values(eventVisibility).filter((x) => - isDateBetween(date, x.start, x.end) - ); + const getEventsOnDate = useCallback( + (date: Date) => + Object.values(eventVisibility).filter((x) => + isDateBetween(date, x.start, x.end) + ), + [eventVisibility, isDateBetween] + ); - const isEventVisible = (key: string) => eventVisibility[key]?.visible; + /** + * This method checks if an event is fully visible, and if not hides it + * We do this via direct Refs because direct updates are faster and cleaner than relying upon + * React to route the property before and after a render. + * + * This could possibly be done in a more React-y way by splitting this context, but this seems pretty straight-forward as it. + */ + const checkIntersection: IntersectionObserverCallback = useCallback( + (entries) => + entries.map((x) => { + var eventId = x.target.attributes.getNamedItem('data-eventid')?.value; - const checkIntersection: IntersectionObserverCallback = (entries) => - entries.map((x) => { - var eventId = x.target.attributes.getNamedItem('data-eventid')?.value; - const matchingEvent = events.find((x) => x.id === eventId); - if (matchingEvent === undefined) return; + const currentStyle = + x.target + .getAttribute('style') + ?.split(';') + .filter((x) => x && !x.includes('visibility')) || []; - setEventVisibility((current) => { - current[matchingEvent.id] = matchingEvent; - current[matchingEvent.id].visible = x.intersectionRatio >= 1; - return { ...current }; - }); - }); + if (x.isIntersecting) + x.target.setAttribute('style', currentStyle.join(';')); + else { + currentStyle.push('visibility: hidden'); + x.target.setAttribute('style', currentStyle.join(';')); + } + + const matchingEvent = events.find((x) => x.id === eventId); + if (matchingEvent === undefined) return; + + setEventVisibility((current) => { + current[matchingEvent.id] = matchingEvent; + current[matchingEvent.id].visible = x.isIntersecting; + return { ...current }; + }); + }), + [events] + ); useEffect(() => { - const observer = new IntersectionObserver(checkIntersection, { + observerRef.current = new IntersectionObserver(checkIntersection, { root: parentContainerRef, rootMargin: '0px 0px -15% 0px', threshold: 1, }); - Object.values(childContainerRefs).map((eventRef) => { - if (eventRef) observer.observe(eventRef!); - }); + const eventContainers = parentContainerRef?.getElementsByClassName( + 'event-position-layout' + ); + + if (eventContainers) + for (const element of eventContainers) + observerRef.current!.observe(element); return () => { - observer.takeRecords().map((x) => observer.unobserve(x.target)); - observer.disconnect(); + observerRef.current!.disconnect(); }; - }, [parentContainerRef, events]); + }, [checkIntersection, parentContainerRef]); const value: EventIntersectionState = { setParentContainerRef, - setRefFromKey, - isEventVisible, getEventsOnDate, }; diff --git a/packages/Schedulely/src/types/state/EventIntersectionContextState.ts b/packages/Schedulely/src/types/state/EventIntersectionContextState.ts index f536b32c..f7b65cca 100644 --- a/packages/Schedulely/src/types/state/EventIntersectionContextState.ts +++ b/packages/Schedulely/src/types/state/EventIntersectionContextState.ts @@ -5,9 +5,7 @@ export type EventIntersectionState = { setParentContainerRef: React.Dispatch< React.SetStateAction >; - setRefFromKey: (key: string) => (element: HTMLElement | null) => void; - /** Lookup an event and retrieve its visibility */ - isEventVisible: (key: string) => boolean; + /** Gets an array of events that occur on or span the supplied date. */ getEventsOnDate: (date: Date) => InternalCalendarEvent[]; };