@aarhus-university/au-lib-react-components 12.5.1 → 12.6.1

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 (115) hide show
  1. package/.claude/settings.local.json +12 -0
  2. package/.eslintrc.js +35 -35
  3. package/.storybook/main.js +34 -34
  4. package/.storybook/preview.js +17 -17
  5. package/.vscode/settings.json +22 -0
  6. package/README.md +19 -19
  7. package/__tests__/jest/AUButtonComponent.test.tsx +165 -165
  8. package/__tests__/jest/AUDynamicContentComponent.test.tsx +386 -0
  9. package/__tests__/jest/AUErrorComponent.test.tsx +142 -142
  10. package/__tests__/jest/AUModalComponent.test.tsx +186 -186
  11. package/__tests__/jest/AUNotificationComponent.test.tsx +115 -115
  12. package/__tests__/jest/AUSpinnerComponent.test.tsx +57 -57
  13. package/__tests__/jest/AUToolbarComponent.test.tsx +46 -46
  14. package/__tests__/jest/context.test.ts +25 -25
  15. package/__tests__/jest/helpers.test.ts +15 -15
  16. package/__tests__/jest/setupTests.ts +2 -2
  17. package/babel.config.js +8 -8
  18. package/build/umd/all.css +3 -2
  19. package/build/umd/all.css.map +1 -7
  20. package/build/umd/all.js +2 -45
  21. package/build/umd/all.js.map +1 -7
  22. package/build/umd/alphabox.js +2 -45
  23. package/build/umd/alphabox.js.map +1 -7
  24. package/build/umd/databox.js +2 -45
  25. package/build/umd/databox.js.map +1 -7
  26. package/build/umd/diagramme.js +2 -44
  27. package/build/umd/diagramme.js.map +1 -7
  28. package/build/umd/flowbox.js +2 -44
  29. package/build/umd/flowbox.js.map +1 -7
  30. package/build/umd/universe.js +1 -1
  31. package/build-storybook.log +386 -386
  32. package/esbuild.mjs +22 -22
  33. package/package.json +107 -105
  34. package/src/components/AUAlertComponent.tsx +128 -128
  35. package/src/components/AUAutoSuggestComponent.js +148 -148
  36. package/src/components/AUButtonComponent.tsx +99 -97
  37. package/src/components/AUCalendarComponent.tsx +497 -497
  38. package/src/components/AUCharacterCountComponent.tsx +56 -56
  39. package/src/components/AUComboBoxComponent.tsx +195 -195
  40. package/src/components/AUContentToggleComponent.tsx +50 -50
  41. package/src/components/AUDatepickerComponent.tsx +124 -124
  42. package/src/components/AUDialogModalComponent.tsx +124 -124
  43. package/src/components/AUDynamicContentComponent.tsx +137 -0
  44. package/src/components/AUEditorComponent.tsx +126 -117
  45. package/src/components/AUErrorComponent.tsx +73 -73
  46. package/src/components/AUMobilePrefixComponent.tsx +20 -20
  47. package/src/components/AUModalComponent.tsx +72 -72
  48. package/src/components/AUNotificationComponent.tsx +44 -44
  49. package/src/components/AUReceiptComponent.tsx +34 -34
  50. package/src/components/AUSpinnerComponent.tsx +40 -40
  51. package/src/components/AUStepComponent.tsx +75 -75
  52. package/src/components/AUSubNavComponent.tsx +57 -57
  53. package/src/components/AUSubmitButtonContainerComponent.tsx +38 -38
  54. package/src/components/AUTabbedContentComponent.tsx +154 -154
  55. package/src/components/AUTableComponent.tsx +29 -29
  56. package/src/components/AUToastComponent.tsx +104 -104
  57. package/src/components/AUToolbarComponent.tsx +108 -108
  58. package/src/components/AUTruncatorComponent.tsx +141 -141
  59. package/src/components/wrapping/AUEmbedComponent.js +47 -47
  60. package/src/layout-2016/components/alphabox/AlphaBoxComponent.js +142 -143
  61. package/src/layout-2016/components/alphabox/AlphaBoxContentComponent.js +136 -136
  62. package/src/layout-2016/components/common/AUCollapsibleComponent.js +152 -152
  63. package/src/layout-2016/components/common/AUSpinnerComponent.js +103 -103
  64. package/src/layout-2016/components/databox/DataBoxAlphabetComponent.js +144 -144
  65. package/src/layout-2016/components/databox/DataBoxAssociationComponent.js +122 -122
  66. package/src/layout-2016/components/databox/DataBoxButtonComponent.js +157 -157
  67. package/src/layout-2016/components/databox/DataBoxComponent.js +297 -297
  68. package/src/layout-2016/components/databox/DataBoxGroupingComponent.js +64 -64
  69. package/src/layout-2016/components/databox/DataBoxSearchResultComponent.js +36 -36
  70. package/src/layout-2016/components/databox/DataBoxStackedAssociationComponent.js +54 -54
  71. package/src/layout-2016/components/databox/DataBoxSuggestionComponent.js +39 -39
  72. package/src/layout-2016/components/diagramme/AUDiagrammeComponent.js +309 -309
  73. package/src/layout-2016/components/flowbox/FlowBoxComponent.js +126 -126
  74. package/src/layout-2016/components/flowbox/FlowBoxPhoneComponent.js +104 -104
  75. package/src/layout-2016/lib/all.js +3 -3
  76. package/src/layout-2016/lib/au-alphabox.js +99 -100
  77. package/src/layout-2016/lib/au-databox.js +399 -400
  78. package/src/layout-2016/lib/au-diagramme.js +85 -85
  79. package/src/layout-2016/lib/au-flowbox.js +119 -93
  80. package/src/lib/context.tsx +59 -59
  81. package/src/lib/dates.ts +52 -52
  82. package/src/lib/helpers.ts +208 -208
  83. package/src/lib/hooks.ts +157 -157
  84. package/src/lib/i18n.ts +600 -600
  85. package/src/lib/portals.tsx +150 -150
  86. package/src/lib/tinymce.ts +84 -84
  87. package/src/lib/wrapping.ts +21 -21
  88. package/src/styles/_settings.scss +10 -10
  89. package/src/styles/alphabox.scss +222 -222
  90. package/src/styles/app.scss +7 -7
  91. package/src/styles/autosuggest.scss +57 -57
  92. package/src/styles/databox.scss +563 -563
  93. package/src/styles/diagramme.scss +119 -119
  94. package/src/styles/flowbox.scss +72 -72
  95. package/src/styles/maps.scss +395 -395
  96. package/stories/AUAlertComponent.stories.tsx +133 -133
  97. package/stories/AUAutoSuggestComponent.stories.tsx +95 -95
  98. package/stories/AUButtonComponent.stories.tsx +139 -139
  99. package/stories/AUCharacterCountComponent.stories.tsx +121 -121
  100. package/stories/AUComboBoxComponent.stories.tsx +101 -101
  101. package/stories/AUContentToggleComponent.stories.tsx +87 -87
  102. package/stories/AUDialogModalComponent.stories.tsx +75 -75
  103. package/stories/AUDynamicContentComponent.stories.tsx +119 -0
  104. package/stories/AUEditorComponent.stories.tsx +66 -66
  105. package/stories/AUErrorComponent.stories.tsx +132 -132
  106. package/stories/AUModalComponent.stories.tsx +160 -160
  107. package/stories/AUNotificationComponent.stories.tsx +151 -151
  108. package/stories/AUSpinnerComponent.stories.tsx +44 -44
  109. package/stories/AUStepComponent.stories.tsx +91 -91
  110. package/stories/AUToolbarComponent.stories.tsx +389 -389
  111. package/stories/AUTruncatorComponent.stories.tsx +123 -123
  112. package/stories/lib/helpers.tsx +146 -146
  113. package/tsconfig.json +46 -46
  114. package/webpack.config.js +88 -88
  115. package/src/lib/tracking.ts +0 -69
@@ -1,497 +1,497 @@
1
- /* eslint-disable @typescript-eslint/no-empty-function */
2
- import React, { useState, FC } from 'react';
3
- import dayjs from 'dayjs';
4
- import 'dayjs/locale/da';
5
- import advancedFormat from 'dayjs/plugin/advancedFormat';
6
-
7
- const months = {
8
- da: ['Januar', 'Februar', 'Marts', 'April', 'Maj', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'December'],
9
- en: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
10
- };
11
- const dayTitles = {
12
- da: ['Man', 'Tir', 'Ons', 'Tor', 'Fre', 'Lør', 'Søn'],
13
- en: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
14
- };
15
-
16
- const daysInMonth = (year: number, month: number): number => new Date(year, month, 0).getDate();
17
-
18
- // eslint-disable-next-line max-len
19
- const addDays = (days: number, date: Date): Date => new Date(date.getTime() + days * 60 * 24 * 60000);
20
-
21
- const calcEaster = (year: number): Date => {
22
- const a = year % 19;
23
- const b = Math.floor(year / 100);
24
- const c = year % 100;
25
- const d = Math.floor(b / 4);
26
- const e = b % 4;
27
- const f = Math.floor((b + 8) / 25);
28
- const g = Math.floor((b - f + 1) / 3);
29
- const h = (19 * a + b - d - g + 15) % 30;
30
- const i = Math.floor(c / 4);
31
- const k = c % 4;
32
- const l = (32 + 2 * e + 2 * i - h - k) % 7;
33
- const m = Math.floor((a + 11 * h + 22 * l) / 451);
34
- const n0 = (h + l + 7 * m + 114);
35
- const n = Math.floor(n0 / 31) - 1;
36
- const p = (n0 % 31) + 1;
37
- const date = new Date(year, n, p);
38
- return date;
39
- };
40
-
41
- const isHoliday = (date: Date): boolean => {
42
- const year = date.getFullYear();
43
- const holidays: Date[] = [];
44
- const newYear = new Date(year, 0, 1);
45
- holidays.push(newYear);
46
- const easter = calcEaster(year);
47
- holidays.push(easter);
48
- const easterThu = addDays(-3, easter);
49
- holidays.push(easterThu);
50
- const easterFri = addDays(-2, easter);
51
- holidays.push(easterFri);
52
- const easter2 = addDays(1, easter);
53
- holidays.push(easter2);
54
- const stBededag = addDays(26, easter);
55
- holidays.push(stBededag);
56
- const kristiHf = addDays(39, easter);
57
- holidays.push(kristiHf);
58
- const whitsun = addDays(49, easter);
59
- holidays.push(whitsun);
60
- const whitsun2 = addDays(1, whitsun);
61
- holidays.push(whitsun2);
62
- const grundlov = new Date(year, 5, 5);
63
- holidays.push(grundlov);
64
- const christmas = new Date(year, 11, 25);
65
- holidays.push(christmas);
66
- const christmas2 = addDays(1, christmas);
67
- holidays.push(christmas2);
68
-
69
- return holidays.map((h) => h.toString()).filter((x) => x === date.toString()).length > 0;
70
- };
71
-
72
- const dateAllowed = (date: Date, limitStart: Date, limitEnd: Date): boolean => {
73
- if (limitStart && !limitEnd) {
74
- return date >= limitStart;
75
- }
76
-
77
- if (!limitStart && limitEnd) {
78
- return date <= limitEnd;
79
- }
80
-
81
- if (limitStart && limitEnd) {
82
- return date >= limitStart && date <= limitEnd;
83
- }
84
-
85
- return true;
86
- };
87
-
88
- const skipDays = [6, 0, 1, 2, 3, 4, 5];
89
-
90
- type Props = {
91
- lang?: string;
92
- day: number;
93
- month: number;
94
- year: number;
95
- hour: number;
96
- minute: number;
97
- selected?: Date;
98
- onSelected: (selected: Date) => void;
99
- highlightWeekend?: boolean;
100
- highlightHolidays?: boolean;
101
- yearSpanMinus?: number;
102
- yearSpanPlus?: number;
103
- minuteInterval?: number;
104
- showTime?: boolean;
105
- closeable?: boolean;
106
- onClose?: () => void;
107
- limitStart?: Date | null;
108
- limitEnd?: Date | null;
109
- controls: string;
110
- };
111
-
112
- const AUCalendarComponent: FC<Props> = ({
113
- lang = 'da',
114
- day,
115
- month,
116
- year,
117
- hour,
118
- minute,
119
- selected = new Date(),
120
- onSelected,
121
- highlightWeekend = true,
122
- highlightHolidays = true,
123
- yearSpanMinus = 5,
124
- yearSpanPlus = 5,
125
- minuteInterval = 5,
126
- showTime = true,
127
- closeable = false,
128
- onClose = () => { },
129
- limitStart = null,
130
- limitEnd = null,
131
- controls,
132
- }) => {
133
- dayjs.locale(lang);
134
- dayjs.extend(advancedFormat);
135
-
136
- const [calendarState, setCalendarState] = useState<ICalendarState>({
137
- day,
138
- month,
139
- year,
140
- hour,
141
- minute,
142
- selected: selected as Date,
143
- });
144
-
145
- const {
146
- day: sDay,
147
- month: sMonth,
148
- year: sYear,
149
- hour: sHour,
150
- minute: sMinute,
151
- selected: sSelected,
152
- } = calendarState;
153
-
154
- const goBack = () => {
155
- const newMonth = sMonth - 1 < 1 ? 12 : (sMonth - 1);
156
- const newYear = newMonth === 12 ? (sYear - 1) : sYear;
157
- setCalendarState({
158
- ...calendarState,
159
- ...{
160
- month: newMonth,
161
- year: newYear,
162
- },
163
- });
164
- };
165
-
166
- const goForward = () => {
167
- const newMonth = sMonth + 1 > 12 ? 1 : (sMonth + 1);
168
- const newYear = newMonth === 1 ? (sYear + 1) : sYear;
169
- setCalendarState({
170
- ...calendarState,
171
- ...{
172
- month: newMonth,
173
- year: newYear,
174
- },
175
- });
176
- };
177
-
178
- const renderMonths = months[lang as string].map(
179
- (m: string, i: number) => (
180
- <option key={m} value={i + 1}>
181
- {m}
182
- </option>
183
- ),
184
- );
185
-
186
- let yearStart = yearSpanMinus;
187
- // eslint-disable-next-line max-len
188
- if (limitStart && limitStart.getFullYear() > (sSelected.getFullYear() - (yearSpanMinus as number))) {
189
- yearStart = sSelected.getFullYear() - limitStart.getFullYear();
190
- }
191
-
192
- let yearEnd = yearSpanPlus;
193
- if (limitEnd && limitEnd.getFullYear() < (sSelected.getFullYear() + (yearSpanPlus as number))) {
194
- yearEnd = limitEnd.getFullYear() + sSelected.getFullYear();
195
- }
196
-
197
- const yearArr: number[] = [];
198
- for (let y = sYear - (yearStart as number); y <= sYear + (yearEnd as number); y += 1) {
199
- yearArr.push(y);
200
- }
201
-
202
- const renderYears = yearArr.map(
203
- (y) => (
204
- <option key={y}>
205
- {y}
206
- </option>
207
- ),
208
- );
209
-
210
- const renderWeekDays = dayTitles[lang as string].map(
211
- (w: string) => (
212
- <span key={w} className="calendar__days__day">
213
- {w}
214
- </span>
215
- ),
216
- );
217
-
218
- const firstDayInMonth = new Date(sYear, sMonth - 1, 1);
219
- const emptyDays = skipDays[firstDayInMonth.getDay()];
220
- const prevYear = sMonth === 1 ? sYear - 1 : sYear;
221
- const prevMonth = sMonth === 1 ? 12 : sMonth - 1;
222
- const numberOfDaysInPrevMonth = daysInMonth(prevYear, prevMonth);
223
-
224
- let rowCount = -1;
225
- const renderEmptyDaysBefore = Array(emptyDays).fill(null).map((_, i) => {
226
- rowCount = i;
227
- const beforeDay = numberOfDaysInPrevMonth - emptyDays + i + 1;
228
- const date = new Date(prevYear, prevMonth - 1, beforeDay);
229
- const isWeekend = (highlightWeekend && rowCount > 4)
230
- || (highlightHolidays
231
- && isHoliday(date));
232
- return (
233
- <button
234
- type="button"
235
- key={rowCount}
236
- disabled={(limitStart && date < limitStart) as boolean}
237
- className={`calendar__days__date ${isWeekend ? 'calendar__days__date--weekend calendar__days__date--empty' : 'calendar__days__date--empty'}`}
238
- onClick={() => {
239
- goBack();
240
- }}
241
- aria-label={dayjs(date).format('LL')}
242
- >
243
- {beforeDay}
244
- </button>
245
- );
246
- });
247
-
248
- const renderDays = Array(daysInMonth(sYear, sMonth)).fill(null).map((_, i) => {
249
- const key = i; // suck it
250
- if (rowCount === 6) {
251
- rowCount = 0;
252
- } else {
253
- rowCount += 1;
254
- }
255
- const newDay = i + 1;
256
- const isSelected = (
257
- newDay === sSelected.getDate()
258
- && sYear === sSelected.getFullYear()
259
- && sMonth === sSelected.getMonth() + 1
260
- );
261
- const isWeekend = (highlightWeekend && rowCount > 4)
262
- || (highlightHolidays && isHoliday(new Date(sYear, sMonth - 1, newDay)));
263
- let className = 'calendar__days__date';
264
- if (isSelected) {
265
- className = `${className} calendar__days__date--selected`;
266
- }
267
- if (isWeekend) {
268
- className = `${className} calendar__days__date--weekend`;
269
- }
270
- const date = new Date(sYear, sMonth - 1, newDay, sHour, sMinute);
271
- return (
272
- <button
273
- key={key}
274
- disabled={((limitStart && date < limitStart) || (limitEnd && date > limitEnd) as boolean)}
275
- type="button"
276
- className={className}
277
- onClick={() => {
278
- const newSelected = date;
279
- setCalendarState({
280
- ...calendarState,
281
- ...{
282
- day: newDay,
283
- selected: newSelected,
284
- },
285
- });
286
- onSelected(newSelected);
287
- }}
288
- aria-label={dayjs(date).format('LL')}
289
- >
290
- {i + 1}
291
- </button>
292
- );
293
- });
294
-
295
- const nextYear = sMonth === 12 ? (sYear + 1) : sYear;
296
- const nextMonth = sMonth === 12 ? 1 : (sMonth + 1);
297
- const renderEmptyDaysAfter = Array(6 - rowCount).fill(null).map((_, i) => {
298
- const key = i;
299
- const afterDay = i + 1;
300
- if (rowCount === 6) {
301
- rowCount = 0;
302
- } else {
303
- rowCount += 1;
304
- }
305
- const date = new Date(nextYear, nextMonth - 1, afterDay);
306
- const isWeekend = (highlightWeekend && rowCount > 4)
307
- || (highlightHolidays
308
- && isHoliday(date));
309
- return (
310
- <button
311
- type="button"
312
- key={key}
313
- disabled={(limitEnd && date > limitEnd) as boolean}
314
- className={`calendar__days__date ${isWeekend ? 'calendar__days__date--weekend calendar__days__date--empty' : 'calendar__days__date--empty'}`}
315
- onClick={() => {
316
- goForward();
317
- }}
318
- aria-label={dayjs(date).format('LL')}
319
- >
320
- {afterDay}
321
- </button>
322
- );
323
- });
324
-
325
- const renderHours = Array(24).fill(null).map((_, i) => {
326
- const key = i;
327
- const optHour = i < 10 ? `0${i}` : i;
328
- return (
329
- <option key={key} value={i}>
330
- {optHour}
331
- </option>
332
- );
333
- });
334
-
335
- const renderMinutes = Array(60).fill(null).map((_, i) => {
336
- const key = i;
337
- if (i % (minuteInterval as number) === 0) {
338
- const optMinute = i < 10 ? `0${i}` : i;
339
- return (
340
- <option key={key} value={i}>
341
- {optMinute}
342
- </option>
343
- );
344
- }
345
- return null;
346
- });
347
-
348
- return (
349
- <div id={`${controls}-calendar`} className="calendar" aria-controls={controls}>
350
- <div className="calendar__year-month">
351
- <div className="form__field">
352
- <select
353
- value={sMonth}
354
- title={lang === 'da' ? 'Vælg måned' : 'Select month'}
355
- onChange={(e) => {
356
- const newMonth = parseInt(e.target.value, 10);
357
- const newSelected = new Date(sYear, newMonth - 1, sDay, sHour, sMinute);
358
- if (dateAllowed(newSelected, limitStart as Date, limitEnd as Date)) {
359
- setCalendarState({
360
- ...calendarState,
361
- ...{
362
- month: newMonth,
363
- selected: newSelected,
364
- },
365
- });
366
- onSelected(newSelected);
367
- } else {
368
- setCalendarState({
369
- ...calendarState,
370
- ...{
371
- month: newMonth,
372
- },
373
- });
374
- }
375
- }}
376
- >
377
- {renderMonths}
378
- </select>
379
- </div>
380
- <div className="form__field">
381
- <select
382
- value={sYear}
383
- title={lang === 'da' ? 'Vælg år' : 'Select year'}
384
- onChange={(e) => {
385
- const newYear = parseInt(e.target.value, 10);
386
- const newSelected = new Date(newYear, sMonth - 1, sDay, sHour, sMinute);
387
- if (dateAllowed(newSelected, limitStart as Date, limitEnd as Date)) {
388
- setCalendarState({
389
- ...calendarState,
390
- ...{
391
- year: newYear,
392
- selected: newSelected,
393
- },
394
- });
395
- onSelected(newSelected);
396
- } else {
397
- setCalendarState({
398
- ...calendarState,
399
- ...{
400
- year: newYear,
401
- },
402
- });
403
- }
404
- }}
405
- >
406
- {renderYears}
407
- </select>
408
- </div>
409
- {
410
- closeable && (
411
- <button
412
- className="button button--icon--hide-label button--small button--icon"
413
- type="button"
414
- data-icon=""
415
- onClick={onClose}
416
- >
417
- {lang === 'da' ? 'Luk' : 'Close'}
418
- </button>
419
- )
420
- }
421
- </div>
422
- <div className="calendar__days">
423
- {renderWeekDays}
424
- {renderEmptyDaysBefore}
425
- {renderDays}
426
- {renderEmptyDaysAfter}
427
- </div>
428
- {
429
- showTime && (
430
- <div className="calendar__time">
431
- <div className="form__field">
432
- <select
433
- value={sHour}
434
- title={lang === 'da' ? 'Vælg time' : 'Select hour'}
435
- onChange={(e) => {
436
- const newHour = parseInt(e.target.value, 10);
437
- const newSelected = new Date(sYear, sMonth - 1, sDay, newHour, sMinute);
438
- if (dateAllowed(newSelected, limitStart as Date, limitEnd as Date)) {
439
- setCalendarState({
440
- ...calendarState,
441
- ...{
442
- hour: newHour,
443
- selected: newSelected,
444
- },
445
- });
446
- onSelected(newSelected);
447
- } else {
448
- setCalendarState({
449
- ...calendarState,
450
- ...{
451
- hour: newHour,
452
- },
453
- });
454
- }
455
- }}
456
- >
457
- {renderHours}
458
- </select>
459
- </div>
460
- <div className="form__field">
461
- <select
462
- value={sMinute}
463
- title={lang === 'da' ? 'Vælg minut' : 'Select minute'}
464
- onChange={(e) => {
465
- const newMinute = parseInt(e.target.value, 10);
466
- const newSelected = new Date(sYear, sMonth - 1, sDay, sHour, newMinute);
467
- if (dateAllowed(newSelected, limitStart as Date, limitEnd as Date)) {
468
- setCalendarState({
469
- ...calendarState,
470
- ...{
471
- minute: newMinute,
472
- selected: newSelected,
473
- },
474
- });
475
- onSelected(newSelected);
476
- } else {
477
- setCalendarState({
478
- ...calendarState,
479
- ...{
480
- minute: newMinute,
481
- },
482
- });
483
- }
484
- }}
485
- >
486
- {renderMinutes}
487
- </select>
488
- </div>
489
- </div>
490
- )
491
- }
492
- </div>
493
- );
494
- };
495
-
496
- AUCalendarComponent.displayName = 'AUCalendarComponent';
497
- export default AUCalendarComponent;
1
+ /* eslint-disable @typescript-eslint/no-empty-function */
2
+ import React, { useState, FC } from 'react';
3
+ import dayjs from 'dayjs';
4
+ import 'dayjs/locale/da';
5
+ import advancedFormat from 'dayjs/plugin/advancedFormat';
6
+
7
+ const months = {
8
+ da: ['Januar', 'Februar', 'Marts', 'April', 'Maj', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'December'],
9
+ en: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
10
+ };
11
+ const dayTitles = {
12
+ da: ['Man', 'Tir', 'Ons', 'Tor', 'Fre', 'Lør', 'Søn'],
13
+ en: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
14
+ };
15
+
16
+ const daysInMonth = (year: number, month: number): number => new Date(year, month, 0).getDate();
17
+
18
+ // eslint-disable-next-line max-len
19
+ const addDays = (days: number, date: Date): Date => new Date(date.getTime() + days * 60 * 24 * 60000);
20
+
21
+ const calcEaster = (year: number): Date => {
22
+ const a = year % 19;
23
+ const b = Math.floor(year / 100);
24
+ const c = year % 100;
25
+ const d = Math.floor(b / 4);
26
+ const e = b % 4;
27
+ const f = Math.floor((b + 8) / 25);
28
+ const g = Math.floor((b - f + 1) / 3);
29
+ const h = (19 * a + b - d - g + 15) % 30;
30
+ const i = Math.floor(c / 4);
31
+ const k = c % 4;
32
+ const l = (32 + 2 * e + 2 * i - h - k) % 7;
33
+ const m = Math.floor((a + 11 * h + 22 * l) / 451);
34
+ const n0 = (h + l + 7 * m + 114);
35
+ const n = Math.floor(n0 / 31) - 1;
36
+ const p = (n0 % 31) + 1;
37
+ const date = new Date(year, n, p);
38
+ return date;
39
+ };
40
+
41
+ const isHoliday = (date: Date): boolean => {
42
+ const year = date.getFullYear();
43
+ const holidays: Date[] = [];
44
+ const newYear = new Date(year, 0, 1);
45
+ holidays.push(newYear);
46
+ const easter = calcEaster(year);
47
+ holidays.push(easter);
48
+ const easterThu = addDays(-3, easter);
49
+ holidays.push(easterThu);
50
+ const easterFri = addDays(-2, easter);
51
+ holidays.push(easterFri);
52
+ const easter2 = addDays(1, easter);
53
+ holidays.push(easter2);
54
+ const stBededag = addDays(26, easter);
55
+ holidays.push(stBededag);
56
+ const kristiHf = addDays(39, easter);
57
+ holidays.push(kristiHf);
58
+ const whitsun = addDays(49, easter);
59
+ holidays.push(whitsun);
60
+ const whitsun2 = addDays(1, whitsun);
61
+ holidays.push(whitsun2);
62
+ const grundlov = new Date(year, 5, 5);
63
+ holidays.push(grundlov);
64
+ const christmas = new Date(year, 11, 25);
65
+ holidays.push(christmas);
66
+ const christmas2 = addDays(1, christmas);
67
+ holidays.push(christmas2);
68
+
69
+ return holidays.map((h) => h.toString()).filter((x) => x === date.toString()).length > 0;
70
+ };
71
+
72
+ const dateAllowed = (date: Date, limitStart: Date, limitEnd: Date): boolean => {
73
+ if (limitStart && !limitEnd) {
74
+ return date >= limitStart;
75
+ }
76
+
77
+ if (!limitStart && limitEnd) {
78
+ return date <= limitEnd;
79
+ }
80
+
81
+ if (limitStart && limitEnd) {
82
+ return date >= limitStart && date <= limitEnd;
83
+ }
84
+
85
+ return true;
86
+ };
87
+
88
+ const skipDays = [6, 0, 1, 2, 3, 4, 5];
89
+
90
+ type Props = {
91
+ lang?: string;
92
+ day: number;
93
+ month: number;
94
+ year: number;
95
+ hour: number;
96
+ minute: number;
97
+ selected?: Date;
98
+ onSelected: (selected: Date) => void;
99
+ highlightWeekend?: boolean;
100
+ highlightHolidays?: boolean;
101
+ yearSpanMinus?: number;
102
+ yearSpanPlus?: number;
103
+ minuteInterval?: number;
104
+ showTime?: boolean;
105
+ closeable?: boolean;
106
+ onClose?: () => void;
107
+ limitStart?: Date | null;
108
+ limitEnd?: Date | null;
109
+ controls: string;
110
+ };
111
+
112
+ const AUCalendarComponent: FC<Props> = ({
113
+ lang = 'da',
114
+ day,
115
+ month,
116
+ year,
117
+ hour,
118
+ minute,
119
+ selected = new Date(),
120
+ onSelected,
121
+ highlightWeekend = true,
122
+ highlightHolidays = true,
123
+ yearSpanMinus = 5,
124
+ yearSpanPlus = 5,
125
+ minuteInterval = 5,
126
+ showTime = true,
127
+ closeable = false,
128
+ onClose = () => { },
129
+ limitStart = null,
130
+ limitEnd = null,
131
+ controls,
132
+ }) => {
133
+ dayjs.locale(lang);
134
+ dayjs.extend(advancedFormat);
135
+
136
+ const [calendarState, setCalendarState] = useState<ICalendarState>({
137
+ day,
138
+ month,
139
+ year,
140
+ hour,
141
+ minute,
142
+ selected: selected as Date,
143
+ });
144
+
145
+ const {
146
+ day: sDay,
147
+ month: sMonth,
148
+ year: sYear,
149
+ hour: sHour,
150
+ minute: sMinute,
151
+ selected: sSelected,
152
+ } = calendarState;
153
+
154
+ const goBack = () => {
155
+ const newMonth = sMonth - 1 < 1 ? 12 : (sMonth - 1);
156
+ const newYear = newMonth === 12 ? (sYear - 1) : sYear;
157
+ setCalendarState({
158
+ ...calendarState,
159
+ ...{
160
+ month: newMonth,
161
+ year: newYear,
162
+ },
163
+ });
164
+ };
165
+
166
+ const goForward = () => {
167
+ const newMonth = sMonth + 1 > 12 ? 1 : (sMonth + 1);
168
+ const newYear = newMonth === 1 ? (sYear + 1) : sYear;
169
+ setCalendarState({
170
+ ...calendarState,
171
+ ...{
172
+ month: newMonth,
173
+ year: newYear,
174
+ },
175
+ });
176
+ };
177
+
178
+ const renderMonths = months[lang as string].map(
179
+ (m: string, i: number) => (
180
+ <option key={m} value={i + 1}>
181
+ {m}
182
+ </option>
183
+ ),
184
+ );
185
+
186
+ let yearStart = yearSpanMinus;
187
+ // eslint-disable-next-line max-len
188
+ if (limitStart && limitStart.getFullYear() > (sSelected.getFullYear() - (yearSpanMinus as number))) {
189
+ yearStart = sSelected.getFullYear() - limitStart.getFullYear();
190
+ }
191
+
192
+ let yearEnd = yearSpanPlus;
193
+ if (limitEnd && limitEnd.getFullYear() < (sSelected.getFullYear() + (yearSpanPlus as number))) {
194
+ yearEnd = limitEnd.getFullYear() + sSelected.getFullYear();
195
+ }
196
+
197
+ const yearArr: number[] = [];
198
+ for (let y = sYear - (yearStart as number); y <= sYear + (yearEnd as number); y += 1) {
199
+ yearArr.push(y);
200
+ }
201
+
202
+ const renderYears = yearArr.map(
203
+ (y) => (
204
+ <option key={y}>
205
+ {y}
206
+ </option>
207
+ ),
208
+ );
209
+
210
+ const renderWeekDays = dayTitles[lang as string].map(
211
+ (w: string) => (
212
+ <span key={w} className="calendar__days__day">
213
+ {w}
214
+ </span>
215
+ ),
216
+ );
217
+
218
+ const firstDayInMonth = new Date(sYear, sMonth - 1, 1);
219
+ const emptyDays = skipDays[firstDayInMonth.getDay()];
220
+ const prevYear = sMonth === 1 ? sYear - 1 : sYear;
221
+ const prevMonth = sMonth === 1 ? 12 : sMonth - 1;
222
+ const numberOfDaysInPrevMonth = daysInMonth(prevYear, prevMonth);
223
+
224
+ let rowCount = -1;
225
+ const renderEmptyDaysBefore = Array(emptyDays).fill(null).map((_, i) => {
226
+ rowCount = i;
227
+ const beforeDay = numberOfDaysInPrevMonth - emptyDays + i + 1;
228
+ const date = new Date(prevYear, prevMonth - 1, beforeDay);
229
+ const isWeekend = (highlightWeekend && rowCount > 4)
230
+ || (highlightHolidays
231
+ && isHoliday(date));
232
+ return (
233
+ <button
234
+ type="button"
235
+ key={rowCount}
236
+ disabled={(limitStart && date < limitStart) as boolean}
237
+ className={`calendar__days__date ${isWeekend ? 'calendar__days__date--weekend calendar__days__date--empty' : 'calendar__days__date--empty'}`}
238
+ onClick={() => {
239
+ goBack();
240
+ }}
241
+ aria-label={dayjs(date).format('LL')}
242
+ >
243
+ {beforeDay}
244
+ </button>
245
+ );
246
+ });
247
+
248
+ const renderDays = Array(daysInMonth(sYear, sMonth)).fill(null).map((_, i) => {
249
+ const key = i; // suck it
250
+ if (rowCount === 6) {
251
+ rowCount = 0;
252
+ } else {
253
+ rowCount += 1;
254
+ }
255
+ const newDay = i + 1;
256
+ const isSelected = (
257
+ newDay === sSelected.getDate()
258
+ && sYear === sSelected.getFullYear()
259
+ && sMonth === sSelected.getMonth() + 1
260
+ );
261
+ const isWeekend = (highlightWeekend && rowCount > 4)
262
+ || (highlightHolidays && isHoliday(new Date(sYear, sMonth - 1, newDay)));
263
+ let className = 'calendar__days__date';
264
+ if (isSelected) {
265
+ className = `${className} calendar__days__date--selected`;
266
+ }
267
+ if (isWeekend) {
268
+ className = `${className} calendar__days__date--weekend`;
269
+ }
270
+ const date = new Date(sYear, sMonth - 1, newDay, sHour, sMinute);
271
+ return (
272
+ <button
273
+ key={key}
274
+ disabled={((limitStart && date < limitStart) || (limitEnd && date > limitEnd) as boolean)}
275
+ type="button"
276
+ className={className}
277
+ onClick={() => {
278
+ const newSelected = date;
279
+ setCalendarState({
280
+ ...calendarState,
281
+ ...{
282
+ day: newDay,
283
+ selected: newSelected,
284
+ },
285
+ });
286
+ onSelected(newSelected);
287
+ }}
288
+ aria-label={dayjs(date).format('LL')}
289
+ >
290
+ {i + 1}
291
+ </button>
292
+ );
293
+ });
294
+
295
+ const nextYear = sMonth === 12 ? (sYear + 1) : sYear;
296
+ const nextMonth = sMonth === 12 ? 1 : (sMonth + 1);
297
+ const renderEmptyDaysAfter = Array(6 - rowCount).fill(null).map((_, i) => {
298
+ const key = i;
299
+ const afterDay = i + 1;
300
+ if (rowCount === 6) {
301
+ rowCount = 0;
302
+ } else {
303
+ rowCount += 1;
304
+ }
305
+ const date = new Date(nextYear, nextMonth - 1, afterDay);
306
+ const isWeekend = (highlightWeekend && rowCount > 4)
307
+ || (highlightHolidays
308
+ && isHoliday(date));
309
+ return (
310
+ <button
311
+ type="button"
312
+ key={key}
313
+ disabled={(limitEnd && date > limitEnd) as boolean}
314
+ className={`calendar__days__date ${isWeekend ? 'calendar__days__date--weekend calendar__days__date--empty' : 'calendar__days__date--empty'}`}
315
+ onClick={() => {
316
+ goForward();
317
+ }}
318
+ aria-label={dayjs(date).format('LL')}
319
+ >
320
+ {afterDay}
321
+ </button>
322
+ );
323
+ });
324
+
325
+ const renderHours = Array(24).fill(null).map((_, i) => {
326
+ const key = i;
327
+ const optHour = i < 10 ? `0${i}` : i;
328
+ return (
329
+ <option key={key} value={i}>
330
+ {optHour}
331
+ </option>
332
+ );
333
+ });
334
+
335
+ const renderMinutes = Array(60).fill(null).map((_, i) => {
336
+ const key = i;
337
+ if (i % (minuteInterval as number) === 0) {
338
+ const optMinute = i < 10 ? `0${i}` : i;
339
+ return (
340
+ <option key={key} value={i}>
341
+ {optMinute}
342
+ </option>
343
+ );
344
+ }
345
+ return null;
346
+ });
347
+
348
+ return (
349
+ <div id={`${controls}-calendar`} className="calendar" aria-controls={controls}>
350
+ <div className="calendar__year-month">
351
+ <div className="form__field">
352
+ <select
353
+ value={sMonth}
354
+ title={lang === 'da' ? 'Vælg måned' : 'Select month'}
355
+ onChange={(e) => {
356
+ const newMonth = parseInt(e.target.value, 10);
357
+ const newSelected = new Date(sYear, newMonth - 1, sDay, sHour, sMinute);
358
+ if (dateAllowed(newSelected, limitStart as Date, limitEnd as Date)) {
359
+ setCalendarState({
360
+ ...calendarState,
361
+ ...{
362
+ month: newMonth,
363
+ selected: newSelected,
364
+ },
365
+ });
366
+ onSelected(newSelected);
367
+ } else {
368
+ setCalendarState({
369
+ ...calendarState,
370
+ ...{
371
+ month: newMonth,
372
+ },
373
+ });
374
+ }
375
+ }}
376
+ >
377
+ {renderMonths}
378
+ </select>
379
+ </div>
380
+ <div className="form__field">
381
+ <select
382
+ value={sYear}
383
+ title={lang === 'da' ? 'Vælg år' : 'Select year'}
384
+ onChange={(e) => {
385
+ const newYear = parseInt(e.target.value, 10);
386
+ const newSelected = new Date(newYear, sMonth - 1, sDay, sHour, sMinute);
387
+ if (dateAllowed(newSelected, limitStart as Date, limitEnd as Date)) {
388
+ setCalendarState({
389
+ ...calendarState,
390
+ ...{
391
+ year: newYear,
392
+ selected: newSelected,
393
+ },
394
+ });
395
+ onSelected(newSelected);
396
+ } else {
397
+ setCalendarState({
398
+ ...calendarState,
399
+ ...{
400
+ year: newYear,
401
+ },
402
+ });
403
+ }
404
+ }}
405
+ >
406
+ {renderYears}
407
+ </select>
408
+ </div>
409
+ {
410
+ closeable && (
411
+ <button
412
+ className="button button--icon--hide-label button--small button--icon"
413
+ type="button"
414
+ data-icon=""
415
+ onClick={onClose}
416
+ >
417
+ {lang === 'da' ? 'Luk' : 'Close'}
418
+ </button>
419
+ )
420
+ }
421
+ </div>
422
+ <div className="calendar__days">
423
+ {renderWeekDays}
424
+ {renderEmptyDaysBefore}
425
+ {renderDays}
426
+ {renderEmptyDaysAfter}
427
+ </div>
428
+ {
429
+ showTime && (
430
+ <div className="calendar__time">
431
+ <div className="form__field">
432
+ <select
433
+ value={sHour}
434
+ title={lang === 'da' ? 'Vælg time' : 'Select hour'}
435
+ onChange={(e) => {
436
+ const newHour = parseInt(e.target.value, 10);
437
+ const newSelected = new Date(sYear, sMonth - 1, sDay, newHour, sMinute);
438
+ if (dateAllowed(newSelected, limitStart as Date, limitEnd as Date)) {
439
+ setCalendarState({
440
+ ...calendarState,
441
+ ...{
442
+ hour: newHour,
443
+ selected: newSelected,
444
+ },
445
+ });
446
+ onSelected(newSelected);
447
+ } else {
448
+ setCalendarState({
449
+ ...calendarState,
450
+ ...{
451
+ hour: newHour,
452
+ },
453
+ });
454
+ }
455
+ }}
456
+ >
457
+ {renderHours}
458
+ </select>
459
+ </div>
460
+ <div className="form__field">
461
+ <select
462
+ value={sMinute}
463
+ title={lang === 'da' ? 'Vælg minut' : 'Select minute'}
464
+ onChange={(e) => {
465
+ const newMinute = parseInt(e.target.value, 10);
466
+ const newSelected = new Date(sYear, sMonth - 1, sDay, sHour, newMinute);
467
+ if (dateAllowed(newSelected, limitStart as Date, limitEnd as Date)) {
468
+ setCalendarState({
469
+ ...calendarState,
470
+ ...{
471
+ minute: newMinute,
472
+ selected: newSelected,
473
+ },
474
+ });
475
+ onSelected(newSelected);
476
+ } else {
477
+ setCalendarState({
478
+ ...calendarState,
479
+ ...{
480
+ minute: newMinute,
481
+ },
482
+ });
483
+ }
484
+ }}
485
+ >
486
+ {renderMinutes}
487
+ </select>
488
+ </div>
489
+ </div>
490
+ )
491
+ }
492
+ </div>
493
+ );
494
+ };
495
+
496
+ AUCalendarComponent.displayName = 'AUCalendarComponent';
497
+ export default AUCalendarComponent;