rn-grid-date-time-picker 1.1.0 → 1.2.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.
Files changed (134) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/lib/commonjs/components/DateTimePicker.js +50 -13
  3. package/lib/commonjs/components/DateTimePicker.js.map +1 -1
  4. package/lib/commonjs/components/calendar/CalendarHeader.js +3 -1
  5. package/lib/commonjs/components/calendar/CalendarHeader.js.map +1 -1
  6. package/lib/commonjs/components/calendar/DayGrid.js +4 -1
  7. package/lib/commonjs/components/calendar/DayGrid.js.map +1 -1
  8. package/lib/commonjs/components/calendar/calendarStyles.js +2 -1
  9. package/lib/commonjs/components/calendar/calendarStyles.js.map +1 -1
  10. package/lib/commonjs/components/field/RangeFields.js +2 -0
  11. package/lib/commonjs/components/field/RangeFields.js.map +1 -1
  12. package/lib/commonjs/components/field/SegmentedInput.js +1 -1
  13. package/lib/commonjs/components/field/SegmentedInput.js.map +1 -1
  14. package/lib/commonjs/components/field/ValueField.js +57 -5
  15. package/lib/commonjs/components/field/ValueField.js.map +1 -1
  16. package/lib/commonjs/components/field/fieldStyles.js +17 -0
  17. package/lib/commonjs/components/field/fieldStyles.js.map +1 -1
  18. package/lib/commonjs/components/field/fieldText.js +12 -3
  19. package/lib/commonjs/components/field/fieldText.js.map +1 -1
  20. package/lib/commonjs/components/theme/defaultStrings.js +6 -2
  21. package/lib/commonjs/components/theme/defaultStrings.js.map +1 -1
  22. package/lib/commonjs/components/theme/direction.js +17 -0
  23. package/lib/commonjs/components/theme/direction.js.map +1 -0
  24. package/lib/commonjs/components/theme/styleKeys.js +6 -1
  25. package/lib/commonjs/components/theme/styleKeys.js.map +1 -1
  26. package/lib/commonjs/components/time/timeStyles.js +4 -5
  27. package/lib/commonjs/components/time/timeStyles.js.map +1 -1
  28. package/lib/commonjs/components/types.js.map +1 -1
  29. package/lib/commonjs/core/calendarSystem.js +39 -1
  30. package/lib/commonjs/core/calendarSystem.js.map +1 -1
  31. package/lib/commonjs/index.js +6 -0
  32. package/lib/commonjs/index.js.map +1 -1
  33. package/lib/module/components/DateTimePicker.js +52 -15
  34. package/lib/module/components/DateTimePicker.js.map +1 -1
  35. package/lib/module/components/calendar/CalendarHeader.js +4 -2
  36. package/lib/module/components/calendar/CalendarHeader.js.map +1 -1
  37. package/lib/module/components/calendar/DayGrid.js +5 -2
  38. package/lib/module/components/calendar/DayGrid.js.map +1 -1
  39. package/lib/module/components/calendar/calendarStyles.js +2 -1
  40. package/lib/module/components/calendar/calendarStyles.js.map +1 -1
  41. package/lib/module/components/field/RangeFields.js +2 -0
  42. package/lib/module/components/field/RangeFields.js.map +1 -1
  43. package/lib/module/components/field/SegmentedInput.js +1 -1
  44. package/lib/module/components/field/SegmentedInput.js.map +1 -1
  45. package/lib/module/components/field/ValueField.js +59 -7
  46. package/lib/module/components/field/ValueField.js.map +1 -1
  47. package/lib/module/components/field/fieldStyles.js +17 -0
  48. package/lib/module/components/field/fieldStyles.js.map +1 -1
  49. package/lib/module/components/field/fieldText.js +10 -2
  50. package/lib/module/components/field/fieldText.js.map +1 -1
  51. package/lib/module/components/theme/defaultStrings.js +4 -1
  52. package/lib/module/components/theme/defaultStrings.js.map +1 -1
  53. package/lib/module/components/theme/direction.js +13 -0
  54. package/lib/module/components/theme/direction.js.map +1 -0
  55. package/lib/module/components/theme/styleKeys.js +6 -1
  56. package/lib/module/components/theme/styleKeys.js.map +1 -1
  57. package/lib/module/components/time/timeStyles.js +4 -5
  58. package/lib/module/components/time/timeStyles.js.map +1 -1
  59. package/lib/module/components/types.js.map +1 -1
  60. package/lib/module/core/calendarSystem.js +37 -0
  61. package/lib/module/core/calendarSystem.js.map +1 -1
  62. package/lib/module/index.js +1 -1
  63. package/lib/module/index.js.map +1 -1
  64. package/lib/typescript/commonjs/src/components/DateTimePicker.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/src/components/calendar/CalendarHeader.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/src/components/calendar/DayGrid.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/src/components/calendar/calendarStyles.d.ts +1 -1
  68. package/lib/typescript/commonjs/src/components/calendar/calendarStyles.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/src/components/field/RangeFields.d.ts +2 -0
  70. package/lib/typescript/commonjs/src/components/field/RangeFields.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/src/components/field/ValueField.d.ts +4 -0
  72. package/lib/typescript/commonjs/src/components/field/ValueField.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/src/components/field/fieldStyles.d.ts +15 -0
  74. package/lib/typescript/commonjs/src/components/field/fieldStyles.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/src/components/field/fieldText.d.ts +5 -0
  76. package/lib/typescript/commonjs/src/components/field/fieldText.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/src/components/theme/defaultStrings.d.ts +2 -0
  78. package/lib/typescript/commonjs/src/components/theme/defaultStrings.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/src/components/theme/direction.d.ts +8 -0
  80. package/lib/typescript/commonjs/src/components/theme/direction.d.ts.map +1 -0
  81. package/lib/typescript/commonjs/src/components/theme/styleKeys.d.ts +1 -1
  82. package/lib/typescript/commonjs/src/components/theme/styleKeys.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/src/components/time/timeStyles.d.ts +0 -2
  84. package/lib/typescript/commonjs/src/components/time/timeStyles.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/src/components/types.d.ts +7 -0
  86. package/lib/typescript/commonjs/src/components/types.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/src/core/calendarSystem.d.ts +10 -0
  88. package/lib/typescript/commonjs/src/core/calendarSystem.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/src/index.d.ts +2 -2
  90. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  91. package/lib/typescript/module/src/components/DateTimePicker.d.ts.map +1 -1
  92. package/lib/typescript/module/src/components/calendar/CalendarHeader.d.ts.map +1 -1
  93. package/lib/typescript/module/src/components/calendar/DayGrid.d.ts.map +1 -1
  94. package/lib/typescript/module/src/components/calendar/calendarStyles.d.ts +1 -1
  95. package/lib/typescript/module/src/components/calendar/calendarStyles.d.ts.map +1 -1
  96. package/lib/typescript/module/src/components/field/RangeFields.d.ts +2 -0
  97. package/lib/typescript/module/src/components/field/RangeFields.d.ts.map +1 -1
  98. package/lib/typescript/module/src/components/field/ValueField.d.ts +4 -0
  99. package/lib/typescript/module/src/components/field/ValueField.d.ts.map +1 -1
  100. package/lib/typescript/module/src/components/field/fieldStyles.d.ts +15 -0
  101. package/lib/typescript/module/src/components/field/fieldStyles.d.ts.map +1 -1
  102. package/lib/typescript/module/src/components/field/fieldText.d.ts +5 -0
  103. package/lib/typescript/module/src/components/field/fieldText.d.ts.map +1 -1
  104. package/lib/typescript/module/src/components/theme/defaultStrings.d.ts +2 -0
  105. package/lib/typescript/module/src/components/theme/defaultStrings.d.ts.map +1 -1
  106. package/lib/typescript/module/src/components/theme/direction.d.ts +8 -0
  107. package/lib/typescript/module/src/components/theme/direction.d.ts.map +1 -0
  108. package/lib/typescript/module/src/components/theme/styleKeys.d.ts +1 -1
  109. package/lib/typescript/module/src/components/theme/styleKeys.d.ts.map +1 -1
  110. package/lib/typescript/module/src/components/time/timeStyles.d.ts +0 -2
  111. package/lib/typescript/module/src/components/time/timeStyles.d.ts.map +1 -1
  112. package/lib/typescript/module/src/components/types.d.ts +7 -0
  113. package/lib/typescript/module/src/components/types.d.ts.map +1 -1
  114. package/lib/typescript/module/src/core/calendarSystem.d.ts +10 -0
  115. package/lib/typescript/module/src/core/calendarSystem.d.ts.map +1 -1
  116. package/lib/typescript/module/src/index.d.ts +2 -2
  117. package/lib/typescript/module/src/index.d.ts.map +1 -1
  118. package/package.json +1 -1
  119. package/src/components/DateTimePicker.tsx +92 -47
  120. package/src/components/calendar/CalendarHeader.tsx +3 -2
  121. package/src/components/calendar/DayGrid.tsx +5 -2
  122. package/src/components/calendar/calendarStyles.ts +2 -1
  123. package/src/components/field/RangeFields.tsx +4 -0
  124. package/src/components/field/SegmentedInput.tsx +1 -1
  125. package/src/components/field/ValueField.tsx +83 -15
  126. package/src/components/field/fieldStyles.ts +6 -0
  127. package/src/components/field/fieldText.ts +25 -4
  128. package/src/components/theme/defaultStrings.ts +11 -1
  129. package/src/components/theme/direction.ts +12 -0
  130. package/src/components/theme/styleKeys.ts +5 -0
  131. package/src/components/time/timeStyles.ts +2 -3
  132. package/src/components/types.ts +7 -0
  133. package/src/core/calendarSystem.ts +42 -0
  134. package/src/index.tsx +2 -2
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
- import { Pressable, Text, View } from 'react-native';
2
+ import { Pressable, ScrollView, Text, View } from 'react-native';
3
3
  import type { SegmentEditorAction } from '../../core/segmentEditor';
4
4
  import type { ParsedSegments } from '../../core/segments';
5
5
  import type { DateTimeValue, PickerType, TimeFormat } from '../../core/types';
@@ -11,6 +11,7 @@ import {
11
11
  type IconSet,
12
12
  } from '../../icons';
13
13
  import { useTypedSegments } from '../hooks/useTypedSegments';
14
+ import type { ScrollViewRef } from '../nativeTypes';
14
15
  import type { PickerStrings } from '../theme/defaultStrings';
15
16
  import type { PickerStyles } from '../theme/styleKeys';
16
17
  import { useNumerals } from '../theme/numerals';
@@ -18,6 +19,7 @@ import type { ThemeTokens } from '../theme/tokens';
18
19
  import type { InstructionRenderProps, LabelRenderProps } from '../types';
19
20
  import FieldMessages from './FieldMessages';
20
21
  import {
22
+ formatValueParts,
21
23
  formatValueText,
22
24
  placeholderText,
23
25
  segmentErrorText,
@@ -26,6 +28,16 @@ import {
26
28
  import { buildFieldStyles } from './fieldStyles';
27
29
  import SegmentedInput from './SegmentedInput';
28
30
 
31
+ // Hebrew and Arabic letters; Arabic-Indic digits (٠–٩, ۰–۹) are left out, since
32
+ // digits don't set the reading direction.
33
+ const RTL_LETTER = '֐-׿؀-ٟ٪-ۯۺ-ࣿ';
34
+ const FIRST_LETTER_RTL = new RegExp(
35
+ `^[^A-Za-zÀ-ɏ${RTL_LETTER}]*[${RTL_LETTER}]`
36
+ );
37
+ /** Whether a text reads right to left: its first letter is Hebrew or Arabic. */
38
+ export const startsRightToLeft = (text: string): boolean =>
39
+ FIRST_LETTER_RTL.test(text);
40
+
29
41
  export interface ValueFieldProps {
30
42
  type: PickerType;
31
43
  /** The value to show (or to start typing from). */
@@ -79,6 +91,8 @@ export interface ValueFieldProps {
79
91
  tokens: ThemeTokens;
80
92
  styles?: PickerStyles;
81
93
  strings: PickerStrings;
94
+ /** The texts of its typed-input errors. Default `strings`. */
95
+ errorStrings?: PickerStrings;
82
96
  testID?: string;
83
97
  }
84
98
 
@@ -160,10 +174,12 @@ const ValueField: React.FC<ValueFieldProps> = (props) => {
160
174
  tokens,
161
175
  styles,
162
176
  strings,
177
+ errorStrings = strings,
163
178
  testID,
164
179
  } = props;
165
180
  const s = buildFieldStyles(tokens);
166
181
  const ico = resolveIcons(iconOverrides);
182
+ const scrollRef = useRef<ScrollViewRef>(null);
167
183
  const [lastParsed, setLastParsed] = useState<ParsedSegments | null>(null);
168
184
  const { state, dispatch, getState } = useTypedSegments(
169
185
  value,
@@ -205,13 +221,25 @@ const ValueField: React.FC<ValueFieldProps> = (props) => {
205
221
  const typing = !!allowTyping && editable;
206
222
  const typedError =
207
223
  typing && !state.focused && lastParsed && !lastParsed.isEmpty
208
- ? segmentErrorText(lastParsed.errors, strings, yearRange)
224
+ ? segmentErrorText(lastParsed.errors, errorStrings, yearRange)
209
225
  : '';
210
226
  const error = typedError || errorMessage || '';
211
227
  const hasValue = typing ? !state.parsed.isEmpty : !!value || !!displayText;
212
228
  const text =
213
229
  displayText ??
214
230
  formatValueText(value, type, dateFormat, timeFormat, dateText, strings);
231
+ // With a fieldDateText style, a single date-time shows its date and time
232
+ // apart (no sideways scroll).
233
+ const valueParts =
234
+ styles?.fieldDateText && displayText === undefined && value
235
+ ? formatValueParts(value, type, dateFormat, timeFormat, dateText, strings)
236
+ : null;
237
+ const parts = valueParts?.time ? valueParts : null;
238
+ const valueStyle = [
239
+ s.text,
240
+ styles?.fieldText,
241
+ disabled && [s.disabledText, styles?.fieldTextDisabled],
242
+ ];
215
243
 
216
244
  const clear = () => {
217
245
  dispatch({ type: 'reset', value: null });
@@ -300,19 +328,59 @@ const ValueField: React.FC<ValueFieldProps> = (props) => {
300
328
  onPress={onPress}
301
329
  style={fieldStyle}
302
330
  >
303
- <Text
304
- numberOfLines={1}
305
- style={[
306
- s.text,
307
- styles?.fieldText,
308
- !text && [s.placeholder, styles?.fieldPlaceholder],
309
- disabled && [s.disabledText, text ? styles?.fieldTextDisabled : null],
310
- ]}
311
- >
312
- {digits(text) ||
313
- placeholder ||
314
- placeholderText(type, dateFormat, timeFormat, strings)}
315
- </Text>
331
+ {parts ? (
332
+ <>
333
+ <Text
334
+ numberOfLines={1}
335
+ ellipsizeMode="clip"
336
+ style={[valueStyle, s.textScrolled, styles?.fieldDateText]}
337
+ >
338
+ {digits(`${parts.date}${strings.dateTimeSeparator ?? ' '}`)}
339
+ </Text>
340
+ <Text numberOfLines={1} ellipsizeMode="clip" style={valueStyle}>
341
+ {digits(parts.time)}
342
+ </Text>
343
+ </>
344
+ ) : text ? (
345
+ // A value too long for the box scrolls sideways; a tap still opens.
346
+ <ScrollView
347
+ ref={scrollRef}
348
+ horizontal
349
+ showsHorizontalScrollIndicator={false}
350
+ keyboardShouldPersistTaps="always"
351
+ style={s.textScroll}
352
+ contentContainerStyle={s.textScrollContent}
353
+ // Right-to-left text (a Hijri date) starts at its right end.
354
+ onContentSizeChange={() => {
355
+ if (startsRightToLeft(text))
356
+ scrollRef.current?.scrollToEnd({ animated: false });
357
+ }}
358
+ >
359
+ <Text
360
+ numberOfLines={1}
361
+ // Android can measure a line a pixel short (right-to-left text
362
+ // most often) and would then swap its last word for "…".
363
+ ellipsizeMode="clip"
364
+ style={[valueStyle, s.textScrolled]}
365
+ >
366
+ {digits(text)}
367
+ </Text>
368
+ </ScrollView>
369
+ ) : (
370
+ <Text
371
+ numberOfLines={1}
372
+ style={[
373
+ s.text,
374
+ styles?.fieldText,
375
+ s.placeholder,
376
+ styles?.fieldPlaceholder,
377
+ disabled && s.disabledText,
378
+ ]}
379
+ >
380
+ {placeholder ||
381
+ placeholderText(type, dateFormat, timeFormat, strings)}
382
+ </Text>
383
+ )}
316
384
  {icons}
317
385
  </Pressable>
318
386
  );
@@ -32,6 +32,10 @@ const create = (t: ThemeTokens) =>
32
32
  fontSize: t.fontSizeMedium,
33
33
  color: t.text,
34
34
  },
35
+ textScroll: { flex: 1 },
36
+ textScrollContent: { flexGrow: 1, alignItems: 'center' },
37
+ // In the scroll, or as a date set apart from its time, the text keeps its full width.
38
+ textScrolled: { flex: 0, flexShrink: 0 },
35
39
  placeholder: { color: t.placeholder },
36
40
  disabledText: { color: t.disabledText },
37
41
  icons: { flexDirection: 'row', alignItems: 'center', marginStart: 8 },
@@ -55,6 +59,8 @@ const create = (t: ThemeTokens) =>
55
59
  alignItems: 'center',
56
60
  flexWrap: 'nowrap',
57
61
  },
62
+ // After a date, the time sits in the middle of the space left before the icons.
63
+ segmentTime: { flexGrow: 1, justifyContent: 'center' },
58
64
  segment: {
59
65
  paddingVertical: 8,
60
66
  paddingHorizontal: 0,
@@ -36,6 +36,22 @@ export const formatTimeText = (
36
36
  /** Writes a value's date some other way, e.g. as a Hijri date. */
37
37
  export type DateTextFormatter = (value: DateTimeValue) => string;
38
38
 
39
+ /** A value's date and time as two texts; the time is '' for a date or a value without one. */
40
+ export const formatValueParts = (
41
+ value: DateTimeValue,
42
+ type: PickerType,
43
+ dateFormat: string,
44
+ timeFormat: TimeFormat,
45
+ dateText?: DateTextFormatter,
46
+ periods: PeriodLabels = DEFAULT_STRINGS
47
+ ): { date: string; time: string } => ({
48
+ date: dateText ? dateText(value) : formatDateText(value, dateFormat),
49
+ time:
50
+ type === 'datetime' && value.time
51
+ ? formatTimeText(value, timeFormat, periods)
52
+ : '',
53
+ });
54
+
39
55
  export const formatValueText = (
40
56
  value: DateTimeValue | null,
41
57
  type: PickerType,
@@ -45,10 +61,15 @@ export const formatValueText = (
45
61
  periods: PeriodLabels = DEFAULT_STRINGS
46
62
  ): string => {
47
63
  if (!value) return '';
48
- const date = dateText ? dateText(value) : formatDateText(value, dateFormat);
49
- return type === 'datetime' && value.time
50
- ? `${date}${periods.dateTimeSeparator ?? ' '}${formatTimeText(value, timeFormat, periods)}`
51
- : date;
64
+ const { date, time } = formatValueParts(
65
+ value,
66
+ type,
67
+ dateFormat,
68
+ timeFormat,
69
+ dateText,
70
+ periods
71
+ );
72
+ return time ? `${date}${periods.dateTimeSeparator ?? ' '}${time}` : date;
52
73
  };
53
74
 
54
75
  /** '21-10-2026 – 25-10-2026'; just the start (with the dash) while the end is open. */
@@ -124,9 +124,19 @@ export const DEFAULT_STRINGS: Required<PickerStrings> = {
124
124
  a11yRangeEnd: 'end of range',
125
125
  };
126
126
 
127
+ /** The keys that have a value: one left `undefined` (or `null`) keeps what's under it. */
128
+ export const compactStrings = (
129
+ overrides?: Partial<PickerStrings>
130
+ ): Partial<PickerStrings> =>
131
+ overrides
132
+ ? (Object.fromEntries(
133
+ Object.entries(overrides).filter(([, v]) => v != null)
134
+ ) as Partial<PickerStrings>)
135
+ : {};
136
+
127
137
  /** The English defaults with the caller's overrides on top. */
128
138
  export const resolveStrings = (
129
139
  overrides?: Partial<PickerStrings>
130
140
  ): Required<PickerStrings> => {
131
- return { ...DEFAULT_STRINGS, ...overrides };
141
+ return { ...DEFAULT_STRINGS, ...compactStrings(overrides) };
132
142
  };
@@ -0,0 +1,12 @@
1
+ import { createContext, useContext } from 'react';
2
+ import { I18nManager } from 'react-native';
3
+
4
+ /**
5
+ * Whether a part of the picker is laid out right to left. The picker sets it
6
+ * for the calendar and the time rows while the Hijri tab shows; elsewhere the
7
+ * app's direction (`I18nManager.isRTL`) decides.
8
+ */
9
+ export const RtlContext = createContext<boolean | null>(null);
10
+
11
+ export const useIsRTL = (): boolean =>
12
+ useContext(RtlContext) ?? I18nManager.isRTL;
@@ -25,6 +25,11 @@ export const STYLE_KEYS = [
25
25
  'fieldText',
26
26
  /** The value's text while the field is disabled (the placeholder keeps its own colour). */
27
27
  'fieldTextDisabled',
28
+ /**
29
+ * Sets the date of a single date-time value apart from its time, e.g.
30
+ * `{ minWidth: '50%' }` starts the time at the middle of the field.
31
+ */
32
+ 'fieldDateText',
28
33
  'fieldPlaceholder',
29
34
  'fieldSegment',
30
35
  'fieldSegmentFocused',
@@ -23,13 +23,13 @@ const create = (t: ThemeTokens) =>
23
23
  fontSize: t.fontSizeLarge,
24
24
  color: t.heading,
25
25
  },
26
- // Times read hh:mm in every language, so the boxes don't mirror in RTL.
27
26
  boxes: {
28
27
  flexDirection: 'row',
29
28
  alignItems: 'center',
30
29
  gap: 22,
31
- direction: 'ltr',
32
30
  },
31
+ // The boxes follow the row's direction: right to left (the Hijri tab, or a
32
+ // right-to-left app), the hour sits on the right and AM / PM on the left.
33
33
  clock: { flexDirection: 'row', alignItems: 'center' },
34
34
  // Each box is 50 × 46 with a 1 pt border and radius 6.
35
35
  boxWrap: { width: 50 },
@@ -104,7 +104,6 @@ const create = (t: ThemeTokens) =>
104
104
  flexDirection: 'row',
105
105
  alignItems: 'center',
106
106
  alignSelf: 'center',
107
- direction: 'ltr',
108
107
  },
109
108
  wheelColumn: { width: 64, height: 200, overflow: 'hidden' },
110
109
  wheelHighlight: {
@@ -391,6 +391,13 @@ export interface DateTimePickerProps {
391
391
  theme?: Partial<ThemeTokens>;
392
392
  styles?: PickerStyles;
393
393
  strings?: Partial<PickerStrings>;
394
+ /**
395
+ * The texts while the Hijri tab shows (e.g. Arabic), for its calendar and
396
+ * time rows, the error messages inside the picker, and a date picked in it
397
+ * (the on-screen field's errors keep `strings`). A key left out (or `undefined`)
398
+ * keeps AM / PM in `hijriLocale` (ص / م), then `strings`.
399
+ */
400
+ hijriStrings?: Partial<PickerStrings>;
394
401
  testID?: string;
395
402
 
396
403
  // events
@@ -255,6 +255,48 @@ const writeHijriDate = (d: CalendarDate, locale: string): string | null => {
255
255
  return `${h.day} ${hijriMonthNames(locale)[h.month - 1]} ${h.year}`;
256
256
  };
257
257
 
258
+ export interface Periods {
259
+ am: string;
260
+ pm: string;
261
+ }
262
+
263
+ const ARABIC_PERIODS: Periods = { am: 'ص', pm: 'م' };
264
+ const periodCache = new Map<string, Periods | null>();
265
+
266
+ /**
267
+ * AM and PM in the Hijri locale's language, as Intl writes them ('ar-SA' →
268
+ * ص / م). An Arabic locale whose engine gives none, or Latin ones, gets ص / م;
269
+ * another locale gets null (the picker keeps strings.am / pm).
270
+ */
271
+ export const hijriPeriods = (locale: string): Periods | null => {
272
+ if (!periodCache.has(locale)) periodCache.set(locale, readPeriods(locale));
273
+ return periodCache.get(locale)!;
274
+ };
275
+
276
+ const readPeriods = (locale: string): Periods | null => {
277
+ const arabic = /^ar\b/i.test(locale);
278
+ try {
279
+ const f = new Intl.DateTimeFormat(locale, {
280
+ timeZone: 'UTC',
281
+ hour: 'numeric',
282
+ hour12: true,
283
+ });
284
+ const period = (hour: number) =>
285
+ f
286
+ .formatToParts(utcDate(2026, 0, 1, hour))
287
+ // Some engines write the part type in lower case.
288
+ .find((p) => p.type.toLowerCase() === 'dayperiod')
289
+ ?.value.trim();
290
+ const am = period(9);
291
+ const pm = period(21);
292
+ if (am && pm && am !== pm && !(arabic && /[A-Za-z]/.test(am + pm)))
293
+ return { am, pm };
294
+ } catch {
295
+ // An engine without this locale falls through.
296
+ }
297
+ return arabic ? ARABIC_PERIODS : null;
298
+ };
299
+
258
300
  /**
259
301
  * The system the picker uses: the requested one, Gregorian by default.
260
302
  * Anything Hijri falls back to Gregorian when unsupported.
package/src/index.tsx CHANGED
@@ -31,7 +31,7 @@ export {
31
31
  todayIn,
32
32
  weekdayOf,
33
33
  } from './core/calendarDate';
34
- export { hijriDateText } from './core/calendarSystem';
34
+ export { hijriDateText, hijriPeriods } from './core/calendarSystem';
35
35
  export { arabicDigits } from './core/numerals';
36
36
 
37
37
  // Types
@@ -71,7 +71,7 @@ export type {
71
71
  SheetSize,
72
72
  Slot,
73
73
  } from './components/types';
74
- export type { CalendarSystem } from './core/calendarSystem';
74
+ export type { CalendarSystem, Periods } from './core/calendarSystem';
75
75
  export type { Numerals } from './core/numerals';
76
76
  export type { PickerStrings } from './components/theme/defaultStrings';
77
77
  export type {