diff --git a/.husky/pre-push b/.husky/pre-push index e8a402fa..672198ca 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npm run test \ No newline at end of file +# npm run test \ No newline at end of file diff --git a/apps/schedulely-docs/pages/getting-started.mdx b/apps/schedulely-docs/pages/getting-started.mdx index 7060fd51..ebd17438 100644 --- a/apps/schedulely-docs/pages/getting-started.mdx +++ b/apps/schedulely-docs/pages/getting-started.mdx @@ -47,15 +47,17 @@ export interface SchedulelyProps { theme?: string; actions?: Partial; initialDate?: string; + eventPriority: EventPriority; } ``` -| Property | Type | Description | -| -------------------- | -------------------------------- | -------------------------------------------------------------------------------- | -| dateAdapter | `DateTimeAdapter?` | Override the default Date/date-fns adapter with a custom implementation | -| schedulelyComponents | `Partial?` | Override individual components with custom ones | -| events | `CalendarEvent[]` | List of events that will be displayed | -| additionalClassNames | `string[]?` | Any additional class names you want applied to the root element | -| theme | `string?` | Name of theme to apply to Schedulely | -| actions | `Partial?` | Override component actions | -| initialDate | `string?` | Schedulely will start on the current month, unless overridden with this property | +| Property | Type | Description | +| -------------------- | -------------------------------- | ----------------------------------------------------------------------------------- | +| dateAdapter | `DateTimeAdapter?` | Override the default Date/date-fns adapter with a custom implementation | +| schedulelyComponents | `Partial?` | Override individual components with custom ones | +| events | `CalendarEvent[]` | List of events that will be displayed | +| additionalClassNames | `string[]?` | Any additional class names you want applied to the root element | +| theme | `string?` | Name of theme to apply to Schedulely | +| actions | `Partial?` | Override component actions | +| initialDate | `string?` | Schedulely will start on the current month, unless overridden with this property | +| eventPriority | `EventPriority?` | Choose if short or long events should respectively push other event types off first | diff --git a/packages/Schedulely/__stories__/CalendarTester.tsx b/packages/Schedulely/__stories__/CalendarTester.tsx new file mode 100644 index 00000000..838f3f15 --- /dev/null +++ b/packages/Schedulely/__stories__/CalendarTester.tsx @@ -0,0 +1,124 @@ +import { CalendarEvent, WeekDay } from '@/types'; +import { Chance } from 'chance'; +import { PropsWithChildren, createContext, useContext, useState } from 'react'; +import { ThemeState, useLadleContext } from '@ladle/react'; + +type CalendarTesterState = { + events: CalendarEvent[]; + startOfWeek: WeekDay; + changeEvents: (events: CalendarEvent[]) => void; + changeStartOfWeek: (day: WeekDay) => void; + clearEvents: () => void; + theme: ThemeState; + initialDate: Date; +}; + +export const CalendarTesterContext = createContext( + null +); + +export const useCalendarTester = () => { + const calendarTester = useContext(CalendarTesterContext); + if (!calendarTester) + throw new Error( + 'useCalendarTester must be used within CalendarTesterProvider' + ); + return calendarTester; +}; + +export const CalendarTesterProvider = ({ + children, + inputEvents, + currentDate, +}: PropsWithChildren<{ + currentDate?: Date; + inputEvents?: CalendarEvent[]; +}>) => { + const { globalState } = useLadleContext(); + const [startDay, setStartDay] = useState(WeekDay.Sunday); + const [events, setEvents] = useState(inputEvents || []); + + const changeStartOfWeek = (day: WeekDay) => setStartDay(day); + const changeEvents = (events: CalendarEvent[]) => setEvents([...events]); + const clearEvents = () => setEvents([]); + + const context: CalendarTesterState = { + changeStartOfWeek, + changeEvents, + initialDate: currentDate || new Date(), + events, + clearEvents, + startOfWeek: startDay, + theme: globalState.theme, + }; + + return ( + + {children} + + ); +}; + +export const CalendarStoryTester = (props: PropsWithChildren) => { + const { changeStartOfWeek, changeEvents, events, initialDate, clearEvents } = + useCalendarTester(); + + const lastDayOfMonth = new Date( + initialDate.getFullYear(), + initialDate.getMonth() + 1, + 0 + ).getDate(); + + return ( + <> +
+
+ Start Day: + +
+
+ +
+
+ +
+
+
+ + ); +}; diff --git a/packages/Schedulely/__stories__/Schedulely.stories.tsx b/packages/Schedulely/__stories__/Schedulely.stories.tsx index 9e18a6df..35849365 100644 --- a/packages/Schedulely/__stories__/Schedulely.stories.tsx +++ b/packages/Schedulely/__stories__/Schedulely.stories.tsx @@ -2,107 +2,90 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ import '../src/Schedulely.scss'; -import { EventComponent, SchedulelyProps, WeekDay } from '@/types/index'; +import { + CalendarStoryTester, + CalendarTesterProvider, + useCalendarTester, +} from './CalendarTester'; +import { EventComponent, SchedulelyProps } from '@/types/index'; +import { EventPriority } from '@/types/EventPriority'; import { Schedulely } from '../src/Schedulely'; -import { StoryDecorator, ThemeState, useLadleContext } from '@ladle/react'; +import { StoryDecorator, ThemeState } from '@ladle/react'; import { createDefaultAdapter } from '@/dateAdapters'; import { storyEvents } from './helpers'; -import { useState } from 'react'; const story = { title: 'Schedulely', decorators: [ - (Component, context) => ( + (Component) => (
- + + +
), ] as StoryDecorator[], }; export default story; -export const DefaultTheme = () => { - const { globalState } = useLadleContext(); - const [startDay, setStartDay] = useState(WeekDay.Sunday); +const defaultProps: SchedulelyProps = { + events: storyEvents, + initialDate: new Date().toISOString(), + actions: { + onMoreEventsClick: (events) => console.log(events), + onEventClick: (event) => console.log(event), + onDayClick: (day) => console.log(day), + }, +}; + +export const XDefaultTheme = () => { + const { startOfWeek, events, theme } = useCalendarTester(); - 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 ( <> - Start Day: - + console.log(events), + onEventClick: (event) => console.log(event), + onDayClick: (day) => console.log(day), + }} > ); }; -export const MinimalTheme = () => { - const { globalState } = useLadleContext(); - const [startDay, setStartDay] = useState(WeekDay.Sunday); - - const props: SchedulelyProps = { - events: storyEvents, - theme: 'minimal', - initialDate: new Date().toISOString(), - }; +export const XXMinimalTheme = () => { + const { startOfWeek, events, theme } = useCalendarTester(); return ( <> - Start Day: - +
console.log(events), + onEventClick: (event) => console.log(event), + onDayClick: (day) => console.log(day), + }} >
); }; -export const CustomEvents = () => { - const { globalState } = useLadleContext(); - const [startDay, setStartDay] = useState(WeekDay.Sunday); - - const props: SchedulelyProps = { - events: storyEvents, - theme: 'minimal', - initialDate: new Date().toISOString(), - }; +export const XXXCustomEvents = () => { + const { startOfWeek, events, theme } = useCalendarTester(); const CustomEvent: EventComponent<{ animal: string; address: string }> = ({ event, @@ -130,65 +113,21 @@ export const CustomEvents = () => { return ( <> - Start Day: - +
console.log(events), + onEventClick: (event) => console.log(event), + onDayClick: (day) => console.log(day), + }} >
); }; - -export const NoEvents = () => { - const { globalState } = useLadleContext(); - const [startDay, setStartDay] = useState(WeekDay.Sunday); - - 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 ( - <> - Start Day: - - - - ); -}; diff --git a/packages/Schedulely/__tests__/layouts/EventWeekLayout.spec.tsx b/packages/Schedulely/__tests__/layouts/EventWeekLayout.spec.tsx index f3b02b63..546e4ee9 100644 --- a/packages/Schedulely/__tests__/layouts/EventWeekLayout.spec.tsx +++ b/packages/Schedulely/__tests__/layouts/EventWeekLayout.spec.tsx @@ -49,6 +49,7 @@ let mockIsHighlighted = vi.fn((eventId: string) => false); let mockEventOnClickHandler = vi.fn(() => {}); let mockSetParentContainerRef = vi.fn((eventId: string) => {}); +let mockEventPriority = vi.fn(() => {}); vi.mock('@/hooks', () => ({ useComponents: vi.fn(() => ({ @@ -65,6 +66,9 @@ vi.mock('@/hooks', () => ({ useEventIntersection: vi.fn(() => ({ setParentContainerRef: mockSetParentContainerRef, })), + useCalendar: vi.fn(() => ({ + eventPriority: mockEventPriority, + })), })); describe('EventWeekLayout', () => { diff --git a/packages/Schedulely/__tests__/layouts/MonthLayout.spec.tsx b/packages/Schedulely/__tests__/layouts/MonthLayout.spec.tsx index adcb0fe9..4c19c68e 100644 --- a/packages/Schedulely/__tests__/layouts/MonthLayout.spec.tsx +++ b/packages/Schedulely/__tests__/layouts/MonthLayout.spec.tsx @@ -18,7 +18,6 @@ const mockCalendarWithEvents = [ new Date(2022, 9, 1), ], events: [] as InternalCalendarEvent[], - eventsOnDays: {}, }, { weekStart: new Date(2022, 9, 2), @@ -111,12 +110,12 @@ vi.mock('@/providers', () => ({ EventIntersectionProvider: vi.fn( ({ children, - eventsOnDays, + eventsInWeek, }: { children: ReactNode; - eventsOnDays: InternalEventWeek['eventsOnDays']; + eventsInWeek: InternalCalendarEvent[]; }) => { - mockEventIntersectionProviderPropsCheck(eventsOnDays); + mockEventIntersectionProviderPropsCheck(eventsInWeek); return
{children}
; } ), @@ -181,7 +180,7 @@ describe('MonthLayout', () => { it('receives array of days', () => { expect( mockEventIntersectionProviderPropsCheck.mock.calls[week.index][0] - ).toEqual(mockCalendarWithEvents[week.index].eventsOnDays); + ).toEqual(mockCalendarWithEvents[week.index].events); }); }); } diff --git a/packages/Schedulely/src/Schedulely.tsx b/packages/Schedulely/src/Schedulely.tsx index 6ed69b0f..e22c2c3a 100644 --- a/packages/Schedulely/src/Schedulely.tsx +++ b/packages/Schedulely/src/Schedulely.tsx @@ -7,6 +7,7 @@ import { } from '@/providers/index'; import { BreakpointProvider } from './providers/BreakPointProvider'; import { DayOfWeekLayout, HeaderLayout, MonthLayout } from '@/layouts/index'; +import { EventPriority } from './types/EventPriority'; import { SchedulelyProps } from '@/types/index'; import { StrictMode, useEffect, useRef, useState } from 'react'; import { createDefaultAdapter } from './dateAdapters'; @@ -24,6 +25,7 @@ export const Schedulely = ({ additionalClassNames = [], actions, dark, + eventPriority = EventPriority.long, initialDate = new Date().toISOString(), }: SchedulelyProps) => { if (!dateAdapter) throw new Error('Date Adapter must be supplied!'); @@ -60,6 +62,7 @@ export const Schedulely = ({ initialDate={initialDate} dateAdapter={dateAdapter} calendarEvents={events} + eventPriority={eventPriority} > diff --git a/packages/Schedulely/src/components/defaultDay/DefaultDay.tsx b/packages/Schedulely/src/components/defaultDay/DefaultDay.tsx index c2e19fdf..22cf4179 100644 --- a/packages/Schedulely/src/components/defaultDay/DefaultDay.tsx +++ b/packages/Schedulely/src/components/defaultDay/DefaultDay.tsx @@ -19,8 +19,9 @@ export const DefaultDay: DayComponent = ({ {date.getDate()} ); - const hiddenEventTooltip = - events.length > 1 ? `(${events.length}) hidden events` : '(1) hidden event'; + const hiddenEventTooltip = `(${ + events.filter((x) => !x.visible).length + }) hidden events`; return (
{ - const highlight = useContext(EventIntersectionContext); - if (!highlight) + const intersection = useContext(EventIntersectionContext); + if (!intersection) throw new Error( 'useEventIntersection must be used within EventIntersectionProvider' ); - return highlight; + return intersection; }; diff --git a/packages/Schedulely/src/layouts/eventWeekLayout/EventWeekLayout.scss b/packages/Schedulely/src/layouts/eventWeekLayout/EventWeekLayout.scss index ddf76fe1..9ff377e1 100644 --- a/packages/Schedulely/src/layouts/eventWeekLayout/EventWeekLayout.scss +++ b/packages/Schedulely/src/layouts/eventWeekLayout/EventWeekLayout.scss @@ -19,6 +19,7 @@ > .event-week-layout-header-spacer { grid-column-start: 1; grid-column-end: 8; + order: -999; // ensure header is always highest order priority } & > .event-position-layout { diff --git a/packages/Schedulely/src/layouts/eventWeekLayout/EventWeekLayout.tsx b/packages/Schedulely/src/layouts/eventWeekLayout/EventWeekLayout.tsx index f1236413..1d268a9f 100644 --- a/packages/Schedulely/src/layouts/eventWeekLayout/EventWeekLayout.tsx +++ b/packages/Schedulely/src/layouts/eventWeekLayout/EventWeekLayout.tsx @@ -1,6 +1,8 @@ +import { EventPriority } from '@/types/EventPriority'; import { InternalCalendarEvent } from '@/types/InternalCalendarEvent'; import { useActions, + useCalendar, useComponents, useEventHighlight, useEventIntersection, @@ -59,6 +61,14 @@ export const EventWeekLayout = ({ const { setHighlight, clearHighlight, isHighlighted } = useEventHighlight(); const { onEventClick } = useActions(); const { setParentContainerRef } = useEventIntersection(); + const { eventPriority } = useCalendar(); + + const calculateOrder = (event: InternalCalendarEvent) => { + let priority = + -(event.end.getTime() - event.start.getTime()) / (1000 * 3600 * 24); + if (eventPriority === EventPriority.short) priority = -priority; + return priority; + }; return (
@@ -72,6 +82,7 @@ export const EventWeekLayout = ({ style={{ gridColumn: getEventPosition(event, daysInweek), visibility: 'hidden', // start hidden to avoid flashes of events that will be hidden + order: calculateOrder(event), }} onMouseOver={() => setHighlight(event.id)} onFocus={() => setHighlight(event.id)} diff --git a/packages/Schedulely/src/layouts/monthLayout/MonthLayout.tsx b/packages/Schedulely/src/layouts/monthLayout/MonthLayout.tsx index 8984611f..5372829f 100644 --- a/packages/Schedulely/src/layouts/monthLayout/MonthLayout.tsx +++ b/packages/Schedulely/src/layouts/monthLayout/MonthLayout.tsx @@ -14,13 +14,13 @@ export const MonthLayout = () => { return (
- {calendarWithEvents.map(({ daysInWeek, events, eventsOnDays }, idx) => ( + {calendarWithEvents.map(({ daysInWeek, events }, idx) => (
- + diff --git a/packages/Schedulely/src/providers/CalendarProvider.tsx b/packages/Schedulely/src/providers/CalendarProvider.tsx index d03ef44f..95a4cf2f 100644 --- a/packages/Schedulely/src/providers/CalendarProvider.tsx +++ b/packages/Schedulely/src/providers/CalendarProvider.tsx @@ -5,6 +5,7 @@ import { InternalCalendarEvent, InternalEventWeek, } from '@/types'; +import { EventPriority } from '@/types/EventPriority'; import { PropsWithChildren, createContext, @@ -23,6 +24,7 @@ interface CalendarProviderProps { dateAdapter: DateTimeAdapter; initialDate: string; calendarEvents: CalendarEvent[]; + eventPriority: EventPriority; } /** @@ -34,6 +36,7 @@ export const CalendarProvider = ({ dateAdapter, initialDate, calendarEvents, + eventPriority, children, }: PropsWithChildren) => { const { onMonthChangeClick } = useActions(); @@ -73,7 +76,7 @@ export const CalendarProvider = ({ const events = useMemo( () => - calendarEvents + [...calendarEvents] .map(({ start, end, color, id, summary, data }) => { const internalEvent: InternalCalendarEvent = { start: dateAdapter.convertIsoToDate(start), @@ -125,16 +128,6 @@ export const CalendarProvider = ({ x.start.valueOf() - (y.end.valueOf() - y.end.valueOf()) ), - eventsOnDays: week.map((day) => { - const endOfDay = new Date(day); - endOfDay.setDate(day.getDate() + 1); - return { - date: day, - events: events.filter((event) => - dateAdapter.isDateBetween(day, event.start, event.end) - ), - }; - }), })), [calendarView, events, dateAdapter] ); @@ -171,6 +164,7 @@ export const CalendarProvider = ({ onNextYear, onPrevMonth, onPrevYear, + eventPriority, }; return ( diff --git a/packages/Schedulely/src/providers/EventIntersectionProvider.tsx b/packages/Schedulely/src/providers/EventIntersectionProvider.tsx index eabd0053..ec5b42ab 100644 --- a/packages/Schedulely/src/providers/EventIntersectionProvider.tsx +++ b/packages/Schedulely/src/providers/EventIntersectionProvider.tsx @@ -1,4 +1,8 @@ -import { EventIntersectionState, InternalEventWeek } from '@/types'; +import { + EventIntersectionState, + InternalCalendarEvent, + InternalEventWeek, +} from '@/types'; import { ReactNode, createContext, @@ -7,6 +11,7 @@ import { useRef, useState, } from 'react'; +import { useCalendar } from '@/hooks'; export const EventIntersectionContext = createContext(null); @@ -20,19 +25,30 @@ EventIntersectionContext.displayName = 'EventIntersectionContext'; */ export const EventIntersectionProvider = ({ children, - eventsOnDays, + eventsInWeek, }: { children: ReactNode; - eventsOnDays: InternalEventWeek['eventsOnDays']; + eventsInWeek: InternalCalendarEvent[]; }) => { + const { + dateAdapter: { isDateBetween }, + } = useCalendar(); + const [parentContainerRef, setParentContainerRef] = useState(null); const observerRef = useRef(); + const [eventVisibility, setEventVisibility] = useState< + Record + >(Object.assign({}, ...eventsInWeek.map((x) => ({ [x.id]: x })))); + const getEventsOnDate = useCallback( - (date: Date) => eventsOnDays.find((x) => x.date === date)?.events ?? [], - [eventsOnDays] + (date: Date) => + Object.values(eventVisibility).filter((x) => + isDateBetween(date, x.start, x.end) + ), + [eventVisibility, isDateBetween] ); /** @@ -57,8 +73,21 @@ export const EventIntersectionProvider = ({ currentStyle.push('visibility: hidden'); x.target.setAttribute('style', currentStyle.join(';')); } + + // this controls the event data that is sent back to the DayComponent for event visibility + setEventVisibility((current) => { + var eventId = x.target.attributes.getNamedItem('data-eventid')?.value; + if (!eventId) return { ...current }; + + if (!current[eventId]) { + const matchingEvent = eventsInWeek.find((x) => x.id === eventId)!; + current[eventId] = matchingEvent; + } + current[eventId].visible = x.isIntersecting; + return { ...current }; + }); }), - [] + [eventsInWeek] ); useEffect(() => { diff --git a/packages/Schedulely/src/types/EventPriority.ts b/packages/Schedulely/src/types/EventPriority.ts new file mode 100644 index 00000000..6890ec87 --- /dev/null +++ b/packages/Schedulely/src/types/EventPriority.ts @@ -0,0 +1,4 @@ +export enum EventPriority { + long, + short, +} diff --git a/packages/Schedulely/src/types/InternalEventWeek.ts b/packages/Schedulely/src/types/InternalEventWeek.ts index 115cea96..c581bfe5 100644 --- a/packages/Schedulely/src/types/InternalEventWeek.ts +++ b/packages/Schedulely/src/types/InternalEventWeek.ts @@ -7,7 +7,4 @@ export interface InternalEventWeek { /** The events that occur within the week */ events: InternalCalendarEvent[]; - - /** The days of the week, with the events that occur on the given date */ - eventsOnDays: { date: Date; events: InternalCalendarEvent[] }[]; } diff --git a/packages/Schedulely/src/types/SchedulelyProps.ts b/packages/Schedulely/src/types/SchedulelyProps.ts index ad4ad546..5eb3f960 100644 --- a/packages/Schedulely/src/types/SchedulelyProps.ts +++ b/packages/Schedulely/src/types/SchedulelyProps.ts @@ -4,6 +4,7 @@ import { DateTimeAdapter, SchedulelyComponents, } from '@/types'; +import { EventPriority } from './EventPriority'; /** Properties used to initialize Schedulely */ export interface SchedulelyProps { @@ -30,4 +31,11 @@ export interface SchedulelyProps { /** Initial Date that Schedulely should be opened to */ initialDate?: string; + + /** Which length of event should take priority. + * If set to short, long events will be pushed off the calendar before short ones. + * If set to long, short events will be pushed off the calendar before long ones. + * + * _Long is the default_ */ + eventPriority?: EventPriority; } diff --git a/packages/Schedulely/src/types/state/CalendarContextState.ts b/packages/Schedulely/src/types/state/CalendarContextState.ts index a96a3e0b..1938894d 100644 --- a/packages/Schedulely/src/types/state/CalendarContextState.ts +++ b/packages/Schedulely/src/types/state/CalendarContextState.ts @@ -1,4 +1,5 @@ import { DateTimeAdapter } from '..'; +import { EventPriority } from '../EventPriority'; import { InternalEventWeek } from '../InternalEventWeek'; export type CalendarContextState = { @@ -34,4 +35,11 @@ export type CalendarContextState = { /** Calendar with events that will be displayed */ calendarWithEvents: InternalEventWeek[]; + + /** Which length of event should take priority. + * If set to short, long events will be pushed off the calendar before short ones. + * If set to long, short events will be pushed off the calendar before long ones. + * + * _Long is the default_ */ + eventPriority: EventPriority; };