@utrecht/calendar-react 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE.md +288 -0
  2. package/dist/CalendarButton.d.ts +6 -0
  3. package/dist/CalendarButton.d.ts.map +1 -0
  4. package/dist/CalendarIcon.d.ts +5 -0
  5. package/dist/CalendarIcon.d.ts.map +1 -0
  6. package/dist/CalendarNavigation.d.ts +5 -0
  7. package/dist/CalendarNavigation.d.ts.map +1 -0
  8. package/dist/CalendarNavigationButtons.d.ts +13 -0
  9. package/dist/CalendarNavigationButtons.d.ts.map +1 -0
  10. package/dist/CalendarNavigationLabel.d.ts +5 -0
  11. package/dist/CalendarNavigationLabel.d.ts.map +1 -0
  12. package/dist/CalendarTableDaysContainer.d.ts +5 -0
  13. package/dist/CalendarTableDaysContainer.d.ts.map +1 -0
  14. package/dist/CalendarTableDaysItem.d.ts +5 -0
  15. package/dist/CalendarTableDaysItem.d.ts.map +1 -0
  16. package/dist/CalendarTableDaysItemDay.d.ts +12 -0
  17. package/dist/CalendarTableDaysItemDay.d.ts.map +1 -0
  18. package/dist/CalendarTableWeeksContainer.d.ts +3 -0
  19. package/dist/CalendarTableWeeksContainer.d.ts.map +1 -0
  20. package/dist/CalendarTableWeeksItem.d.ts +5 -0
  21. package/dist/CalendarTableWeeksItem.d.ts.map +1 -0
  22. package/dist/IconArrowLeft.d.ts +8 -0
  23. package/dist/IconArrowLeft.d.ts.map +1 -0
  24. package/dist/IconArrowLeftDouble.d.ts +8 -0
  25. package/dist/IconArrowLeftDouble.d.ts.map +1 -0
  26. package/dist/IconArrowRight.d.ts +8 -0
  27. package/dist/IconArrowRight.d.ts.map +1 -0
  28. package/dist/IconArrowRightDouble.d.ts +8 -0
  29. package/dist/IconArrowRightDouble.d.ts.map +1 -0
  30. package/dist/css.d.ts +8 -0
  31. package/dist/css.d.ts.map +1 -0
  32. package/dist/css.js +493 -0
  33. package/dist/css.js.map +1 -0
  34. package/dist/css.mjs +491 -0
  35. package/dist/css.mjs.map +1 -0
  36. package/dist/index.cjs.js +493 -0
  37. package/dist/index.cjs.js.map +1 -0
  38. package/dist/index.d.ts +42 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.esm.js +491 -0
  41. package/dist/index.esm.js.map +1 -0
  42. package/package.json +43 -0
  43. package/src/CalendarButton.tsx +11 -0
  44. package/src/CalendarIcon.tsx +10 -0
  45. package/src/CalendarNavigation.tsx +9 -0
  46. package/src/CalendarNavigationButtons.tsx +30 -0
  47. package/src/CalendarNavigationLabel.tsx +9 -0
  48. package/src/CalendarTableDaysContainer.tsx +13 -0
  49. package/src/CalendarTableDaysItem.tsx +8 -0
  50. package/src/CalendarTableDaysItemDay.tsx +37 -0
  51. package/src/CalendarTableWeeksContainer.tsx +7 -0
  52. package/src/CalendarTableWeeksItem.tsx +10 -0
  53. package/src/IconArrowLeft.tsx +12 -0
  54. package/src/IconArrowLeftDouble.tsx +13 -0
  55. package/src/IconArrowRight.tsx +12 -0
  56. package/src/IconArrowRightDouble.tsx +13 -0
  57. package/src/css.tsx +9 -0
  58. package/src/index.test.tsx +145 -0
  59. package/src/index.tsx +199 -0
@@ -0,0 +1,7 @@
1
+ import { PropsWithChildren } from 'react';
2
+
3
+ export const CalendarTableWeeksContainer = ({ children }: PropsWithChildren) => (
4
+ <thead className="utrecht-calendar__table-weeks-container">
5
+ <tr className="utrecht-calendar__table-weeks-container-content">{children}</tr>
6
+ </thead>
7
+ );
@@ -0,0 +1,10 @@
1
+ import { DetailedHTMLProps, PropsWithChildren, ThHTMLAttributes } from 'react';
2
+
3
+ export interface CalendarTableWeeksItemProps
4
+ extends DetailedHTMLProps<ThHTMLAttributes<HTMLTableCellElement>, HTMLTableCellElement> {}
5
+
6
+ export const CalendarTableWeeksItem = ({ children, ...props }: PropsWithChildren<CalendarTableWeeksItemProps>) => (
7
+ <th {...props} className="utrecht-calendar__table-weeks-item">
8
+ {children}
9
+ </th>
10
+ );
@@ -0,0 +1,12 @@
1
+ import { SVGProps } from 'react';
2
+ interface SVGRProps {
3
+ title?: string;
4
+ titleId?: string;
5
+ }
6
+
7
+ export const IconArrowLeft = ({ title, titleId, ...props }: SVGProps<SVGSVGElement> & SVGRProps) => (
8
+ <svg width="100%" height="100%" fill="none" xmlns="http://www.w3.org/2000/svg" aria-labelledby={titleId} {...props}>
9
+ {title ? <title id={titleId}>{title}</title> : null}
10
+ <path d="M15.41 16.59 10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41Z" fill="currentColor" />
11
+ </svg>
12
+ );
@@ -0,0 +1,13 @@
1
+ import { SVGProps } from 'react';
2
+ interface SVGRProps {
3
+ title?: string;
4
+ titleId?: string;
5
+ }
6
+
7
+ export const IconArrowLeftDouble = ({ title, titleId, ...props }: SVGProps<SVGSVGElement> & SVGRProps) => (
8
+ <svg width="100%" height="100%" fill="none" xmlns="http://www.w3.org/2000/svg" aria-labelledby={titleId} {...props}>
9
+ {title ? <title id={titleId}>{title}</title> : null}
10
+ <path d="M17.59 18 19 16.59 14.42 12 19 7.41 17.59 6l-6 6 6 6Z" fill="currentColor" />
11
+ <path d="m11 18 1.41-1.41L7.83 12l4.58-4.59L11 6l-6 6 6 6Z" fill="currentColor" />
12
+ </svg>
13
+ );
@@ -0,0 +1,12 @@
1
+ import { SVGProps } from 'react';
2
+ interface SVGRProps {
3
+ title?: string;
4
+ titleId?: string;
5
+ }
6
+
7
+ export const IconArrowRight = ({ title, titleId, ...props }: SVGProps<SVGSVGElement> & SVGRProps) => (
8
+ <svg width="100%" height="100%" fill="none" xmlns="http://www.w3.org/2000/svg" aria-labelledby={titleId} {...props}>
9
+ {title ? <title id={titleId}>{title}</title> : null}
10
+ <path d="M8.59 16.59 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41Z" fill="currentColor" />
11
+ </svg>
12
+ );
@@ -0,0 +1,13 @@
1
+ import { SVGProps } from 'react';
2
+ interface SVGRProps {
3
+ title?: string;
4
+ titleId?: string;
5
+ }
6
+
7
+ export const IconArrowRightDouble = ({ title, titleId, ...props }: SVGProps<SVGSVGElement> & SVGRProps) => (
8
+ <svg fill="none" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" aria-labelledby={titleId} {...props}>
9
+ {title ? <title id={titleId}>{title}</title> : null}
10
+ <path d="M6.41 6 5 7.41 9.58 12 5 16.59 6.41 18l6-6-6-6Z" fill="currentColor" />
11
+ <path d="m13 6-1.41 1.41L16.17 12l-4.58 4.59L13 18l6-6-6-6Z" fill="currentColor" />
12
+ </svg>
13
+ );
package/src/css.tsx ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @license EUPL-1.2
3
+ * Copyright (c) 2021 Robbert Broersma
4
+ */
5
+
6
+ import '@utrecht/calendar-css/src/index.scss';
7
+
8
+ export type { CalendarProps } from './index';
9
+ export { Calendar } from './index';
@@ -0,0 +1,145 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import '@testing-library/jest-dom';
3
+ import { formatISO } from 'date-fns';
4
+ import { nl } from 'date-fns/locale';
5
+ import { Calendar, Events } from './index';
6
+
7
+ describe('Calendar', () => {
8
+ it('invokes an onclick callback with a selected date', () => {
9
+ let selectedDate = '';
10
+ render(
11
+ <Calendar
12
+ currentDate={new Date(2023, 5, 11)}
13
+ onCalendarClick={(date) => {
14
+ selectedDate = date;
15
+ }}
16
+ locale={nl}
17
+ />,
18
+ );
19
+
20
+ let dayButton = screen.getByRole('button', { name: 'donderdag 15 juni 2023' });
21
+ fireEvent.click(dayButton);
22
+
23
+ expect(selectedDate).toEqual(formatISO(new Date(2023, 5, 15)));
24
+ });
25
+
26
+ it('renders a design system BEM class name', () => {
27
+ const { container } = render(<Calendar onCalendarClick={() => {}} />);
28
+
29
+ const calendar = container.querySelector(':only-child');
30
+
31
+ expect(calendar).toHaveClass('utrecht-calendar');
32
+ });
33
+
34
+ it('renders the selected date', () => {
35
+ const currentDate = new Date(2023, 5, 15);
36
+ render(<Calendar currentDate={currentDate} onCalendarClick={() => {}} locale={nl} />);
37
+
38
+ let dayButton = screen.getByRole('button', { name: 'woensdag 14 juni 2023' });
39
+ fireEvent.click(dayButton);
40
+
41
+ expect(dayButton).toHaveClass('utrecht-calendar__table-days-item-day--selected');
42
+ });
43
+
44
+ it('disables dates before the minDate value', () => {
45
+ const minDate = new Date(2023, 5, 15);
46
+ const currentDate = new Date(2023, 5, 15);
47
+ render(<Calendar onCalendarClick={() => {}} locale={nl} currentDate={currentDate} minDate={minDate} />);
48
+
49
+ let previousDayButton = screen.getByRole('button', { name: 'woensdag 14 juni 2023' });
50
+ let currentDayButton = screen.getByRole('button', { name: 'donderdag 15 juni 2023' });
51
+
52
+ expect(previousDayButton).toBeDisabled();
53
+ expect(previousDayButton).toHaveClass('utrecht-button--disabled');
54
+ expect(currentDayButton).not.toBeDisabled();
55
+ });
56
+
57
+ it('disables dates after the maxDate value', () => {
58
+ const maxDate = new Date(2023, 5, 15);
59
+ const currentDate = new Date(2023, 5, 15);
60
+ render(<Calendar onCalendarClick={() => {}} locale={nl} currentDate={currentDate} maxDate={maxDate} />);
61
+
62
+ let currentDayButton = screen.getByRole('button', { name: 'donderdag 15 juni 2023' });
63
+ let nextDayButton = screen.getByRole('button', { name: 'vrijdag 16 juni 2023' });
64
+
65
+ expect(currentDayButton).not.toBeDisabled();
66
+ expect(nextDayButton).toBeDisabled();
67
+ expect(nextDayButton).toHaveClass('utrecht-button--disabled');
68
+ });
69
+
70
+ it('correctly renders disabled events', () => {
71
+ const currentDate = new Date(2023, 5, 1);
72
+ const events: Events[] = [
73
+ {
74
+ date: new Date(2023, 5, 14).toISOString(),
75
+ disabled: true,
76
+ },
77
+ {
78
+ date: new Date(2023, 5, 15).toISOString(),
79
+ emphasis: true,
80
+ },
81
+ {
82
+ date: new Date(2023, 5, 16).toISOString(),
83
+ selected: true,
84
+ },
85
+ ];
86
+ render(<Calendar onCalendarClick={() => {}} locale={nl} currentDate={currentDate} events={events} />);
87
+
88
+ let disabledEventButton = screen.getByRole('button', { name: 'woensdag 14 juni 2023' });
89
+ let emphasisEventButton = screen.getByRole('button', { name: 'donderdag 15 juni 2023' });
90
+ let selectedEventButton = screen.getByRole('button', { name: 'vrijdag 16 juni 2023' });
91
+
92
+ expect(disabledEventButton).toHaveClass('utrecht-button--disabled');
93
+ expect(disabledEventButton).toBeDisabled();
94
+ expect(emphasisEventButton).toHaveClass('utrecht-calendar__table-days-item-day--emphasis');
95
+ expect(selectedEventButton).toHaveClass('utrecht-calendar__table-days-item-day--selected');
96
+ });
97
+
98
+ it('navigates to previous year', () => {
99
+ const currentDate = new Date(2023, 2, 1);
100
+ const { container } = render(<Calendar onCalendarClick={() => {}} locale={nl} currentDate={currentDate} />);
101
+
102
+ let previousYearButton = screen.getByRole('button', { name: 'Previous year' });
103
+ let currentDateLabel = container.querySelector('.utrecht-calendar__navigation-label');
104
+
105
+ expect(currentDateLabel).toContainHTML('maart 2023');
106
+ if (previousYearButton) fireEvent.click(previousYearButton);
107
+ expect(currentDateLabel).toContainHTML('maart 2022');
108
+ });
109
+
110
+ it('navigates to next year', () => {
111
+ const currentDate = new Date(2023, 2, 1);
112
+ const { container } = render(<Calendar onCalendarClick={() => {}} locale={nl} currentDate={currentDate} />);
113
+
114
+ let nextYearButton = screen.getByRole('button', { name: 'Next year' });
115
+ let currentDateLabel = container.querySelector('.utrecht-calendar__navigation-label');
116
+
117
+ expect(currentDateLabel).toContainHTML('maart 2023');
118
+ if (nextYearButton) fireEvent.click(nextYearButton);
119
+ expect(currentDateLabel).toContainHTML('maart 2024');
120
+ });
121
+
122
+ it('navigates to previous month', () => {
123
+ const currentDate = new Date(2023, 2, 1);
124
+ const { container } = render(<Calendar onCalendarClick={() => {}} locale={nl} currentDate={currentDate} />);
125
+
126
+ let previousMonthButton = screen.getByRole('button', { name: 'Previous month' });
127
+ let currentDateLabel = container.querySelector('.utrecht-calendar__navigation-label');
128
+
129
+ expect(currentDateLabel).toContainHTML('maart 2023');
130
+ if (previousMonthButton) fireEvent.click(previousMonthButton);
131
+ expect(currentDateLabel).toContainHTML('februari 2023');
132
+ });
133
+
134
+ it('navigates to next month', () => {
135
+ const currentDate = new Date(2023, 2, 1);
136
+ const { container } = render(<Calendar onCalendarClick={() => {}} locale={nl} currentDate={currentDate} />);
137
+
138
+ let nextMonthButton = screen.getByRole('button', { name: 'Next month' });
139
+ let currentDateLabel = container.querySelector('.utrecht-calendar__navigation-label');
140
+
141
+ expect(currentDateLabel).toContainHTML('maart 2023');
142
+ if (nextMonthButton) fireEvent.click(nextMonthButton);
143
+ expect(currentDateLabel).toContainHTML('april 2023');
144
+ });
145
+ });
package/src/index.tsx ADDED
@@ -0,0 +1,199 @@
1
+ import {
2
+ addMonths,
3
+ addWeeks,
4
+ addYears,
5
+ eachDayOfInterval,
6
+ endOfDay,
7
+ endOfWeek,
8
+ format,
9
+ formatISO,
10
+ getYear,
11
+ isAfter,
12
+ isBefore,
13
+ isSameDay,
14
+ isSameMonth,
15
+ Locale,
16
+ parseISO,
17
+ setMonth,
18
+ setYear,
19
+ startOfDay,
20
+ startOfMonth,
21
+ startOfWeek,
22
+ } from 'date-fns';
23
+ import { enUS } from 'date-fns/locale';
24
+ import chunk from 'lodash.chunk';
25
+ import { useState } from 'react';
26
+ import { CalendarNavigation } from './CalendarNavigation';
27
+ import { CalendarNavigationButtons } from './CalendarNavigationButtons';
28
+ import { CalendarNavigationLabel } from './CalendarNavigationLabel';
29
+ import { CalendarTableDaysContainer } from './CalendarTableDaysContainer';
30
+ import { CalendarTableDaysItem } from './CalendarTableDaysItem';
31
+ import { CalendarTableDaysItemDay } from './CalendarTableDaysItemDay';
32
+ import { CalendarTableWeeksContainer } from './CalendarTableWeeksContainer';
33
+ import { CalendarTableWeeksItem } from './CalendarTableWeeksItem';
34
+ import { IconArrowLeft } from './IconArrowLeft';
35
+ import { IconArrowLeftDouble } from './IconArrowLeftDouble';
36
+ import { IconArrowRight } from './IconArrowRight';
37
+ import { IconArrowRightDouble } from './IconArrowRightDouble';
38
+
39
+ function createCalendar(today: Date): Date[] {
40
+ const start = startOfWeek(startOfMonth(today), {
41
+ weekStartsOn: 1 /* Monday */,
42
+ });
43
+ const end = endOfWeek(addWeeks(start, 5), {
44
+ weekStartsOn: 1 /* Monday */,
45
+ });
46
+ return eachDayOfInterval({ start, end });
47
+ }
48
+
49
+ export type Events = {
50
+ date: string;
51
+ emphasis?: boolean;
52
+ selected?: boolean;
53
+ disabled?: boolean;
54
+ };
55
+
56
+ export interface CalendarProps {
57
+ /**
58
+ * `onCalendarClick` It's a callback function that returns the selected date, triggered when you click on the day
59
+ *
60
+ * */
61
+ onCalendarClick: (date: string) => void;
62
+ /**
63
+ * `events` An array of event objects that contain some properties that allow you to change the calendar day style base on your value
64
+ * `{date?: Date; emphasis?: boolean; selected?: boolean; disabled?: boolean;}`
65
+ *
66
+ * */
67
+ events?: Events[];
68
+ /**
69
+ * `currentDate` The default value is `new Date()`, but you can provide a different date
70
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
71
+ * */
72
+ currentDate?: Date;
73
+ /**
74
+ * `locale` to change the calendar language by using 'date-fns/locale'
75
+ * `import { nl, enUS } from 'date-fns/locale';`
76
+ * */
77
+ locale?: Locale;
78
+ previousYearButtonTitle?: string;
79
+ nextYearButtonTitle?: string;
80
+ previousMonthButtonTitle?: string;
81
+ nextMonthButtonTitle?: string;
82
+ minDate?: Date;
83
+ maxDate?: Date;
84
+ }
85
+
86
+ /**
87
+ * Calendar Component
88
+ * powered by date-fns, so that make it easy to use the `date-fns` date functions & locale
89
+ * */
90
+
91
+ export const Calendar = ({
92
+ onCalendarClick,
93
+ events,
94
+ currentDate,
95
+ locale = enUS,
96
+ previousYearButtonTitle = 'Previous year',
97
+ nextYearButtonTitle = 'Next year',
98
+ previousMonthButtonTitle = 'Previous month',
99
+ nextMonthButtonTitle = 'Next month',
100
+ minDate,
101
+ maxDate,
102
+ }: CalendarProps) => {
103
+ const [visibleMonth, setVisibleMonth] = useState(currentDate || new Date());
104
+ const [selectedDate, setSelectedDate] = useState(currentDate);
105
+ const calendar = createCalendar(visibleMonth);
106
+ const start = startOfWeek(visibleMonth, { weekStartsOn: 1 });
107
+ const end = endOfWeek(visibleMonth, { weekStartsOn: 1 });
108
+
109
+ const currentWeek = eachDayOfInterval({ start, end }).map((day) => day);
110
+ const chunksWeeks = chunk(calendar, calendar.length / 6);
111
+
112
+ const weeks = chunksWeeks.map((week) =>
113
+ week.map((date) => {
114
+ const currentEvent =
115
+ events && events.length > 0 && events.find((e) => isSameDay(endOfDay(parseISO(e.date)), date));
116
+ if (currentEvent) {
117
+ return {
118
+ date,
119
+ emphasis: currentEvent.emphasis,
120
+ selected: currentEvent.selected,
121
+ disabled: currentEvent.disabled,
122
+ };
123
+ } else {
124
+ return {
125
+ date,
126
+ emphasis: false,
127
+ selected: false,
128
+ disabled: false,
129
+ };
130
+ }
131
+ }),
132
+ );
133
+
134
+ return (
135
+ <div className="utrecht-calendar">
136
+ <CalendarNavigation>
137
+ <CalendarNavigationButtons
138
+ previousIcon={<IconArrowLeftDouble title={previousYearButtonTitle} />}
139
+ nextIcon={<IconArrowRightDouble title={nextYearButtonTitle} />}
140
+ onPreviousClick={() => setVisibleMonth(setYear(visibleMonth, getYear(visibleMonth) - 1))}
141
+ onNextClick={() => setVisibleMonth(addYears(visibleMonth, 1))}
142
+ >
143
+ <CalendarNavigationButtons
144
+ previousIcon={<IconArrowLeft title={previousMonthButtonTitle} />}
145
+ nextIcon={<IconArrowRight title={nextMonthButtonTitle} />}
146
+ onPreviousClick={() => setVisibleMonth(setMonth(visibleMonth, visibleMonth.getMonth() - 1))}
147
+ onNextClick={() => setVisibleMonth(addMonths(visibleMonth, 1))}
148
+ >
149
+ <CalendarNavigationLabel dateTime={format(visibleMonth, 'yyyy-mm')}>
150
+ {format(visibleMonth, 'LLLL Y', { locale })}
151
+ </CalendarNavigationLabel>
152
+ </CalendarNavigationButtons>
153
+ </CalendarNavigationButtons>
154
+ </CalendarNavigation>
155
+ <table className="utrecht-calendar__table" role="grid">
156
+ <CalendarTableWeeksContainer>
157
+ {currentWeek &&
158
+ currentWeek.length > 0 &&
159
+ currentWeek.map((day, index) => (
160
+ <CalendarTableWeeksItem scope="col" abbr={format(day, 'EEEE', { locale })} key={index}>
161
+ {format(day, 'EEEEEE', { locale })}
162
+ </CalendarTableWeeksItem>
163
+ ))}
164
+ </CalendarTableWeeksContainer>
165
+ <CalendarTableDaysContainer>
166
+ {weeks &&
167
+ weeks.length > 0 &&
168
+ weeks.map((week, index) => (
169
+ <CalendarTableDaysItem key={index}>
170
+ {week.map((day, index) => {
171
+ return (
172
+ <CalendarTableDaysItemDay
173
+ isToday={isSameDay(day.date, Date.now())}
174
+ dayOutOfTheMonth={!isSameMonth(day.date, visibleMonth)}
175
+ key={index}
176
+ onClick={() => {
177
+ setVisibleMonth(day.date);
178
+ setSelectedDate(day.date);
179
+ onCalendarClick(formatISO(day.date));
180
+ }}
181
+ aria-label={format(day.date, 'eeee dd LLLL Y', { locale })}
182
+ day={day.date.getDate().toString()}
183
+ emphasis={day.emphasis}
184
+ selected={day.selected || (selectedDate && isSameDay(day.date, selectedDate))}
185
+ disabled={
186
+ day.disabled ||
187
+ (minDate && isBefore(day.date, startOfDay(minDate))) ||
188
+ (maxDate && isAfter(day.date, endOfDay(maxDate)))
189
+ }
190
+ />
191
+ );
192
+ })}
193
+ </CalendarTableDaysItem>
194
+ ))}
195
+ </CalendarTableDaysContainer>
196
+ </table>
197
+ </div>
198
+ );
199
+ };