Skip to content

Commit

Permalink
ui-core: timepicker handles seconds
Browse files Browse the repository at this point in the history
Signed-off-by: Valentin Chanas <[email protected]>
  • Loading branch information
anisometropie committed Oct 1, 2024
1 parent 27042bd commit 9f1147d
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 26 deletions.
94 changes: 83 additions & 11 deletions ui-core/src/components/inputs/TimePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@ import InputModal from '../Modal';
export type TimePickerProps = InputProps & {
hours?: number;
minutes?: number;
onTimeChange: ({ hours, minutes }: { hours: number; minutes: number }) => void;
seconds?: number;
displaySeconds?: boolean;
onTimeChange: ({
hours,
minutes,
seconds,
}: {
hours: number;
minutes: number;
seconds?: number;
}) => void;
};

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

const TimePicker = ({ onTimeChange, hours = 0, minutes = 0, ...otherProps }: TimePickerProps) => {
const TimePicker = ({
onTimeChange,
hours = 0,
minutes = 0,
seconds = 0,
displaySeconds,
...otherProps
}: TimePickerProps) => {
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const inputRef = useRef<HTMLInputElement>(null);

Expand All @@ -47,23 +64,56 @@ const TimePicker = ({ onTimeChange, hours = 0, minutes = 0, ...otherProps }: Tim
newMinutes = 59;
newHours = newHours === 0 ? 23 : newHours - 1;
}
onTimeChange({ hours: newHours, minutes: newMinutes });
onTimeChange({ hours: newHours, minutes: newMinutes, seconds });
};

const incrementSeconds = (increment: number) => {
let newSeconds = seconds + increment;
let newMinutes = minutes;
let newHours = hours;

if (newSeconds >= 60) {
newSeconds = 0;
newMinutes += 1;

if (newMinutes >= 60) {
newMinutes = 0;
newHours = newHours === 23 ? 0 : newHours + 1;
}
} else if (newSeconds < 0) {
newSeconds = 59;
newMinutes -= 1;

if (newMinutes < 0) {
newMinutes = 59;
newHours = newHours === 0 ? 23 : newHours - 1;
}
}
onTimeChange({ hours: newHours, minutes: newMinutes, seconds: newSeconds });
};

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
const [h, m] = value.split(':');
if (h !== undefined && m !== undefined) {
onTimeChange({ hours: parseInt(h), minutes: parseInt(m) });
if (!displaySeconds) {
const [h, m] = value.split(':');
if (h !== undefined && m !== undefined) {
onTimeChange({ hours: parseInt(h), minutes: parseInt(m) });
}
} else {
const [h, m, s] = value.split(':');
if (h !== undefined && m !== undefined && s !== undefined) {
onTimeChange({ hours: parseInt(h), minutes: parseInt(m), seconds: parseInt(s) });
}
}
};

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

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

const openModal = () => {
setIsModalOpen(true);
Expand All @@ -80,6 +130,7 @@ const TimePicker = ({ onTimeChange, hours = 0, minutes = 0, ...otherProps }: Tim
onClick={openModal}
onChange={handleChange}
ref={inputRef}
step={displaySeconds ? 1 : 60}
{...otherProps}
/>
<InputModal inputRef={inputRef} isOpen={isModalOpen} onClose={closeModal}>
Expand All @@ -88,17 +139,17 @@ const TimePicker = ({ onTimeChange, hours = 0, minutes = 0, ...otherProps }: Tim
range={hoursRange}
selectedItem={hours}
className="hour"
onSelectItem={(h) => onTimeChange({ hours: h, minutes })}
onSelectItem={(h) => onTimeChange({ hours: h, minutes, seconds })}
/>

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

<div className="minute-container">
<TimeRange
range={minutesRange}
range={minutesAndSecondsRange}
selectedItem={minutes}
className="minute"
onSelectItem={(m) => onTimeChange({ hours: hours, minutes: m })}
onSelectItem={(m) => onTimeChange({ hours, minutes: m, seconds })}
/>
<div className="minute-buttons">
<button onClick={() => incrementMinute(-1)} className="minute-button">
Expand All @@ -109,6 +160,27 @@ const TimePicker = ({ onTimeChange, hours = 0, minutes = 0, ...otherProps }: Tim
</button>
</div>
</div>
{displaySeconds && (
<>
<div className="time-separator">:</div>
<div className="second-container">
<TimeRange
range={minutesAndSecondsRange}
selectedItem={seconds}
className="second"
onSelectItem={(s) => onTimeChange({ hours, minutes, seconds: s })}
/>
<div className="second-buttons">
<button onClick={() => incrementSeconds(-1)} className="second-button">
-1s
</button>
<button onClick={() => incrementSeconds(1)} className="second-button">
+1s
</button>
</div>
</div>
</>
)}
</div>
</InputModal>
</div>
Expand Down
44 changes: 35 additions & 9 deletions ui-core/src/stories/TimePicker.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,23 @@ import '@osrd-project/ui-core/dist/theme.css';
const TimePickerStory = (props: TimePickerProps) => {
const [selectedHour, setSelectedHour] = useState(props.hours);
const [selectedMinute, setSelectedMinute] = useState(props.minutes);
const onTimeChange = (newTime: { hours: number; minutes: number }) => {
const [selectedSecond, setSelectedSecond] = useState(props.seconds);
const onTimeChange = (newTime: { hours: number; minutes: number; seconds?: number }) => {
setSelectedHour(newTime.hours);
setSelectedMinute(newTime.minutes);
setSelectedSecond(newTime.seconds);
};
useEffect(() => {
setSelectedHour(props.hours);
setSelectedMinute(props.minutes);
}, [props.hours, props.minutes]);
setSelectedSecond(props.seconds);
}, [props.hours, props.minutes, props.seconds]);
return (
<TimePicker
{...props}
hours={selectedHour}
minutes={selectedMinute}
seconds={selectedSecond}
onTimeChange={onTimeChange}
/>
);
Expand All @@ -32,6 +36,7 @@ const meta: Meta<typeof TimePicker> = {
args: {
disabled: false,
readOnly: false,
displaySeconds: false,
},
argTypes: {
hours: {
Expand All @@ -53,13 +58,6 @@ const meta: Meta<typeof TimePicker> = {
},
title: 'Core/TimePicker',
tags: ['autodocs'],
decorators: [
(Story) => (
<div style={{ maxWidth: '7rem' }}>
<Story />
</div>
),
],
render: TimePickerStory,
};

Expand All @@ -70,11 +68,39 @@ export const Default: Story = {
args: {
label: 'Time',
},
decorators: [
(Story) => (
<div style={{ maxWidth: '6.6rem', minHeight: '500px' }}>
<Story />
</div>
),
],
};

export const DisabledTimePicker: Story = {
args: {
disabled: true,
label: 'Time',
},
decorators: [
(Story) => (
<div style={{ maxWidth: '6.6rem', minHeight: '500px' }}>
<Story />
</div>
),
],
};

export const TimePickerWithSeconds: Story = {
args: {
displaySeconds: true,
label: 'Time',
},
decorators: [
(Story) => (
<div style={{ maxWidth: '8.3rem', minHeight: '500px' }}>
<Story />
</div>
),
],
};
9 changes: 7 additions & 2 deletions ui-core/src/styles/inputs/timePicker.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,14 @@
font-size: 1.5rem;
}

.second-container,
.minute-container {
position: relative;
height: 14.188rem;
align-content: center;
}

.second-buttons,
.minute-buttons {
display: flex;
position: absolute;
Expand All @@ -60,6 +62,7 @@
gap: 0.75rem;
}

.second-button,
.minute-button {
cursor: pointer;
display: block;
Expand All @@ -83,11 +86,13 @@
}

.hour,
.minute {
.minute,
.second {
font-size: 1.125rem;
font-weight: 600;
text-align: center;
line-height: 1.5rem;
vertical-align: center;
line-height: 1.9rem;
cursor: pointer;
width: 2.875rem;
height: 1.875rem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ export const drawSpeedLimits = ({ ctx, width, height, store }: DrawFunctionParam
const realHeight = height - MARGIN_BOTTOM - MARGIN_TOP;
const maxSpeed = maxSpeedValue(store);
const maxPosition = maxPositionValue(store);
// Add the last boundary to the boundaries array
mrsp.boundaries.push(maxPosition);

ctx.lineCap = 'round';

Expand All @@ -35,7 +33,8 @@ export const drawSpeedLimits = ({ ctx, width, height, store }: DrawFunctionParam
for (let i = 0; i < mrsp.values.length; i++) {
const { speed, isTemporary } = mrsp.values[i];

const currentBoundaryX = positionToPosX(mrsp.boundaries[i], maxPosition, width, ratioX);
const currentBoundary = i < mrsp.boundaries.length - 1 ? mrsp.boundaries[i] : maxPosition;
const currentBoundaryX = positionToPosX(currentBoundary, maxPosition, width, ratioX);

const speedY = realHeight - (speed / maxSpeed) * (realHeight - CURVE_MARGIN_TOP) + MARGIN_TOP;
// Draw vertical line joining 2 speed limits
Expand Down Expand Up @@ -66,7 +65,7 @@ export const drawSpeedLimits = ({ ctx, width, height, store }: DrawFunctionParam
let extendedBoundaryX: number | null = null;
if (i < mrsp.values.length - 1 && mrsp.values[i + 1].speed > speed) {
extendedBoundaryX = positionToPosX(
Math.min(mrsp.boundaries[i] + convertMToKm(trainLength), maxPosition),
Math.min(currentBoundary + convertMToKm(trainLength), maxPosition),
maxPosition,
width,
ratioX
Expand Down

0 comments on commit 9f1147d

Please sign in to comment.