@react-stately/datepicker 3.15.3 → 3.16.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.
@@ -10,13 +10,15 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {Calendar, CalendarIdentifier, DateFormatter, getMinimumDayInMonth, getMinimumMonthInYear, GregorianCalendar, isEqualCalendar, toCalendar} from '@internationalized/date';
13
+ import {Calendar, CalendarIdentifier, DateFormatter, GregorianCalendar, isEqualCalendar, toCalendar} from '@internationalized/date';
14
14
  import {convertValue, createPlaceholderDate, FieldOptions, FormatterOptions, getFormatOptions, getValidationResult, useDefaultProps} from './utils';
15
15
  import {DatePickerProps, DateValue, Granularity, MappedDateValue} from '@react-types/datepicker';
16
16
  import {FormValidationState, useFormValidationState} from '@react-stately/form';
17
17
  import {getPlaceholder} from './placeholders';
18
+ import {IncompleteDate} from './IncompleteDate';
19
+ import {NumberFormatter} from '@internationalized/number';
18
20
  import {useControlledState} from '@react-stately/utils';
19
- import {useEffect, useMemo, useRef, useState} from 'react';
21
+ import {useMemo, useState} from 'react';
20
22
  import {ValidationState} from '@react-types/shared';
21
23
 
22
24
  export type SegmentType = 'era' | 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second' | 'dayPeriod' | 'literal' | 'timeZoneName';
@@ -26,7 +28,7 @@ export interface DateSegment {
26
28
  /** The formatted text for the segment. */
27
29
  text: string,
28
30
  /** The numeric value for the segment, if applicable. */
29
- value?: number,
31
+ value?: number | null,
30
32
  /** The minimum numeric value for the segment, if applicable. */
31
33
  minValue?: number,
32
34
  /** The maximum numeric value for the segment, if applicable. */
@@ -87,6 +89,10 @@ export interface DateFieldState extends FormValidationState {
87
89
  * Upon reaching the minimum or maximum value, the value wraps around to the opposite limit.
88
90
  */
89
91
  decrementPage(type: SegmentType): void,
92
+ /** Increments the given segment to its maxiumum value. */
93
+ incrementToMax(type: SegmentType): void,
94
+ /** Decrements the given segment to its minimum value. */
95
+ decrementToMin(type: SegmentType): void,
90
96
  /** Sets the value of the given segment. */
91
97
  setSegment(type: 'era', value: string): void,
92
98
  setSegment(type: SegmentType, value: number): void,
@@ -173,8 +179,17 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
173
179
  throw new Error('Invalid granularity ' + granularity + ' for value ' + v.toString());
174
180
  }
175
181
 
176
- let defaultFormatter = useMemo(() => new DateFormatter(locale), [locale]);
177
- let calendar = useMemo(() => createCalendar(defaultFormatter.resolvedOptions().calendar as CalendarIdentifier), [createCalendar, defaultFormatter]);
182
+ // Resolve default hour cycle and calendar system.
183
+ let [calendar, hourCycle] = useMemo(() => {
184
+ let formatter = new DateFormatter(locale, {
185
+ dateStyle: 'short',
186
+ timeStyle: 'short',
187
+ hour12: props.hourCycle != null ? props.hourCycle === 12 : undefined
188
+ });
189
+ let opts = formatter.resolvedOptions();
190
+ let calendar = createCalendar(opts.calendar as CalendarIdentifier);
191
+ return [calendar, opts.hourCycle!];
192
+ }, [locale, props.hourCycle, createCalendar]);
178
193
 
179
194
  let [value, setDate] = useControlledState<DateValue | null, MappedDateValue<T> | null>(
180
195
  props.value,
@@ -184,17 +199,11 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
184
199
 
185
200
  let [initialValue] = useState(value);
186
201
  let calendarValue = useMemo(() => convertValue(value, calendar) ?? null, [value, calendar]);
187
-
188
- // We keep track of the placeholder date separately in state so that onChange is not called
189
- // until all segments are set. If the value === null (not undefined), then assume the component
190
- // is controlled, so use the placeholder as the value until all segments are entered so it doesn't
191
- // change from uncontrolled to controlled and emit a warning.
192
- let [placeholderDate, setPlaceholderDate] = useState(
193
- () => createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone)
202
+ let [displayValue, setDisplayValue] = useState(
203
+ () => new IncompleteDate(calendar, hourCycle, calendarValue)
194
204
  );
195
205
 
196
- let val = calendarValue || placeholderDate;
197
- let showEra = calendar.identifier === 'gregory' && val.era === 'BC';
206
+ let showEra = calendar.identifier === 'gregory' && displayValue.era === 'BC';
198
207
  let formatOpts = useMemo(() => ({
199
208
  granularity,
200
209
  maxGranularity: props.maxGranularity ?? 'year',
@@ -208,117 +217,73 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
208
217
 
209
218
  let dateFormatter = useMemo(() => new DateFormatter(locale, opts), [locale, opts]);
210
219
  let resolvedOptions = useMemo(() => dateFormatter.resolvedOptions(), [dateFormatter]);
211
-
212
- // Determine how many editable segments there are for validation purposes.
213
- // The result is cached for performance.
214
- let allSegments: Partial<typeof EDITABLE_SEGMENTS> = useMemo(() =>
215
- dateFormatter.formatToParts(new Date())
216
- .filter(seg => EDITABLE_SEGMENTS[seg.type])
217
- .reduce((p, seg) => (p[TYPE_MAPPING[seg.type] || seg.type] = true, p), {})
218
- , [dateFormatter]);
219
-
220
- let [validSegments, setValidSegments] = useState<Partial<typeof EDITABLE_SEGMENTS>>(
221
- () => props.value || props.defaultValue ? {...allSegments} : {}
222
- );
223
-
224
- let clearedSegment = useRef<string | null>(null);
225
-
226
- // Reset placeholder when calendar changes
227
- let lastCalendar = useRef(calendar);
228
- useEffect(() => {
229
- if (!isEqualCalendar(calendar, lastCalendar.current)) {
230
- lastCalendar.current = calendar;
231
- setPlaceholderDate(placeholder =>
232
- Object.keys(validSegments).length > 0
233
- ? toCalendar(placeholder, calendar)
234
- : createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone)
235
- );
236
- }
237
- }, [calendar, granularity, validSegments, defaultTimeZone, props.placeholderValue]);
238
-
239
- // If there is a value prop, and some segments were previously placeholders, mark them all as valid.
240
- if (value && Object.keys(validSegments).length < Object.keys(allSegments).length) {
241
- validSegments = {...allSegments};
242
- setValidSegments(validSegments);
243
- }
244
-
245
- // If the value is set to null and all segments are valid, reset the placeholder.
246
- if (value == null && Object.keys(validSegments).length === Object.keys(allSegments).length) {
247
- validSegments = {};
248
- setValidSegments(validSegments);
249
- setPlaceholderDate(createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone));
220
+ let placeholder = useMemo(() => createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone), [props.placeholderValue, granularity, calendar, defaultTimeZone]);
221
+ let displaySegments = useMemo(() => {
222
+ let is12HourClock = hourCycle === 'h11' || hourCycle === 'h12';
223
+ let segments: SegmentType[] = ['era', 'year', 'month', 'day', 'hour', ...(is12HourClock ? ['dayPeriod' as const] : []), 'minute', 'second'];
224
+ let minIndex = segments.indexOf(props.maxGranularity || 'era');
225
+ let maxIndex = segments.indexOf(granularity === 'hour' && is12HourClock ? 'dayPeriod' : granularity);
226
+ return segments.slice(minIndex, maxIndex + 1);
227
+ }, [props.maxGranularity, granularity, hourCycle]);
228
+
229
+ let [lastValue, setLastValue] = useState(calendarValue);
230
+ let [lastCalendar, setLastCalendar] = useState(calendar);
231
+ let [lastHourCycle, setLastHourCycle] = useState(hourCycle);
232
+ if (calendarValue !== lastValue || hourCycle !== lastHourCycle || !isEqualCalendar(calendar, lastCalendar)) {
233
+ displayValue = new IncompleteDate(calendar, hourCycle, calendarValue);
234
+ setLastValue(calendarValue);
235
+ setLastCalendar(calendar);
236
+ setLastHourCycle(hourCycle);
237
+ setDisplayValue(displayValue);
250
238
  }
251
239
 
252
- // If all segments are valid, use the date from state, otherwise use the placeholder date.
253
- let displayValue = calendarValue && Object.keys(validSegments).length >= Object.keys(allSegments).length ? calendarValue : placeholderDate;
254
- let setValue = (newValue: DateValue) => {
240
+ let setValue = (newValue: DateValue | IncompleteDate | null) => {
255
241
  if (props.isDisabled || props.isReadOnly) {
256
242
  return;
257
243
  }
258
- let validKeys = Object.keys(validSegments);
259
- let allKeys = Object.keys(allSegments);
260
244
 
261
- // if all the segments are completed or a timefield with everything but am/pm set the time, also ignore when am/pm cleared
262
- if (newValue == null) {
245
+ if (newValue == null || (newValue instanceof IncompleteDate && newValue.isCleared(displaySegments))) {
246
+ setDisplayValue(new IncompleteDate(calendar, hourCycle, calendarValue));
263
247
  setDate(null);
264
- setPlaceholderDate(createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone));
265
- setValidSegments({});
266
- } else if (
267
- (validKeys.length === 0 && clearedSegment.current == null) ||
268
- validKeys.length >= allKeys.length ||
269
- (validKeys.length === allKeys.length - 1 && allSegments.dayPeriod && !validSegments.dayPeriod && clearedSegment.current !== 'dayPeriod')
270
- ) {
271
- // If the field was empty (no valid segments) or all segments are completed, commit the new value.
272
- // When committing from an empty state, mark every segment as valid so value is committed.
273
- if (validKeys.length === 0) {
274
- validSegments = {...allSegments};
275
- setValidSegments(validSegments);
276
- }
277
-
248
+ } else if (!(newValue instanceof IncompleteDate)) {
278
249
  // The display calendar should not have any effect on the emitted value.
279
250
  // Emit dates in the same calendar as the original value, if any, otherwise gregorian.
280
251
  newValue = toCalendar(newValue, v?.calendar || new GregorianCalendar());
252
+ setDisplayValue(new IncompleteDate(calendar, hourCycle, calendarValue));
281
253
  setDate(newValue);
282
254
  } else {
283
- setPlaceholderDate(newValue);
255
+ // If the new value is complete and valid, trigger onChange eagerly.
256
+ // If it represents an incomplete or invalid value (e.g. February 30th),
257
+ // wait until the field is blurred to trigger onChange.
258
+ if (newValue.isComplete(displaySegments)) {
259
+ let dateValue = newValue.toValue(calendarValue ?? placeholder);
260
+ if (newValue.validate(dateValue, displaySegments)) {
261
+ let newDateValue = toCalendar(dateValue, v?.calendar || new GregorianCalendar());
262
+ if (!value || newDateValue.compare(value) !== 0) {
263
+ setDisplayValue(new IncompleteDate(calendar, hourCycle, calendarValue)); // reset in case prop isn't updated
264
+ setDate(newDateValue);
265
+ return;
266
+ }
267
+ }
268
+ }
269
+
270
+ // Incomplete/invalid value. Set temporary display override.
271
+ setDisplayValue(newValue);
284
272
  }
285
- clearedSegment.current = null;
286
273
  };
287
274
 
288
- let dateValue = useMemo(() => displayValue.toDate(timeZone), [displayValue, timeZone]);
289
- let segments = useMemo(() =>
290
- processSegments(dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale, granularity),
291
- [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale, granularity]);
292
-
293
- // When the era field appears, mark it valid if the year field is already valid.
294
- // If the era field disappears, remove it from the valid segments.
295
- if (allSegments.era && validSegments.year && !validSegments.era) {
296
- validSegments.era = true;
297
- setValidSegments({...validSegments});
298
- } else if (!allSegments.era && validSegments.era) {
299
- delete validSegments.era;
300
- setValidSegments({...validSegments});
301
- }
275
+ let dateValue = useMemo(() => {
276
+ let v = displayValue.toValue(calendarValue ?? placeholder);
277
+ return v.toDate(timeZone);
278
+ }, [displayValue, timeZone, calendarValue, placeholder]);
302
279
 
303
- let markValid = (part: Intl.DateTimeFormatPartTypes) => {
304
- validSegments[part] = true;
305
- if (part === 'year' && allSegments.era) {
306
- validSegments.era = true;
307
- }
308
- setValidSegments({...validSegments});
309
- };
280
+ let segments = useMemo(
281
+ () => processSegments(dateValue, displayValue, dateFormatter, resolvedOptions, calendar, locale, granularity),
282
+ [dateValue, dateFormatter, resolvedOptions, displayValue, calendar, locale, granularity]
283
+ );
310
284
 
311
- let adjustSegment = (type: Intl.DateTimeFormatPartTypes, amount: number) => {
312
- if (!validSegments[type]) {
313
- markValid(type);
314
- let validKeys = Object.keys(validSegments);
315
- let allKeys = Object.keys(allSegments);
316
- if (validKeys.length >= allKeys.length || (validKeys.length === allKeys.length - 1 && allSegments.dayPeriod && !validSegments.dayPeriod)) {
317
- setValue(displayValue);
318
- }
319
- } else {
320
- setValue(addSegment(displayValue, type, amount, resolvedOptions));
321
- }
285
+ let adjustSegment = (type: SegmentType, amount: number) => {
286
+ setValue(displayValue.cycle(type, amount, placeholder, displaySegments));
322
287
  };
323
288
 
324
289
  let builtinValidation = useMemo(() => getValidationResult(
@@ -366,48 +331,43 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
366
331
  decrementPage(part) {
367
332
  adjustSegment(part, -(PAGE_STEP[part] || 1));
368
333
  },
334
+ incrementToMax(part) {
335
+ let maxValue = part === 'hour' && hourCycle === 'h12'
336
+ ? 11
337
+ : displayValue.getSegmentLimits(part)!.maxValue;
338
+ setValue(displayValue.set(part, maxValue, placeholder));
339
+ },
340
+ decrementToMin(part) {
341
+ let minValue = part === 'hour' && hourCycle === 'h12'
342
+ ? 12
343
+ : displayValue.getSegmentLimits(part)!.minValue;
344
+ setValue(displayValue.set(part, minValue, placeholder));
345
+ },
369
346
  setSegment(part, v: string | number) {
370
- markValid(part);
371
- setValue(setSegment(displayValue, part, v, resolvedOptions));
347
+ setValue(displayValue.set(part, v, placeholder));
372
348
  },
373
349
  confirmPlaceholder() {
374
350
  if (props.isDisabled || props.isReadOnly) {
375
351
  return;
376
352
  }
377
353
 
378
- // Confirm the placeholder if only the day period is not filled in.
379
- let validKeys = Object.keys(validSegments);
380
- let allKeys = Object.keys(allSegments);
381
- if (validKeys.length === allKeys.length - 1 && allSegments.dayPeriod && !validSegments.dayPeriod) {
382
- validSegments = {...allSegments};
383
- setValidSegments(validSegments);
384
- setValue(displayValue.copy());
354
+ // If the display value is complete but invalid, we need to constrain it and emit onChange on blur.
355
+ if (displayValue.isComplete(displaySegments)) {
356
+ let dateValue = displayValue.toValue(calendarValue ?? placeholder);
357
+ let newDateValue = toCalendar(dateValue, v?.calendar || new GregorianCalendar());
358
+ if (!value || newDateValue.compare(value) !== 0) {
359
+ setDate(newDateValue);
360
+ }
361
+ setDisplayValue(new IncompleteDate(calendar, hourCycle, calendarValue));
385
362
  }
386
363
  },
387
364
  clearSegment(part) {
388
- delete validSegments[part];
389
- clearedSegment.current = part;
390
- setValidSegments({...validSegments});
391
-
392
- let placeholder = createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone);
393
365
  let value = displayValue;
394
366
 
395
- // Reset day period to default without changing the hour.
396
- if (part === 'dayPeriod' && 'hour' in displayValue && 'hour' in placeholder) {
397
- let isPM = displayValue.hour >= 12;
398
- let shouldBePM = placeholder.hour >= 12;
399
- if (isPM && !shouldBePM) {
400
- value = displayValue.set({hour: displayValue.hour - 12});
401
- } else if (!isPM && shouldBePM) {
402
- value = displayValue.set({hour: displayValue.hour + 12});
403
- }
404
- } else if (part === 'hour' && 'hour' in displayValue && displayValue.hour >= 12 && validSegments.dayPeriod) {
405
- value = displayValue.set({hour: placeholder['hour'] + 12});
406
- } else if (part in displayValue) {
407
- value = displayValue.set({[part]: placeholder[part]});
367
+ if (part !== 'timeZoneName' && part !== 'literal') {
368
+ value = displayValue.clear(part);
408
369
  }
409
370
 
410
- setDate(null);
411
371
  setValue(value);
412
372
  },
413
373
  formatValue(fieldOptions: FieldOptions) {
@@ -427,9 +387,34 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
427
387
  };
428
388
  }
429
389
 
430
- function processSegments(dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale, granularity) : DateSegment[] {
390
+ function processSegments(
391
+ dateValue: Date,
392
+ displayValue: IncompleteDate,
393
+ dateFormatter: Intl.DateTimeFormat,
394
+ resolvedOptions: Intl.ResolvedDateTimeFormatOptions,
395
+ calendar: Calendar,
396
+ locale: string,
397
+ granularity: Granularity
398
+ ) : DateSegment[] {
431
399
  let timeValue = ['hour', 'minute', 'second'];
432
400
  let segments = dateFormatter.formatToParts(dateValue);
401
+
402
+ // In order to allow formatting temporarily invalid dates during editing (e.g. February 30th),
403
+ // use a NumberFormatter to manually format segments directly from raw numbers.
404
+ // When the user blurs the date field, the invalid segments will be constrained.
405
+ let numberFormatter = new NumberFormatter(locale, {useGrouping: false});
406
+ let twoDigitFormatter = new NumberFormatter(locale, {useGrouping: false, minimumIntegerDigits: 2});
407
+ for (let segment of segments) {
408
+ if (segment.type === 'year' || segment.type === 'month' || segment.type === 'day' || segment.type === 'hour') {
409
+ let value = displayValue[segment.type] ?? 0;
410
+ if (resolvedOptions[segment.type] === '2-digit') {
411
+ segment.value = twoDigitFormatter.format(value);
412
+ } else {
413
+ segment.value = numberFormatter.format(value);
414
+ }
415
+ }
416
+ }
417
+
433
418
  let processedSegments: DateSegment[] = [];
434
419
  for (let segment of segments) {
435
420
  let type = TYPE_MAPPING[segment.type] || segment.type;
@@ -438,13 +423,13 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
438
423
  isEditable = false;
439
424
  }
440
425
 
441
- let isPlaceholder = EDITABLE_SEGMENTS[type] && !validSegments[type];
426
+ let isPlaceholder = EDITABLE_SEGMENTS[type] && displayValue[segment.type] == null;
442
427
  let placeholder = EDITABLE_SEGMENTS[type] ? getPlaceholder(type, segment.value, locale) : null;
443
428
 
444
429
  let dateSegment = {
445
430
  type,
446
431
  text: isPlaceholder ? placeholder : segment.value,
447
- ...getSegmentLimits(displayValue, type, resolvedOptions),
432
+ ...displayValue.getSegmentLimits(type),
448
433
  isPlaceholder,
449
434
  placeholder,
450
435
  isEditable
@@ -458,7 +443,6 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
458
443
  processedSegments.push({
459
444
  type: 'literal',
460
445
  text: '\u2066',
461
- ...getSegmentLimits(displayValue, 'literal', resolvedOptions),
462
446
  isPlaceholder: false,
463
447
  placeholder: '',
464
448
  isEditable: false
@@ -469,7 +453,6 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
469
453
  processedSegments.push({
470
454
  type: 'literal',
471
455
  text: '\u2069',
472
- ...getSegmentLimits(displayValue, 'literal', resolvedOptions),
473
456
  isPlaceholder: false,
474
457
  placeholder: '',
475
458
  isEditable: false
@@ -481,7 +464,6 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
481
464
  processedSegments.push({
482
465
  type: 'literal',
483
466
  text: '\u2069',
484
- ...getSegmentLimits(displayValue, 'literal', resolvedOptions),
485
467
  isPlaceholder: false,
486
468
  placeholder: '',
487
469
  isEditable: false
@@ -494,145 +476,3 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
494
476
 
495
477
  return processedSegments;
496
478
  }
497
-
498
- function getSegmentLimits(date: DateValue, type: string, options: Intl.ResolvedDateTimeFormatOptions) {
499
- switch (type) {
500
- case 'era': {
501
- let eras = date.calendar.getEras();
502
- return {
503
- value: eras.indexOf(date.era),
504
- minValue: 0,
505
- maxValue: eras.length - 1
506
- };
507
- }
508
- case 'year':
509
- return {
510
- value: date.year,
511
- minValue: 1,
512
- maxValue: date.calendar.getYearsInEra(date)
513
- };
514
- case 'month':
515
- return {
516
- value: date.month,
517
- minValue: getMinimumMonthInYear(date),
518
- maxValue: date.calendar.getMonthsInYear(date)
519
- };
520
- case 'day':
521
- return {
522
- value: date.day,
523
- minValue: getMinimumDayInMonth(date),
524
- maxValue: date.calendar.getDaysInMonth(date)
525
- };
526
- }
527
-
528
- if ('hour' in date) {
529
- switch (type) {
530
- case 'dayPeriod':
531
- return {
532
- value: date.hour >= 12 ? 12 : 0,
533
- minValue: 0,
534
- maxValue: 12
535
- };
536
- case 'hour':
537
- if (options.hour12) {
538
- let isPM = date.hour >= 12;
539
- return {
540
- value: date.hour,
541
- minValue: isPM ? 12 : 0,
542
- maxValue: isPM ? 23 : 11
543
- };
544
- }
545
-
546
- return {
547
- value: date.hour,
548
- minValue: 0,
549
- maxValue: 23
550
- };
551
- case 'minute':
552
- return {
553
- value: date.minute,
554
- minValue: 0,
555
- maxValue: 59
556
- };
557
- case 'second':
558
- return {
559
- value: date.second,
560
- minValue: 0,
561
- maxValue: 59
562
- };
563
- }
564
- }
565
-
566
- return {};
567
- }
568
-
569
- function addSegment(value: DateValue, part: string, amount: number, options: Intl.ResolvedDateTimeFormatOptions) {
570
- switch (part) {
571
- case 'era':
572
- case 'year':
573
- case 'month':
574
- case 'day':
575
- return value.cycle(part, amount, {round: part === 'year'});
576
- }
577
-
578
- if ('hour' in value) {
579
- switch (part) {
580
- case 'dayPeriod': {
581
- let hours = value.hour;
582
- let isPM = hours >= 12;
583
- return value.set({hour: isPM ? hours - 12 : hours + 12});
584
- }
585
- case 'hour':
586
- case 'minute':
587
- case 'second':
588
- return value.cycle(part, amount, {
589
- round: part !== 'hour',
590
- hourCycle: options.hour12 ? 12 : 24
591
- });
592
- }
593
- }
594
-
595
- throw new Error('Unknown segment: ' + part);
596
- }
597
-
598
- function setSegment(value: DateValue, part: string, segmentValue: number | string, options: Intl.ResolvedDateTimeFormatOptions) {
599
- switch (part) {
600
- case 'day':
601
- case 'month':
602
- case 'year':
603
- case 'era':
604
- return value.set({[part]: segmentValue});
605
- }
606
-
607
- if ('hour' in value && typeof segmentValue === 'number') {
608
- switch (part) {
609
- case 'dayPeriod': {
610
- let hours = value.hour;
611
- let wasPM = hours >= 12;
612
- let isPM = segmentValue >= 12;
613
- if (isPM === wasPM) {
614
- return value;
615
- }
616
- return value.set({hour: wasPM ? hours - 12 : hours + 12});
617
- }
618
- case 'hour':
619
- // In 12 hour time, ensure that AM/PM does not change
620
- if (options.hour12) {
621
- let hours = value.hour;
622
- let wasPM = hours >= 12;
623
- if (!wasPM && segmentValue === 12) {
624
- segmentValue = 0;
625
- }
626
- if (wasPM && segmentValue < 12) {
627
- segmentValue += 12;
628
- }
629
- }
630
- // fallthrough
631
- case 'minute':
632
- case 'second':
633
- return value.set({[part]: segmentValue});
634
- }
635
- }
636
-
637
- throw new Error('Unknown segment: ' + part);
638
- }