@react-stately/datepicker 3.12.0 → 3.14.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.
@@ -57,7 +57,7 @@ const placeholders = new LocalizedStringDictionary({
57
57
  ia: {year: 'aaaa', month: 'mm', day: 'dd'},
58
58
  id: {year: 'tttt', month: 'bb', day: 'hh'},
59
59
  it: {year: 'aaaa', month: 'mm', day: 'gg'},
60
- ja: {year: ' ', month: '月', day: '日'},
60
+ ja: {year: '年', month: '月', day: '日'},
61
61
  ka: {year: 'წწწწ', month: 'თთ', day: 'რრ'},
62
62
  kk: {year: 'жжжж', month: 'аа', day: 'кк'},
63
63
  kn: {year: 'ವವವವ', month: 'ಮಿಮೀ', day: 'ದಿದಿ'},
@@ -93,7 +93,7 @@ const placeholders = new LocalizedStringDictionary({
93
93
  'zh-TW': {year: '年', month: '月', day: '日'}
94
94
  }, 'en');
95
95
 
96
- export function getPlaceholder(field: string, value: string, locale: string) {
96
+ export function getPlaceholder(field: string, value: string, locale: string): string {
97
97
  // Use the actual placeholder value for the era and day period fields.
98
98
  if (field === 'era' || field === 'dayPeriod') {
99
99
  return value;
@@ -10,7 +10,7 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {Calendar, DateFormatter, getMinimumDayInMonth, getMinimumMonthInYear, GregorianCalendar, toCalendar} from '@internationalized/date';
13
+ import {Calendar, CalendarIdentifier, DateFormatter, getMinimumDayInMonth, getMinimumMonthInYear, 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';
@@ -137,7 +137,7 @@ export interface DateFieldStateOptions<T extends DateValue = DateValue> extends
137
137
  * `@internationalized/date` package, or manually implemented to include support for
138
138
  * only certain calendars.
139
139
  */
140
- createCalendar: (name: string) => Calendar
140
+ createCalendar: (name: CalendarIdentifier) => Calendar
141
141
  }
142
142
 
143
143
  /**
@@ -168,7 +168,7 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
168
168
  }
169
169
 
170
170
  let defaultFormatter = useMemo(() => new DateFormatter(locale), [locale]);
171
- let calendar = useMemo(() => createCalendar(defaultFormatter.resolvedOptions().calendar), [createCalendar, defaultFormatter]);
171
+ let calendar = useMemo(() => createCalendar(defaultFormatter.resolvedOptions().calendar as CalendarIdentifier), [createCalendar, defaultFormatter]);
172
172
 
173
173
  let [value, setDate] = useControlledState<DateValue | null, MappedDateValue<T> | null>(
174
174
  props.value,
@@ -217,10 +217,10 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
217
217
  let clearedSegment = useRef<string | null>(null);
218
218
 
219
219
  // Reset placeholder when calendar changes
220
- let lastCalendarIdentifier = useRef(calendar.identifier);
220
+ let lastCalendar = useRef(calendar);
221
221
  useEffect(() => {
222
- if (calendar.identifier !== lastCalendarIdentifier.current) {
223
- lastCalendarIdentifier.current = calendar.identifier;
222
+ if (!isEqualCalendar(calendar, lastCalendar.current)) {
223
+ lastCalendar.current = calendar;
224
224
  setPlaceholderDate(placeholder =>
225
225
  Object.keys(validSegments).length > 0
226
226
  ? toCalendar(placeholder, calendar)
@@ -268,26 +268,9 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
268
268
  };
269
269
 
270
270
  let dateValue = useMemo(() => displayValue.toDate(timeZone), [displayValue, timeZone]);
271
- let segments = useMemo(() =>
272
- dateFormatter.formatToParts(dateValue)
273
- .map(segment => {
274
- let isEditable = EDITABLE_SEGMENTS[segment.type];
275
- if (segment.type === 'era' && calendar.getEras().length === 1) {
276
- isEditable = false;
277
- }
278
-
279
- let isPlaceholder = EDITABLE_SEGMENTS[segment.type] && !validSegments[segment.type];
280
- let placeholder = EDITABLE_SEGMENTS[segment.type] ? getPlaceholder(segment.type, segment.value, locale) : null;
281
- return {
282
- type: TYPE_MAPPING[segment.type] || segment.type,
283
- text: isPlaceholder ? placeholder : segment.value,
284
- ...getSegmentLimits(displayValue, segment.type, resolvedOptions),
285
- isPlaceholder,
286
- placeholder,
287
- isEditable
288
- } as DateSegment;
289
- })
290
- , [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale]);
271
+ let segments = useMemo(() =>
272
+ processSegments(dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale, granularity),
273
+ [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale, granularity]);
291
274
 
292
275
  // When the era field appears, mark it valid if the year field is already valid.
293
276
  // If the era field disappears, remove it from the valid segments.
@@ -399,6 +382,8 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
399
382
  } else if (!isPM && shouldBePM) {
400
383
  value = displayValue.set({hour: displayValue.hour + 12});
401
384
  }
385
+ } else if (part === 'hour' && 'hour' in displayValue && displayValue.hour >= 12 && validSegments.dayPeriod) {
386
+ value = displayValue.set({hour: placeholder['hour'] + 12});
402
387
  } else if (part in displayValue) {
403
388
  value = displayValue.set({[part]: placeholder[part]});
404
389
  }
@@ -423,6 +408,73 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
423
408
  };
424
409
  }
425
410
 
411
+ function processSegments(dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale, granularity) : DateSegment[] {
412
+ let timeValue = ['hour', 'minute', 'second'];
413
+ let segments = dateFormatter.formatToParts(dateValue);
414
+ let processedSegments: DateSegment[] = [];
415
+ for (let segment of segments) {
416
+ let isEditable = EDITABLE_SEGMENTS[segment.type];
417
+ if (segment.type === 'era' && calendar.getEras().length === 1) {
418
+ isEditable = false;
419
+ }
420
+
421
+ let isPlaceholder = EDITABLE_SEGMENTS[segment.type] && !validSegments[segment.type];
422
+ let placeholder = EDITABLE_SEGMENTS[segment.type] ? getPlaceholder(segment.type, segment.value, locale) : null;
423
+
424
+ let dateSegment = {
425
+ type: TYPE_MAPPING[segment.type] || segment.type,
426
+ text: isPlaceholder ? placeholder : segment.value,
427
+ ...getSegmentLimits(displayValue, segment.type, resolvedOptions),
428
+ isPlaceholder,
429
+ placeholder,
430
+ isEditable
431
+ } as DateSegment;
432
+
433
+ // There is an issue in RTL languages where time fields render (minute:hour) instead of (hour:minute).
434
+ // To force an LTR direction on the time field since, we wrap the time segments in LRI (left-to-right) isolate unicode. See https://www.w3.org/International/questions/qa-bidi-unicode-controls.
435
+ // These unicode characters will be added to the array of processed segments as literals and will mark the start and end of the embedded direction change.
436
+ if (segment.type === 'hour') {
437
+ // This marks the start of the embedded direction change.
438
+ processedSegments.push({
439
+ type: 'literal',
440
+ text: '\u2066',
441
+ ...getSegmentLimits(displayValue, 'literal', resolvedOptions),
442
+ isPlaceholder: false,
443
+ placeholder: '',
444
+ isEditable: false
445
+ });
446
+ processedSegments.push(dateSegment);
447
+ // This marks the end of the embedded direction change in the case that the granularity it set to "hour".
448
+ if (segment.type === granularity) {
449
+ processedSegments.push({
450
+ type: 'literal',
451
+ text: '\u2069',
452
+ ...getSegmentLimits(displayValue, 'literal', resolvedOptions),
453
+ isPlaceholder: false,
454
+ placeholder: '',
455
+ isEditable: false
456
+ });
457
+ }
458
+ } else if (timeValue.includes(segment.type) && segment.type === granularity) {
459
+ processedSegments.push(dateSegment);
460
+ // This marks the end of the embedded direction change.
461
+ processedSegments.push({
462
+ type: 'literal',
463
+ text: '\u2069',
464
+ ...getSegmentLimits(displayValue, 'literal', resolvedOptions),
465
+ isPlaceholder: false,
466
+ placeholder: '',
467
+ isEditable: false
468
+ });
469
+ } else {
470
+ // We only want to "wrap" the unicode around segments that are hour, minute, or second. If they aren't, just process as normal.
471
+ processedSegments.push(dateSegment);
472
+ }
473
+ }
474
+
475
+ return processedSegments;
476
+ }
477
+
426
478
  function getSegmentLimits(date: DateValue, type: string, options: Intl.ResolvedDateTimeFormatOptions) {
427
479
  switch (type) {
428
480
  case 'era': {
package/src/utils.ts CHANGED
@@ -86,7 +86,7 @@ export function getRangeValidationResult(
86
86
  maxValue: DateValue | null | undefined,
87
87
  isDateUnavailable: ((v: DateValue) => boolean) | undefined,
88
88
  options: FormatterOptions
89
- ) {
89
+ ): ValidationResult {
90
90
  let startValidation = getValidationResult(
91
91
  value?.start ?? null,
92
92
  minValue,