react-native-molecules 0.5.0-beta.17 → 0.5.0-beta.19
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.
- package/components/DatePicker/utils.ts +1 -0
- package/components/DatePickerInline/HeaderItem.tsx +1 -1
- package/components/Menu/Menu.tsx +3 -18
- package/components/Popover/Popover.tsx +121 -122
- package/components/Popover/PopoverRoot.tsx +74 -0
- package/components/Popover/common.ts +50 -34
- package/components/Popover/index.ts +18 -1
- package/components/Popover/{Popover.native.tsx → usePlatformMeasure.native.ts} +12 -86
- package/components/Popover/usePlatformMeasure.ts +118 -0
- package/components/Popover/utils.ts +2 -9
- package/components/TimePicker/TimeInput.tsx +87 -37
- package/components/TimePicker/TimeInputs.tsx +131 -49
- package/components/TimePicker/TimePicker.tsx +6 -1
- package/components/TimePicker/utils.ts +43 -0
- package/package.json +1 -1
|
@@ -1,52 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { AppState, Dimensions, Platform
|
|
3
|
-
import { ScopedTheme, UnistylesRuntime } from 'react-native-unistyles';
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect } from 'react';
|
|
2
|
+
import { AppState, Dimensions, Platform } from 'react-native';
|
|
4
3
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
DEFAULT_ARROW_SIZE,
|
|
8
|
-
popoverDefaultStyles,
|
|
9
|
-
type PopoverProps,
|
|
10
|
-
useArrowStyles,
|
|
11
|
-
usePopover,
|
|
12
|
-
} from './common';
|
|
13
|
-
import { popoverStyles } from './utils';
|
|
4
|
+
import { popoverDefaultStyles } from './common';
|
|
5
|
+
import type { UsePlatformMeasureArgs, UsePlatformMeasureResult } from './usePlatformMeasure';
|
|
14
6
|
|
|
15
|
-
const
|
|
7
|
+
export const usePlatformMeasure = ({
|
|
16
8
|
triggerRef,
|
|
17
|
-
children,
|
|
18
9
|
isOpen,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
style,
|
|
23
|
-
showArrow = false,
|
|
24
|
-
arrowSize = DEFAULT_ARROW_SIZE,
|
|
25
|
-
inverted = false,
|
|
10
|
+
calculatedPosition,
|
|
11
|
+
calculateAndSetPosition,
|
|
12
|
+
targetLayoutRef,
|
|
26
13
|
triggerDimensions,
|
|
27
|
-
|
|
28
|
-
...rest
|
|
29
|
-
}: PopoverProps) => {
|
|
30
|
-
const {
|
|
31
|
-
popoverLayoutRef,
|
|
32
|
-
targetLayoutRef,
|
|
33
|
-
actualPositionRef,
|
|
34
|
-
calculatedPosition,
|
|
35
|
-
calculateAndSetPosition,
|
|
36
|
-
handlePopoverLayout,
|
|
37
|
-
} = usePopover({
|
|
38
|
-
isOpen,
|
|
39
|
-
position,
|
|
40
|
-
align,
|
|
41
|
-
showArrow,
|
|
42
|
-
arrowSize,
|
|
43
|
-
offset,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const popoverRef = useRef<View>(null);
|
|
47
|
-
|
|
14
|
+
}: UsePlatformMeasureArgs): UsePlatformMeasureResult => {
|
|
48
15
|
const measureTarget = useCallback(() => {
|
|
49
|
-
if (triggerRef
|
|
16
|
+
if (triggerRef?.current) {
|
|
50
17
|
triggerRef.current.measure(
|
|
51
18
|
(
|
|
52
19
|
_fx: number,
|
|
@@ -117,48 +84,7 @@ const Popover = ({
|
|
|
117
84
|
};
|
|
118
85
|
}, [isOpen, measureTarget]);
|
|
119
86
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
arrowSize,
|
|
123
|
-
style,
|
|
124
|
-
calculatedPosition,
|
|
125
|
-
targetLayoutRef,
|
|
126
|
-
popoverLayoutRef,
|
|
127
|
-
actualPositionRef,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const popoverStyle = calculatedPosition ?? popoverDefaultStyles;
|
|
131
|
-
const Wrapper = inverted ? ScopedTheme : Fragment;
|
|
132
|
-
|
|
133
|
-
if (!isOpen && popoverStyle.opacity === 0) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const handleOutsidePress = () => {
|
|
138
|
-
if (isOpen && onClose) {
|
|
139
|
-
onClose();
|
|
140
|
-
}
|
|
87
|
+
return {
|
|
88
|
+
popoverStyle: (calculatedPosition ?? popoverDefaultStyles) as any,
|
|
141
89
|
};
|
|
142
|
-
|
|
143
|
-
return (
|
|
144
|
-
<Portal>
|
|
145
|
-
<Wrapper
|
|
146
|
-
{...(inverted
|
|
147
|
-
? { name: UnistylesRuntime.themeName === 'dark' ? 'light' : 'dark' }
|
|
148
|
-
: ({} as { name: 'light' }))}>
|
|
149
|
-
<Pressable onPress={handleOutsidePress} style={popoverStyles.overlay} />
|
|
150
|
-
|
|
151
|
-
<View
|
|
152
|
-
ref={popoverRef}
|
|
153
|
-
onLayout={handlePopoverLayout}
|
|
154
|
-
style={[popoverStyles.popoverContainer, style, popoverStyle]}
|
|
155
|
-
{...rest}>
|
|
156
|
-
{children}
|
|
157
|
-
{showArrow && popoverStyle.opacity === 1 && <View style={arrowStyles} />}
|
|
158
|
-
</View>
|
|
159
|
-
</Wrapper>
|
|
160
|
-
</Portal>
|
|
161
|
-
);
|
|
162
90
|
};
|
|
163
|
-
|
|
164
|
-
export default memo(Popover);
|
|
@@ -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
|
+
};
|
|
@@ -15,24 +15,17 @@ const popoverStylesDefault = StyleSheet.create(theme => ({
|
|
|
15
15
|
elevation: 5,
|
|
16
16
|
zIndex: 100,
|
|
17
17
|
},
|
|
18
|
-
|
|
18
|
+
overlay: {
|
|
19
19
|
position: 'absolute',
|
|
20
20
|
top: 0,
|
|
21
21
|
left: 0,
|
|
22
22
|
right: 0,
|
|
23
23
|
bottom: 0,
|
|
24
|
+
backgroundColor: 'transparent',
|
|
24
25
|
_web: {
|
|
25
26
|
cursor: 'default',
|
|
26
27
|
},
|
|
27
28
|
},
|
|
28
|
-
overlay: {
|
|
29
|
-
position: 'absolute',
|
|
30
|
-
top: 0,
|
|
31
|
-
bottom: 0,
|
|
32
|
-
left: 0,
|
|
33
|
-
right: 0,
|
|
34
|
-
backgroundColor: 'transparent',
|
|
35
|
-
},
|
|
36
29
|
}));
|
|
37
30
|
|
|
38
31
|
export const popoverStyles = getRegisteredComponentStylesWithFallback(
|
|
@@ -1,24 +1,28 @@
|
|
|
1
|
-
import { forwardRef, memo, useCallback, useMemo, useState } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import { forwardRef, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
type NativeSyntheticEvent,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
TextInput as NativeTextInput,
|
|
6
|
+
type TextInputProps as NativeTextInputProps,
|
|
7
|
+
type TextInputSelectionChangeEventData,
|
|
8
|
+
View,
|
|
9
|
+
} from 'react-native';
|
|
3
10
|
|
|
4
11
|
import { useTheme } from '../../hooks';
|
|
5
12
|
import { resolveStateVariant } from '../../utils';
|
|
6
|
-
import { TextInput, type TextInputProps } from '../TextInput';
|
|
7
13
|
import { TouchableRipple } from '../TouchableRipple';
|
|
8
14
|
import { inputTypes, type PossibleClockTypes, type PossibleInputTypes } from './timeUtils';
|
|
9
15
|
import { timePickerInputStyles } from './utils';
|
|
10
16
|
|
|
11
|
-
interface TimeInputProps
|
|
12
|
-
extends Omit<
|
|
13
|
-
Omit<TextInputProps, 'value' | 'variant' | 'onChangeText' | 'onPress'>,
|
|
14
|
-
'onFocus'
|
|
15
|
-
> {
|
|
17
|
+
interface TimeInputProps extends Omit<NativeTextInputProps, 'value' | 'onChangeText' | 'onPress'> {
|
|
16
18
|
value: number;
|
|
17
19
|
clockType: PossibleClockTypes;
|
|
18
20
|
onPress?: (type: PossibleClockTypes) => any;
|
|
19
21
|
pressed: boolean;
|
|
20
|
-
onChanged: (n: number) => any;
|
|
22
|
+
onChanged: (n: number, text: string) => any;
|
|
21
23
|
inputType: PossibleInputTypes;
|
|
24
|
+
error?: boolean;
|
|
25
|
+
inputStyle?: NativeTextInputProps['style'];
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
function TimeInput(
|
|
@@ -29,18 +33,22 @@ function TimeInput(
|
|
|
29
33
|
onPress,
|
|
30
34
|
onChanged,
|
|
31
35
|
inputType,
|
|
36
|
+
error = false,
|
|
32
37
|
inputStyle,
|
|
33
38
|
style,
|
|
34
39
|
...rest
|
|
35
40
|
}: TimeInputProps,
|
|
36
41
|
ref: any,
|
|
37
42
|
) {
|
|
38
|
-
const onInnerChange = (text: string) => {
|
|
39
|
-
onChanged(Number(text));
|
|
40
|
-
};
|
|
41
|
-
|
|
42
43
|
const theme = useTheme();
|
|
43
44
|
const [inputFocused, setInputFocused] = useState<boolean>(false);
|
|
45
|
+
const [rawText, setRawText] = useState<string | null>(null);
|
|
46
|
+
const [selection, setSelection] = useState<{ start: number; end: number } | undefined>();
|
|
47
|
+
|
|
48
|
+
const onInnerChange = (text: string) => {
|
|
49
|
+
setRawText(text);
|
|
50
|
+
onChanged(Number(text), text);
|
|
51
|
+
};
|
|
44
52
|
|
|
45
53
|
const highlighted = inputType === inputTypes.picker ? pressed : inputFocused;
|
|
46
54
|
|
|
@@ -52,46 +60,88 @@ function TimeInput(
|
|
|
52
60
|
});
|
|
53
61
|
|
|
54
62
|
const formattedValue = useMemo(() => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (!inputFocused) {
|
|
58
|
-
_formattedValue = `${value}`.length === 1 ? `0${value}` : `${value}`;
|
|
59
|
-
}
|
|
63
|
+
if (rawText !== null && (inputFocused || error)) return rawText;
|
|
60
64
|
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
const str = `${value}`;
|
|
66
|
+
return str.length === 1 ? `0${str}` : str;
|
|
67
|
+
}, [value, inputFocused, rawText, error]);
|
|
63
68
|
|
|
64
|
-
const { rippleColor, containerStyle,
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
const { rippleColor, containerStyle, textInputStyle, buttonStyle } = useMemo(() => {
|
|
70
|
+
const {
|
|
71
|
+
container,
|
|
72
|
+
input,
|
|
73
|
+
keyboardInput,
|
|
74
|
+
keyboardInputHighlighted,
|
|
75
|
+
inputError,
|
|
76
|
+
keyboardInputError,
|
|
77
|
+
button,
|
|
78
|
+
} = timePickerInputStyles;
|
|
79
|
+
const isKeyboardInput = inputType === inputTypes.keyboard;
|
|
67
80
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
return {
|
|
82
|
+
rippleColor: timePickerInputStyles.root?._rippleColor,
|
|
83
|
+
containerStyle: container,
|
|
84
|
+
textInputStyle: [
|
|
85
|
+
input,
|
|
86
|
+
isKeyboardInput ? keyboardInput : null,
|
|
87
|
+
isKeyboardInput && highlighted ? keyboardInputHighlighted : null,
|
|
88
|
+
error ? inputError : null,
|
|
89
|
+
isKeyboardInput && error ? keyboardInputError : null,
|
|
90
|
+
style,
|
|
91
|
+
inputStyle,
|
|
92
|
+
],
|
|
93
|
+
buttonStyle: [StyleSheet.absoluteFill, button],
|
|
94
|
+
};
|
|
95
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
|
+
}, [error, highlighted, inputStyle, inputType, style, state]);
|
|
77
97
|
|
|
78
98
|
const onFocus = useCallback(() => setInputFocused(true), []);
|
|
79
|
-
|
|
99
|
+
|
|
100
|
+
const onBlur = useCallback(() => {
|
|
101
|
+
setInputFocused(false);
|
|
102
|
+
setSelection(undefined);
|
|
103
|
+
if (!error) {
|
|
104
|
+
setRawText(null);
|
|
105
|
+
}
|
|
106
|
+
}, [error]);
|
|
107
|
+
|
|
80
108
|
const onPressInput = useCallback(() => onPress?.(clockType), [clockType, onPress]);
|
|
81
109
|
|
|
110
|
+
const onSelectionChange = useCallback(
|
|
111
|
+
(e: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
|
|
112
|
+
if (selection) {
|
|
113
|
+
setSelection(undefined);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
rest.onSelectionChange?.(e);
|
|
117
|
+
},
|
|
118
|
+
[rest, selection],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (error && inputFocused && rawText?.length) {
|
|
123
|
+
setSelection({ start: 0, end: rawText.length });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
setSelection(undefined);
|
|
128
|
+
}, [error, inputFocused, rawText]);
|
|
129
|
+
|
|
82
130
|
return (
|
|
83
131
|
<View style={containerStyle}>
|
|
84
|
-
<
|
|
85
|
-
variant="plain"
|
|
132
|
+
<NativeTextInput
|
|
86
133
|
ref={ref}
|
|
87
|
-
inputStyle={textInputStyle}
|
|
88
|
-
style={textInputContainerStyle}
|
|
89
134
|
onFocus={onFocus}
|
|
90
135
|
onBlur={onBlur}
|
|
91
136
|
keyboardAppearance={theme.dark ? 'dark' : 'default'}
|
|
92
137
|
value={formattedValue}
|
|
93
138
|
maxLength={2}
|
|
139
|
+
placeholderTextColor={theme.colors.onSurfaceVariant}
|
|
140
|
+
selectTextOnFocus={inputType === inputTypes.picker || error}
|
|
141
|
+
selection={selection}
|
|
142
|
+
onSelectionChange={onSelectionChange}
|
|
94
143
|
onChangeText={onInnerChange}
|
|
144
|
+
style={textInputStyle}
|
|
95
145
|
{...rest}
|
|
96
146
|
/>
|
|
97
147
|
<>
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// @typescript-eslint/no-unused-vars
|
|
2
2
|
// WORK IN PROGRESS
|
|
3
3
|
|
|
4
|
-
import { memo, useCallback, useRef } from 'react';
|
|
4
|
+
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
|
5
5
|
import { TextInput as TextInputNative, useWindowDimensions, View } from 'react-native';
|
|
6
6
|
|
|
7
7
|
import { useLatest } from '../../hooks';
|
|
8
8
|
import { resolveStateVariant } from '../../utils';
|
|
9
|
+
import { Text } from '../Text';
|
|
9
10
|
import AmPmSwitcher from './AmPmSwitcher';
|
|
10
11
|
import TimeInput from './TimeInput';
|
|
11
12
|
import {
|
|
@@ -41,6 +42,8 @@ function TimeInputs({
|
|
|
41
42
|
}: Props) {
|
|
42
43
|
const dimensions = useWindowDimensions();
|
|
43
44
|
const isLandscape = dimensions.width > dimensions.height;
|
|
45
|
+
const [hourError, setHourError] = useState(false);
|
|
46
|
+
const [minuteError, setMinuteError] = useState(false);
|
|
44
47
|
|
|
45
48
|
timePickerInputsStyles.useVariants({
|
|
46
49
|
state: resolveStateVariant({
|
|
@@ -50,6 +53,17 @@ function TimeInputs({
|
|
|
50
53
|
const startInput = useRef<TextInputNative | null>(null);
|
|
51
54
|
const endInput = useRef<TextInputNative | null>(null);
|
|
52
55
|
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (inputType !== 'keyboard') {
|
|
58
|
+
setHourError(false);
|
|
59
|
+
setMinuteError(false);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const id = setTimeout(() => startInput.current?.focus(), 0);
|
|
64
|
+
return () => clearTimeout(id);
|
|
65
|
+
}, [inputType]);
|
|
66
|
+
|
|
53
67
|
const onSubmitStartInput = useCallback(() => {
|
|
54
68
|
if (endInput.current) {
|
|
55
69
|
endInput.current.focus();
|
|
@@ -61,6 +75,9 @@ function TimeInputs({
|
|
|
61
75
|
}, []);
|
|
62
76
|
|
|
63
77
|
const minutesRef = useLatest(minutes);
|
|
78
|
+
const isPm = hours >= 12;
|
|
79
|
+
const hourErrorText = is24Hour ? 'Hour must be 0-23' : 'Hour must be 1-12';
|
|
80
|
+
const minuteErrorText = 'Minute must be 0-59';
|
|
64
81
|
const onChangeHours = useCallback(
|
|
65
82
|
(newHours: number) => {
|
|
66
83
|
onChange({
|
|
@@ -73,74 +90,139 @@ function TimeInputs({
|
|
|
73
90
|
);
|
|
74
91
|
|
|
75
92
|
const onHourChange = useCallback(
|
|
76
|
-
(newHoursFromInput: number) => {
|
|
93
|
+
(newHoursFromInput: number, text: string) => {
|
|
94
|
+
if (text.length === 0) {
|
|
95
|
+
setHourError(false);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isNumeric = /^\d+$/.test(text);
|
|
100
|
+
const minHours = is24Hour ? 0 : 1;
|
|
101
|
+
const maxHours = is24Hour ? 23 : 12;
|
|
102
|
+
const isValid =
|
|
103
|
+
isNumeric && newHoursFromInput >= minHours && newHoursFromInput <= maxHours;
|
|
104
|
+
|
|
105
|
+
setHourError(!isValid);
|
|
106
|
+
|
|
107
|
+
if (!isValid) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
77
111
|
let newHours = newHoursFromInput;
|
|
78
|
-
if (
|
|
79
|
-
|
|
112
|
+
if (!is24Hour) {
|
|
113
|
+
if (isPm) {
|
|
114
|
+
newHours = newHoursFromInput === 12 ? 12 : newHoursFromInput + 12;
|
|
115
|
+
} else {
|
|
116
|
+
newHours = newHoursFromInput === 12 ? 0 : newHoursFromInput;
|
|
117
|
+
}
|
|
80
118
|
}
|
|
119
|
+
|
|
81
120
|
onChange({
|
|
82
121
|
hours: newHours,
|
|
83
|
-
minutes,
|
|
122
|
+
minutes: minutesRef.current,
|
|
84
123
|
});
|
|
124
|
+
|
|
125
|
+
const maxStartDigit = is24Hour ? 2 : 1;
|
|
126
|
+
const shouldAdvance = text.length >= 2 || newHoursFromInput > maxStartDigit;
|
|
127
|
+
if (shouldAdvance) endInput.current?.focus();
|
|
85
128
|
},
|
|
86
|
-
[
|
|
129
|
+
[is24Hour, isPm, minutesRef, onChange],
|
|
87
130
|
);
|
|
88
131
|
|
|
89
132
|
const onMinuteChange = useCallback(
|
|
90
|
-
(newMinutesFromInput: number) => {
|
|
91
|
-
|
|
133
|
+
(newMinutesFromInput: number, text: string) => {
|
|
134
|
+
if (text.length === 0) {
|
|
135
|
+
setMinuteError(false);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const isNumeric = /^\d+$/.test(text);
|
|
140
|
+
const isValid = isNumeric && newMinutesFromInput >= 0 && newMinutesFromInput <= 59;
|
|
92
141
|
|
|
93
|
-
|
|
94
|
-
|
|
142
|
+
setMinuteError(!isValid);
|
|
143
|
+
|
|
144
|
+
if (!isValid) {
|
|
145
|
+
return;
|
|
95
146
|
}
|
|
147
|
+
|
|
96
148
|
onChange({
|
|
97
149
|
hours: hours,
|
|
98
|
-
minutes:
|
|
150
|
+
minutes: newMinutesFromInput,
|
|
99
151
|
});
|
|
100
152
|
},
|
|
101
153
|
[hours, onChange],
|
|
102
154
|
);
|
|
103
155
|
|
|
104
156
|
return (
|
|
105
|
-
<View style={timePickerInputsStyles.
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<View style={timePickerInputsStyles.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
157
|
+
<View style={timePickerInputsStyles.wrapper}>
|
|
158
|
+
<View style={timePickerInputsStyles.inputContainer}>
|
|
159
|
+
<TimeInput
|
|
160
|
+
ref={startInput}
|
|
161
|
+
placeholder={''}
|
|
162
|
+
value={toHourInputFormat(hours, is24Hour)}
|
|
163
|
+
clockType={clockTypes.hours}
|
|
164
|
+
pressed={focused === clockTypes.hours}
|
|
165
|
+
onPress={onFocusInput}
|
|
166
|
+
inputType={inputType}
|
|
167
|
+
returnKeyType={'next'}
|
|
168
|
+
onSubmitEditing={onSubmitStartInput}
|
|
169
|
+
blurOnSubmit={false}
|
|
170
|
+
error={hourError}
|
|
171
|
+
onChanged={onHourChange}
|
|
172
|
+
// onChangeText={onChangeStartInput}
|
|
173
|
+
/>
|
|
174
|
+
<View style={timePickerInputsStyles.hoursAndMinutesSeparator}>
|
|
175
|
+
<View style={timePickerInputsStyles.spaceDot} />
|
|
176
|
+
<View style={timePickerInputsStyles.dot} />
|
|
177
|
+
<View style={timePickerInputsStyles.betweenDot} />
|
|
178
|
+
<View style={timePickerInputsStyles.dot} />
|
|
179
|
+
<View style={timePickerInputsStyles.spaceDot} />
|
|
180
|
+
</View>
|
|
181
|
+
<TimeInput
|
|
182
|
+
ref={endInput}
|
|
183
|
+
placeholder={'00'}
|
|
184
|
+
value={minutes}
|
|
185
|
+
clockType={clockTypes.minutes}
|
|
186
|
+
pressed={focused === clockTypes.minutes}
|
|
187
|
+
onPress={onFocusInput}
|
|
188
|
+
inputType={inputType}
|
|
189
|
+
error={minuteError}
|
|
190
|
+
onSubmitEditing={onSubmitEndInput}
|
|
191
|
+
onChanged={onMinuteChange}
|
|
192
|
+
/>
|
|
193
|
+
{!is24Hour && (
|
|
194
|
+
<>
|
|
195
|
+
<View style={timePickerInputsStyles.spaceBetweenInputsAndSwitcher} />
|
|
196
|
+
<AmPmSwitcher hours={hours} onChange={onChangeHours} />
|
|
197
|
+
</>
|
|
198
|
+
)}
|
|
126
199
|
</View>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
200
|
+
{inputType === 'keyboard' ? (
|
|
201
|
+
<View style={timePickerInputsStyles.supportingRow}>
|
|
202
|
+
<View style={timePickerInputsStyles.supportingSlot}>
|
|
203
|
+
<Text
|
|
204
|
+
style={[
|
|
205
|
+
timePickerInputsStyles.supportingText,
|
|
206
|
+
hourError ? timePickerInputsStyles.supportingTextError : null,
|
|
207
|
+
]}>
|
|
208
|
+
{hourError ? hourErrorText : 'Hour'}
|
|
209
|
+
</Text>
|
|
210
|
+
</View>
|
|
211
|
+
<View style={timePickerInputsStyles.hoursAndMinutesSeparator} />
|
|
212
|
+
<View style={timePickerInputsStyles.supportingSlot}>
|
|
213
|
+
<Text
|
|
214
|
+
style={[
|
|
215
|
+
timePickerInputsStyles.supportingText,
|
|
216
|
+
minuteError ? timePickerInputsStyles.supportingTextError : null,
|
|
217
|
+
]}>
|
|
218
|
+
{minuteError ? minuteErrorText : 'Minute'}
|
|
219
|
+
</Text>
|
|
220
|
+
</View>
|
|
221
|
+
{!is24Hour && (
|
|
222
|
+
<View style={timePickerInputsStyles.spaceBetweenInputsAndSwitcher} />
|
|
223
|
+
)}
|
|
224
|
+
</View>
|
|
225
|
+
) : null}
|
|
144
226
|
</View>
|
|
145
227
|
);
|
|
146
228
|
}
|