1
- import React , { useCallback , useEffect , useRef , useState } from 'react' ;
1
+ import React from 'react' ;
2
2
3
3
import { Textfield } from '@digdir/designsystemet-react' ;
4
4
5
- import {
6
- handleSegmentKeyDown ,
7
- handleValueDecrement ,
8
- handleValueIncrement ,
9
- } from 'src/app-components/TimePicker/utils/keyboardNavigation' ;
10
- import {
11
- clearSegment ,
12
- commitSegmentValue ,
13
- handleSegmentCharacterInput ,
14
- processSegmentBuffer ,
15
- } from 'src/app-components/TimePicker/utils/segmentTyping' ;
16
- import { formatSegmentValue } from 'src/app-components/TimePicker/utils/timeFormatUtils' ;
5
+ import { useSegmentDisplay } from 'src/app-components/TimePicker/hooks/useSegmentDisplay' ;
6
+ import { useSegmentInputHandlers } from 'src/app-components/TimePicker/hooks/useSegmentInputHandlers' ;
7
+ import { useTypingBuffer } from 'src/app-components/TimePicker/hooks/useTypingBuffer' ;
17
8
import type { TimeFormat } from 'src/app-components/TimePicker/components/TimePicker' ;
18
9
import type { SegmentType } from 'src/app-components/TimePicker/utils/keyboardNavigation' ;
19
10
@@ -30,6 +21,7 @@ export interface TimeSegmentProps {
30
21
placeholder ?: string ;
31
22
disabled ?: boolean ;
32
23
readOnly ?: boolean ;
24
+ required ?: boolean ;
33
25
'aria-label' : string ;
34
26
className ?: string ;
35
27
autoFocus ?: boolean ;
@@ -48,240 +40,105 @@ export const TimeSegment = React.forwardRef<HTMLInputElement, TimeSegmentProps>(
48
40
placeholder,
49
41
disabled,
50
42
readOnly,
43
+ required,
51
44
'aria-label' : ariaLabel ,
52
45
className,
53
46
autoFocus,
54
47
} ,
55
48
ref ,
56
49
) => {
57
- const [ localValue , setLocalValue ] = useState ( ( ) => formatSegmentValue ( value , type , format ) ) ;
58
- const [ segmentBuffer , setSegmentBuffer ] = useState ( '' ) ;
59
- const [ bufferTimeout , setBufferTimeout ] = useState < ReturnType < typeof setTimeout > | null > ( null ) ;
60
- const [ typingEndTimeout , setTypingEndTimeout ] = useState < ReturnType < typeof setTimeout > | null > ( null ) ;
61
- const inputRef = useRef < HTMLInputElement > ( null ) ;
62
- const isTypingRef = useRef ( false ) ;
63
- const bufferRef = useRef ( '' ) ; // Keep current buffer in a ref for timeouts
50
+ const { displayValue, updateDisplayFromBuffer, syncWithExternalValue } = useSegmentDisplay ( value , type , format ) ;
64
51
65
- // Sync external value changes
66
- React . useEffect ( ( ) => {
67
- const formattedValue = formatSegmentValue ( value , type , format ) ;
68
- setLocalValue ( formattedValue ) ;
69
-
70
- // Only clear buffer if we're not currently typing
71
- // This prevents clearing the buffer when our own input triggers an external value change
72
- if ( ! isTypingRef . current ) {
73
- setSegmentBuffer ( '' ) ;
74
- bufferRef . current = '' ;
52
+ const inputHandlers = useSegmentInputHandlers ( {
53
+ segmentType : type ,
54
+ timeFormat : format ,
55
+ currentValue : value ,
56
+ onValueChange,
57
+ onNavigate,
58
+ onUpdateDisplay : updateDisplayFromBuffer ,
59
+ } ) ;
60
+
61
+ const typingBuffer = useTypingBuffer ( {
62
+ onCommit : inputHandlers . commitBufferValue ,
63
+ commitDelayMs : 1000 ,
64
+ typingEndDelayMs : 2000 ,
65
+ } ) ;
66
+
67
+ const syncExternalChangesWhenNotTyping = ( ) => {
68
+ if ( ! typingBuffer . isTyping ) {
69
+ syncWithExternalValue ( ) ;
70
+ typingBuffer . resetToIdleState ( ) ;
75
71
}
76
- } , [ value , type , format ] ) ;
72
+ } ;
77
73
78
- // Clear timeouts on unmount
79
- useEffect (
80
- ( ) => ( ) => {
81
- if ( bufferTimeout ) {
82
- clearTimeout ( bufferTimeout ) ;
83
- }
84
- if ( typingEndTimeout ) {
85
- clearTimeout ( typingEndTimeout ) ;
86
- }
87
- } ,
88
- [ bufferTimeout , typingEndTimeout ] ,
89
- ) ;
74
+ React . useEffect ( syncExternalChangesWhenNotTyping , [ value , type , format , typingBuffer , syncWithExternalValue ] ) ;
90
75
91
- const commitBuffer = useCallback (
92
- ( shouldEndTyping = true ) => {
93
- // Use the current buffer from ref to avoid stale closures
94
- const currentBuffer = bufferRef . current ;
95
- if ( currentBuffer ) {
96
- const buffer = processSegmentBuffer ( currentBuffer , type , format . includes ( 'a' ) ) ;
97
- if ( buffer . actualValue !== null ) {
98
- const committedValue = commitSegmentValue ( buffer . actualValue , type ) ;
99
- onValueChange ( committedValue ) ;
100
- }
101
- setSegmentBuffer ( '' ) ;
102
- bufferRef . current = '' ;
103
- }
104
- // Only end typing state if explicitly requested
105
- // This allows us to keep typing state during timeout commits
106
- if ( shouldEndTyping ) {
107
- isTypingRef . current = false ;
108
- }
109
- } ,
110
- [ type , format , onValueChange ] ,
111
- ) ; // Remove segmentBuffer dependency
76
+ const handleCharacterTyping = ( event : React . KeyboardEvent < HTMLInputElement > ) => {
77
+ const character = event . key ;
112
78
113
- const resetBufferTimeout = useCallback ( ( ) => {
114
- // Clear any existing timeouts
115
- if ( bufferTimeout ) {
116
- clearTimeout ( bufferTimeout ) ;
117
- setBufferTimeout ( null ) ;
118
- }
119
- if ( typingEndTimeout ) {
120
- clearTimeout ( typingEndTimeout ) ;
121
- setTypingEndTimeout ( null ) ;
122
- }
79
+ if ( character . length === 1 ) {
80
+ event . preventDefault ( ) ;
123
81
124
- const timeout = setTimeout ( ( ) => {
125
- commitBuffer ( false ) ; // Don't end typing on timeout - keep buffer alive
126
- } , 1000 ) ; // 1 second timeout
127
- setBufferTimeout ( timeout ) ;
82
+ const currentBuffer = typingBuffer . buffer ;
83
+ const inputResult = inputHandlers . processCharacterInput ( character , currentBuffer ) ;
128
84
129
- // End typing after a longer delay to allow multi-digit input
130
- const endTimeout = setTimeout ( ( ) => {
131
- isTypingRef . current = false ;
132
- } , 2000 ) ; // 2 second timeout to end typing
133
- setTypingEndTimeout ( endTimeout ) ;
134
- } , [ bufferTimeout , typingEndTimeout , commitBuffer ] ) ;
85
+ // Use the processed buffer result, not the raw character
86
+ typingBuffer . replaceBuffer ( inputResult . newBuffer ) ;
135
87
136
- const handleKeyDown = ( e : React . KeyboardEvent < HTMLInputElement > ) => {
137
- // Handle special keys (arrows, delete, backspace, etc.)
138
- if ( e . key === 'Delete' || e . key === 'Backspace' ) {
139
- e . preventDefault ( ) ;
140
- const cleared = clearSegment ( ) ;
141
- setLocalValue ( cleared . displayValue ) ;
142
- setSegmentBuffer ( '' ) ;
143
- bufferRef . current = '' ;
144
- isTypingRef . current = false ; // End typing state
145
- if ( bufferTimeout ) {
146
- clearTimeout ( bufferTimeout ) ;
147
- setBufferTimeout ( null ) ;
148
- }
149
- if ( typingEndTimeout ) {
150
- clearTimeout ( typingEndTimeout ) ;
151
- setTypingEndTimeout ( null ) ;
88
+ if ( inputResult . shouldNavigateRight ) {
89
+ typingBuffer . commitImmediatelyAndEndTyping ( ) ;
90
+ onNavigate ( 'right' ) ;
152
91
}
153
- return ;
154
- }
155
-
156
- const result = handleSegmentKeyDown ( e ) ;
157
-
158
- if ( result . shouldNavigate && result . direction ) {
159
- commitBuffer ( true ) ; // End typing when navigating away
160
- onNavigate ( result . direction ) ;
161
- } else if ( result . shouldIncrement ) {
162
- commitBuffer ( true ) ; // End typing when using arrows
163
- const newValue = handleValueIncrement ( value , type , format ) ;
164
- onValueChange ( newValue ) ;
165
- } else if ( result . shouldDecrement ) {
166
- commitBuffer ( true ) ; // End typing when using arrows
167
- const newValue = handleValueDecrement ( value , type , format ) ;
168
- onValueChange ( newValue ) ;
169
92
}
170
93
} ;
171
94
172
- const handleKeyPress = ( e : React . KeyboardEvent < HTMLInputElement > ) => {
173
- // Handle character input with Chrome-like segment typing
174
- const char = e . key ;
175
-
176
- if ( char . length === 1 ) {
177
- e . preventDefault ( ) ;
95
+ const handleSpecialKeys = ( event : React . KeyboardEvent < HTMLInputElement > ) => {
96
+ const isDeleteOrBackspace = event . key === 'Delete' || event . key === 'Backspace' ;
178
97
179
- // Set typing state when we start typing
180
- isTypingRef . current = true ;
181
-
182
- const result = handleSegmentCharacterInput ( char , type , segmentBuffer , format ) ;
183
-
184
- if ( result . shouldNavigate ) {
185
- commitBuffer ( true ) ; // End typing when navigating
186
- onNavigate ( 'right' ) ;
187
- return ;
188
- }
189
-
190
- setSegmentBuffer ( result . newBuffer ) ;
191
- bufferRef . current = result . newBuffer ; // Keep ref in sync
192
- const buffer = processSegmentBuffer ( result . newBuffer , type , format . includes ( 'a' ) ) ;
193
- setLocalValue ( buffer . displayValue ) ;
98
+ if ( isDeleteOrBackspace ) {
99
+ event . preventDefault ( ) ;
100
+ inputHandlers . handleDeleteOrBackspace ( ) ;
101
+ typingBuffer . resetToIdleState ( ) ;
102
+ return ;
103
+ }
194
104
195
- if ( result . shouldAdvance ) {
196
- // Commit immediately and advance
197
- if ( buffer . actualValue !== null ) {
198
- const committedValue = commitSegmentValue ( buffer . actualValue , type ) ;
199
- onValueChange ( committedValue ) ;
200
- }
201
- setSegmentBuffer ( '' ) ;
202
- bufferRef . current = '' ;
203
- isTypingRef . current = false ; // End typing state on immediate commit
204
- if ( bufferTimeout ) {
205
- clearTimeout ( bufferTimeout ) ;
206
- setBufferTimeout ( null ) ;
207
- }
208
- if ( typingEndTimeout ) {
209
- clearTimeout ( typingEndTimeout ) ;
210
- setTypingEndTimeout ( null ) ;
211
- }
212
- onNavigate ( 'right' ) ;
213
- } else {
214
- // Start or reset timeout
215
- resetBufferTimeout ( ) ;
216
- }
105
+ const wasArrowKeyHandled = inputHandlers . handleArrowKeyNavigation ( event ) ;
106
+ if ( wasArrowKeyHandled ) {
107
+ typingBuffer . commitImmediatelyAndEndTyping ( ) ;
217
108
}
218
109
} ;
219
110
220
- const handleFocus = ( e : React . FocusEvent < HTMLInputElement > ) => {
221
- // Don't clear buffer if we're already focused and typing
222
- const wasAlreadyFocused = inputRef . current === document . activeElement ;
111
+ const handleFocusEvent = ( event : React . FocusEvent < HTMLInputElement > ) => {
112
+ const wasFreshFocus = event . currentTarget !== document . activeElement ;
223
113
224
- if ( ! wasAlreadyFocused ) {
225
- // Clear buffer and select all text only on fresh focus
226
- setSegmentBuffer ( '' ) ;
227
- bufferRef . current = '' ;
228
- isTypingRef . current = false ; // End typing state on fresh focus
229
- if ( bufferTimeout ) {
230
- clearTimeout ( bufferTimeout ) ;
231
- setBufferTimeout ( null ) ;
232
- }
233
- if ( typingEndTimeout ) {
234
- clearTimeout ( typingEndTimeout ) ;
235
- setTypingEndTimeout ( null ) ;
236
- }
237
- e . target . select ( ) ;
114
+ if ( wasFreshFocus ) {
115
+ typingBuffer . resetToIdleState ( ) ;
116
+ event . target . select ( ) ;
238
117
}
239
118
240
119
onFocus ?.( ) ;
241
120
} ;
242
121
243
- const handleBlur = ( ) => {
244
- // Commit any pending buffer and fill empty minutes with 00
245
- commitBuffer ( true ) ; // End typing on blur
246
-
247
- if (
248
- ( value === null || value === '' || ( typeof value === 'number' && isNaN ( value ) ) ) &&
249
- ( type === 'minutes' || type === 'seconds' )
250
- ) {
251
- onValueChange ( 0 ) ; // Fill empty minutes/seconds with 00
252
- }
253
-
122
+ const handleBlurEvent = ( ) => {
123
+ typingBuffer . commitImmediatelyAndEndTyping ( ) ;
124
+ inputHandlers . fillEmptyMinutesOrSecondsWithZero ( ) ;
254
125
onBlur ?.( ) ;
255
126
} ;
256
127
257
- const combinedRef = React . useCallback (
258
- ( node : HTMLInputElement | null ) => {
259
- // Handle both external ref and internal ref
260
- if ( ref ) {
261
- if ( typeof ref === 'function' ) {
262
- ref ( node ) ;
263
- } else {
264
- ref . current = node ;
265
- }
266
- }
267
- inputRef . current = node ;
268
- } ,
269
- [ ref ] ,
270
- ) ;
271
-
272
128
return (
273
129
< Textfield
274
- ref = { combinedRef }
130
+ ref = { ref }
275
131
type = 'text'
276
- value = { localValue }
277
- onChange = { ( ) => { } } // Prevent React warnings - actual input handled by onKeyPress
278
- onKeyPress = { handleKeyPress }
279
- onKeyDown = { handleKeyDown }
280
- onFocus = { handleFocus }
281
- onBlur = { handleBlur }
132
+ value = { displayValue }
133
+ onChange = { ( ) => { } }
134
+ onKeyPress = { handleCharacterTyping }
135
+ onKeyDown = { handleSpecialKeys }
136
+ onFocus = { handleFocusEvent }
137
+ onBlur = { handleBlurEvent }
282
138
placeholder = { placeholder }
283
139
disabled = { disabled }
284
140
readOnly = { readOnly }
141
+ required = { required }
285
142
aria-label = { ariaLabel }
286
143
className = { className }
287
144
autoFocus = { autoFocus }
0 commit comments