Skip to content

Commit 32d8585

Browse files
committed
cleanup
1 parent ecceadb commit 32d8585

File tree

5 files changed

+324
-209
lines changed

5 files changed

+324
-209
lines changed

src/app-components/TimePicker/components/TimeSegment.tsx

Lines changed: 66 additions & 209 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
1-
import React, { useCallback, useEffect, useRef, useState } from 'react';
1+
import React from 'react';
22

33
import { Textfield } from '@digdir/designsystemet-react';
44

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';
178
import type { TimeFormat } from 'src/app-components/TimePicker/components/TimePicker';
189
import type { SegmentType } from 'src/app-components/TimePicker/utils/keyboardNavigation';
1910

@@ -30,6 +21,7 @@ export interface TimeSegmentProps {
3021
placeholder?: string;
3122
disabled?: boolean;
3223
readOnly?: boolean;
24+
required?: boolean;
3325
'aria-label': string;
3426
className?: string;
3527
autoFocus?: boolean;
@@ -48,240 +40,105 @@ export const TimeSegment = React.forwardRef<HTMLInputElement, TimeSegmentProps>(
4840
placeholder,
4941
disabled,
5042
readOnly,
43+
required,
5144
'aria-label': ariaLabel,
5245
className,
5346
autoFocus,
5447
},
5548
ref,
5649
) => {
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);
6451

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();
7571
}
76-
}, [value, type, format]);
72+
};
7773

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]);
9075

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;
11278

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();
12381

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);
12884

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);
13587

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');
15291
}
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);
16992
}
17093
};
17194

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';
17897

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+
}
194104

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();
217108
}
218109
};
219110

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;
223113

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();
238117
}
239118

240119
onFocus?.();
241120
};
242121

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();
254125
onBlur?.();
255126
};
256127

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-
272128
return (
273129
<Textfield
274-
ref={combinedRef}
130+
ref={ref}
275131
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}
282138
placeholder={placeholder}
283139
disabled={disabled}
284140
readOnly={readOnly}
141+
required={required}
285142
aria-label={ariaLabel}
286143
className={className}
287144
autoFocus={autoFocus}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useCallback, useEffect, useState } from 'react';
2+
3+
import { formatSegmentValue } from 'src/app-components/TimePicker/utils/timeFormatUtils';
4+
import type { TimeFormat } from 'src/app-components/TimePicker/components/TimePicker';
5+
import type { SegmentType } from 'src/app-components/TimePicker/utils/keyboardNavigation';
6+
7+
export function useSegmentDisplay(externalValue: number | string, segmentType: SegmentType, timeFormat: TimeFormat) {
8+
const [displayValue, setDisplayValue] = useState(() => formatSegmentValue(externalValue, segmentType, timeFormat));
9+
10+
const updateDisplayFromBuffer = useCallback((bufferValue: string) => {
11+
setDisplayValue(bufferValue);
12+
}, []);
13+
14+
const syncWithExternalValue = useCallback(() => {
15+
const formattedValue = formatSegmentValue(externalValue, segmentType, timeFormat);
16+
setDisplayValue(formattedValue);
17+
}, [externalValue, segmentType, timeFormat]);
18+
19+
useEffect(() => {
20+
syncWithExternalValue();
21+
}, [syncWithExternalValue]);
22+
23+
return {
24+
displayValue,
25+
updateDisplayFromBuffer,
26+
syncWithExternalValue,
27+
};
28+
}

0 commit comments

Comments
 (0)