Skip to content

Commit 76cb1ad

Browse files
committed
Display a placeholder in the position of the cursor
1 parent de64cd7 commit 76cb1ad

File tree

4 files changed

+209
-133
lines changed

4 files changed

+209
-133
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
/**
4+
* Displays a placeholder for a candidate time slot when the Timeline is hovered.
5+
*/
6+
export default function CandidatePlaceholder({xPosition, yPosition, height, widthPercent}) {
7+
return (
8+
<div
9+
style={{
10+
background: 'rgba(0, 0, 0, 0.3)',
11+
borderRadius: '3px',
12+
color: 'white',
13+
display: 'block',
14+
height: height,
15+
left: xPosition,
16+
padding: '4px',
17+
position: 'fixed',
18+
pointerEvents: 'none',
19+
top: yPosition,
20+
transform: 'translate(-50%, -100%)',
21+
width: `${widthPercent}%`,
22+
zIndex: 1000,
23+
}}
24+
/>
25+
);
26+
}
27+
28+
CandidatePlaceholder.propTypes = {
29+
height: PropTypes.number.isRequired,
30+
widthPercent: PropTypes.number.isRequired,
31+
xPosition: PropTypes.number.isRequired,
32+
yPosition: PropTypes.number.isRequired,
33+
};

newdle/client/src/components/creation/timeslots/Timeline.js

+162-123
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import {hourRange, toMoment, getHourSpan, DEFAULT_TIME_FORMAT} from '../../../util/date';
2020
import {useIsSmallScreen} from '../../../util/hooks';
2121
import TimezonePicker from '../../common/TimezonePicker';
22+
import CandidatePlaceholder from './CandidatePlaceholder';
2223
import CandidateSlot from './CandidateSlot';
2324
import DurationPicker from './DurationPicker';
2425
import TimelineHeader from './TimelineHeader';
@@ -172,9 +173,16 @@ function TimelineInput({minHour, maxHour}) {
172173
const [timeslotTime, setTimeslotTime] = useState(latestStartTime);
173174
const [newTimeslotPopupOpen, setTimeslotPopupOpen] = useState(false);
174175
// Indicates the position of the mouse in the timeline
175-
const [tooltip, setTooltip] = useState({visible: false, time: '', x: 0, y: 0});
176+
const [candidatePlaceholder, setCandidatePlaceholder] = useState({
177+
visible: false,
178+
time: '',
179+
x: 0,
180+
y: 0,
181+
});
176182
// We don't want to show the tooltip when the mouse is hovering over a slot
177183
const [isHoveringSlot, setIsHoveringSlot] = useState(false);
184+
// const baseWidth = calculateWidth("00:00", duration, minHour, maxHour);
185+
const placeHolderSlot = getCandidateSlotProps('00:00', duration, minHour, maxHour);
178186

179187
useEffect(() => {
180188
setTimeslotTime(latestStartTime);
@@ -238,159 +246,190 @@ function TimelineInput({minHour, maxHour}) {
238246
}
239247
};
240248

241-
const handleMouseMove = e => {
249+
/**
250+
* Tracks the mouse movement in the timeline and updates the candidatePlaceholder state
251+
* @param {Event} e
252+
* @returns
253+
*/
254+
const handleTimelineMouseMove = e => {
242255
if (isHoveringSlot) {
243-
setTooltip({visible: false});
256+
setCandidatePlaceholder({visible: false});
244257
return;
245258
}
246-
247259
const parentRect = e.target.getBoundingClientRect();
248-
const totalMinutes = (maxHour - minHour) * 60;
249-
const mouseX = e.clientX - parentRect.left;
250-
let timeInMinutes = (mouseX / parentRect.width) * totalMinutes;
260+
const relativeMouseXPosition = e.clientX - parentRect.left;
261+
const relativeMouseYPosition = parentRect.top - e.clientY;
251262

263+
const totalMinutes = (maxHour - minHour) * 60; // Total minutes in the timeline
264+
let timeInMinutes = (relativeMouseXPosition / parentRect.width) * totalMinutes;
252265
// Round timeInMinutes to the nearest 15-minute interval
253266
timeInMinutes = Math.round(timeInMinutes / 15) * 15;
267+
const slotWidth = (placeHolderSlot.width / parentRect.width) * 100;
254268

255269
const hours = Math.floor(timeInMinutes / 60) + minHour;
256270
const minutes = Math.floor(timeInMinutes % 60);
257271
const time = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
258272

259-
const tempTooltip = {
273+
const tempPlaceholder = {
260274
visible: true,
261275
time,
262-
x: e.clientX,
263-
y: e.clientY,
276+
x: relativeMouseXPosition,
277+
y: relativeMouseYPosition,
278+
clientX: e.clientX,
279+
clientY: parentRect.top + parentRect.height,
280+
height: parentRect.height,
281+
parentHeight: parentRect.height - 10,
282+
width: slotWidth,
264283
};
265-
if (e.clientX >= 0 && e.clientY >= 0 && hours >= 0 && minutes >= 0) {
266-
setTooltip(tempTooltip);
284+
285+
if (hours >= 0 && minutes >= 0) {
286+
setCandidatePlaceholder(tempPlaceholder);
267287
}
268288
};
269289

270-
const handleMouseLeave = () => {
271-
setTooltip({visible: false, time: '', x: 0, y: 0});
290+
const handleTimelineMouseLeave = () => {
291+
setCandidatePlaceholder({visible: false});
272292
};
273293

274294
const groupedCandidates = splitOverlappingCandidates(candidates, duration);
275295

276296
return editing ? (
277-
<div
278-
className={`${styles['timeline-input']} ${styles['edit']}`}
279-
onClick={event => handleMouseDown(event)}
280-
onMouseMove={handleMouseMove}
281-
onMouseLeave={handleMouseLeave}
282-
>
283-
<div className={styles['timeline-candidates']}>
284-
{groupedCandidates.map((rowCandidates, i) => (
285-
<div
286-
className={styles['candidates-group']}
287-
key={i}
288-
onMouseEnter={() => {
289-
setIsHoveringSlot(true);
290-
}}
291-
onMouseLeave={() => {
292-
setIsHoveringSlot(false);
293-
}}
294-
>
295-
{rowCandidates.map(time => {
296-
const slotProps = getCandidateSlotProps(time, duration, minHour, maxHour);
297-
const participants = availability?.find(a => a.startDt === `${date}T${time}`);
298-
return (
299-
<CandidateSlot
300-
{...slotProps}
301-
key={time}
302-
isValidTime={time => !candidates.includes(time)}
303-
onDelete={event => {
304-
// Prevent the event from bubbling up to the parent div
305-
event.stopPropagation();
306-
handleRemoveSlot(event, time);
307-
}}
308-
onChangeSlotTime={newStartTime => handleUpdateSlot(time, newStartTime)}
309-
text={
310-
participants &&
311-
plural(participants.availableCount, {
312-
0: 'No participants registered',
313-
one: '# participant registered',
314-
other: '# participants registered',
315-
})
316-
}
317-
/>
318-
);
319-
})}
320-
{tooltip.visible && (
297+
<Popup
298+
content={candidatePlaceholder.time}
299+
open={candidatePlaceholder.visible}
300+
popperModifiers={[
301+
{
302+
name: 'offset',
303+
enabled: true,
304+
options: {
305+
offset: [candidatePlaceholder.x, 0],
306+
},
307+
},
308+
]}
309+
trigger={
310+
<div
311+
className={`${styles['timeline-input']} ${styles['edit']}`}
312+
onClick={event => handleMouseDown(event)}
313+
onMouseMove={handleTimelineMouseMove}
314+
onMouseLeave={handleTimelineMouseLeave}
315+
>
316+
<div className={styles['timeline-candidates']}>
317+
{groupedCandidates.map((rowCandidates, i) => (
321318
<div
322-
style={{
323-
position: 'fixed',
324-
top: tooltip.y,
325-
left: tooltip.x,
326-
backgroundColor: 'black',
327-
color: 'white',
328-
padding: '5px',
329-
borderRadius: '3px',
330-
pointerEvents: 'none',
331-
transform: 'translate(-50%, -100%)',
332-
zIndex: 1000,
319+
className={styles['candidates-group']}
320+
key={i}
321+
onMouseEnter={() => {
322+
// Prevent the candidate placeholder from showing when hovering over a slot
323+
setIsHoveringSlot(true);
324+
}}
325+
onMouseLeave={() => {
326+
setIsHoveringSlot(false);
333327
}}
334328
>
335-
{tooltip.time}
329+
{rowCandidates.map(time => {
330+
const slotProps = getCandidateSlotProps(time, duration, minHour, maxHour);
331+
const participants = availability?.find(a => a.startDt === `${date}T${time}`);
332+
return (
333+
<CandidateSlot
334+
{...slotProps}
335+
key={time}
336+
isValidTime={time => !candidates.includes(time)}
337+
onDelete={event => {
338+
// Prevent the event from bubbling up to the parent div
339+
event.stopPropagation();
340+
handleRemoveSlot(event, time);
341+
}}
342+
onChangeSlotTime={newStartTime => handleUpdateSlot(time, newStartTime)}
343+
text={
344+
participants &&
345+
plural(participants.availableCount, {
346+
0: 'No participants registered',
347+
one: '# participant registered',
348+
other: '# participants registered',
349+
})
350+
}
351+
/>
352+
);
353+
})}
336354
</div>
355+
))}
356+
{candidatePlaceholder.visible && (
357+
<CandidatePlaceholder
358+
xPosition={candidatePlaceholder.clientX}
359+
yPosition={candidatePlaceholder.clientY}
360+
height={candidatePlaceholder.parentHeight}
361+
widthPercent={candidatePlaceholder.width}
362+
/>
337363
)}
338364
</div>
339-
))}
340-
</div>
341-
<Popup
342-
trigger={
343-
<Icon
344-
className={`${styles['clickable']} ${styles['add-btn']}`}
345-
name="plus circle"
346-
size="large"
347-
/>
348-
}
349-
on="click"
350-
position="bottom center"
351-
onOpen={evt => {
352-
// Prevent the event from bubbling up to the parent div
353-
evt.stopPropagation();
354-
setTimeslotPopupOpen(true);
355-
}}
356-
onClose={handlePopupClose}
357-
open={newTimeslotPopupOpen}
358-
onKeyDown={evt => {
359-
const canBeAdded = timeslotTime && !candidates.includes(timeslotTime);
360-
if (evt.key === 'Enter' && canBeAdded) {
361-
handleAddSlot(timeslotTime);
362-
handlePopupClose();
363-
}
364-
}}
365-
className={styles['timepicker-popup']}
366-
content={
367-
<div
368-
// We need a div to attach events
369-
onClick={e => e.stopPropagation()}
370-
>
371-
<TimePicker
372-
showSecond={false}
373-
value={toMoment(timeslotTime, DEFAULT_TIME_FORMAT)}
374-
format={DEFAULT_TIME_FORMAT}
375-
onChange={time => setTimeslotTime(time ? time.format(DEFAULT_TIME_FORMAT) : null)}
376-
allowEmpty={false}
377-
// keep the picker in the DOM tree of the surrounding element
378-
getPopupContainer={node => node}
379-
/>
380-
<Button
381-
icon
382-
onClick={() => {
383-
handleAddSlot(timeslotTime);
384-
handlePopupClose();
365+
<div onMouseMove={e => e.stopPropagation()} className={styles['add-btn-wrapper']}>
366+
<Popup
367+
trigger={
368+
<Icon
369+
className={`${styles['clickable']} ${styles['add-btn']}`}
370+
name="plus circle"
371+
size="large"
372+
onMouseMove={e => e.stopPropagation()}
373+
/>
374+
}
375+
on="click"
376+
onMouseMove={e => {
377+
e.stopPropagation();
385378
}}
386-
disabled={!timeslotTime || candidates.includes(timeslotTime)}
387-
>
388-
<Icon name="check" />
389-
</Button>
379+
position="bottom center"
380+
onOpen={evt => {
381+
// Prevent the event from bubbling up to the parent div
382+
evt.stopPropagation();
383+
setTimeslotPopupOpen(true);
384+
}}
385+
onClose={handlePopupClose}
386+
open={newTimeslotPopupOpen}
387+
onKeyDown={evt => {
388+
const canBeAdded = timeslotTime && !candidates.includes(timeslotTime);
389+
if (evt.key === 'Enter' && canBeAdded) {
390+
handleAddSlot(timeslotTime);
391+
handlePopupClose();
392+
}
393+
}}
394+
className={styles['timepicker-popup']}
395+
content={
396+
<div
397+
// We need a div to attach events
398+
onClick={e => e.stopPropagation()}
399+
onMouseMove={e => {
400+
e.stopPropagation();
401+
}}
402+
>
403+
<TimePicker
404+
showSecond={false}
405+
value={toMoment(timeslotTime, DEFAULT_TIME_FORMAT)}
406+
format={DEFAULT_TIME_FORMAT}
407+
onChange={time =>
408+
setTimeslotTime(time ? time.format(DEFAULT_TIME_FORMAT) : null)
409+
}
410+
onMouseMove={e => e.stopPropagation()}
411+
allowEmpty={false}
412+
// keep the picker in the DOM tree of the surrounding element
413+
getPopupContainer={node => node}
414+
/>
415+
<Button
416+
icon
417+
onMouseMove={e => e.stopPropagation()}
418+
onClick={() => {
419+
handleAddSlot(timeslotTime);
420+
handlePopupClose();
421+
}}
422+
disabled={!timeslotTime || candidates.includes(timeslotTime)}
423+
>
424+
<Icon name="check" onMouseMove={e => e.stopPropagation()} />
425+
</Button>
426+
</div>
427+
}
428+
/>
390429
</div>
391-
}
392-
/>
393-
</div>
430+
</div>
431+
}
432+
/>
394433
) : (
395434
<div className={styles['timeline-input-wrapper']}>
396435
<div className={`${styles['timeline-input']} ${styles['msg']}`} onClick={handleStartEditing}>

newdle/client/src/components/creation/timeslots/Timeline.module.scss

+8-4
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ $label-width: 180px;
185185
border-radius: 3px;
186186
box-sizing: content-box;
187187
flex-basis: 100%;
188-
// margin-left: 5px;
188+
cursor: pointer;
189189

190190
.timeline-candidates {
191191
display: flex;
@@ -212,9 +212,13 @@ $label-width: 180px;
212212

213213
&.edit {
214214
background-color: lighten($green, 27%);
215-
// border: 5px solid lighten($green, 22%);
216-
// margin: -5px -40px;
217-
// padding: 10px;
215+
border: 5px solid lighten($green, 22%);
216+
padding: 10px;
217+
218+
.add-btn-wrapper {
219+
display: flex;
220+
align-items: center;
221+
}
218222

219223
.add-btn {
220224
margin-left: auto;

0 commit comments

Comments
 (0)