Skip to content

Commit 6ac108f

Browse files
committed
ui-core: timepicker handles seconds
Signed-off-by: Valentin Chanas <[email protected]>
1 parent 080df00 commit 6ac108f

File tree

3 files changed

+134
-28
lines changed

3 files changed

+134
-28
lines changed

ui-core/src/components/inputs/TimePicker.tsx

+86-13
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,17 @@ import InputModal from '../Modal';
88
export type TimePickerProps = InputProps & {
99
hours?: number;
1010
minutes?: number;
11-
onTimeChange: ({ hours, minutes }: { hours: number; minutes: number }) => void;
11+
seconds?: number;
12+
displaySeconds?: boolean;
13+
onTimeChange: ({
14+
hours,
15+
minutes,
16+
seconds,
17+
}: {
18+
hours: number;
19+
minutes: number;
20+
seconds?: number;
21+
}) => void;
1222
};
1323

1424
type TimeRangeProps = {
@@ -32,7 +42,14 @@ const TimeRange = ({ range, selectedItem, className, onSelectItem }: TimeRangePr
3242
</div>
3343
);
3444

35-
const TimePicker = ({ onTimeChange, hours = 0, minutes = 0, ...otherProps }: TimePickerProps) => {
45+
const TimePicker = ({
46+
onTimeChange,
47+
hours = 0,
48+
minutes = 0,
49+
seconds = 0,
50+
displaySeconds,
51+
...otherProps
52+
}: TimePickerProps) => {
3653
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
3754
const inputRef = useRef<HTMLInputElement>(null);
3855

@@ -42,28 +59,61 @@ const TimePicker = ({ onTimeChange, hours = 0, minutes = 0, ...otherProps }: Tim
4259

4360
if (newMinutes >= 60) {
4461
newMinutes = 0;
45-
newHours = newHours === 23 ? 0 : newHours + 1;
62+
newHours = (newHours + 1) % 24;
4663
} else if (newMinutes < 0) {
4764
newMinutes = 59;
48-
newHours = newHours === 0 ? 23 : newHours - 1;
65+
newHours = (newHours + 23) % 24; // minus 1 hour
4966
}
50-
onTimeChange({ hours: newHours, minutes: newMinutes });
67+
onTimeChange({ hours: newHours, minutes: newMinutes, seconds });
68+
};
69+
70+
const incrementSeconds = (increment: number) => {
71+
let newSeconds = seconds + increment;
72+
let newMinutes = minutes;
73+
let newHours = hours;
74+
75+
if (newSeconds >= 60) {
76+
newSeconds = 0;
77+
newMinutes += 1;
78+
79+
if (newMinutes >= 60) {
80+
newMinutes = 0;
81+
newHours = (newHours + 1) % 24;
82+
}
83+
} else if (newSeconds < 0) {
84+
newSeconds = 59;
85+
newMinutes -= 1;
86+
87+
if (newMinutes < 0) {
88+
newMinutes = 59;
89+
newHours = (newHours + 23) % 24; // minus 1 hour
90+
}
91+
}
92+
onTimeChange({ hours: newHours, minutes: newMinutes, seconds: newSeconds });
5193
};
5294

5395
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
5496
const value = event.target.value;
55-
const [h, m] = value.split(':');
56-
if (h !== undefined && m !== undefined) {
57-
onTimeChange({ hours: parseInt(h), minutes: parseInt(m) });
97+
if (!displaySeconds) {
98+
const [h, m] = value.split(':');
99+
if (h !== undefined && m !== undefined) {
100+
onTimeChange({ hours: parseInt(h), minutes: parseInt(m) });
101+
}
102+
} else {
103+
const [h, m, s] = value.split(':');
104+
if (h !== undefined && m !== undefined && s !== undefined) {
105+
onTimeChange({ hours: parseInt(h), minutes: parseInt(m), seconds: parseInt(s) });
106+
}
58107
}
59108
};
60109

61110
const formatTimeValue = (value: number, max: number) =>
62111
Math.max(0, Math.min(max, value)).toString().padStart(2, '0');
63112

64-
const selectedTime = `${formatTimeValue(hours, 23)}:${formatTimeValue(minutes, 59)}`;
113+
const displayedSecondsPart = displaySeconds ? `:${formatTimeValue(seconds, 59)}` : '';
114+
const selectedTime = `${formatTimeValue(hours, 23)}:${formatTimeValue(minutes, 59)}${displayedSecondsPart}`;
65115
const hoursRange = [...Array(24).keys()];
66-
const minutesRange = [...Array(12).keys()].map((i) => i * 5);
116+
const minutesAndSecondsRange = [...Array(12).keys()].map((i) => i * 5);
67117

68118
const openModal = () => {
69119
setIsModalOpen(true);
@@ -74,12 +124,14 @@ const TimePicker = ({ onTimeChange, hours = 0, minutes = 0, ...otherProps }: Tim
74124
return (
75125
<div className="time-picker">
76126
<Input
127+
className={cx('input', 'time-input')}
77128
type="time"
78129
name="time"
79130
value={selectedTime}
80131
onClick={openModal}
81132
onChange={handleChange}
82133
ref={inputRef}
134+
step={displaySeconds ? 1 : 60}
83135
{...otherProps}
84136
/>
85137
<InputModal inputRef={inputRef} isOpen={isModalOpen} onClose={closeModal}>
@@ -88,17 +140,17 @@ const TimePicker = ({ onTimeChange, hours = 0, minutes = 0, ...otherProps }: Tim
88140
range={hoursRange}
89141
selectedItem={hours}
90142
className="hour"
91-
onSelectItem={(h) => onTimeChange({ hours: h, minutes })}
143+
onSelectItem={(h) => onTimeChange({ hours: h, minutes, seconds })}
92144
/>
93145

94146
<div className="time-separator">:</div>
95147

96148
<div className="minute-container">
97149
<TimeRange
98-
range={minutesRange}
150+
range={minutesAndSecondsRange}
99151
selectedItem={minutes}
100152
className="minute"
101-
onSelectItem={(m) => onTimeChange({ hours: hours, minutes: m })}
153+
onSelectItem={(m) => onTimeChange({ hours, minutes: m, seconds })}
102154
/>
103155
<div className="minute-buttons">
104156
<button onClick={() => incrementMinute(-1)} className="minute-button">
@@ -109,6 +161,27 @@ const TimePicker = ({ onTimeChange, hours = 0, minutes = 0, ...otherProps }: Tim
109161
</button>
110162
</div>
111163
</div>
164+
{displaySeconds && (
165+
<>
166+
<div className="time-separator">:</div>
167+
<div className="second-container">
168+
<TimeRange
169+
range={minutesAndSecondsRange}
170+
selectedItem={seconds}
171+
className="second"
172+
onSelectItem={(s) => onTimeChange({ hours, minutes, seconds: s })}
173+
/>
174+
<div className="second-buttons">
175+
<button onClick={() => incrementSeconds(-1)} className="second-button">
176+
-1s
177+
</button>
178+
<button onClick={() => incrementSeconds(1)} className="second-button">
179+
+1s
180+
</button>
181+
</div>
182+
</div>
183+
</>
184+
)}
112185
</div>
113186
</InputModal>
114187
</div>

ui-core/src/stories/TimePicker.stories.tsx

+35-9
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,23 @@ import '@osrd-project/ui-core/dist/theme.css';
99
const TimePickerStory = (props: TimePickerProps) => {
1010
const [selectedHour, setSelectedHour] = useState(props.hours);
1111
const [selectedMinute, setSelectedMinute] = useState(props.minutes);
12-
const onTimeChange = (newTime: { hours: number; minutes: number }) => {
12+
const [selectedSecond, setSelectedSecond] = useState(props.seconds);
13+
const onTimeChange = (newTime: { hours: number; minutes: number; seconds?: number }) => {
1314
setSelectedHour(newTime.hours);
1415
setSelectedMinute(newTime.minutes);
16+
setSelectedSecond(newTime.seconds);
1517
};
1618
useEffect(() => {
1719
setSelectedHour(props.hours);
1820
setSelectedMinute(props.minutes);
19-
}, [props.hours, props.minutes]);
21+
setSelectedSecond(props.seconds);
22+
}, [props.hours, props.minutes, props.seconds]);
2023
return (
2124
<TimePicker
2225
{...props}
2326
hours={selectedHour}
2427
minutes={selectedMinute}
28+
seconds={selectedSecond}
2529
onTimeChange={onTimeChange}
2630
/>
2731
);
@@ -32,6 +36,7 @@ const meta: Meta<typeof TimePicker> = {
3236
args: {
3337
disabled: false,
3438
readOnly: false,
39+
displaySeconds: false,
3540
},
3641
argTypes: {
3742
hours: {
@@ -53,13 +58,6 @@ const meta: Meta<typeof TimePicker> = {
5358
},
5459
title: 'Core/TimePicker',
5560
tags: ['autodocs'],
56-
decorators: [
57-
(Story) => (
58-
<div style={{ maxWidth: '7rem' }}>
59-
<Story />
60-
</div>
61-
),
62-
],
6361
render: TimePickerStory,
6462
};
6563

@@ -70,11 +68,39 @@ export const Default: Story = {
7068
args: {
7169
label: 'Time',
7270
},
71+
decorators: [
72+
(Story) => (
73+
<div style={{ maxWidth: '6.7rem', minHeight: '500px' }}>
74+
<Story />
75+
</div>
76+
),
77+
],
7378
};
7479

7580
export const DisabledTimePicker: Story = {
7681
args: {
7782
disabled: true,
7883
label: 'Time',
7984
},
85+
decorators: [
86+
(Story) => (
87+
<div style={{ maxWidth: '6.7rem', minHeight: '500px' }}>
88+
<Story />
89+
</div>
90+
),
91+
],
92+
};
93+
94+
export const TimePickerWithSeconds: Story = {
95+
args: {
96+
displaySeconds: true,
97+
label: 'Time',
98+
},
99+
decorators: [
100+
(Story) => (
101+
<div style={{ maxWidth: '8.5rem', minHeight: '500px' }}>
102+
<Story />
103+
</div>
104+
),
105+
],
80106
};

ui-core/src/styles/inputs/timePicker.css

+13-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
.time-picker {
2-
.time-input {
3-
cursor: pointer;
1+
@-moz-document url-prefix() {
2+
.time-picker .time-input {
3+
letter-spacing: -0.9px;
44
}
5+
}
56

6-
input[type='time']::-webkit-calendar-picker-indicator {
7+
.time-picker {
8+
.time-input::-webkit-calendar-picker-indicator {
79
background: none;
810
display: none;
911
}
@@ -45,12 +47,14 @@
4547
font-size: 1.5rem;
4648
}
4749

50+
.second-container,
4851
.minute-container {
4952
position: relative;
5053
height: 14.188rem;
5154
align-content: center;
5255
}
5356

57+
.second-buttons,
5458
.minute-buttons {
5559
display: flex;
5660
position: absolute;
@@ -60,6 +64,7 @@
6064
gap: 0.75rem;
6165
}
6266

67+
.second-button,
6368
.minute-button {
6469
cursor: pointer;
6570
display: block;
@@ -83,11 +88,13 @@
8388
}
8489

8590
.hour,
86-
.minute {
91+
.minute,
92+
.second {
8793
font-size: 1.125rem;
8894
font-weight: 600;
8995
text-align: center;
90-
line-height: 1.5rem;
96+
vertical-align: center;
97+
line-height: 1.9rem;
9198
cursor: pointer;
9299
width: 2.875rem;
93100
height: 1.875rem;

0 commit comments

Comments
 (0)