dce-reactkit 4.0.0 → 4.0.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.
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * A very simple, lightweight date chooser
3
3
  * @author Gabe Abrams
4
+ * @author Gardenia Liu
4
5
  */
5
6
  import React from 'react';
6
7
  type Props = {
@@ -17,7 +18,8 @@ type Props = {
17
18
  */
18
19
  onChange: (month: number, day: number, year: number) => void;
19
20
  numMonthsToShow?: number;
20
- chooseFromPast?: boolean;
21
+ dontAllowPast?: boolean;
22
+ dontAllowFuture?: boolean;
21
23
  };
22
24
  declare const SimpleDateChooser: React.FC<Props>;
23
25
  export default SimpleDateChooser;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A very simple, lightweight time chooser
3
+ * @author Gardenia Liu
4
+ */
5
+ import React from 'react';
6
+ type Props = {
7
+ ariaLabel: string;
8
+ name: string;
9
+ hour: number;
10
+ minute: number;
11
+ /**
12
+ * Handler for when time changes
13
+ * @param hour new 24hr hour number
14
+ * @param minute new minute number
15
+ */
16
+ onChange: (hour: number, minute: number) => void;
17
+ intervalMin?: number;
18
+ };
19
+ declare const SimpleTimeChooser: React.FC<Props>;
20
+ export default SimpleTimeChooser;
@@ -7,6 +7,7 @@ import RadioButton from './components/RadioButton';
7
7
  import CheckboxButton from './components/CheckboxButton';
8
8
  import ButtonInputGroup from './components/ButtonInputGroup';
9
9
  import SimpleDateChooser from './components/SimpleDateChooser';
10
+ import SimpleTimeChooser from './components/SimpleTimeChooser';
10
11
  import Drawer from './components/Drawer';
11
12
  import PopSuccessMark from './components/PopSuccessMark';
12
13
  import PopFailureMark from './components/PopFailureMark';
@@ -100,4 +101,4 @@ import PickableItem from './components/ItemPicker/types/PickableItem';
100
101
  import DBEntry from './components/DBEntryManagerPanel/types/DBEntry';
101
102
  import DBEntryField from './components/DBEntryManagerPanel/types/DBEntryField';
102
103
  import DBEntryFieldType from './components/DBEntryManagerPanel/types/DBEntryFieldType';
103
- export { AppWrapper, LoadingSpinner, ErrorBox, Modal, TabBox, RadioButton, CheckboxButton, ButtonInputGroup, SimpleDateChooser, Drawer, PopSuccessMark, PopFailureMark, PopPendingMark, CopiableBox, ItemPicker, LogReviewer, IntelliTable, CSVDownloadButton, DBEntryManagerPanel, Tooltip, ToggleSwitch, AutoscrollToBottomContainer, MultiSwitch, Dropdown, alert, prompt, confirm, showFatalError, ErrorWithCode, MINUTE_IN_MS, HOUR_IN_MS, DAY_IN_MS, LOG_REVIEW_ROUTE_PATH_PREFIX, LOG_ROUTE_PATH, LOG_REVIEW_STATUS_ROUTE, DynamicWord, abbreviate, avg, ceilToNumDecimals, floorToNumDecimals, forceNumIntoBounds, padDecimalZeros, padZerosLeft, roundToNumDecimals, sum, waitMs, getOrdinal, getTimeInfoInET, stubServerEndpoint, startMinWait, getHumanReadableDate, getPartOfDay, stringsToHumanReadableList, onlyKeepLetters, parallelLimit, getMonthName, genCSV, canReviewLogs, isMobileOrTablet, extractProp, compareArraysByProp, genCommaList, validateEmail, validatePhoneNumber, validateString, getLocalTimeInfo, idify, makeLinksClickable, prefixWithAOrAn, everyAsync, filterAsync, forEachAsync, mapAsync, someAsync, capitalize, shuffleArray, getWordCount, initClient, visitServerEndpoint, logClientEvent, addFatalErrorHandler, leaveToURL, combineClassNames, useForceRender, setClientEventMetadataPopulator, ParamType, ModalButtonType, ModalSize, ModalType, ReactKitErrorCode, Variant, DayOfWeek, Log, LogType, LogSource, LogAction, LogBuiltInMetadata, LogMetadataType, LogFunction, LogTypeSpecificInfo, LogMainInfo, LogSourceSpecificInfo, LogLevel, IntelliTableColumn, DropdownItemType, PickableItem, DBEntry, DBEntryField, DBEntryFieldType, };
104
+ export { AppWrapper, LoadingSpinner, ErrorBox, Modal, TabBox, RadioButton, CheckboxButton, ButtonInputGroup, SimpleDateChooser, SimpleTimeChooser, Drawer, PopSuccessMark, PopFailureMark, PopPendingMark, CopiableBox, ItemPicker, LogReviewer, IntelliTable, CSVDownloadButton, DBEntryManagerPanel, Tooltip, ToggleSwitch, AutoscrollToBottomContainer, MultiSwitch, Dropdown, alert, prompt, confirm, showFatalError, ErrorWithCode, MINUTE_IN_MS, HOUR_IN_MS, DAY_IN_MS, LOG_REVIEW_ROUTE_PATH_PREFIX, LOG_ROUTE_PATH, LOG_REVIEW_STATUS_ROUTE, DynamicWord, abbreviate, avg, ceilToNumDecimals, floorToNumDecimals, forceNumIntoBounds, padDecimalZeros, padZerosLeft, roundToNumDecimals, sum, waitMs, getOrdinal, getTimeInfoInET, stubServerEndpoint, startMinWait, getHumanReadableDate, getPartOfDay, stringsToHumanReadableList, onlyKeepLetters, parallelLimit, getMonthName, genCSV, canReviewLogs, isMobileOrTablet, extractProp, compareArraysByProp, genCommaList, validateEmail, validatePhoneNumber, validateString, getLocalTimeInfo, idify, makeLinksClickable, prefixWithAOrAn, everyAsync, filterAsync, forEachAsync, mapAsync, someAsync, capitalize, shuffleArray, getWordCount, initClient, visitServerEndpoint, logClientEvent, addFatalErrorHandler, leaveToURL, combineClassNames, useForceRender, setClientEventMetadataPopulator, ParamType, ModalButtonType, ModalSize, ModalType, ReactKitErrorCode, Variant, DayOfWeek, Log, LogType, LogSource, LogAction, LogBuiltInMetadata, LogMetadataType, LogFunction, LogTypeSpecificInfo, LogMainInfo, LogSourceSpecificInfo, LogLevel, IntelliTableColumn, DropdownItemType, PickableItem, DBEntry, DBEntryField, DBEntryFieldType, };
@@ -6,6 +6,8 @@ declare enum ReactKitErrorCode {
6
6
  NoResponse = "DRK1",
7
7
  NoCode = "DRK2",
8
8
  SessionExpired = "DRK3",
9
- NoCACCLSendRequestFunction = "DRK7"
9
+ NoCACCLSendRequestFunction = "DRK7",
10
+ SimpleDateChooserInvalidDateRange = "DRK35",
11
+ SimpleDateChooserInvalidNumMonths = "DRK36"
10
12
  }
11
13
  export default ReactKitErrorCode;
package/dist/index.d.ts CHANGED
@@ -23,7 +23,7 @@ declare enum Variant {
23
23
  * @author Gabe Abrams
24
24
  */
25
25
 
26
- type Props$l = {
26
+ type Props$m = {
27
27
  children: React$1.ReactNode;
28
28
  };
29
29
  /**
@@ -106,7 +106,7 @@ declare const showFatalError: (error: any, errorTitle?: string) => Promise<void>
106
106
  * @author Gabe Abrams
107
107
  */
108
108
  declare const addFatalErrorHandler: (handler: () => void) => void;
109
- declare const AppWrapper: React$1.FC<Props$l>;
109
+ declare const AppWrapper: React$1.FC<Props$m>;
110
110
 
111
111
  /**
112
112
  * Loading spinner/indicator
@@ -120,14 +120,14 @@ declare const LoadingSpinner: () => React$1.JSX.Element;
120
120
  * @author Gabe Abrams
121
121
  */
122
122
 
123
- type Props$k = {
123
+ type Props$l = {
124
124
  error: any;
125
125
  title?: string;
126
126
  onClose?: () => void;
127
127
  variant?: Variant;
128
128
  icon?: IconProp;
129
129
  };
130
- declare const ErrorBox: React$1.FC<Props$k>;
130
+ declare const ErrorBox: React$1.FC<Props$l>;
131
131
 
132
132
  /**
133
133
  * Types of buttons in the modal
@@ -226,21 +226,21 @@ declare const Modal: React$1.FC<ModalProps>;
226
226
  * @author Gabe Abrams
227
227
  */
228
228
 
229
- type Props$j = {
229
+ type Props$k = {
230
230
  title: React$1.ReactNode;
231
231
  children: React$1.ReactNode;
232
232
  topRightChildren?: React$1.ReactNode;
233
233
  noBottomMargin?: boolean;
234
234
  noBottomPadding?: boolean;
235
235
  };
236
- declare const TabBox: React$1.FC<Props$j>;
236
+ declare const TabBox: React$1.FC<Props$k>;
237
237
 
238
238
  /**
239
239
  * A radio selection button
240
240
  * @author Gabe Abrams
241
241
  */
242
242
 
243
- type Props$i = {
243
+ type Props$j = {
244
244
  text: React$1.ReactNode;
245
245
  onSelected: () => void;
246
246
  ariaLabel: string;
@@ -254,14 +254,14 @@ type Props$i = {
254
254
  small?: boolean;
255
255
  useComplexFormatting?: boolean;
256
256
  };
257
- declare const RadioButton: React$1.FC<Props$i>;
257
+ declare const RadioButton: React$1.FC<Props$j>;
258
258
 
259
259
  /**
260
260
  * A checkbox button
261
261
  * @author Gabe Abrams
262
262
  */
263
263
 
264
- type Props$h = {
264
+ type Props$i = {
265
265
  text: React$1.ReactNode;
266
266
  onChanged: (checked: boolean) => void;
267
267
  ariaLabel: string;
@@ -276,14 +276,14 @@ type Props$h = {
276
276
  dashed?: boolean;
277
277
  useComplexFormatting?: boolean;
278
278
  };
279
- declare const CheckboxButton: React$1.FC<Props$h>;
279
+ declare const CheckboxButton: React$1.FC<Props$i>;
280
280
 
281
281
  /**
282
282
  * Input group with a title and space for buttons
283
283
  * @author Gabe Abrams
284
284
  */
285
285
 
286
- type Props$g = {
286
+ type Props$h = {
287
287
  label: React$1.ReactNode;
288
288
  minLabelWidth?: string;
289
289
  children: React$1.ReactNode;
@@ -292,14 +292,15 @@ type Props$g = {
292
292
  isAdminFeature?: boolean;
293
293
  noMarginOnBottom?: boolean;
294
294
  };
295
- declare const ButtonInputGroup: React$1.FC<Props$g>;
295
+ declare const ButtonInputGroup: React$1.FC<Props$h>;
296
296
 
297
297
  /**
298
298
  * A very simple, lightweight date chooser
299
299
  * @author Gabe Abrams
300
+ * @author Gardenia Liu
300
301
  */
301
302
 
302
- type Props$f = {
303
+ type Props$g = {
303
304
  ariaLabel: string;
304
305
  name: string;
305
306
  month: number;
@@ -313,9 +314,30 @@ type Props$f = {
313
314
  */
314
315
  onChange: (month: number, day: number, year: number) => void;
315
316
  numMonthsToShow?: number;
316
- chooseFromPast?: boolean;
317
+ dontAllowPast?: boolean;
318
+ dontAllowFuture?: boolean;
319
+ };
320
+ declare const SimpleDateChooser: React$1.FC<Props$g>;
321
+
322
+ /**
323
+ * A very simple, lightweight time chooser
324
+ * @author Gardenia Liu
325
+ */
326
+
327
+ type Props$f = {
328
+ ariaLabel: string;
329
+ name: string;
330
+ hour: number;
331
+ minute: number;
332
+ /**
333
+ * Handler for when time changes
334
+ * @param hour new 24hr hour number
335
+ * @param minute new minute number
336
+ */
337
+ onChange: (hour: number, minute: number) => void;
338
+ intervalMin?: number;
317
339
  };
318
- declare const SimpleDateChooser: React$1.FC<Props$f>;
340
+ declare const SimpleTimeChooser: React$1.FC<Props$f>;
319
341
 
320
342
  /**
321
343
  * Drawer container
@@ -1550,7 +1572,9 @@ declare enum ReactKitErrorCode {
1550
1572
  NoResponse = "DRK1",
1551
1573
  NoCode = "DRK2",
1552
1574
  SessionExpired = "DRK3",
1553
- NoCACCLSendRequestFunction = "DRK7"
1575
+ NoCACCLSendRequestFunction = "DRK7",
1576
+ SimpleDateChooserInvalidDateRange = "DRK35",
1577
+ SimpleDateChooserInvalidNumMonths = "DRK36"
1554
1578
  }
1555
1579
 
1556
- export { AppWrapper, AutoscrollToBottomContainer, ButtonInputGroup, CSVDownloadButton, CheckboxButton, CopiableBox, DAY_IN_MS, DBEntry, DBEntryField, DBEntryFieldType, DBEntryManagerPanel, DayOfWeek, Drawer, Dropdown, DropdownItemType, DynamicWord, ErrorBox, ErrorWithCode, HOUR_IN_MS, IntelliTable, IntelliTableColumn, ItemPicker, LOG_REVIEW_ROUTE_PATH_PREFIX, LOG_REVIEW_STATUS_ROUTE, LOG_ROUTE_PATH, LoadingSpinner, Log, LogAction, LogBuiltInMetadata, LogFunction, LogLevel, LogMainInfo, LogMetadataType, LogReviewer, LogSource, LogSourceSpecificInfo, LogType, LogTypeSpecificInfo, MINUTE_IN_MS, Modal, ModalButtonType, ModalSize, ModalType, MultiSwitch, ParamType, PickableItem, PopFailureMark, PopPendingMark, PopSuccessMark, RadioButton, ReactKitErrorCode, SimpleDateChooser, TabBox, ToggleSwitch, Tooltip, Variant, abbreviate, addFatalErrorHandler, alert, avg, canReviewLogs, capitalize, ceilToNumDecimals, combineClassNames, compareArraysByProp, confirm, everyAsync, extractProp, filterAsync, floorToNumDecimals, forEachAsync, forceNumIntoBounds, genCSV, genCommaList, getHumanReadableDate, getLocalTimeInfo, getMonthName, getOrdinal, getPartOfDay, getTimeInfoInET, getWordCount, idify, initClient, isMobileOrTablet, leaveToURL, logClientEvent, makeLinksClickable, mapAsync, onlyKeepLetters, padDecimalZeros, padZerosLeft, parallelLimit, prefixWithAOrAn, prompt, roundToNumDecimals, setClientEventMetadataPopulator, showFatalError, shuffleArray, someAsync, startMinWait, stringsToHumanReadableList, stubServerEndpoint, sum, useForceRender, validateEmail, validatePhoneNumber, validateString, visitServerEndpoint, waitMs };
1580
+ export { AppWrapper, AutoscrollToBottomContainer, ButtonInputGroup, CSVDownloadButton, CheckboxButton, CopiableBox, DAY_IN_MS, DBEntry, DBEntryField, DBEntryFieldType, DBEntryManagerPanel, DayOfWeek, Drawer, Dropdown, DropdownItemType, DynamicWord, ErrorBox, ErrorWithCode, HOUR_IN_MS, IntelliTable, IntelliTableColumn, ItemPicker, LOG_REVIEW_ROUTE_PATH_PREFIX, LOG_REVIEW_STATUS_ROUTE, LOG_ROUTE_PATH, LoadingSpinner, Log, LogAction, LogBuiltInMetadata, LogFunction, LogLevel, LogMainInfo, LogMetadataType, LogReviewer, LogSource, LogSourceSpecificInfo, LogType, LogTypeSpecificInfo, MINUTE_IN_MS, Modal, ModalButtonType, ModalSize, ModalType, MultiSwitch, ParamType, PickableItem, PopFailureMark, PopPendingMark, PopSuccessMark, RadioButton, ReactKitErrorCode, SimpleDateChooser, SimpleTimeChooser, TabBox, ToggleSwitch, Tooltip, Variant, abbreviate, addFatalErrorHandler, alert, avg, canReviewLogs, capitalize, ceilToNumDecimals, combineClassNames, compareArraysByProp, confirm, everyAsync, extractProp, filterAsync, floorToNumDecimals, forEachAsync, forceNumIntoBounds, genCSV, genCommaList, getHumanReadableDate, getLocalTimeInfo, getMonthName, getOrdinal, getPartOfDay, getTimeInfoInET, getWordCount, idify, initClient, isMobileOrTablet, leaveToURL, logClientEvent, makeLinksClickable, mapAsync, onlyKeepLetters, padDecimalZeros, padZerosLeft, parallelLimit, prefixWithAOrAn, prompt, roundToNumDecimals, setClientEventMetadataPopulator, showFatalError, shuffleArray, someAsync, startMinWait, stringsToHumanReadableList, stubServerEndpoint, sum, useForceRender, validateEmail, validatePhoneNumber, validateString, visitServerEndpoint, waitMs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dce-reactkit",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "Shared components for Harvard DCE apps",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -1182,7 +1182,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1182
1182
  year={dateFilterState.startDate.year}
1183
1183
  month={dateFilterState.startDate.month}
1184
1184
  day={dateFilterState.startDate.day}
1185
- chooseFromPast
1185
+ dontAllowFuture
1186
1186
  numMonthsToShow={36}
1187
1187
  onChange={(month, day, year) => {
1188
1188
  dateFilterState.startDate = { month, day, year };
@@ -1198,7 +1198,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1198
1198
  year={dateFilterState.endDate.year}
1199
1199
  month={dateFilterState.endDate.month}
1200
1200
  day={dateFilterState.endDate.day}
1201
- chooseFromPast
1201
+ dontAllowFuture
1202
1202
  numMonthsToShow={12}
1203
1203
  onChange={(month, day, year) => {
1204
1204
  if (
@@ -1,16 +1,23 @@
1
1
  /**
2
2
  * A very simple, lightweight date chooser
3
3
  * @author Gabe Abrams
4
+ * @author Gardenia Liu
4
5
  */
5
6
 
6
7
  // Import React
7
8
  import React from 'react';
8
- import getMonthName from '../helpers/getMonthName';
9
9
 
10
10
  // Import helpers
11
11
  import getOrdinal from '../helpers/getOrdinal';
12
+ import getMonthName from '../helpers/getMonthName';
12
13
  import getTimeInfoInET from '../helpers/getTimeInfoInET';
13
14
 
15
+ // Import classes
16
+ import ErrorWithCode from '../errors/ErrorWithCode';
17
+
18
+ // Import types
19
+ import ReactKitErrorCode from '../types/ReactKitErrorCode';
20
+
14
21
  /*------------------------------------------------------------------------*/
15
22
  /* -------------------------------- Types ------------------------------- */
16
23
  /*------------------------------------------------------------------------*/
@@ -33,12 +40,14 @@ type Props = {
33
40
  * @param year new full year number
34
41
  */
35
42
  onChange: (month: number, day: number, year: number) => void,
36
- // Number of months to allow the user to choose from
37
- // (max is 12, default is 6)
43
+ // Number of months in either the past or future to allow the user to choose from
44
+ // If we aren't allowing the past or the future, we will throw an error
45
+ // (max is 12, default is 6 in the past and 6 in the future)
38
46
  numMonthsToShow?: number,
39
- // If true, instead of showing numMonthsToShow months into the future,
40
- // show numMonthsToShow months into the past
41
- chooseFromPast?: boolean,
47
+ // If true, the user isn't allowed to select dates in the past
48
+ dontAllowPast?: boolean,
49
+ // If true, the user isn't allowed to select dates in the future
50
+ dontAllowFuture?: boolean,
42
51
  };
43
52
 
44
53
  /*------------------------------------------------------------------------*/
@@ -56,8 +65,9 @@ const SimpleDateChooser: React.FC<Props> = (props) => {
56
65
  ariaLabel,
57
66
  name,
58
67
  onChange,
59
- chooseFromPast,
60
68
  numMonthsToShow = 6,
69
+ dontAllowFuture,
70
+ dontAllowPast,
61
71
  } = props;
62
72
 
63
73
  /*------------------------------------------------------------------------*/
@@ -76,16 +86,41 @@ const SimpleDateChooser: React.FC<Props> = (props) => {
76
86
  days: number[],
77
87
  year: number,
78
88
  }[] = [];
89
+
79
90
  let startYear = today.year;
80
91
  let startMonth = today.month;
81
- if (chooseFromPast) {
92
+
93
+ // Don't allow past or future dates
94
+ if (dontAllowPast && dontAllowFuture) {
95
+ throw new ErrorWithCode(
96
+ 'No past or future dates allowed',
97
+ ReactKitErrorCode.SimpleDateChooserInvalidDateRange,
98
+ );
99
+ }
100
+
101
+ // Require numMonthsToShow to be positive
102
+ if (numMonthsToShow <= 0) {
103
+ throw new ErrorWithCode(
104
+ 'numMonthsToShow must be positive',
105
+ ReactKitErrorCode.SimpleDateChooserInvalidNumMonths,
106
+ );
107
+ }
108
+
109
+ // Recalculate startMonth and startYear when allowing past dates
110
+ if (!dontAllowPast) {
82
111
  startMonth -= Math.max(0, numMonthsToShow - 1);
83
112
  while (startMonth <= 0) {
84
113
  startMonth += 12;
85
114
  startYear -= 1;
86
115
  }
87
116
  }
88
- for (let i = 0; i < numMonthsToShow; i++) {
117
+ // Calculate total number of months to show
118
+ let totalMonthsToShow = numMonthsToShow;
119
+ if (!dontAllowPast && !dontAllowFuture) {
120
+ totalMonthsToShow = totalMonthsToShow * 2 - 1;
121
+ }
122
+
123
+ for (let i = 0; i < totalMonthsToShow; i++) {
89
124
  // Get month and year info
90
125
  const unmoddedMonth = (startMonth + i);
91
126
  let month = unmoddedMonth;
@@ -105,24 +140,25 @@ const SimpleDateChooser: React.FC<Props> = (props) => {
105
140
  // Figure out which days are allowed
106
141
  const days = [];
107
142
  const numDaysInMonth = (new Date(year, month, 0)).getDate();
108
- if (chooseFromPast) {
109
- // Past selection
110
- const numDaysToAdd = (
111
- (month === today.month)
112
- ? today.day // Current month, only add up to today
113
- : numDaysInMonth // Past month, add all days
114
- );
115
- for (let day = 1; day <= numDaysToAdd; day++) {
116
- days.push(day);
143
+
144
+ // Current month
145
+ if (month === today.month && year === today.year) {
146
+ // Past selection: add all previous days of the month
147
+ if (!dontAllowPast) {
148
+ for (let day = 1; day < today.day; day++) {
149
+ days.push(day);
150
+ }
117
151
  }
118
- } else {
152
+ days.push(today.day); // Add current day
119
153
  // Future selection: add all remaining days of the month
120
- const firstDay = (
121
- month === today.month
122
- ? today.day // Current month: start at current date
123
- : 1 // Future month: start at beginning of month
124
- );
125
- for (let day = firstDay; day <= numDaysInMonth; day++) {
154
+ if (!dontAllowFuture) {
155
+ for (let day = today.day + 1; day <= numDaysInMonth; day++) {
156
+ days.push(day);
157
+ }
158
+ }
159
+ } else { // Past or future month
160
+ // Include all days in the month
161
+ for (let day = 1; day <= numDaysInMonth; day++) {
126
162
  days.push(day);
127
163
  }
128
164
  }
@@ -158,8 +194,8 @@ const SimpleDateChooser: React.FC<Props> = (props) => {
158
194
  </option>,
159
195
  );
160
196
 
197
+ // This is the currently selected month
161
198
  if (month === choice.month) {
162
- // This is the currently selected month
163
199
  // Create day options
164
200
  choice.days.forEach((dayChoice) => {
165
201
  const ordinal = getOrdinal(dayChoice);
@@ -0,0 +1,196 @@
1
+ /**
2
+ * A very simple, lightweight time chooser
3
+ * @author Gardenia Liu
4
+ */
5
+
6
+ // Import React
7
+ import React from 'react';
8
+
9
+ // Import helpers
10
+ import padZerosLeft from '../helpers/padZerosLeft';
11
+
12
+ /*------------------------------------------------------------------------*/
13
+ /* -------------------------------- Types ------------------------------- */
14
+ /*------------------------------------------------------------------------*/
15
+
16
+ type Props = {
17
+ // Aria label
18
+ ariaLabel: string,
19
+ // Name of the chooser (machine-readable, hyphenated)
20
+ name: string,
21
+ // Currently selected hour of the day (24hr)
22
+ hour: number,
23
+ // Currently selected minute within the hour
24
+ minute: number,
25
+ /**
26
+ * Handler for when time changes
27
+ * @param hour new 24hr hour number
28
+ * @param minute new minute number
29
+ */
30
+ onChange: (hour: number, minute: number) => void,
31
+ // Interval in minutes between each choice
32
+ // Allowed options: 15, 30, 60, defaults to 15
33
+ // If an unsupported interval is passed in, it will default to 15
34
+ intervalMin?: number,
35
+ };
36
+
37
+ /*------------------------------------------------------------------------*/
38
+ /* ------------------------------ Constants ----------------------------- */
39
+ /*------------------------------------------------------------------------*/
40
+
41
+ // Allowed intervals between options
42
+ const ALLOWED_INTERVALS = [15, 30, 60]; // min
43
+
44
+ // Default interval to use if an unsupported interval is passed in
45
+ const DEFAULT_INTERVAL = ALLOWED_INTERVALS[0]; // min
46
+
47
+ /*------------------------------------------------------------------------*/
48
+ /* ------------------------------ Component ----------------------------- */
49
+ /*------------------------------------------------------------------------*/
50
+
51
+ const SimpleTimeChooser: React.FC<Props> = (props) => {
52
+ /*------------------------------------------------------------------------*/
53
+ /* -------------------------------- Setup ------------------------------- */
54
+ /*------------------------------------------------------------------------*/
55
+
56
+ /* -------------- Props ------------- */
57
+
58
+ const {
59
+ ariaLabel,
60
+ name,
61
+ hour,
62
+ minute,
63
+ onChange,
64
+ } = props;
65
+ let {
66
+ intervalMin = DEFAULT_INTERVAL,
67
+ } = props;
68
+
69
+ // Use default interval if not supported
70
+ if (!ALLOWED_INTERVALS.includes(intervalMin)) {
71
+ intervalMin = DEFAULT_INTERVAL;
72
+ }
73
+
74
+ /*------------------------------------------------------------------------*/
75
+ /* ------------------------- Component Functions ------------------------ */
76
+ /*------------------------------------------------------------------------*/
77
+
78
+ /**
79
+ * Convert number of minutes since midnight into 24hour and minute format
80
+ * @author Gabe Abrams
81
+ * @param minSinceMidnight total minutes since midnight
82
+ * @returns hours (24) and minutes
83
+ */
84
+ const convertMinSinceMidnightToHoursAndMin = (minSinceMidnight: number): {
85
+ hours: number,
86
+ minutes: number,
87
+ } => {
88
+ return {
89
+ hours: Math.floor(minSinceMidnight / 60),
90
+ minutes: minSinceMidnight % 60,
91
+ };
92
+ };
93
+
94
+ /**
95
+ * Convert time in minutes into HH:MM format
96
+ * @author Gardenia Liu
97
+ * @param totalMinutes total minutes since midnight
98
+ * @returns formatted time string
99
+ */
100
+ const formatTime = (totalMinutes: number): string => {
101
+ // Handle special cases
102
+ if (totalMinutes === 0) {
103
+ return '12:00 Midnight';
104
+ }
105
+ if (totalMinutes === 12 * 60) {
106
+ return '12:00 Noon';
107
+ }
108
+
109
+ // All normal cases:
110
+ const timeInfo = convertMinSinceMidnightToHoursAndMin(totalMinutes);
111
+ let { hours } = timeInfo;
112
+ const { minutes } = timeInfo;
113
+
114
+ // Process 24hr -> 12hr
115
+ const isAM = (hours < 12);
116
+ if (hours === 0) {
117
+ hours = 12;
118
+ } else if (hours > 12) {
119
+ hours %= 12;
120
+ }
121
+
122
+ // Pad with zeros
123
+ const paddedMinutes = padZerosLeft(minutes, 2);
124
+
125
+ // Assemble time string
126
+ return `${hours}:${paddedMinutes} ${isAM ? 'AM' : 'PM'}`;
127
+ };
128
+
129
+ /*------------------------------------------------------------------------*/
130
+ /* ------------------------------- Render ------------------------------- */
131
+ /*------------------------------------------------------------------------*/
132
+
133
+ /*----------------------------------------*/
134
+ /* --------------- Main UI -------------- */
135
+ /*----------------------------------------*/
136
+
137
+ // Generate list of time options
138
+ const times: string[] = [];
139
+ for (let time = 0; time < 24 * 60; time += intervalMin) {
140
+ times.push(formatTime(time));
141
+ }
142
+
143
+ // Currently selected time in minutes since midnight
144
+ const selectedTimeMin = hour * 60 + minute;
145
+
146
+ // Create choice options
147
+ const timeOptions: React.ReactNode[] = times.map((timeString, timeIndex) => {
148
+ const numMinutesForChoice = timeIndex * intervalMin;
149
+
150
+ // Render the option
151
+ return (
152
+ <option
153
+ key={numMinutesForChoice}
154
+ value={numMinutesForChoice}
155
+ aria-label={`choose ${timeString}`}
156
+ >
157
+ {timeString}
158
+ </option>
159
+ );
160
+ });
161
+
162
+ return (
163
+ <div
164
+ className="SimpleTimeChooser-container"
165
+ aria-label={`time chooser with selected time: ${formatTime(selectedTimeMin)}`}
166
+ >
167
+ {/* Time Chooser */}
168
+ <select
169
+ aria-label={`time for ${ariaLabel}`}
170
+ className="custom-select d-inline-block"
171
+ style={{ width: 'auto' }}
172
+ id={`SimpleTimeChooser-${name}-time`}
173
+ value={selectedTimeMin}
174
+ onChange={(e) => {
175
+ // Parse selector value (string)
176
+ const newTime = Number.parseInt(e.target.value, 10);
177
+
178
+ // Convert minutes since midnight to hour and minute
179
+ const timeInfo = convertMinSinceMidnightToHoursAndMin(newTime);
180
+
181
+ // Notify parent
182
+ onChange(timeInfo.hours, timeInfo.minutes);
183
+ }}
184
+ >
185
+ {timeOptions}
186
+ </select>
187
+ </div>
188
+ );
189
+ };
190
+
191
+ /*------------------------------------------------------------------------*/
192
+ /* ------------------------------- Wrap Up ------------------------------ */
193
+ /*------------------------------------------------------------------------*/
194
+
195
+ // Export component
196
+ export default SimpleTimeChooser;
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ import RadioButton from './components/RadioButton';
15
15
  import CheckboxButton from './components/CheckboxButton';
16
16
  import ButtonInputGroup from './components/ButtonInputGroup';
17
17
  import SimpleDateChooser from './components/SimpleDateChooser';
18
+ import SimpleTimeChooser from './components/SimpleTimeChooser';
18
19
  import Drawer from './components/Drawer';
19
20
  import PopSuccessMark from './components/PopSuccessMark';
20
21
  import PopFailureMark from './components/PopFailureMark';
@@ -133,6 +134,7 @@ export {
133
134
  CheckboxButton,
134
135
  ButtonInputGroup,
135
136
  SimpleDateChooser,
137
+ SimpleTimeChooser,
136
138
  Drawer,
137
139
  PopSuccessMark,
138
140
  PopFailureMark,
@@ -7,6 +7,6 @@ enum LogSource {
7
7
  Client = 'client',
8
8
  // Server
9
9
  Server = 'server',
10
- };
10
+ }
11
11
 
12
12
  export default LogSource;
@@ -1,4 +1,4 @@
1
- // Highest error code = DRK34
1
+ // Highest error code = DRK36
2
2
 
3
3
  /**
4
4
  * List of error codes built into the react kit
@@ -9,6 +9,8 @@ enum ReactKitErrorCode {
9
9
  NoCode = 'DRK2',
10
10
  SessionExpired = 'DRK3',
11
11
  NoCACCLSendRequestFunction = 'DRK7',
12
+ SimpleDateChooserInvalidDateRange = 'DRK35',
13
+ SimpleDateChooserInvalidNumMonths = 'DRK36',
12
14
  }
13
15
 
14
16
  export default ReactKitErrorCode;