react-native-molecules 0.5.0-beta.16 → 0.5.0-beta.18

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 (87) hide show
  1. package/components/DateField/DateField.tsx +110 -0
  2. package/components/DateField/index.tsx +6 -0
  3. package/components/{DatePickerInput/inputUtils.ts → DateField/useDateFieldState.ts} +17 -49
  4. package/components/DatePicker/DateCalendar.tsx +83 -0
  5. package/components/DatePicker/DatePickerActions.tsx +73 -0
  6. package/components/DatePicker/DatePickerModal.tsx +234 -0
  7. package/components/DatePicker/DatePickerPopover.tsx +79 -0
  8. package/components/DatePicker/DatePickerProvider.tsx +152 -0
  9. package/components/DatePicker/DatePickerTrigger.tsx +23 -0
  10. package/components/DatePicker/context.tsx +82 -0
  11. package/components/DatePicker/index.tsx +44 -0
  12. package/components/DatePicker/utils.ts +292 -0
  13. package/components/DatePickerInline/DatePickerContext.tsx +1 -0
  14. package/components/DatePickerInline/DatePickerDockedHeader.tsx +113 -0
  15. package/components/DatePickerInline/DatePickerInline.tsx +16 -15
  16. package/components/DatePickerInline/DatePickerInlineBase.tsx +7 -1
  17. package/components/DatePickerInline/Day.tsx +25 -1
  18. package/components/DatePickerInline/DayRange.tsx +2 -4
  19. package/components/DatePickerInline/HeaderItem.tsx +42 -27
  20. package/components/DatePickerInline/Month.tsx +45 -65
  21. package/components/DatePickerInline/MonthPicker.tsx +25 -41
  22. package/components/DatePickerInline/Swiper.native.tsx +21 -4
  23. package/components/DatePickerInline/Swiper.tsx +168 -13
  24. package/components/DatePickerInline/Week.tsx +6 -1
  25. package/components/DatePickerInline/YearPicker.tsx +206 -53
  26. package/components/DatePickerInline/dateUtils.tsx +17 -12
  27. package/components/DatePickerInline/types.ts +3 -0
  28. package/components/DatePickerInline/utils.ts +66 -29
  29. package/components/ListItem/ListItem.tsx +3 -1
  30. package/components/ListItem/utils.ts +1 -1
  31. package/components/LoadingIndicator/index.tsx +1 -1
  32. package/components/Menu/Menu.tsx +3 -18
  33. package/components/Popover/Popover.tsx +122 -145
  34. package/components/Popover/PopoverRoot.tsx +74 -0
  35. package/components/Popover/common.ts +50 -34
  36. package/components/Popover/index.ts +18 -1
  37. package/components/Popover/usePlatformMeasure.native.ts +90 -0
  38. package/components/Popover/usePlatformMeasure.ts +118 -0
  39. package/components/Popover/utils.ts +34 -0
  40. package/components/Select/Select.tsx +7 -8
  41. package/components/Select/context.tsx +72 -0
  42. package/components/Select/index.ts +1 -0
  43. package/components/Select/utils.ts +0 -71
  44. package/components/Slot/compose-refs.tsx +2 -0
  45. package/components/TimeField/TimeField.tsx +75 -0
  46. package/components/TimeField/index.tsx +6 -0
  47. package/components/TimeField/useTimeFieldState.ts +70 -0
  48. package/components/{TimePickerField/sanitizeTime.ts → TimeField/utils.ts} +77 -10
  49. package/components/TimePicker/TimePicker.tsx +53 -9
  50. package/components/TimePicker/TimePickerModal.tsx +186 -0
  51. package/components/TimePicker/context.tsx +17 -0
  52. package/components/TimePicker/index.tsx +15 -3
  53. package/components/TimePicker/utils.ts +50 -0
  54. package/hooks/useActionState.tsx +19 -8
  55. package/package.json +6 -1
  56. package/components/DatePickerDocked/DatePickerDocked.tsx +0 -30
  57. package/components/DatePickerDocked/DatePickerDockedHeader.tsx +0 -129
  58. package/components/DatePickerDocked/index.tsx +0 -17
  59. package/components/DatePickerDocked/types.ts +0 -11
  60. package/components/DatePickerDocked/utils.ts +0 -157
  61. package/components/DatePickerInput/DatePickerInput.tsx +0 -130
  62. package/components/DatePickerInput/DatePickerInputModal.tsx +0 -48
  63. package/components/DatePickerInput/DatePickerInputWithoutModal.tsx +0 -73
  64. package/components/DatePickerInput/DateRangeInput.tsx +0 -88
  65. package/components/DatePickerInput/index.tsx +0 -11
  66. package/components/DatePickerInput/types.ts +0 -26
  67. package/components/DatePickerInput/utils.ts +0 -24
  68. package/components/DatePickerModal/AnimatedCrossView.tsx +0 -94
  69. package/components/DatePickerModal/CalendarEdit.tsx +0 -140
  70. package/components/DatePickerModal/DatePickerModal.tsx +0 -85
  71. package/components/DatePickerModal/DatePickerModalContent.tsx +0 -155
  72. package/components/DatePickerModal/DatePickerModalContentHeader.tsx +0 -213
  73. package/components/DatePickerModal/DatePickerModalHeader.tsx +0 -74
  74. package/components/DatePickerModal/DatePickerModalHeaderBackground.tsx +0 -13
  75. package/components/DatePickerModal/index.tsx +0 -16
  76. package/components/DatePickerModal/types.ts +0 -92
  77. package/components/DatePickerModal/utils.ts +0 -122
  78. package/components/DateTimePicker/DateTimePicker.tsx +0 -172
  79. package/components/DateTimePicker/index.tsx +0 -10
  80. package/components/DateTimePicker/utils.ts +0 -12
  81. package/components/Popover/Popover.native.tsx +0 -185
  82. package/components/TimePickerField/TimePickerField.tsx +0 -154
  83. package/components/TimePickerField/index.tsx +0 -10
  84. package/components/TimePickerField/utils.ts +0 -94
  85. package/components/TimePickerModal/TimePickerModal.tsx +0 -119
  86. package/components/TimePickerModal/index.tsx +0 -10
  87. package/components/TimePickerModal/utils.ts +0 -47
@@ -0,0 +1,90 @@
1
+ import { useCallback, useEffect, useLayoutEffect } from 'react';
2
+ import { AppState, Dimensions, Platform } from 'react-native';
3
+
4
+ import { popoverDefaultStyles } from './common';
5
+ import type { UsePlatformMeasureArgs, UsePlatformMeasureResult } from './usePlatformMeasure';
6
+
7
+ export const usePlatformMeasure = ({
8
+ triggerRef,
9
+ isOpen,
10
+ calculatedPosition,
11
+ calculateAndSetPosition,
12
+ targetLayoutRef,
13
+ triggerDimensions,
14
+ }: UsePlatformMeasureArgs): UsePlatformMeasureResult => {
15
+ const measureTarget = useCallback(() => {
16
+ if (triggerRef?.current) {
17
+ triggerRef.current.measure(
18
+ (
19
+ _fx: number,
20
+ _fy: number,
21
+ width: number,
22
+ height: number,
23
+ px: number,
24
+ py: number,
25
+ ) => {
26
+ if (width !== 0 || height !== 0) {
27
+ const newLayout = { x: px, y: py, width, height };
28
+ const changed =
29
+ !targetLayoutRef.current ||
30
+ targetLayoutRef.current.x !== newLayout.x ||
31
+ targetLayoutRef.current.y !== newLayout.y ||
32
+ targetLayoutRef.current.width !== newLayout.width ||
33
+ targetLayoutRef.current.height !== newLayout.height;
34
+
35
+ if (changed) {
36
+ targetLayoutRef.current = newLayout;
37
+ calculateAndSetPosition();
38
+ }
39
+ } else {
40
+ targetLayoutRef.current = null;
41
+ calculateAndSetPosition();
42
+ }
43
+ },
44
+ () => {
45
+ console.error('Failed to measure target element for Popover.');
46
+ targetLayoutRef.current = null;
47
+ calculateAndSetPosition();
48
+ },
49
+ );
50
+ } else {
51
+ targetLayoutRef.current = null;
52
+ calculateAndSetPosition();
53
+ }
54
+ }, [triggerRef, calculateAndSetPosition, targetLayoutRef]);
55
+
56
+ useLayoutEffect(() => {
57
+ if (isOpen) {
58
+ measureTarget();
59
+ }
60
+ }, [isOpen, measureTarget, triggerDimensions]);
61
+
62
+ useEffect(() => {
63
+ if (!isOpen) return;
64
+ const subscription = Dimensions.addEventListener('change', measureTarget);
65
+ return () => {
66
+ if (typeof subscription?.remove === 'function') {
67
+ subscription.remove();
68
+ }
69
+ };
70
+ }, [isOpen, measureTarget]);
71
+
72
+ useEffect(() => {
73
+ if (!isOpen || Platform.OS === 'web') return;
74
+ const handleAppStateChange = (nextAppState: string) => {
75
+ if (nextAppState === 'active') {
76
+ setTimeout(measureTarget, 50);
77
+ }
78
+ };
79
+ const subscription = AppState.addEventListener('change', handleAppStateChange);
80
+ return () => {
81
+ if (typeof subscription?.remove === 'function') {
82
+ subscription.remove();
83
+ }
84
+ };
85
+ }, [isOpen, measureTarget]);
86
+
87
+ return {
88
+ popoverStyle: (calculatedPosition ?? popoverDefaultStyles) as any,
89
+ };
90
+ };
@@ -0,0 +1,118 @@
1
+ import { type RefObject, useCallback, useEffect, useLayoutEffect, useMemo } from 'react';
2
+ import type { LayoutRectangle, View, ViewStyle } from 'react-native';
3
+
4
+ import { popoverDefaultStyles } from './common';
5
+
6
+ export type UsePlatformMeasureArgs = {
7
+ triggerRef: RefObject<View | any> | undefined;
8
+ isOpen: boolean;
9
+ onClose?: () => void;
10
+ calculatedPosition: ViewStyle | null;
11
+ calculateAndSetPosition: () => void;
12
+ targetLayoutRef: RefObject<LayoutRectangle | null>;
13
+ popoverRef: RefObject<View | null>;
14
+ triggerDimensions?: { width: number; height: number } | null;
15
+ };
16
+
17
+ export type UsePlatformMeasureResult = {
18
+ /** Platform-adjusted popover position (includes scroll offset on web) */
19
+ popoverStyle: ViewStyle;
20
+ };
21
+
22
+ export const usePlatformMeasure = ({
23
+ triggerRef,
24
+ isOpen,
25
+ onClose,
26
+ calculatedPosition,
27
+ calculateAndSetPosition,
28
+ targetLayoutRef,
29
+ popoverRef,
30
+ triggerDimensions,
31
+ }: UsePlatformMeasureArgs): UsePlatformMeasureResult => {
32
+ const measureTarget = useCallback(() => {
33
+ if (triggerRef?.current) {
34
+ triggerRef.current.measureInWindow(
35
+ (x: number, y: number, width: number, height: number) => {
36
+ if (width !== 0 || height !== 0) {
37
+ const newLayout = { x, y, width, height };
38
+ const changed =
39
+ !targetLayoutRef.current ||
40
+ targetLayoutRef.current.x !== newLayout.x ||
41
+ targetLayoutRef.current.y !== newLayout.y ||
42
+ targetLayoutRef.current.width !== newLayout.width ||
43
+ targetLayoutRef.current.height !== newLayout.height;
44
+
45
+ if (changed) {
46
+ targetLayoutRef.current = newLayout;
47
+ calculateAndSetPosition();
48
+ }
49
+ } else {
50
+ targetLayoutRef.current = null;
51
+ calculateAndSetPosition();
52
+ }
53
+ },
54
+ );
55
+ } else {
56
+ targetLayoutRef.current = null;
57
+ calculateAndSetPosition();
58
+ }
59
+ }, [triggerRef, calculateAndSetPosition, targetLayoutRef]);
60
+
61
+ useLayoutEffect(() => {
62
+ if (isOpen) {
63
+ const timeoutId = setTimeout(measureTarget, 0);
64
+ return () => clearTimeout(timeoutId);
65
+ }
66
+ return;
67
+ }, [isOpen, measureTarget, triggerDimensions]);
68
+
69
+ useLayoutEffect(() => {
70
+ if (!isOpen) return;
71
+ const handleResize = () => {
72
+ if (triggerRef?.current && isOpen) {
73
+ window.requestAnimationFrame(measureTarget);
74
+ }
75
+ };
76
+ window.addEventListener('resize', handleResize);
77
+ window.addEventListener('scroll', handleResize, true);
78
+ return () => {
79
+ window.removeEventListener('resize', handleResize);
80
+ window.removeEventListener('scroll', handleResize, true);
81
+ };
82
+ }, [isOpen, measureTarget, triggerRef]);
83
+
84
+ useEffect(() => {
85
+ if (!isOpen || !onClose) return;
86
+ const handleClickOutside = (event: MouseEvent) => {
87
+ const popoverElement = popoverRef.current as any as HTMLElement;
88
+ const targetElement = triggerRef?.current as any as HTMLElement;
89
+ if (
90
+ popoverElement &&
91
+ !popoverElement.contains(event.target as Node) &&
92
+ targetElement &&
93
+ !targetElement.contains(event.target as Node)
94
+ ) {
95
+ onClose();
96
+ }
97
+ };
98
+ document.addEventListener('mousedown', handleClickOutside, { capture: true });
99
+ return () => {
100
+ document.removeEventListener('mousedown', handleClickOutside, { capture: true });
101
+ };
102
+ }, [isOpen, onClose, popoverRef, triggerRef]);
103
+
104
+ const popoverStyle = useMemo(() => {
105
+ if (!calculatedPosition) return popoverDefaultStyles;
106
+
107
+ const scrollX = window.scrollX ?? window.pageXOffset ?? 0;
108
+ const scrollY = window.scrollY ?? window.pageYOffset ?? 0;
109
+
110
+ return {
111
+ ...calculatedPosition,
112
+ left: (calculatedPosition.left as number) + scrollX,
113
+ top: (calculatedPosition.top as number) + scrollY,
114
+ };
115
+ }, [calculatedPosition]);
116
+
117
+ return { popoverStyle };
118
+ };
@@ -0,0 +1,34 @@
1
+ import { StyleSheet } from 'react-native-unistyles';
2
+
3
+ import { getRegisteredComponentStylesWithFallback } from '../../core';
4
+ import { popoverDefaultStyles } from './common';
5
+
6
+ const popoverStylesDefault = StyleSheet.create(theme => ({
7
+ popoverContainer: {
8
+ ...popoverDefaultStyles,
9
+ backgroundColor: theme.colors.surface,
10
+ borderRadius: 4,
11
+ shadowColor: 'rgba(0, 0, 0, 1)',
12
+ shadowOffset: { width: 0, height: 2 },
13
+ shadowOpacity: theme.dark ? 0.7 : 0.3,
14
+ shadowRadius: 10,
15
+ elevation: 5,
16
+ zIndex: 100,
17
+ },
18
+ overlay: {
19
+ position: 'absolute',
20
+ top: 0,
21
+ left: 0,
22
+ right: 0,
23
+ bottom: 0,
24
+ backgroundColor: 'transparent',
25
+ _web: {
26
+ cursor: 'default',
27
+ },
28
+ },
29
+ }));
30
+
31
+ export const popoverStyles = getRegisteredComponentStylesWithFallback(
32
+ 'Popover',
33
+ popoverStylesDefault,
34
+ );
@@ -19,6 +19,12 @@ import { IconButton } from '../IconButton';
19
19
  import { Popover } from '../Popover';
20
20
  import { Text } from '../Text';
21
21
  import { TextInput, type TextInputHandles, type TextInputProps } from '../TextInput';
22
+ import {
23
+ SelectContextProvider,
24
+ SelectDropdownContextProvider,
25
+ useSelectContextValue,
26
+ useSelectDropdownContextValue,
27
+ } from './context';
22
28
  import type {
23
29
  DefaultItemT,
24
30
  SelectContentProps,
@@ -31,14 +37,7 @@ import type {
31
37
  SelectTriggerProps,
32
38
  SelectValueProps,
33
39
  } from './types';
34
- import {
35
- SelectContextProvider,
36
- SelectDropdownContextProvider,
37
- styles,
38
- triggerStyles,
39
- useSelectContextValue,
40
- useSelectDropdownContextValue,
41
- } from './utils';
40
+ import { styles, triggerStyles } from './utils';
42
41
 
43
42
  const emptyArr: unknown[] = [];
44
43
 
@@ -0,0 +1,72 @@
1
+ import type { View } from 'react-native';
2
+
3
+ import { createFastContext } from '../../fast-context';
4
+ import { registerPortalContext } from '../Portal';
5
+ import type { DefaultItemT, SelectContextValue, SelectDropdownContextValue } from './types';
6
+
7
+ // SelectContext - holds value, onAdd, onRemove with fast-context for optimized rendering
8
+ const selectContextDefaultValue: SelectContextValue<DefaultItemT> = {
9
+ value: null,
10
+ multiple: false,
11
+ onAdd: () => {},
12
+ onRemove: () => {},
13
+ disabled: false,
14
+ error: false,
15
+ labelKey: 'label',
16
+ options: [],
17
+ searchQuery: '',
18
+ setSearchQuery: () => {},
19
+ filteredOptions: [],
20
+ };
21
+
22
+ const {
23
+ useStoreRef: useSelectStoreRef,
24
+ Provider: SelectContextProvider,
25
+ useContext: useSelectContext,
26
+ useContextValue: useSelectContextValue,
27
+ Context: SelectContext,
28
+ } = createFastContext<SelectContextValue<DefaultItemT>>(selectContextDefaultValue, true);
29
+
30
+ export {
31
+ SelectContext,
32
+ SelectContextProvider,
33
+ useSelectContext,
34
+ useSelectContextValue,
35
+ useSelectStoreRef,
36
+ };
37
+
38
+ // SelectDropdownContext - holds isOpen, onClose, triggerRef with fast-context
39
+ export type SelectDropdownContextType = SelectDropdownContextValue & {
40
+ triggerRef: React.RefObject<View> | null;
41
+ contentRef: React.RefObject<any> | null;
42
+ triggerLayout: { width: number; height: number } | null;
43
+ setTriggerLayout: (layout: { width: number; height: number }) => void;
44
+ };
45
+
46
+ const selectDropdownContextDefaultValue: SelectDropdownContextType = {
47
+ isOpen: false,
48
+ onClose: () => {},
49
+ onOpen: () => {},
50
+ triggerRef: null,
51
+ contentRef: null,
52
+ triggerLayout: null,
53
+ setTriggerLayout: () => {},
54
+ };
55
+
56
+ const {
57
+ useStoreRef: useSelectDropdownStoreRef,
58
+ Provider: SelectDropdownContextProvider,
59
+ useContext: useSelectDropdownContext,
60
+ useContextValue: useSelectDropdownContextValue,
61
+ Context: SelectDropdownContext,
62
+ } = createFastContext<SelectDropdownContextType>(selectDropdownContextDefaultValue, true);
63
+
64
+ export {
65
+ SelectDropdownContext,
66
+ SelectDropdownContextProvider,
67
+ useSelectDropdownContext,
68
+ useSelectDropdownContextValue,
69
+ useSelectDropdownStoreRef,
70
+ };
71
+
72
+ registerPortalContext([SelectContext, SelectDropdownContext]);
@@ -3,5 +3,6 @@ import SelectDefault from './Select';
3
3
 
4
4
  export const Select = getRegisteredComponentWithFallback('Select', SelectDefault);
5
5
 
6
+ export * from './context';
6
7
  export type * from './types';
7
8
  export * from './utils';
@@ -1,77 +1,6 @@
1
- import type { View } from 'react-native';
2
1
  import { StyleSheet } from 'react-native-unistyles';
3
2
 
4
3
  import { getRegisteredComponentStylesWithFallback } from '../../core';
5
- import { createFastContext } from '../../fast-context';
6
- import { registerPortalContext } from '../Portal';
7
- import type { DefaultItemT, SelectContextValue, SelectDropdownContextValue } from './types';
8
-
9
- // SelectContext - holds value, onAdd, onRemove with fast-context for optimized rendering
10
- const selectContextDefaultValue: SelectContextValue<DefaultItemT> = {
11
- value: null,
12
- multiple: false,
13
- onAdd: () => {},
14
- onRemove: () => {},
15
- disabled: false,
16
- error: false,
17
- labelKey: 'label',
18
- options: [],
19
- searchQuery: '',
20
- setSearchQuery: () => {},
21
- filteredOptions: [],
22
- };
23
-
24
- const {
25
- useStoreRef: useSelectStoreRef,
26
- Provider: SelectContextProvider,
27
- useContext: useSelectContext,
28
- useContextValue: useSelectContextValue,
29
- Context: SelectContext,
30
- } = createFastContext<SelectContextValue<DefaultItemT>>(selectContextDefaultValue, true);
31
-
32
- export {
33
- SelectContext,
34
- SelectContextProvider,
35
- useSelectContext,
36
- useSelectContextValue,
37
- useSelectStoreRef,
38
- };
39
-
40
- // SelectDropdownContext - holds isOpen, onClose, triggerRef with fast-context
41
- export type SelectDropdownContextType = SelectDropdownContextValue & {
42
- triggerRef: React.RefObject<View> | null;
43
- contentRef: React.RefObject<any> | null;
44
- triggerLayout: { width: number; height: number } | null;
45
- setTriggerLayout: (layout: { width: number; height: number }) => void;
46
- };
47
-
48
- const selectDropdownContextDefaultValue: SelectDropdownContextType = {
49
- isOpen: false,
50
- onClose: () => {},
51
- onOpen: () => {},
52
- triggerRef: null,
53
- contentRef: null,
54
- triggerLayout: null,
55
- setTriggerLayout: () => {},
56
- };
57
-
58
- const {
59
- useStoreRef: useSelectDropdownStoreRef,
60
- Provider: SelectDropdownContextProvider,
61
- useContext: useSelectDropdownContext,
62
- useContextValue: useSelectDropdownContextValue,
63
- Context: SelectDropdownContext,
64
- } = createFastContext<SelectDropdownContextType>(selectDropdownContextDefaultValue, true);
65
-
66
- export {
67
- SelectDropdownContext,
68
- SelectDropdownContextProvider,
69
- useSelectDropdownContext,
70
- useSelectDropdownContextValue,
71
- useSelectDropdownStoreRef,
72
- };
73
-
74
- registerPortalContext([SelectContext, SelectDropdownContext]);
75
4
 
76
5
  const triggerDefaultStyles = StyleSheet.create(theme => ({
77
6
  trigger: {
@@ -45,6 +45,8 @@ function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
45
45
  }
46
46
  };
47
47
  }
48
+
49
+ return undefined;
48
50
  };
49
51
  }
50
52
 
@@ -0,0 +1,75 @@
1
+ import { memo, useCallback, useMemo } from 'react';
2
+
3
+ import type { DatePickerValue, RangeValue } from '../DatePicker/context';
4
+ import { useDatePickerContext } from '../DatePicker/context';
5
+ import { TextInput } from '../TextInput';
6
+ import type { Props as TextInputProps } from '../TextInput/TextInput';
7
+ import { useTimeFieldState } from './useTimeFieldState';
8
+ import { timeFormat } from './utils';
9
+
10
+ export type TimeFieldProps = Omit<TextInputProps, 'value' | 'defaultValue' | 'onChangeText'>;
11
+
12
+ const isRange = (value: DatePickerValue): value is RangeValue =>
13
+ value !== null && typeof value === 'object' && 'start' in value && 'end' in value;
14
+
15
+ const toTimeString = (value: Date | null): string => {
16
+ if (!value) return '';
17
+ const h = value.getHours().toString().padStart(2, '0');
18
+ const m = value.getMinutes().toString().padStart(2, '0');
19
+ return `${h}:${m}`;
20
+ };
21
+
22
+ const applyTimeToDate = (base: Date | null, time: string): Date | null => {
23
+ if (!time) return null;
24
+ const [h, m] = time.split(':').map(n => parseInt(n, 10));
25
+ if (Number.isNaN(h) || Number.isNaN(m)) return base;
26
+ const next = base ? new Date(base) : new Date();
27
+ next.setHours(h, m, 0, 0);
28
+ return next;
29
+ };
30
+
31
+ function TimeField({ ref, disabled: disabledProp, onBlur, onFocus, ...rest }: TimeFieldProps) {
32
+ const { value, commitValue, is24Hour, disabled: providerDisabled } = useDatePickerContext();
33
+
34
+ const disabled = disabledProp ?? providerDisabled;
35
+
36
+ const dateValue = useMemo<Date | null>(() => (isRange(value) ? null : value), [value]);
37
+
38
+ const timeString = useMemo(() => toTimeString(dateValue), [dateValue]);
39
+
40
+ const onChange = useCallback(
41
+ (next: string) => {
42
+ commitValue(applyTimeToDate(dateValue, next));
43
+ },
44
+ [dateValue, commitValue],
45
+ );
46
+
47
+ const {
48
+ timeString: formatted,
49
+ onChangeText,
50
+ onBlur: onInnerBlur,
51
+ onFocus: onInnerFocus,
52
+ } = useTimeFieldState({
53
+ time: timeString,
54
+ is24Hour,
55
+ disabled,
56
+ onChange,
57
+ onBlur,
58
+ onFocus,
59
+ });
60
+
61
+ return (
62
+ <TextInput
63
+ {...rest}
64
+ ref={ref}
65
+ disabled={disabled}
66
+ value={formatted}
67
+ placeholder={timeFormat[is24Hour ? '24' : '12'].format}
68
+ onChangeText={onChangeText}
69
+ onBlur={onInnerBlur}
70
+ onFocus={onInnerFocus}
71
+ />
72
+ );
73
+ }
74
+
75
+ export default memo(TimeField);
@@ -0,0 +1,6 @@
1
+ import { getRegisteredComponentWithFallback } from '../../core';
2
+ import TimeFieldDefault from './TimeField';
3
+
4
+ export const TimeField = getRegisteredComponentWithFallback('TimeField', TimeFieldDefault);
5
+
6
+ export type { TimeFieldProps } from './TimeField';
@@ -0,0 +1,70 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import type { BlurEvent, FocusEvent } from 'react-native';
3
+
4
+ import { getFormattedTime, getOutputTime, sanitizeTime } from './utils';
5
+
6
+ type Props = {
7
+ time: string;
8
+ is24Hour: boolean;
9
+ disabled?: boolean;
10
+ onChange?: (time: string) => void;
11
+ onBlur?: (e: BlurEvent) => void;
12
+ onFocus?: (e: FocusEvent) => void;
13
+ };
14
+
15
+ export function useTimeFieldState({
16
+ time,
17
+ is24Hour,
18
+ disabled,
19
+ onChange,
20
+ onBlur: onBlurProp,
21
+ onFocus: onFocusProp,
22
+ }: Props) {
23
+ const [timeString, setTimeString] = useState(() => getFormattedTime({ time, is24Hour }));
24
+ const isBlurredRef = useRef(true);
25
+
26
+ const onChangeText = useCallback(
27
+ (_text: string) => {
28
+ const text = sanitizeTime(_text, is24Hour);
29
+ setTimeString(_text);
30
+
31
+ if (disabled || !text) return;
32
+
33
+ onChange?.(getOutputTime({ time: text || time, is24Hour }));
34
+ },
35
+ [disabled, is24Hour, onChange, time],
36
+ );
37
+
38
+ const onBlur = useCallback(
39
+ (e: BlurEvent) => {
40
+ isBlurredRef.current = true;
41
+ onBlurProp?.(e);
42
+
43
+ if (disabled) return;
44
+
45
+ setTimeString(sanitizeTime(getFormattedTime({ time, is24Hour }), is24Hour));
46
+ },
47
+ [disabled, is24Hour, onBlurProp, time],
48
+ );
49
+
50
+ const onFocus = useCallback(
51
+ (e: FocusEvent) => {
52
+ isBlurredRef.current = false;
53
+ onFocusProp?.(e);
54
+ },
55
+ [onFocusProp],
56
+ );
57
+
58
+ useEffect(() => {
59
+ if (!isBlurredRef.current) return;
60
+
61
+ setTimeString(getFormattedTime({ time, is24Hour }));
62
+ }, [is24Hour, time]);
63
+
64
+ return {
65
+ timeString,
66
+ onChangeText,
67
+ onBlur,
68
+ onFocus,
69
+ };
70
+ }