@digitransit-component/digitransit-component-datetimepicker 6.0.10 → 7.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.
package/lib/index.js ADDED
@@ -0,0 +1,1586 @@
1
+ import PropTypes from 'prop-types';
2
+ import React, { useState, useEffect, useRef, useMemo } from 'react';
3
+ import { useTranslation, I18nextProvider } from 'react-i18next';
4
+ import { DateTime, Settings } from 'luxon';
5
+ import debounce from 'lodash/debounce.js';
6
+ import uniqueId from 'lodash/uniqueId.js';
7
+ import Icon from '@digitransit-component/digitransit-component-icon';
8
+ import Select from 'react-select';
9
+ import cx from 'classnames';
10
+ import Modal from 'react-modal';
11
+ import { useSelect } from 'downshift';
12
+ import { createInstance } from 'i18next';
13
+
14
+ /**
15
+ * Handle typing time and adding necessary :
16
+ *
17
+ * @param {string} typedValue
18
+ *
19
+ * @return {string}
20
+ */
21
+ function parseTypedTime(typedValue) {
22
+ let actualValue = typedValue;
23
+ if (actualValue.length === 3 && actualValue.split('').every(n => Number.isInteger(Number(n)))) {
24
+ // add ':' if user types three numbers in a row
25
+ if (Number(actualValue.slice(0, 2)) <= 23) {
26
+ actualValue = `${actualValue.slice(0, 2)}:${actualValue[2]}`;
27
+ } else {
28
+ actualValue = `${actualValue[0]}:${actualValue.slice(1)}`;
29
+ }
30
+ }
31
+ return actualValue;
32
+ }
33
+ const validateClock = (hours, minutes) => {
34
+ const hoursValid = !Number.isNaN(hours) && hours >= 0 && hours <= 23;
35
+ const minutesLen = minutes.length;
36
+ const minutesValid = minutesLen === 2 ? !Number.isNaN(minutes) && Number(minutes) >= 0 && Number(minutes <= 59) : !Number.isNaN(minutes) && Number(minutes) >= 0 && Number(minutes <= 5);
37
+ return hoursValid && minutesValid;
38
+ };
39
+ function validateInput(inputValue) {
40
+ // We don't accept seconds in the input
41
+ if (inputValue.split(':').length > 2) {
42
+ return false;
43
+ }
44
+ if (inputValue.length <= 2) {
45
+ // Too many options, don't validate
46
+ return true;
47
+ }
48
+ if (inputValue.length === 3) {
49
+ let hours;
50
+ let minutes;
51
+ if (inputValue.includes(':')) {
52
+ [hours, minutes] = inputValue.split(':');
53
+ } else if (inputValue.startsWith('0')) {
54
+ hours = inputValue.substring(0, inputValue.length - 1);
55
+ minutes = inputValue.substring(inputValue.length - 1 || 0);
56
+ if (Number(minutes) > 5) {
57
+ return false;
58
+ }
59
+ } else {
60
+ // This is how basically moment handles string formatting.
61
+ // If the first letter of the string is 1, then rest are minutes.
62
+ // if the first letter is 2, then if second letter is 0,1,2 or 3, then second letter is hour, else rest are minutes
63
+ // else, first letter is hour, rest of them are minutes.
64
+ const values = inputValue.split('');
65
+ if (Number(values[0]) === 1) {
66
+ [hours, minutes] = [values[0].concat(values[1]), values[2]];
67
+ } else if (Number(values[0]) === 2) {
68
+ if (Number(values[1] <= 3)) {
69
+ [hours, minutes] = [values[0].concat(values[1]), values[2]];
70
+ } else {
71
+ [hours, minutes] = [values[0], values[1].concat(values[2])];
72
+ }
73
+ } else {
74
+ [hours, minutes] = [values[0], values[1].concat(values[2])];
75
+ }
76
+ return validateClock(hours, minutes);
77
+ }
78
+ return validateClock(hours, minutes);
79
+ }
80
+ if (inputValue.length === 5 || inputValue.length === 4) {
81
+ const values = inputValue.split(':');
82
+ const hours = values[0];
83
+ const minutes = values[1];
84
+ if (inputValue.startsWith('0') && minutes.length === 1 && Number(minutes) > 5) {
85
+ return false;
86
+ }
87
+ return validateClock(hours, minutes);
88
+ }
89
+ return true;
90
+ }
91
+ const getTs = (inputValue, currentTimestamp) => {
92
+ const trimmed = inputValue.trim();
93
+ if (trimmed.match(/^[0-9]{1,2}(\.|:)[0-9]{2}$/) !== null) {
94
+ const splitter = trimmed.includes('.') ? '.' : ':';
95
+ const values = trimmed.split(splitter);
96
+ const hours = Number(values[0]);
97
+ const hoursValid = !Number.isNaN(hours) && hours >= 0 && hours <= 23;
98
+ const minutes = Number(values[1]);
99
+ const minutesValid = !Number.isNaN(minutes) && minutes >= 0 && minutes <= 59;
100
+ if (!minutesValid || !hoursValid) {
101
+ return null;
102
+ }
103
+ const newStamp = DateTime.fromMillis(currentTimestamp).set({
104
+ hour: hours,
105
+ minute: minutes,
106
+ second: 0,
107
+ millisecond: 0
108
+ }).valueOf();
109
+ return newStamp;
110
+ }
111
+ return null;
112
+ };
113
+
114
+ function styleInject(css, ref) {
115
+ if ( ref === void 0 ) ref = {};
116
+ var insertAt = ref.insertAt;
117
+
118
+ if (typeof document === 'undefined') { return; }
119
+
120
+ var head = document.head || document.getElementsByTagName('head')[0];
121
+ var style = document.createElement('style');
122
+ style.type = 'text/css';
123
+
124
+ if (insertAt === 'top') {
125
+ if (head.firstChild) {
126
+ head.insertBefore(style, head.firstChild);
127
+ } else {
128
+ head.appendChild(style);
129
+ }
130
+ } else {
131
+ head.appendChild(style);
132
+ }
133
+
134
+ if (style.styleSheet) {
135
+ style.styleSheet.cssText = css;
136
+ } else {
137
+ style.appendChild(document.createTextNode(css));
138
+ }
139
+ }
140
+
141
+ var css_248z = ".styles_sr-only__D6MJ6:not(:focus, :active) {\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n height: 1px;\n overflow: hidden;\n position: absolute;\n white-space: nowrap;\n width: 1px;\n}\n\n.styles_dt-datetimepicker__DSYbh {\n border: none;\n padding: 0;\n margin: 0;\n font-size: 15px;\n font-weight: var(--font-weight-medium);\n}\n.styles_dt-datetimepicker__DSYbh button,\n.styles_dt-datetimepicker__DSYbh label {\n font-size: 1em;\n font-weight: inherit;\n}\n.styles_dt-datetimepicker__DSYbh label button {\n cursor: pointer;\n font-family: inherit;\n}\n.styles_dt-datetimepicker__DSYbh .styles_hidden__TjnWH {\n display: none;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-open-container__zWBiT {\n background-color: #f2f5f7;\n border-radius: 5px;\n padding: 15px;\n margin: 9px 0 9px 0;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n min-height: 36px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj.styles_closed__QYbCF {\n margin: 4px 0 6px 18px;\n}\n@media screen and (max-width: 319px) {\n .styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj {\n margin-left: 14px;\n }\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj .styles_time-icon__ES47Y svg {\n width: 18px;\n height: 19px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj .styles_dropdown-icon__O0GrI {\n margin-left: 9px;\n position: relative;\n top: 1px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj .styles_dropdown-icon__O0GrI svg {\n width: 14px;\n height: 10px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj .styles_close-icon__B5-I5 svg {\n width: 12px;\n height: 12px;\n border-radius: 1px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj .styles_close-icon__B5-I5 svg path {\n stroke-width: 30;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj label {\n cursor: pointer;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj label.styles_label-open__dVDlF {\n align-items: flex-start;\n display: flex;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj button,\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj label {\n font-size: 1em;\n letter-spacing: -0.5px;\n line-height: 1;\n}\n.styles_dt-datetimepicker__DSYbh .styles_textbutton__sVckw {\n margin-left: 9px;\n background: none;\n border: none;\n color: #666;\n}\n@media screen and (min-width: 768px) {\n .styles_dt-datetimepicker__DSYbh .styles_textbutton__sVckw {\n margin-left: 14px;\n }\n}\n.styles_dt-datetimepicker__DSYbh .styles_textbutton__sVckw.styles_active__9LaDz {\n color: var(--color);\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-left__nwwOo {\n position: relative;\n margin-right: 4px;\n width: 63%;\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-right__qlPKD {\n position: relative;\n margin-left: 4px;\n width: 37%;\n}\n.styles_dt-datetimepicker__DSYbh .styles_departure-or-arrival-container__Rj3b9 {\n height: calc(1rem + 20px);\n background-color: #ddd;\n border-radius: 8px;\n display: flex;\n width: calc(5px + 13em);\n}\n.styles_dt-datetimepicker__DSYbh .styles_radio-textbutton-label__LeV2o {\n color: #333;\n margin: 2px;\n background: none;\n border-radius: 6px;\n padding: 9px 0;\n width: 50%;\n cursor: pointer;\n height: calc(1rem + 16px);\n text-align: center;\n font-weight: 400;\n font-size: 15px;\n line-height: 18px;\n letter-spacing: -0.03em;\n}\n.styles_dt-datetimepicker__DSYbh .styles_radio-textbutton-label__LeV2o.styles_active__9LaDz {\n font-weight: 500;\n background: white;\n}\n.styles_dt-datetimepicker__DSYbh .styles_radio-textbutton-label__LeV2o:focus-within {\n /* First apply outline that is used in Edge and Firefox */\n outline: 1px dotted #212121;\n /* Then override outline for webkit browsers like Chrome, this has no effect on other browsers */\n outline: 5px auto -webkit-focus-ring-color; /* stylelint-disable-line declaration-block-no-duplicate-properties */\n}\n.styles_dt-datetimepicker__DSYbh .styles_radio-textbutton-label__LeV2o.styles_first-radio__F8N7J {\n margin: 0;\n display: flex;\n align-items: flex-start;\n}\n.styles_dt-datetimepicker__DSYbh .styles_radio-textbutton__Ad81W {\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n height: 1px;\n overflow: hidden;\n position: absolute;\n white-space: nowrap;\n width: 1px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_departure-now-button__8ZlDU {\n font-family: inherit;\n letter-spacing: -0.02em;\n height: calc(1rem + 18px);\n color: var(--color);\n padding: 8px 0;\n width: 6.6875rem;\n background: white;\n border: 1px solid #888;\n border-radius: 30px;\n margin-left: 11px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_open-button__yl4be {\n display: flex;\n margin-bottom: 0;\n margin-top: 1px;\n padding: 2px 0 2px 0;\n}\n.styles_dt-datetimepicker__DSYbh button.styles_close-button__mdfIU {\n transition: transform 0.2s;\n position: relative;\n top: -14px;\n right: -8px;\n border: 0;\n font-weight: normal;\n background: none;\n color: var(--color);\n padding: 0;\n line-height: normal;\n}\n.styles_dt-datetimepicker__DSYbh button.styles_close-button__mdfIU:hover {\n transform: scale(1.2);\n}\n.styles_dt-datetimepicker__DSYbh .styles_right-edge__XbqgK {\n margin-left: auto;\n margin-right: 0;\n position: relative;\n top: -2px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-container__sR9aU {\n cursor: pointer;\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-icon__wsdX4 {\n color: var(--color);\n height: 40px;\n position: absolute;\n z-index: 7;\n pointer-events: none;\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-icon__wsdX4.styles_date-input-icon__SFXGx {\n display: flex;\n align-items: center;\n padding: 0;\n margin-left: 14px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-icon__wsdX4.styles_date-input-icon__SFXGx svg {\n width: 20px;\n height: 18px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-icon__wsdX4.styles_time-input-icon__J1qei {\n display: flex;\n align-items: center;\n padding: 0;\n margin-left: 8px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-icon__wsdX4.styles_time-input-icon__J1qei svg {\n margin-top: 1px;\n width: 22px;\n height: 22px;\n}\n.styles_dt-datetimepicker__DSYbh .datetimepicker-invalid-input input {\n color: #f00 !important;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE {\n position: relative;\n height: 40px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__control {\n cursor: inherit;\n height: 40px;\n border: 1px solid #888;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__value-container {\n padding: 0;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__single-value {\n margin: 0 0 0 47px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__input {\n background: inherit;\n position: relative;\n margin-left: 40px;\n margin-top: -4px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__input input {\n opacity: 1 !important;\n background: inherit;\n box-shadow: none;\n font-stretch: normal;\n font-style: normal;\n line-height: 1.33;\n letter-spacing: -0.5px;\n color: #333;\n font-family: inherit;\n font-size: 15px;\n font-weight: var(--font-weight-medium);\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__input input:focus {\n box-shadow: none;\n outline: none;\n border: none;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__menu {\n z-index: 19;\n padding: 0;\n background: #fff;\n max-height: 250px;\n border-radius: 5px;\n box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.06);\n border: 1px rgba(0, 0, 0, 0.1) solid;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__menu .datetimepicker-select__menu-list {\n max-height: 250px;\n padding: 0;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__menu .datetimepicker-select__menu-list .datetimepicker-select__option {\n height: 50px;\n border-top: 1px solid #ddd;\n padding: 13px 0 0 12px;\n letter-spacing: -0.6px;\n color: #333;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__menu .datetimepicker-select__menu-list .datetimepicker-select__option--is-selected {\n background: inherit;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__menu .datetimepicker-select__menu-list .datetimepicker-select__option--is-focused {\n background: #dfe5e8;\n}\n.styles_dt-datetimepicker__DSYbh .styles_picker-container__wz2Zj {\n margin: 14px 0 0 0;\n display: flex;\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-mobile-container__ZzG8g {\n flex-grow: 1;\n}\n\n.styles_mobile-modal-overlay__wq8cS {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: rgba(0, 0, 0, 0.6);\n z-index: 10001;\n}\n\n.styles_mobile-modal-content__Gla3M {\n font-family: inherit;\n background: white;\n width: 89%;\n position: absolute;\n left: 5.5%;\n padding: 31px 20px 33px 20px;\n top: 80px;\n border-radius: 5px;\n}\n.styles_mobile-modal-content__Gla3M .styles_top-row__hzke9 {\n display: flex;\n justify-content: space-between;\n margin-bottom: 21px;\n}\n.styles_mobile-modal-content__Gla3M .styles_top-row__hzke9 .styles_modal-title__CztKf {\n font-weight: var(--font-weight-medium);\n font-size: 20px;\n line-height: 18px;\n letter-spacing: -0.5px;\n color: #333;\n padding: 6px 0 0 8px;\n}\n.styles_mobile-modal-content__Gla3M .styles_top-row__hzke9 .styles_departure-now-button__8ZlDU {\n background-color: white;\n font-family: inherit;\n padding: 5px 15px;\n border: 1px solid #888;\n border-radius: 50px;\n font-weight: var(--font-weight-medium);\n font-size: 15px;\n line-height: 18px;\n letter-spacing: -0.02em;\n color: var(--color);\n}\n.styles_mobile-modal-content__Gla3M .styles_tab-row__6WYwM {\n display: flex;\n border: 2px solid #ddd;\n border-radius: 5px;\n margin-bottom: 16px;\n background-color: #ddd;\n}\n.styles_mobile-modal-content__Gla3M .styles_tab-row__6WYwM .styles_radio-tab-label__-7Fx- {\n padding: 9px 0;\n font-size: 15px;\n line-height: 18px;\n letter-spacing: -0.5px;\n border-radius: 3px;\n font-weight: normal;\n width: 50%;\n text-align: center;\n color: #333;\n}\n.styles_mobile-modal-content__Gla3M .styles_tab-row__6WYwM .styles_radio-tab-label__-7Fx-.styles_active__9LaDz {\n font-weight: var(--font-weight-medium);\n background-color: #fff;\n}\n.styles_mobile-modal-content__Gla3M .styles_tab-row__6WYwM .styles_radio-textbutton__Ad81W {\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n height: 1px;\n overflow: hidden;\n position: absolute;\n white-space: nowrap;\n width: 1px;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j {\n display: flex;\n margin-bottom: 25px;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j label:first-child {\n flex-basis: 60%;\n margin-right: 10px;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j label:nth-child(2) {\n flex-basis: 40%;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW {\n border: 1px solid #888;\n border-radius: 5px;\n display: flex;\n align-items: center;\n height: 40px;\n line-height: 1.5;\n font-size: 100%;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW:focus-within {\n background: rgba(0, 122, 201, 0.06);\n border: 2px solid var(--color);\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_combobox-icon__wsdX4 {\n height: 18px;\n font-size: 18px;\n padding: 0 0 0 10px;\n border: none;\n position: relative;\n top: 3px;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_combobox-icon__wsdX4.styles_time-input-icon__J1qei > svg {\n height: 20px;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_combobox-icon__wsdX4.styles_date-input-icon__SFXGx > svg {\n height: 20px;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .mobile-datetimepicker-invalid-input {\n color: #f00 !important;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_time-input-mobile__QGutf {\n font-family: inherit;\n box-shadow: none;\n font-size: 18px;\n letter-spacing: -0.5px;\n line-height: 20px;\n font-weight: var(--font-weight-medium);\n border: none;\n outline: none;\n background: none;\n width: 54px;\n margin-left: 10px;\n color: #333;\n height: 1.5265rem;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_mobile-input-display__Xnjg1 {\n position: relative;\n left: 10px;\n border: none;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_mobile-input-display__Xnjg1:focus {\n outline: none;\n box-shadow: none;\n background: none;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_mobile-input-display__Xnjg1 {\n background: inherit;\n box-shadow: none;\n font-family: inherit;\n font-weight: var(--font-weight-medium);\n font-stretch: normal;\n font-style: normal;\n font-size: 16px;\n line-height: 1.33;\n letter-spacing: -0.5px;\n color: #333;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_mobile-input-hidden__tkKxe {\n font-size: 16px;\n margin-bottom: 12px;\n height: 0;\n width: 0;\n background: inherit;\n color: transparent;\n outline: none;\n box-shadow: none;\n border: none;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_mobile-input-hidden__tkKxe:focus {\n outline: none;\n box-shadow: none;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_container__YXGq1 {\n position: absolute;\n width: calc((100% - 50px) * 0.6);\n height: 25px;\n font-size: 15px;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_container__YXGq1 .styles_suggestion__3Hx-R {\n list-style: none;\n font-size: 16px;\n font-style: normal;\n font-weight: normal;\n line-height: 49px;\n height: 50px;\n margin: 0;\n padding: 0 0 0 16px;\n color: rgba(0, 0, 0, 0.87);\n background: white;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_container__YXGq1 .styles_suggestionHighlighted__lexDG {\n background: #dfe5e8;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_container__YXGq1 .styles_suggestionsContainerOpen__pzu14 {\n position: absolute;\n z-index: 19;\n max-height: 250px;\n overflow-x: hidden;\n overflow-y: scroll;\n border-radius: 5px;\n box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.06);\n border: 1px rgba(0, 0, 0, 0.1) solid;\n top: 42px;\n left: -2px;\n width: 100%;\n font-size: 16px;\n line-height: 1;\n color: rgba(0, 0, 0, 0.87);\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_container__YXGq1 .styles_suggestionsContainerOpen__pzu14 ul {\n padding: 0;\n margin: 0;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_container__YXGq1 .styles_input__Yk2Tv {\n color: black;\n box-shadow: none;\n border: none;\n background: none;\n font-size: 16px;\n font-weight: var(--font-weight-medium);\n height: 25px;\n display: inline-block;\n font-family: inherit;\n line-height: 1.33;\n padding-left: 46px;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_container__YXGq1 .styles_input__Yk2Tv:focus {\n outline: none;\n border: none;\n background: none;\n}\n.styles_mobile-modal-content__Gla3M .styles_buttons-row__zrM35 {\n display: flex;\n flex-direction: column;\n font-size: 16px;\n font-weight: var(--font-weight-medium);\n}\n.styles_mobile-modal-content__Gla3M .styles_buttons-row__zrM35 button {\n background-color: white;\n font-family: inherit;\n font-weight: var(--font-weight-medium);\n font-size: 16px;\n letter-spacing: -0.02em;\n width: 100%;\n border-radius: 50px;\n height: 40px;\n color: var(--color);\n border: 1px solid #888;\n}\n.styles_mobile-modal-content__Gla3M .styles_buttons-row__zrM35 button.styles_ready-button__e23mO {\n color: white;\n background-color: var(--color);\n border: none;\n margin-bottom: 10px;\n}";
142
+ var styles = {"sr-only":"styles_sr-only__D6MJ6","dt-datetimepicker":"styles_dt-datetimepicker__DSYbh","hidden":"styles_hidden__TjnWH","datetimepicker-open-container":"styles_datetimepicker-open-container__zWBiT","top-row-container":"styles_top-row-container__rGdmj","closed":"styles_closed__QYbCF","time-icon":"styles_time-icon__ES47Y","dropdown-icon":"styles_dropdown-icon__O0GrI","close-icon":"styles_close-icon__B5-I5","label-open":"styles_label-open__dVDlF","textbutton":"styles_textbutton__sVckw","active":"styles_active__9LaDz","combobox-left":"styles_combobox-left__nwwOo","combobox-right":"styles_combobox-right__qlPKD","departure-or-arrival-container":"styles_departure-or-arrival-container__Rj3b9","radio-textbutton-label":"styles_radio-textbutton-label__LeV2o","first-radio":"styles_first-radio__F8N7J","radio-textbutton":"styles_radio-textbutton__Ad81W","departure-now-button":"styles_departure-now-button__8ZlDU","open-button":"styles_open-button__yl4be","close-button":"styles_close-button__mdfIU","right-edge":"styles_right-edge__XbqgK","combobox-container":"styles_combobox-container__sR9aU","combobox-icon":"styles_combobox-icon__wsdX4","date-input-icon":"styles_date-input-icon__SFXGx","time-input-icon":"styles_time-input-icon__J1qei","datetimepicker-select-container":"styles_datetimepicker-select-container__Y8wgE","picker-container":"styles_picker-container__wz2Zj","combobox-mobile-container":"styles_combobox-mobile-container__ZzG8g","mobile-modal-overlay":"styles_mobile-modal-overlay__wq8cS","mobile-modal-content":"styles_mobile-modal-content__Gla3M","top-row":"styles_top-row__hzke9","modal-title":"styles_modal-title__CztKf","tab-row":"styles_tab-row__6WYwM","radio-tab-label":"styles_radio-tab-label__-7Fx-","input-row":"styles_input-row__r-26j","input-container":"styles_input-container__US3eW","time-input-mobile":"styles_time-input-mobile__QGutf","mobile-input-display":"styles_mobile-input-display__Xnjg1","mobile-input-hidden":"styles_mobile-input-hidden__tkKxe","container":"styles_container__YXGq1","suggestion":"styles_suggestion__3Hx-R","suggestionHighlighted":"styles_suggestionHighlighted__lexDG","suggestionsContainerOpen":"styles_suggestionsContainerOpen__pzu14","input":"styles_input__Yk2Tv","buttons-row":"styles_buttons-row__zrM35","ready-button":"styles_ready-button__e23mO"};
143
+ styleInject(css_248z);
144
+
145
+ /**
146
+ * Component to display a date or time input on desktop.
147
+ *
148
+ * @param {Object} props
149
+ * @param {Number} props.value Currently selected time. Unix timestamp in milliseconds
150
+ * @param {function} props.onChange This is called with new timestamp when input is changed
151
+ * @param {function} props.getDisplay Function to get a string representation from a timestamp
152
+ * @param {array} props.timeChoices Array of timestamps to choose from
153
+ * @param {string} id Id prefix for labels and aria attributes. Should be unique in page
154
+ * @param {string} label Text to show as input label
155
+ * @param {node} icon JSX for icon to show in input
156
+ * @param {boolean} disableTyping Set to true to disable typing in the input
157
+ * @param {boolean} datePicker Is the picker DatePicker or TimePicker
158
+ *
159
+ * @example
160
+ * <DesktopDatetimepicker
161
+ * value={1590133823000}
162
+ * onChange={timestamp => update(timestamp)}
163
+ * getDisplay={timestamp => formatTime(timestamp)}
164
+ * timeChoices={[1590133823000]}
165
+ * id="timeinput"
166
+ * icon="<Icon />"
167
+ * disableTyping={false} // can be omitted if not true
168
+ * datePicker={true} // can be omitted if not true
169
+ */
170
+ function DesktopDatetimepicker({
171
+ value,
172
+ onChange,
173
+ getDisplay,
174
+ timeChoices,
175
+ id,
176
+ label,
177
+ icon,
178
+ disableTyping,
179
+ timeZone,
180
+ datePicker,
181
+ translationSettings
182
+ }) {
183
+ Settings.defaultZone = timeZone;
184
+ const [t] = useTranslation();
185
+ const [displayValue, changeDisplayValue] = useState(getDisplay(value));
186
+ const [typing, setTyping] = useState(false);
187
+ const [showAllOptions, setShowAllOptions] = useState(true);
188
+ const [invalidInput, setinvalidInput] = useState(false);
189
+ useEffect(() => {
190
+ changeDisplayValue(getDisplay(value));
191
+ if (!datePicker) {
192
+ setinvalidInput(false);
193
+ }
194
+ }, [value]);
195
+
196
+ // newValue is string
197
+ const handleTimestamp = newValue => {
198
+ const asNumber = Number(newValue);
199
+ try {
200
+ DateTime.fromMillis(asNumber);
201
+ } catch (e) {
202
+ // If the value is not a valid timestamp, we don't change it
203
+ return;
204
+ }
205
+ onChange(asNumber);
206
+ };
207
+ function isinvalidInput(str) {
208
+ const regex = /[a-zA-Z§ÄäÖö.-\s]/g;
209
+ if (str.length < displayValue?.length) {
210
+ const valid = validateInput(str);
211
+ setinvalidInput(!valid);
212
+ return true;
213
+ }
214
+ if (regex.test(str)) {
215
+ return false;
216
+ }
217
+ const time = str.length > 5 ? str.slice(0, -1) : str;
218
+ const valid = validateInput(time);
219
+ setinvalidInput(!valid);
220
+ return str.length <= 5;
221
+ }
222
+ const onInputChange = (newValue, {
223
+ action
224
+ }) => {
225
+ if (action === 'menu-close') {
226
+ setShowAllOptions(true);
227
+ } else if (showAllOptions) {
228
+ setShowAllOptions(false);
229
+ }
230
+ if (disableTyping || newValue && !isinvalidInput(newValue)) {
231
+ return;
232
+ }
233
+ if (action === 'input-change') {
234
+ const validated = parseTypedTime(newValue);
235
+ changeDisplayValue(validated);
236
+ setTyping(true);
237
+ }
238
+ };
239
+ const options = timeChoices.map(time => {
240
+ return {
241
+ value: time.toString(),
242
+ label: getDisplay(time)
243
+ };
244
+ });
245
+
246
+ // Time picker has a list of minutes, instead of 15 minutes. We need to filter those out
247
+ // so closest option will show up in the list.
248
+ const closestOptions = datePicker ? options : options.filter(o => o.label.split(':')[1] % 15 === 0);
249
+ const closestOption = closestOptions.length > 0 ? closestOptions?.reduce((a, b) => Math.abs(value - a.value) < Math.abs(value - b.value) ? a : b) : undefined;
250
+ const inputId = `${id}-input`;
251
+ const labelId = `${id}-label`;
252
+ const filterOptions = (option, input) => {
253
+ const hour = input.length <= 2 ? input : input.split(':')[0];
254
+ const isValidHour = /^([0-1]?[0-9]|2[0-3])$/.test(hour);
255
+ const comp = hour.length === 1 ? '0'.concat(input) : input;
256
+ return isValidHour ? option.label.split(':')[0] === comp.split(':')[0] : true;
257
+ };
258
+ const ariaError = t('invalid-input', translationSettings);
259
+ return /*#__PURE__*/React.createElement("label", {
260
+ className: styles['combobox-container'],
261
+ htmlFor: inputId
262
+ }, /*#__PURE__*/React.createElement("span", {
263
+ className: styles['sr-only'],
264
+ id: labelId
265
+ }, label, " ", displayValue), icon, /*#__PURE__*/React.createElement(Select, {
266
+ "aria-labelledby": labelId,
267
+ ariaLiveMessages: {
268
+ guidance: context => {
269
+ // When user types invalid value, isDisabled becomes undefined instead of false
270
+ if (context.isDisabled === undefined && invalidInput) {
271
+ return ariaError;
272
+ }
273
+ return '.'; // this can't be empty for some reason
274
+ },
275
+ onChange: () => {
276
+ return '';
277
+ },
278
+ onFilter: () => {
279
+ return '';
280
+ },
281
+ onFocus: ({
282
+ context,
283
+ label: itemLabel
284
+ }) => {
285
+ if (context === 'menu') {
286
+ return itemLabel;
287
+ }
288
+ return '';
289
+ }
290
+ },
291
+ options: options,
292
+ inputId: inputId,
293
+ onChange: time => {
294
+ const currentTime = DateTime.fromMillis(value).toFormat('HH:mm');
295
+ const ts = getTs(displayValue, value);
296
+ if (typing) {
297
+ if (ts !== null) {
298
+ if (currentTime !== displayValue && time.value === value) {
299
+ handleTimestamp(ts);
300
+ } else {
301
+ handleTimestamp(time.value);
302
+ }
303
+ setTyping(false);
304
+ } else {
305
+ if (time.value !== value) {
306
+ handleTimestamp(time.value);
307
+ } else {
308
+ // reset value
309
+ changeDisplayValue(getDisplay(value));
310
+ }
311
+ setTyping(false);
312
+ }
313
+ if (!datePicker) {
314
+ setinvalidInput(false);
315
+ }
316
+ return;
317
+ }
318
+ if (!datePicker) {
319
+ setinvalidInput(false);
320
+ }
321
+ handleTimestamp(time.value);
322
+ },
323
+ components: {
324
+ IndicatorsContainer: () => null
325
+ },
326
+ className: cx(styles['datetimepicker-select-container'], invalidInput && 'datetimepicker-invalid-input'),
327
+ classNamePrefix: "datetimepicker-select",
328
+ onInputChange: onInputChange,
329
+ inputValue: !disableTyping && displayValue,
330
+ value: closestOption,
331
+ noOptionsMessage: () => {
332
+ return '';
333
+ },
334
+ filterOption: (option, input) => {
335
+ if (datePicker) {
336
+ return true;
337
+ }
338
+ const completeInput = input.length === 5 || input.length === 4 && input.split(':')[0].length === 1;
339
+ const isMod15 = option.label.split(':')[1] % 15 === 0;
340
+ const minuteInput = input.split(':')[1]?.length === 1;
341
+ if (showAllOptions && isMod15) {
342
+ return true;
343
+ }
344
+ if (minuteInput) {
345
+ const inputH = input.split(':')[0].length === 1 ? '0'.concat(input.split(':')[0]) : input.split(':')[0];
346
+ const inputM = input.split(':')[1];
347
+ const optH = option.label.split(':')[0];
348
+ const optM = option.label.split(':')[1];
349
+ if (inputH === optH) {
350
+ const time = Number(inputM) * 10;
351
+ const total = Number(optM) - time;
352
+ if (total >= 0 && total <= 9) {
353
+ return true;
354
+ }
355
+ return false;
356
+ }
357
+ }
358
+ if (completeInput && !showAllOptions) {
359
+ return input.length === 4 ? '0'.concat(input) === option.label : input === option.label;
360
+ }
361
+ if (isMod15) {
362
+ return filterOptions(option, input);
363
+ }
364
+ return false;
365
+ },
366
+ controlShouldRenderValue: disableTyping,
367
+ tabSelectsValue: false,
368
+ placeholder: "",
369
+ onFocus: e => {
370
+ if (!disableTyping) {
371
+ e.target.select();
372
+ }
373
+ },
374
+ onBlur: () => {
375
+ // removing focus also locks in value
376
+ if (typing) {
377
+ const validated = getTs(displayValue, value);
378
+ if (validated !== null) {
379
+ handleTimestamp(validated);
380
+ setTyping(false);
381
+ } else {
382
+ changeDisplayValue(getDisplay(value));
383
+ setTyping(false);
384
+ }
385
+ }
386
+ if (!datePicker) {
387
+ setinvalidInput(false);
388
+ }
389
+ },
390
+ onKeyDown: e => {
391
+ if (['ArrowDown', 'ArrowUp'].includes(e.key)) {
392
+ setTyping(false);
393
+ }
394
+ },
395
+ isSearchable: !disableTyping
396
+ }));
397
+ }
398
+ process.env.NODE_ENV !== "production" ? DesktopDatetimepicker.propTypes = {
399
+ value: PropTypes.number.isRequired,
400
+ onChange: PropTypes.func.isRequired,
401
+ getDisplay: PropTypes.func.isRequired,
402
+ timeChoices: PropTypes.arrayOf(PropTypes.number).isRequired,
403
+ id: PropTypes.string.isRequired,
404
+ label: PropTypes.node.isRequired,
405
+ icon: PropTypes.node.isRequired,
406
+ disableTyping: PropTypes.bool,
407
+ timeZone: PropTypes.string,
408
+ datePicker: PropTypes.bool,
409
+ translationSettings: PropTypes.shape({
410
+ lng: PropTypes.string.isRequired
411
+ })
412
+ } : void 0;
413
+ DesktopDatetimepicker.defaultProps = {
414
+ disableTyping: false,
415
+ timeZone: 'Europe/Helsinki',
416
+ datePicker: false,
417
+ translationSettings: {
418
+ lng: 'fi'
419
+ }
420
+ };
421
+
422
+ /**
423
+ * Detect if user is on a mobile device by parsing userAgent. This is not 100% reliable but close enough.
424
+ *
425
+ * @return {boolean}
426
+ */
427
+ function isMobile() {
428
+ const userAgent = navigator.userAgent || navigator.vendor || window.opera;
429
+ const mobile = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(userAgent);
430
+ return mobile;
431
+ }
432
+
433
+ /**
434
+ * Detect if user is on an android device. Not 100% reliable.
435
+ */
436
+ function isAndroid() {
437
+ const userAgent = navigator.userAgent.toLowerCase();
438
+ return userAgent.indexOf('android') > -1;
439
+ }
440
+
441
+ /**
442
+ * Detect if input type="date" and type="time" are supported in by the browser
443
+ *
444
+ * @return {boolean}
445
+ */
446
+ function dateTimeInputIsSupported() {
447
+ if (typeof window === 'undefined' || window === null) {
448
+ return false;
449
+ }
450
+ const elem = document.createElement('input');
451
+ elem.type = 'date';
452
+ if (elem.type !== 'date') {
453
+ return false;
454
+ }
455
+ elem.type = 'time';
456
+ if (elem.type !== 'time') {
457
+ return false;
458
+ }
459
+ return true;
460
+ }
461
+
462
+ const AndroidSelect = ({
463
+ value,
464
+ dateChoices,
465
+ id,
466
+ getDisplay,
467
+ onChange,
468
+ labelId,
469
+ inputId
470
+ }) => {
471
+ const items = dateChoices.map(date => ({
472
+ value: date,
473
+ label: getDisplay(date)
474
+ }));
475
+ const selectedItem = items.find(option => option.value === value) || items[0] || null;
476
+ const {
477
+ isOpen,
478
+ getToggleButtonProps,
479
+ getMenuProps,
480
+ getItemProps,
481
+ highlightedIndex
482
+ } = useSelect({
483
+ selectedItem,
484
+ items,
485
+ itemToString: item => item?.label ?? '',
486
+ onSelectedItemChange: ({
487
+ selectedItem: nextSelectedItem
488
+ }) => {
489
+ if (nextSelectedItem) {
490
+ onChange(nextSelectedItem.value);
491
+ }
492
+ },
493
+ id
494
+ });
495
+ return /*#__PURE__*/React.createElement("div", {
496
+ className: styles.container
497
+ }, /*#__PURE__*/React.createElement("div", getToggleButtonProps({
498
+ 'aria-labelledby': labelId,
499
+ id: inputId
500
+ }), /*#__PURE__*/React.createElement("div", {
501
+ className: styles.input
502
+ }, selectedItem.label)), /*#__PURE__*/React.createElement("div", {
503
+ className: cx(styles.suggestionsContainerOpen, !isOpen && 'hidden')
504
+ }, /*#__PURE__*/React.createElement("ul", getMenuProps(), isOpen && items.map((option, index) => /*#__PURE__*/React.createElement("li", Object.assign({
505
+ key: option.value,
506
+ className: cx(styles.suggestion, index === highlightedIndex && styles.suggestionHighlighted)
507
+ }, getItemProps({
508
+ index
509
+ })), /*#__PURE__*/React.createElement("span", null, option.label))))));
510
+ };
511
+ process.env.NODE_ENV !== "production" ? AndroidSelect.propTypes = {
512
+ value: PropTypes.number.isRequired,
513
+ dateChoices: PropTypes.arrayOf(PropTypes.number).isRequired,
514
+ id: PropTypes.string.isRequired,
515
+ getDisplay: PropTypes.func.isRequired,
516
+ onChange: PropTypes.func.isRequired,
517
+ labelId: PropTypes.string.isRequired,
518
+ inputId: PropTypes.string.isRequired
519
+ } : void 0;
520
+
521
+ /**
522
+ * Component to display a date input on mobile
523
+ */
524
+ function MobileDatepicker({
525
+ value,
526
+ getDisplay,
527
+ onChange,
528
+ itemCount,
529
+ startTime,
530
+ id,
531
+ label,
532
+ icon,
533
+ timeZone
534
+ }) {
535
+ Settings.defaultZone = timeZone;
536
+ const dateChoices = Array(itemCount).fill().map((_, i) => DateTime.fromMillis(startTime).plus({
537
+ days: i
538
+ }).toMillis());
539
+ const nativeInput = !isAndroid();
540
+ const labelId = `${id}-label`;
541
+ const inputId = `${id}-input`;
542
+ return /*#__PURE__*/React.createElement("label", {
543
+ className: styles['input-container'],
544
+ htmlFor: inputId
545
+ }, /*#__PURE__*/React.createElement("span", null, icon), /*#__PURE__*/React.createElement("span", {
546
+ className: styles['sr-only'],
547
+ id: labelId
548
+ }, label), nativeInput ? /*#__PURE__*/React.createElement("select", {
549
+ id: inputId,
550
+ className: styles['mobile-input-display'],
551
+ onChange: e => onChange(Number(e.target.value)),
552
+ value: value
553
+ }, dateChoices.map(date => {
554
+ return /*#__PURE__*/React.createElement("option", {
555
+ key: date,
556
+ value: date
557
+ }, getDisplay(date));
558
+ })) : /*#__PURE__*/React.createElement(AndroidSelect, {
559
+ value,
560
+ dateChoices,
561
+ id,
562
+ getDisplay,
563
+ onChange,
564
+ labelId,
565
+ inputId
566
+ }));
567
+ }
568
+ process.env.NODE_ENV !== "production" ? MobileDatepicker.propTypes = {
569
+ value: PropTypes.number.isRequired,
570
+ getDisplay: PropTypes.func.isRequired,
571
+ onChange: PropTypes.func.isRequired,
572
+ itemCount: PropTypes.number.isRequired,
573
+ startTime: PropTypes.number.isRequired,
574
+ id: PropTypes.string.isRequired,
575
+ label: PropTypes.string.isRequired,
576
+ icon: PropTypes.node,
577
+ timeZone: PropTypes.string
578
+ } : void 0;
579
+ MobileDatepicker.defaultProps = {
580
+ icon: null,
581
+ timeZone: 'Europe/Helsinki'
582
+ };
583
+
584
+ /**
585
+ * Component to display a time input on mobile
586
+ */
587
+ function MobileTimepicker({
588
+ value,
589
+ getDisplay,
590
+ onChange,
591
+ id,
592
+ label,
593
+ icon,
594
+ timeZone
595
+ }) {
596
+ const [inputValue, changeInputValue] = useState(getDisplay(value));
597
+ const [isValidInput, setValidInput] = useState(true);
598
+ Settings.defaultZone = timeZone;
599
+ const inputId = `${id}-input`;
600
+ const labelId = `${id}-label`;
601
+ const showError = !isValidInput;
602
+ return /*#__PURE__*/React.createElement("label", {
603
+ className: styles['input-container'],
604
+ htmlFor: inputId
605
+ }, /*#__PURE__*/React.createElement("span", null, icon), /*#__PURE__*/React.createElement("span", {
606
+ className: styles['sr-only'],
607
+ id: labelId
608
+ }, label), /*#__PURE__*/React.createElement("input", {
609
+ id: inputId,
610
+ inputMode: "numeric",
611
+ type: "text",
612
+ maxLength: "6",
613
+ className: cx(styles['time-input-mobile'], showError && 'mobile-datetimepicker-invalid-input'),
614
+ value: inputValue,
615
+ onFocus: e => {
616
+ e.target.setSelectionRange(0, 0); // set caret to start of input
617
+ },
618
+ onChange: event => {
619
+ let newValue = event.target.value.replace('.', ':');
620
+ const valid = validateInput(newValue);
621
+ setValidInput(valid);
622
+ if (
623
+ // number typed as first char => clear rest of the input
624
+ newValue.match(/[0-9]{3}:[0-9]{2}/) && newValue.slice(1) === inputValue) {
625
+ newValue = newValue.charAt(0);
626
+ }
627
+ if (newValue.length > 5) {
628
+ return;
629
+ }
630
+ const actual = parseTypedTime(newValue);
631
+ changeInputValue(actual);
632
+ const timestamp = getTs(actual, value);
633
+ if (timestamp) {
634
+ onChange(timestamp);
635
+ }
636
+ }
637
+ }));
638
+ }
639
+ process.env.NODE_ENV !== "production" ? MobileTimepicker.propTypes = {
640
+ value: PropTypes.number.isRequired,
641
+ getDisplay: PropTypes.func.isRequired,
642
+ onChange: PropTypes.func.isRequired,
643
+ id: PropTypes.string.isRequired,
644
+ label: PropTypes.string.isRequired,
645
+ icon: PropTypes.node,
646
+ timeZone: PropTypes.string
647
+ } : void 0;
648
+ MobileTimepicker.defaultProps = {
649
+ icon: null,
650
+ timeZone: 'Europe/Helsinki'
651
+ };
652
+
653
+ Settings.defaultLocale = 'en';
654
+
655
+ /**
656
+ * @param {object} props
657
+ * @param {'arrival'|'departure'} props.departureOrArrival Whether the picker is for arrival or departure time
658
+ * @param {function} props.onNowClick Called when the "Now" button is clicked
659
+ * @param {string} props.lang Language code for translations
660
+ * @param {string} props.color Color for the component, used in styles
661
+ * @param {string} props.timeZone Time zone to use for date and time calculations
662
+ * @param {function} props.onSubmit Called when the user submits the selected date and time
663
+ * @param {function} props.onCancel called when the user cancels the selection
664
+ * @param {number} props.timestamp Initial timestamp to display in the picker
665
+ * @param {function} props.getTimeDisplay Called to get the display string for a given timestamp
666
+ * @param {number} props.dateSelectItemCount Determines how many date options are shown in the date picker
667
+ * @param {function} props.getDateDisplay Called to get the display string for a given date
668
+ * @param {object} props.fontWeights Font weights used in the component styles
669
+ * @returns {JSX.Element}
670
+ */
671
+ function MobilePickerModal({
672
+ departureOrArrival,
673
+ onNowClick,
674
+ lang,
675
+ color,
676
+ timeZone,
677
+ onSubmit,
678
+ onCancel,
679
+ timestamp,
680
+ getTimeDisplay,
681
+ dateSelectItemCount,
682
+ getDateDisplay,
683
+ fontWeights,
684
+ onAfterClose
685
+ }) {
686
+ Settings.defaultZone = timeZone;
687
+ const [t] = useTranslation();
688
+ const translationSettings = {
689
+ lng: lang
690
+ };
691
+ const [displayTimestamp, changeTimestamp] = useState(timestamp);
692
+ const [departureOrArrivalCurrent, changeDepartureOrArrival] = useState(departureOrArrival);
693
+ function onArrivalClick() {
694
+ changeDepartureOrArrival('arrival');
695
+ }
696
+ function onDepartureClick() {
697
+ changeDepartureOrArrival('departure');
698
+ }
699
+
700
+ // for input labels
701
+ const [htmlId] = useState(uniqueId('datetimepicker-'));
702
+ const dateSelectStartTime = DateTime.now().startOf('day').set({
703
+ hour: DateTime.fromMillis(displayTimestamp).hour,
704
+ minute: DateTime.fromMillis(displayTimestamp).minute
705
+ }).toMillis();
706
+ return /*#__PURE__*/React.createElement(Modal, {
707
+ appElement: document ? document.querySelector('#app') : undefined,
708
+ isOpen: true,
709
+ className: styles['mobile-modal-content'],
710
+ overlayClassName: styles['mobile-modal-overlay'],
711
+ onAfterClose: onAfterClose
712
+ }, /*#__PURE__*/React.createElement("div", {
713
+ id: "digitransit-mobile-datetime",
714
+ style: {
715
+ '--color': `${color}`,
716
+ '--font-weight-medium': fontWeights.medium
717
+ }
718
+ }, /*#__PURE__*/React.createElement("div", {
719
+ className: styles['top-row']
720
+ }, /*#__PURE__*/React.createElement("h3", {
721
+ className: styles['modal-title']
722
+ }, t('choose-time', translationSettings)), /*#__PURE__*/React.createElement("button", {
723
+ type: "button",
724
+ className: styles['departure-now-button'],
725
+ onClick: onNowClick
726
+ }, t('departure-now', translationSettings))), /*#__PURE__*/React.createElement("div", {
727
+ className: styles['tab-row']
728
+ }, /*#__PURE__*/React.createElement("label", {
729
+ htmlFor: `${htmlId}-modal-departure`,
730
+ className: `${styles['radio-tab-label']}
731
+ ${styles[departureOrArrivalCurrent === 'departure' ? 'active' : undefined]}`
732
+ }, t('departure', translationSettings), /*#__PURE__*/React.createElement("input", {
733
+ id: `${htmlId}-modal-departure`,
734
+ name: "departureOrArrival",
735
+ type: "radio",
736
+ value: "departure",
737
+ className: styles['radio-textbutton'],
738
+ onChange: () => {
739
+ onDepartureClick();
740
+ },
741
+ checked: departureOrArrivalCurrent === 'departure'
742
+ })), /*#__PURE__*/React.createElement("label", {
743
+ htmlFor: `${htmlId}-modal-arrival`,
744
+ className: `${styles['radio-tab-label']}
745
+ ${styles[departureOrArrivalCurrent === 'arrival' ? 'active' : undefined]}`
746
+ }, t('arrival', translationSettings), /*#__PURE__*/React.createElement("input", {
747
+ id: `${htmlId}-modal-arrival`,
748
+ name: "departureOrArrival",
749
+ type: "radio",
750
+ value: "arrival",
751
+ className: styles['radio-textbutton'],
752
+ onChange: () => {
753
+ onArrivalClick();
754
+ },
755
+ checked: departureOrArrivalCurrent === 'arrival'
756
+ }))), /*#__PURE__*/React.createElement("div", {
757
+ className: styles['input-row']
758
+ }, /*#__PURE__*/React.createElement(MobileDatepicker, {
759
+ value: displayTimestamp,
760
+ getDisplay: getDateDisplay,
761
+ onChange: changeTimestamp,
762
+ itemCount: dateSelectItemCount,
763
+ startTime: dateSelectStartTime,
764
+ id: `${htmlId}-date`,
765
+ label: t('date', translationSettings),
766
+ icon: /*#__PURE__*/React.createElement("span", {
767
+ className: `${styles['combobox-icon']} ${styles['date-input-icon']}`
768
+ }, /*#__PURE__*/React.createElement(Icon, {
769
+ img: "calendar",
770
+ color: color
771
+ })),
772
+ timeZone: timeZone
773
+ }), /*#__PURE__*/React.createElement(MobileTimepicker, {
774
+ value: displayTimestamp,
775
+ getDisplay: getTimeDisplay,
776
+ onChange: changeTimestamp,
777
+ id: `${htmlId}-time`,
778
+ label: t('time', translationSettings),
779
+ icon: /*#__PURE__*/React.createElement("span", {
780
+ className: `${styles['combobox-icon']} ${styles['time-input-icon']}`
781
+ }, /*#__PURE__*/React.createElement(Icon, {
782
+ img: "time",
783
+ color: color
784
+ })),
785
+ timeZone: timeZone
786
+ })), /*#__PURE__*/React.createElement("div", {
787
+ className: styles['buttons-row']
788
+ }, /*#__PURE__*/React.createElement("button", {
789
+ type: "button",
790
+ className: styles['ready-button'],
791
+ onClick: () => onSubmit(displayTimestamp, departureOrArrivalCurrent)
792
+ }, t('ready', translationSettings)), /*#__PURE__*/React.createElement("button", {
793
+ type: "button",
794
+ className: styles['cancel-button'],
795
+ onClick: onCancel
796
+ }, t('cancel', translationSettings)))));
797
+ }
798
+ process.env.NODE_ENV !== "production" ? MobilePickerModal.propTypes = {
799
+ timeZone: PropTypes.string,
800
+ departureOrArrival: PropTypes.oneOf(['departure', 'arrival']).isRequired,
801
+ onNowClick: PropTypes.func.isRequired,
802
+ lang: PropTypes.string.isRequired,
803
+ color: PropTypes.string,
804
+ onSubmit: PropTypes.func.isRequired,
805
+ onCancel: PropTypes.func.isRequired,
806
+ onAfterClose: PropTypes.func.isRequired,
807
+ timestamp: PropTypes.number.isRequired,
808
+ getTimeDisplay: PropTypes.func.isRequired,
809
+ dateSelectItemCount: PropTypes.number.isRequired,
810
+ getDateDisplay: PropTypes.func.isRequired,
811
+ fontWeights: PropTypes.shape({
812
+ medium: PropTypes.number.isRequired
813
+ }).isRequired
814
+ } : void 0;
815
+ MobilePickerModal.defaultProps = {
816
+ color: '#007ac9',
817
+ timeZone: 'Europe/Helsinki'
818
+ };
819
+
820
+ Settings.defaultLocale = 'en';
821
+
822
+ /**
823
+ * This component renders combobox style inputs for selecting date and time. This is a controlled component, timestamp is the current value of both inputs.
824
+ * @param {Object} props
825
+ *
826
+ * @param {Number} props.timestamp Currently selected time as a unix timestamp in milliseconds. Set to null to signify that "now" is selected. Displayed time is updated in realtime when set to null
827
+ * @param {function} props.onTimeChange Called with new timestamp when time input changes
828
+ * @param {function} props.onDateChange Called with new timestamp when date input changes
829
+ * @param {'arrival'|'departure'} props.departureOrArrival Determine if input is set to choose departure or arrival time
830
+ * @param {function} props.onNowClick Called when "depart now" button is clicked
831
+ * @param {function} props.onDepartureClick Called when "departure" button is clicked
832
+ * @param {function} props.onArrivalClick Called when "arrival" button is clicked
833
+ * @param {node} props.embedWhenClosed JSX element to render in the corner when input is closed
834
+ * @param {node} props.embedWhenOpen JSX element to render when input is open
835
+ * @param {string} props.lang Language selection
836
+ * @param {number} props.serviceTimeRange Determine number of days shown in timepicker. Optional. default is 30.
837
+ * @param {function} props.onOpen Determine what to do when timepicker is open. Optional. no default implementation.
838
+ * @param {function} props.onClose Determine what to do when timepicker is closed. Optional. no default implementation.
839
+ * @param {function} props.openPicker Determine if timepicker should be open in intial render. Optional. Default is undefined.
840
+ *
841
+ *
842
+ * @example
843
+ * <Datetimepicker
844
+ * timestamp={1590133823000}
845
+ * onTimeChange={(newTimestamp) => update(newTimestamp)}
846
+ * onDateChange={(newTimestamp) => update(newTimestamp)}
847
+ * departureOrArrival={'departure'}
848
+ * onNowClick={() => setTimestampToNull()}
849
+ * onDepartureClick={() => departureClicked()}
850
+ * onArrivalClick={() => arrivalClicked()}
851
+ * embedWhenClosed={<button />}
852
+ * lang={'en'}
853
+ * serviceTimeRange={15}
854
+ * />
855
+ */
856
+ function Datetimepicker({
857
+ timestamp,
858
+ onTimeChange,
859
+ onDateChange,
860
+ departureOrArrival,
861
+ onNowClick,
862
+ onDepartureClick,
863
+ onArrivalClick,
864
+ embedWhenClosed,
865
+ embedWhenOpen,
866
+ lang,
867
+ color,
868
+ timeZone,
869
+ onModalSubmit,
870
+ fontWeights,
871
+ serviceTimeRange,
872
+ onOpen,
873
+ onClose,
874
+ openPicker
875
+ }) {
876
+ Settings.defaultZone = timeZone;
877
+ const [t] = useTranslation();
878
+ const [isOpen, changeOpen] = useState(openPicker || false);
879
+ const [displayTimestamp, changeDisplayTimestamp] = useState(timestamp || DateTime.now().toMillis());
880
+ // timer for updating displayTimestamp in real time
881
+ const [timerId, setTimer] = useState(null);
882
+ // for input labels
883
+ const [htmlId] = useState(uniqueId('datetimepicker-'));
884
+ const [useMobileInputs] = useState(isMobile() && dateTimeInputIsSupported());
885
+ const openPickerRef = useRef();
886
+ const inputRef = useRef();
887
+ const alertRef = useRef();
888
+ const translationSettings = {
889
+ lng: lang
890
+ };
891
+ useEffect(() => {
892
+ Settings.defaultLocale = lang;
893
+ }, [lang]);
894
+ const nowSelected = timestamp === null;
895
+ useEffect(() => {
896
+ if (nowSelected) {
897
+ changeDisplayTimestamp(DateTime.now().toMillis());
898
+ } else {
899
+ // clear timer
900
+ if (timerId) {
901
+ clearInterval(timerId);
902
+ setTimer(null);
903
+ }
904
+ changeDisplayTimestamp(timestamp);
905
+ }
906
+ }, [timestamp]);
907
+
908
+ // set timer to update displayTimestamp when minute changes if nowSelected
909
+ useEffect(() => {
910
+ if (!nowSelected) {
911
+ return undefined;
912
+ }
913
+ if (timerId) {
914
+ clearInterval(timerId);
915
+ }
916
+ const newId = setInterval(() => {
917
+ const now = DateTime.now();
918
+ const sameMinute = DateTime.fromMillis(displayTimestamp).hasSame(now, 'minute');
919
+ if (!sameMinute) {
920
+ clearInterval(newId);
921
+ setTimer(null);
922
+ changeDisplayTimestamp(now.toMillis());
923
+ }
924
+ }, 5000);
925
+ setTimer(newId);
926
+ return () => clearInterval(newId);
927
+ }, [displayTimestamp]);
928
+
929
+ /**
930
+ * @param {number} date Date in milliseconds since unix epoch
931
+ * @returns {string} formatted date
932
+ */
933
+ const getDateDisplay = date => {
934
+ const time = DateTime.fromMillis(date);
935
+ const now = DateTime.now();
936
+ let formatted;
937
+ if (time.hasSame(now, 'day')) {
938
+ formatted = t('today', translationSettings);
939
+ formatted = `${formatted.charAt(0).toUpperCase()}${formatted.slice(1)}`;
940
+ } else if (time.hasSame(now.plus({
941
+ days: 1
942
+ }), 'day')) {
943
+ formatted = t('tomorrow', translationSettings);
944
+ formatted = `${formatted.charAt(0).toUpperCase()}${formatted.slice(1)}`;
945
+ } else {
946
+ formatted = time.toFormat('ccc d.L.');
947
+ }
948
+ return formatted;
949
+ };
950
+
951
+ // param time is timestamp
952
+ const getTimeDisplay = time => {
953
+ return DateTime.fromMillis(time).toFormat('HH:mm');
954
+ };
955
+
956
+ // compute choices only when timestamp changes instead of every render
957
+ const [timeChoices, dateChoices] = useMemo(() => {
958
+ const selectedMoment = DateTime.fromMillis(timestamp || DateTime.now().toMillis());
959
+ const timeSelectStartTime = selectedMoment.startOf('day').toMillis();
960
+ const isMobileCurrently = isMobile();
961
+ let newTimeChoices = [];
962
+ let current = selectedMoment.startOf('day');
963
+ while (current.hasSame(timeSelectStartTime, 'day')) {
964
+ newTimeChoices.push(current.valueOf());
965
+ if (isMobileCurrently) {
966
+ current = current.plus({
967
+ minutes: 15
968
+ });
969
+ } else {
970
+ current = current.plus({
971
+ minutes: 1
972
+ });
973
+ }
974
+ }
975
+ if (timestamp === null) {
976
+ // if time is set to now
977
+ // add times in 5 min intervals for next 30 mins
978
+ const fiveMinutes = 1000 * 60 * 5;
979
+ const nextFiveMin = Math.ceil(displayTimestamp / fiveMinutes) * fiveMinutes;
980
+ const timesToAdd = Array(7).fill().map((_, i) => nextFiveMin + i * fiveMinutes);
981
+ const closestIndexAfter = newTimeChoices.map(time => displayTimestamp - time).findIndex(time => time <= 0);
982
+ newTimeChoices.splice(closestIndexAfter, 0, ...timesToAdd);
983
+ newTimeChoices = Array.from(new Set(newTimeChoices)); // remove duplicates
984
+ }
985
+ const dateSelectStartTime = DateTime.now().startOf('day').set({
986
+ hour: selectedMoment.hour,
987
+ minute: selectedMoment.minute
988
+ });
989
+ const newDateChoices = Array(serviceTimeRange).fill().map((_, i) => dateSelectStartTime.plus({
990
+ days: i
991
+ }).toMillis());
992
+ return [newTimeChoices, newDateChoices];
993
+ }, [timestamp]);
994
+ function showScreenreaderCloseAlert() {
995
+ if (alertRef.current) {
996
+ alertRef.current.innerHTML = t('accessible-closed', translationSettings);
997
+ setTimeout(() => {
998
+ // The component may have unmounted (clearing the ref) before this
999
+ // fires - don't crash in that case.
1000
+ if (alertRef.current) {
1001
+ alertRef.current.innerHTML = null;
1002
+ }
1003
+ }, 100);
1004
+ }
1005
+ }
1006
+ function showScreenreaderOpenAlert() {
1007
+ if (alertRef.current) {
1008
+ alertRef.current.innerHTML = t('accessible-opened', translationSettings);
1009
+ setTimeout(() => {
1010
+ // The component may have unmounted (clearing the ref) before this
1011
+ // fires - don't crash in that case.
1012
+ if (alertRef.current) {
1013
+ alertRef.current.innerHTML = null;
1014
+ }
1015
+ }, 100);
1016
+ }
1017
+ }
1018
+ const handleClose = () => {
1019
+ changeOpen(false);
1020
+ showScreenreaderCloseAlert();
1021
+ if (onClose) {
1022
+ onClose();
1023
+ }
1024
+ };
1025
+ const handleNowClick = () => {
1026
+ handleClose();
1027
+ onNowClick();
1028
+ };
1029
+ function renderOpen() {
1030
+ if (useMobileInputs) {
1031
+ return isOpen && /*#__PURE__*/React.createElement(MobilePickerModal, {
1032
+ departureOrArrival: departureOrArrival,
1033
+ onNowClick: handleNowClick,
1034
+ lang: lang,
1035
+ color: color,
1036
+ onSubmit: (newTimestamp, newDepartureOrArrival) => {
1037
+ onModalSubmit(newTimestamp, newDepartureOrArrival);
1038
+ changeOpen(false);
1039
+ showScreenreaderCloseAlert();
1040
+ },
1041
+ onCancel: handleClose,
1042
+ onAfterClose: () => requestAnimationFrame(() => {
1043
+ openPickerRef.current?.focus();
1044
+ }),
1045
+ getTimeDisplay: getTimeDisplay,
1046
+ timeZone: timeZone,
1047
+ timestamp: displayTimestamp,
1048
+ getDateDisplay: getDateDisplay,
1049
+ dateSelectItemCount: serviceTimeRange,
1050
+ getDisplay: getTimeDisplay,
1051
+ fontWeights: fontWeights
1052
+ });
1053
+ }
1054
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
1055
+ className: `${styles['datetimepicker-open-container']} ${!isOpen ? styles.hidden : ''}`
1056
+ }, /*#__PURE__*/React.createElement("div", {
1057
+ className: isOpen ? `${styles['top-row-container']} datetimepicker-top-row` : `${styles.hidden} datetimepicker-top-row`
1058
+ }, ' ', /*#__PURE__*/React.createElement("span", {
1059
+ role: "alert",
1060
+ className: styles['sr-only'],
1061
+ ref: alertRef
1062
+ }), /*#__PURE__*/React.createElement("span", null), /*#__PURE__*/React.createElement("span", {
1063
+ className: styles['departure-or-arrival-container']
1064
+ }, /*#__PURE__*/React.createElement("label", {
1065
+ htmlFor: `${htmlId}-departure`,
1066
+ className: `${styles['radio-textbutton-label']}
1067
+ ${styles[departureOrArrival === 'departure' ? 'active' : undefined]}`
1068
+ }, t('departure', translationSettings), /*#__PURE__*/React.createElement("input", {
1069
+ id: `${htmlId}-departure`,
1070
+ name: "departureOrArrival",
1071
+ type: "radio",
1072
+ value: "departure",
1073
+ className: styles['radio-textbutton'],
1074
+ onChange: () => {
1075
+ onDepartureClick();
1076
+ },
1077
+ checked: departureOrArrival === 'departure'
1078
+ })), /*#__PURE__*/React.createElement("label", {
1079
+ htmlFor: `${htmlId}-arrival`,
1080
+ className: `${styles['radio-textbutton-label']}
1081
+ ${styles[departureOrArrival === 'arrival' ? 'active' : undefined]}`
1082
+ }, t('arrival', translationSettings), /*#__PURE__*/React.createElement("input", {
1083
+ id: `${htmlId}-arrival`,
1084
+ name: "departureOrArrival",
1085
+ type: "radio",
1086
+ value: "arrival",
1087
+ className: styles['radio-textbutton'],
1088
+ onChange: () => {
1089
+ onArrivalClick();
1090
+ },
1091
+ checked: departureOrArrival === 'arrival'
1092
+ }))), /*#__PURE__*/React.createElement("button", {
1093
+ id: `${htmlId}-now`,
1094
+ type: "button",
1095
+ className: styles['departure-now-button'],
1096
+ onClick: handleNowClick,
1097
+ ref: inputRef
1098
+ }, t('departure-now', translationSettings)), /*#__PURE__*/React.createElement("span", {
1099
+ className: styles['right-edge']
1100
+ }, /*#__PURE__*/React.createElement("button", {
1101
+ type: "button",
1102
+ className: styles['close-button'],
1103
+ "aria-controls": `${htmlId}-root`,
1104
+ "aria-expanded": "true",
1105
+ onClick: handleClose
1106
+ }, /*#__PURE__*/React.createElement("span", {
1107
+ className: styles['close-icon']
1108
+ }, /*#__PURE__*/React.createElement(Icon, {
1109
+ img: "close",
1110
+ color: color
1111
+ })), /*#__PURE__*/React.createElement("span", {
1112
+ className: styles['sr-only']
1113
+ }, t('accessible-close', translationSettings))))), /*#__PURE__*/React.createElement("div", {
1114
+ className: isOpen ? `${styles['picker-container']}` : `${styles.hidden} datetimepicker-top-row`
1115
+ }, /*#__PURE__*/React.createElement("span", {
1116
+ className: styles['combobox-left']
1117
+ }, /*#__PURE__*/React.createElement(DesktopDatetimepicker, {
1118
+ value: displayTimestamp,
1119
+ onChange: newValue => {
1120
+ onDateChange(newValue);
1121
+ },
1122
+ getDisplay: getDateDisplay,
1123
+ timeChoices: dateChoices,
1124
+ validate: () => null,
1125
+ icon: /*#__PURE__*/React.createElement("span", {
1126
+ className: `${styles['combobox-icon']} ${styles['date-input-icon']}`
1127
+ }, /*#__PURE__*/React.createElement(Icon, {
1128
+ img: "calendar",
1129
+ color: color
1130
+ })),
1131
+ id: `${htmlId}-date`,
1132
+ label: t('date', translationSettings),
1133
+ disableTyping: true,
1134
+ timeZone: timeZone,
1135
+ datePicker: true
1136
+ })), /*#__PURE__*/React.createElement("span", {
1137
+ className: styles['combobox-right']
1138
+ }, /*#__PURE__*/React.createElement(DesktopDatetimepicker, {
1139
+ value: displayTimestamp,
1140
+ onChange: newValue => {
1141
+ onTimeChange(newValue);
1142
+ },
1143
+ getDisplay: getTimeDisplay,
1144
+ timeChoices: timeChoices,
1145
+ icon: /*#__PURE__*/React.createElement("span", {
1146
+ className: `${styles['combobox-icon']} ${styles['time-input-icon']}`
1147
+ }, /*#__PURE__*/React.createElement(Icon, {
1148
+ img: "time",
1149
+ color: color
1150
+ })),
1151
+ id: `${htmlId}-time`,
1152
+ label: t('time', translationSettings),
1153
+ timeZone: timeZone,
1154
+ translationSettings: translationSettings
1155
+ })))), /*#__PURE__*/React.createElement("span", {
1156
+ className: isOpen ? '' : styles.hidden
1157
+ }, embedWhenOpen));
1158
+ }
1159
+ const formatToprowSummary = () => {
1160
+ if (nowSelected && departureOrArrival === 'departure') {
1161
+ return /*#__PURE__*/React.createElement("span", null, t('departure-now', translationSettings));
1162
+ }
1163
+ const dateDisplay = getDateDisplay(displayTimestamp);
1164
+ const timeDisplay = getTimeDisplay(displayTimestamp);
1165
+ const summary = t(departureOrArrival === 'departure' ? 'departure' : 'arrival', translationSettings);
1166
+ const isToday = DateTime.now().hasSame(DateTime.fromMillis(displayTimestamp), 'day');
1167
+ const isTomorrow = DateTime.now().plus({
1168
+ days: 1
1169
+ }).hasSame(DateTime.fromMillis(displayTimestamp), 'day');
1170
+ if (isToday) {
1171
+ return /*#__PURE__*/React.createElement("span", null, `${summary} ${timeDisplay}`);
1172
+ }
1173
+ if (isTomorrow) {
1174
+ return /*#__PURE__*/React.createElement("span", null, `${summary} ${dateDisplay.toLowerCase()} ${timeDisplay}`);
1175
+ }
1176
+ return /*#__PURE__*/React.createElement("span", null, `${summary} ${dateDisplay} ${timeDisplay}`);
1177
+ };
1178
+ return /*#__PURE__*/React.createElement("fieldset", {
1179
+ className: styles['dt-datetimepicker'],
1180
+ id: `${htmlId}-root`,
1181
+ style: {
1182
+ '--color': `${color}`,
1183
+ '--font-weight-medium': fontWeights.medium
1184
+ }
1185
+ }, /*#__PURE__*/React.createElement("legend", {
1186
+ className: styles['sr-only']
1187
+ }, t('accessible-title', translationSettings)), /*#__PURE__*/React.createElement("span", {
1188
+ className: styles['sr-only']
1189
+ }, t('accessible-update-instructions', translationSettings)), /*#__PURE__*/React.createElement("div", {
1190
+ className: !isOpen ? `${styles['top-row-container']} ${styles.closed} datetimepicker-top-row` : `${styles.hidden} datetimepicker-top-row`
1191
+ }, /*#__PURE__*/React.createElement("label", {
1192
+ className: styles['label-open'],
1193
+ htmlFor: `${htmlId}-open`
1194
+ }, /*#__PURE__*/React.createElement("span", {
1195
+ className: styles['time-icon']
1196
+ }, /*#__PURE__*/React.createElement(Icon, {
1197
+ img: "time",
1198
+ color: color
1199
+ })), /*#__PURE__*/React.createElement("span", {
1200
+ className: styles['sr-only']
1201
+ }, t('accessible-open', translationSettings)), /*#__PURE__*/React.createElement("span", {
1202
+ role: "alert",
1203
+ className: styles['sr-only'],
1204
+ ref: alertRef
1205
+ }), /*#__PURE__*/React.createElement("button", {
1206
+ id: `${htmlId}-open`,
1207
+ type: "button",
1208
+ className: `${styles.textbutton} ${styles.active} ${styles['open-button']}`,
1209
+ "aria-controls": `${htmlId}-root`,
1210
+ "aria-expanded": "false",
1211
+ onClick: () => {
1212
+ changeOpen(true);
1213
+ showScreenreaderOpenAlert();
1214
+ if (onOpen) {
1215
+ onOpen();
1216
+ }
1217
+ },
1218
+ ref: openPickerRef
1219
+ }, formatToprowSummary(), /*#__PURE__*/React.createElement("span", {
1220
+ className: styles['dropdown-icon']
1221
+ }, /*#__PURE__*/React.createElement(Icon, {
1222
+ img: "arrow-dropdown",
1223
+ color: color
1224
+ })))), /*#__PURE__*/React.createElement("span", {
1225
+ className: styles['right-edge']
1226
+ }, embedWhenClosed)), /*#__PURE__*/React.createElement("div", null), renderOpen());
1227
+ }
1228
+ process.env.NODE_ENV !== "production" ? Datetimepicker.propTypes = {
1229
+ timestamp: PropTypes.number,
1230
+ timeZone: PropTypes.string,
1231
+ onTimeChange: PropTypes.func.isRequired,
1232
+ onDateChange: PropTypes.func.isRequired,
1233
+ departureOrArrival: PropTypes.oneOf(['departure', 'arrival']).isRequired,
1234
+ onNowClick: PropTypes.func.isRequired,
1235
+ onDepartureClick: PropTypes.func.isRequired,
1236
+ onArrivalClick: PropTypes.func.isRequired,
1237
+ embedWhenClosed: PropTypes.node,
1238
+ embedWhenOpen: PropTypes.node,
1239
+ lang: PropTypes.string.isRequired,
1240
+ color: PropTypes.string,
1241
+ onModalSubmit: PropTypes.func.isRequired,
1242
+ fontWeights: PropTypes.shape({
1243
+ medium: PropTypes.number.isRequired
1244
+ }).isRequired,
1245
+ serviceTimeRange: PropTypes.number.isRequired,
1246
+ onOpen: PropTypes.func,
1247
+ onClose: PropTypes.func,
1248
+ openPicker: PropTypes.bool
1249
+ } : void 0;
1250
+ Datetimepicker.defaultProps = {
1251
+ timestamp: null,
1252
+ embedWhenClosed: null,
1253
+ embedWhenOpen: null,
1254
+ color: '#007ac9',
1255
+ timeZone: 'Europe/Helsinki',
1256
+ onOpen: null,
1257
+ onClose: null,
1258
+ openPicker: undefined
1259
+ };
1260
+
1261
+ /* eslint sort-keys: "error" */
1262
+ const translations = {
1263
+ de: {
1264
+ translation: {
1265
+ 'accessible-close': 'Schließe die Auswahl',
1266
+ 'accessible-closed': 'Auswahl geschlossen',
1267
+ 'accessible-open': 'Öffne die Auswahl',
1268
+ 'accessible-opened': 'Auswahl geöffnet',
1269
+ 'accessible-title': 'Datum und Uhrzeitauswahl',
1270
+ arrival: 'Ankunft',
1271
+ cancel: 'Abbrechen',
1272
+ 'choose-time': 'Zeit wählen',
1273
+ date: 'Datum',
1274
+ departure: 'Abfahrt',
1275
+ 'departure-now': 'Abfahrt jetzt',
1276
+ ready: 'Fertig',
1277
+ time: 'Zeit',
1278
+ today: 'Heute',
1279
+ tomorrow: 'Morgen'
1280
+ }
1281
+ },
1282
+ en: {
1283
+ translation: {
1284
+ 'accessible-close': 'Close the picker',
1285
+ 'accessible-closed': 'Timepicker closed',
1286
+ 'accessible-open': 'Open the picker',
1287
+ 'accessible-opened': 'Timepicker opened',
1288
+ 'accessible-title': 'Date and timepicker',
1289
+ 'accessible-update-instructions': 'Changing time parameters will trigger a new search.',
1290
+ arrival: 'Arrival',
1291
+ cancel: 'Cancel',
1292
+ 'choose-time': 'Choose time',
1293
+ date: 'Date',
1294
+ departure: 'Departure',
1295
+ 'departure-now': 'Leaving now',
1296
+ 'invalid-input': 'The time you entered is invalid',
1297
+ ready: 'Ready',
1298
+ time: 'Time',
1299
+ today: 'Today',
1300
+ tomorrow: 'Tomorrow'
1301
+ }
1302
+ },
1303
+ fi: {
1304
+ translation: {
1305
+ 'accessible-close': 'Sulje valitsin',
1306
+ 'accessible-closed': 'Valitsin suljettu',
1307
+ 'accessible-open': 'Avaa valitsin',
1308
+ 'accessible-opened': 'Valitsin avattu',
1309
+ 'accessible-title': 'Aika- ja päivämäärävalitsin',
1310
+ 'accessible-update-instructions': 'Aikavalintojen muuttaminen laukaisee uuden haun.',
1311
+ arrival: 'Perillä',
1312
+ cancel: 'Peruuta',
1313
+ 'choose-time': 'Valitse aika',
1314
+ date: 'Päivä',
1315
+ departure: 'Lähtöaika',
1316
+ 'departure-now': 'Lähtö nyt',
1317
+ 'invalid-input': 'Syöttämäsi aika on virheellinen',
1318
+ ready: 'Valmis',
1319
+ time: 'Aika',
1320
+ today: 'Tänään',
1321
+ tomorrow: 'Huomenna'
1322
+ }
1323
+ },
1324
+ pl: {
1325
+ translation: {
1326
+ 'accessible-close': 'Zamknij wybornik',
1327
+ 'accessible-closed': 'Zamknięto wybornik',
1328
+ 'accessible-open': 'Otwórz wybornik',
1329
+ 'accessible-opened': 'Otwarto wybornik',
1330
+ 'accessible-title': 'Wybór daty i godziny',
1331
+ 'accessible-update-instructions': 'Zmiana wybranej godziny wywoła nowe wyszukiwanie.',
1332
+ arrival: 'Przyjazd',
1333
+ cancel: 'Anuluj',
1334
+ 'choose-time': 'Wybierz godzinę',
1335
+ date: 'Data',
1336
+ departure: 'Wyjazd',
1337
+ 'departure-now': 'Wyjeżdżam teraz',
1338
+ 'invalid-input': 'Wprowadzona godzina jest nieprawidłowa',
1339
+ ready: 'Gotowe',
1340
+ time: 'Godzina',
1341
+ today: 'Dziś',
1342
+ tomorrow: 'Jutro'
1343
+ }
1344
+ },
1345
+ sv: {
1346
+ translation: {
1347
+ 'accessible-close': 'Stäng väljaren',
1348
+ 'accessible-closed': 'Väljaren stängde',
1349
+ 'accessible-open': 'Öppna väljaren',
1350
+ 'accessible-opened': 'Väljaren öppnade',
1351
+ 'accessible-title': 'Tid- och datumväljaren',
1352
+ 'accessible-update-instructions': 'Om du ändrar tiderna, startas en ny sökning automatiskt.',
1353
+ arrival: 'Anländer',
1354
+ cancel: 'Avbryt',
1355
+ 'choose-time': 'Välj tid',
1356
+ date: 'Datum',
1357
+ departure: 'Avgångstid',
1358
+ 'departure-now': 'Avgår nu',
1359
+ 'invalid-input': 'Tiden du angav är felaktig',
1360
+ ready: 'Färdig',
1361
+ time: 'Tid',
1362
+ today: 'I dag',
1363
+ tomorrow: 'I morgon'
1364
+ }
1365
+ }
1366
+ };
1367
+
1368
+ const i18n = createInstance({
1369
+ fallbackLng: 'fi',
1370
+ // debug: true,
1371
+ resources: translations,
1372
+ defaultNS: 'translation',
1373
+ react: {
1374
+ useSuspense: true
1375
+ }
1376
+ });
1377
+ i18n.init();
1378
+
1379
+ /**
1380
+ * This component renders an input to choose a date and time. Renders separate input fields for date and time selection. Values for timestamp and arriveBy correspond to Digitransit query params time and arriveBy. This component will display a native date input on mobile and a custom one for desktop. Mobile detection is done by parsing user agent.
1381
+ *
1382
+ * @alias Datetimepicker
1383
+ *
1384
+ * @param {Object} props
1385
+ *
1386
+ * @param {boolean} props.realtime Determine if selected time should be updated in realtime when 'now' is selected.
1387
+ * @param {Number} props.initialTimestamp Initial value for selected time. Unix timestamp in seconds. Updating this will change timepicker value but the correct value is kept in component state even if this is not updated.
1388
+ * @param {boolean} props.initialArriveBy Initial value for arriveBy. Determines if picker is in arrival mode (true) or departure mode (false). Correct value is kept in component state even if this is not updated. Changing this will also trigger change in the component.
1389
+ * @param {function} props.onTimeChange Called with (time, arriveBy) when time input changes. time is number timestamp in seconds, arriveBy is boolean
1390
+ * @param {function} props.onDateChange Called with (time, arriveBy) when date input changes. time is number timestamp in seconds, arriveBy is boolean
1391
+ * @param {function} props.onNowClick Called when "depart now" button is clicked. time is current input value in seconds
1392
+ * @param {function} props.onDepartureClick Called with (time) when "departure" button is clicked. time is current input value in seconds
1393
+ * @param {function} props.onArrivalClick Called with (time) when "arrival" button is clicked. time is current input value in seconds
1394
+ * @param {node} props.embedWhenClosed JSX element to render in the corner when input is closed
1395
+ * @param {node} props.embedWhenOpen JSX element to render when input is open
1396
+ * @param {string} props.lang Language selection. Default 'en'
1397
+ * @param {number} props.serviceTimeRange Determine number of days shown in timepicker. Optional. default is 30.
1398
+ * @param {function} props.onOpen Determine what to do when timepicker is open. Optional. no default implementation.
1399
+ * @param {function} props.onClose Determine what to do when timepicker is closed. Optional. no default implementation.
1400
+ * @param {function} props.openPicker Determine if timepicker should be open in intial render. Optional. Default is undefined.
1401
+ * @example
1402
+ * <Datetimepicker
1403
+ * realtime={true}
1404
+ * initialTimestamp={1590133823}
1405
+ * initialArriveBy={false}
1406
+ * onTimeChange={(time, arriveBy) => changeUrl(time, arriveBy)}
1407
+ * onDateChange={(time, arriveBy) => changeUrl(time, arriveBy)}
1408
+ * onNowClick={(time) => changeUrl(undefined, undefined)}
1409
+ * onDepartureClick={(time) => changeUrl(time, 'true')}
1410
+ * onArrivalClick={(time) => changeUrl(time, undefined)}
1411
+ * embedWhenClosed={<button />}
1412
+ * lang={'en'}
1413
+ * serviceTimeRange={15}
1414
+ * />
1415
+ */
1416
+ function DatetimepickerStateContainer({
1417
+ realtime,
1418
+ initialArriveBy,
1419
+ initialTimestamp,
1420
+ onDepartureClick,
1421
+ onArrivalClick,
1422
+ onTimeChange,
1423
+ onDateChange,
1424
+ onNowClick,
1425
+ embedWhenClosed,
1426
+ embedWhenOpen,
1427
+ lang,
1428
+ color,
1429
+ timeZone,
1430
+ fontWeights,
1431
+ serviceTimeRange,
1432
+ onOpen,
1433
+ onClose,
1434
+ openPicker
1435
+ }) {
1436
+ Settings.defaultLocale = lang;
1437
+ Settings.defaultZone = timeZone;
1438
+ const initialNow = realtime ? null : DateTime.now().toMillis();
1439
+ const [timestamp, changeTimestampState] = useState(initialTimestamp ? initialTimestamp * 1000 : initialNow);
1440
+ const [departureOrArrival, changeDepartureOrArrival] = useState(initialArriveBy === true ? 'arrival' : 'departure');
1441
+
1442
+ // update state if props change
1443
+ useEffect(() => {
1444
+ const bothNull = timestamp === null && initialTimestamp === undefined;
1445
+ const oneNull = timestamp === null || initialTimestamp === undefined;
1446
+ const sameTime = Math.round(timestamp / 1000) === initialTimestamp;
1447
+ const timestampChanged = !bothNull && (oneNull || !sameTime);
1448
+ if (timestampChanged) {
1449
+ if (initialTimestamp === undefined) {
1450
+ changeTimestampState(null);
1451
+ } else {
1452
+ changeTimestampState(initialTimestamp * 1000);
1453
+ }
1454
+ }
1455
+ if (!initialArriveBy === (departureOrArrival === 'arrival')) {
1456
+ changeDepartureOrArrival(initialArriveBy === true ? 'arrival' : 'departure');
1457
+ }
1458
+ }, [initialTimestamp, initialArriveBy]);
1459
+ const timeChanged = debounce(newTime => {
1460
+ if (newTime === null) {
1461
+ changeTimestampState(DateTime.now().toMillis());
1462
+ onTimeChange(DateTime.now().toUnixInteger(), departureOrArrival === 'arrival');
1463
+ return;
1464
+ }
1465
+ changeTimestampState(newTime);
1466
+ onTimeChange(Math.round(newTime / 1000), departureOrArrival === 'arrival');
1467
+ }, 10);
1468
+ const dateChanged = debounce(newDate => {
1469
+ if (newDate === null) {
1470
+ changeTimestampState(DateTime.now().toMillis());
1471
+ onDateChange(DateTime.now().toUnixInteger(), departureOrArrival === 'arrival');
1472
+ return;
1473
+ }
1474
+ changeTimestampState(newDate);
1475
+ onDateChange(Math.round(newDate / 1000), departureOrArrival === 'arrival');
1476
+ }, 10);
1477
+ const nowClicked = () => {
1478
+ changeDepartureOrArrival('departure');
1479
+ const newTimestamp = realtime ? null : DateTime.now().toMillis();
1480
+ changeTimestampState(newTimestamp);
1481
+ onNowClick(Math.round(newTimestamp / 1000));
1482
+ };
1483
+ const departureClicked = () => {
1484
+ let changed = false;
1485
+ let newTime = timestamp;
1486
+ if (timestamp === null) {
1487
+ const now = DateTime.now().toMillis();
1488
+ changeTimestampState(now);
1489
+ newTime = now;
1490
+ changed = true;
1491
+ }
1492
+ if (departureOrArrival !== 'departure') {
1493
+ changeDepartureOrArrival('departure');
1494
+ changed = true;
1495
+ }
1496
+ if (changed) {
1497
+ onDepartureClick(Math.round(newTime / 1000));
1498
+ }
1499
+ };
1500
+ const arrivalClicked = () => {
1501
+ let changed = false;
1502
+ let newTime = timestamp;
1503
+ if (timestamp === null) {
1504
+ const now = DateTime.now().toMillis();
1505
+ changeTimestampState(now);
1506
+ newTime = now;
1507
+ changed = true;
1508
+ }
1509
+ if (departureOrArrival !== 'arrival') {
1510
+ changeDepartureOrArrival('arrival');
1511
+ changed = true;
1512
+ }
1513
+ if (changed) {
1514
+ onArrivalClick(Math.round(newTime / 1000));
1515
+ }
1516
+ };
1517
+ const onModalSubmit = (time, mode) => {
1518
+ changeTimestampState(time);
1519
+ changeDepartureOrArrival(mode);
1520
+ onTimeChange(Math.round(time / 1000), mode === 'arrival', true);
1521
+ };
1522
+ return /*#__PURE__*/React.createElement(I18nextProvider, {
1523
+ i18n: i18n
1524
+ }, /*#__PURE__*/React.createElement(Datetimepicker, {
1525
+ timestamp: timestamp,
1526
+ onTimeChange: timeChanged,
1527
+ onDateChange: dateChanged,
1528
+ departureOrArrival: departureOrArrival,
1529
+ onNowClick: nowClicked,
1530
+ onDepartureClick: departureClicked,
1531
+ onArrivalClick: arrivalClicked,
1532
+ embedWhenClosed: embedWhenClosed,
1533
+ embedWhenOpen: embedWhenOpen,
1534
+ lang: lang,
1535
+ color: color,
1536
+ timeZone: timeZone,
1537
+ onModalSubmit: onModalSubmit,
1538
+ fontWeights: fontWeights,
1539
+ serviceTimeRange: serviceTimeRange,
1540
+ onOpen: onOpen,
1541
+ onClose: onClose,
1542
+ openPicker: openPicker
1543
+ }));
1544
+ }
1545
+ process.env.NODE_ENV !== "production" ? DatetimepickerStateContainer.propTypes = {
1546
+ realtime: PropTypes.bool,
1547
+ initialTimestamp: PropTypes.number,
1548
+ initialArriveBy: PropTypes.bool,
1549
+ onDepartureClick: PropTypes.func.isRequired,
1550
+ onArrivalClick: PropTypes.func.isRequired,
1551
+ onTimeChange: PropTypes.func.isRequired,
1552
+ onDateChange: PropTypes.func.isRequired,
1553
+ onNowClick: PropTypes.func.isRequired,
1554
+ embedWhenClosed: PropTypes.node,
1555
+ embedWhenOpen: PropTypes.node,
1556
+ lang: PropTypes.string,
1557
+ color: PropTypes.string,
1558
+ timeZone: PropTypes.string,
1559
+ fontWeights: PropTypes.shape({
1560
+ medium: PropTypes.number
1561
+ }),
1562
+ serviceTimeRange: PropTypes.number,
1563
+ onOpen: PropTypes.func,
1564
+ onClose: PropTypes.func,
1565
+ openPicker: PropTypes.bool
1566
+ } : void 0;
1567
+ DatetimepickerStateContainer.defaultProps = {
1568
+ realtime: false,
1569
+ initialArriveBy: undefined,
1570
+ initialTimestamp: undefined,
1571
+ embedWhenClosed: null,
1572
+ embedWhenOpen: null,
1573
+ lang: 'en',
1574
+ color: '#007ac9',
1575
+ timeZone: 'Europe/Helsinki',
1576
+ fontWeights: {
1577
+ medium: 500
1578
+ },
1579
+ onOpen: null,
1580
+ onClose: null,
1581
+ openPicker: undefined,
1582
+ serviceTimeRange: 30
1583
+ };
1584
+
1585
+ export { DatetimepickerStateContainer as default };
1586
+ //# sourceMappingURL=index.js.map