@@ -19,6 +19,7 @@ import {
19
19
import { hourRange , toMoment , getHourSpan , DEFAULT_TIME_FORMAT } from '../../../util/date' ;
20
20
import { useIsSmallScreen } from '../../../util/hooks' ;
21
21
import TimezonePicker from '../../common/TimezonePicker' ;
22
+ import CandidatePlaceholder from './CandidatePlaceholder' ;
22
23
import CandidateSlot from './CandidateSlot' ;
23
24
import DurationPicker from './DurationPicker' ;
24
25
import TimelineHeader from './TimelineHeader' ;
@@ -172,9 +173,16 @@ function TimelineInput({minHour, maxHour}) {
172
173
const [ timeslotTime , setTimeslotTime ] = useState ( latestStartTime ) ;
173
174
const [ newTimeslotPopupOpen , setTimeslotPopupOpen ] = useState ( false ) ;
174
175
// 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
+ } ) ;
176
182
// We don't want to show the tooltip when the mouse is hovering over a slot
177
183
const [ isHoveringSlot , setIsHoveringSlot ] = useState ( false ) ;
184
+ // const baseWidth = calculateWidth("00:00", duration, minHour, maxHour);
185
+ const placeHolderSlot = getCandidateSlotProps ( '00:00' , duration , minHour , maxHour ) ;
178
186
179
187
useEffect ( ( ) => {
180
188
setTimeslotTime ( latestStartTime ) ;
@@ -238,159 +246,190 @@ function TimelineInput({minHour, maxHour}) {
238
246
}
239
247
} ;
240
248
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 => {
242
255
if ( isHoveringSlot ) {
243
- setTooltip ( { visible : false } ) ;
256
+ setCandidatePlaceholder ( { visible : false } ) ;
244
257
return ;
245
258
}
246
-
247
259
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 ;
251
262
263
+ const totalMinutes = ( maxHour - minHour ) * 60 ; // Total minutes in the timeline
264
+ let timeInMinutes = ( relativeMouseXPosition / parentRect . width ) * totalMinutes ;
252
265
// Round timeInMinutes to the nearest 15-minute interval
253
266
timeInMinutes = Math . round ( timeInMinutes / 15 ) * 15 ;
267
+ const slotWidth = ( placeHolderSlot . width / parentRect . width ) * 100 ;
254
268
255
269
const hours = Math . floor ( timeInMinutes / 60 ) + minHour ;
256
270
const minutes = Math . floor ( timeInMinutes % 60 ) ;
257
271
const time = `${ String ( hours ) . padStart ( 2 , '0' ) } :${ String ( minutes ) . padStart ( 2 , '0' ) } ` ;
258
272
259
- const tempTooltip = {
273
+ const tempPlaceholder = {
260
274
visible : true ,
261
275
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 ,
264
283
} ;
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 ) ;
267
287
}
268
288
} ;
269
289
270
- const handleMouseLeave = ( ) => {
271
- setTooltip ( { visible : false , time : '' , x : 0 , y : 0 } ) ;
290
+ const handleTimelineMouseLeave = ( ) => {
291
+ setCandidatePlaceholder ( { visible : false } ) ;
272
292
} ;
273
293
274
294
const groupedCandidates = splitOverlappingCandidates ( candidates , duration ) ;
275
295
276
296
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 ) => (
321
318
< 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 ) ;
333
327
} }
334
328
>
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
+ } ) }
336
354
</ div >
355
+ ) ) }
356
+ { candidatePlaceholder . visible && (
357
+ < CandidatePlaceholder
358
+ xPosition = { candidatePlaceholder . clientX }
359
+ yPosition = { candidatePlaceholder . clientY }
360
+ height = { candidatePlaceholder . parentHeight }
361
+ widthPercent = { candidatePlaceholder . width }
362
+ />
337
363
) }
338
364
</ 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 ( ) ;
385
378
} }
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
+ />
390
429
</ div >
391
- }
392
- />
393
- </ div >
430
+ </ div >
431
+ }
432
+ / >
394
433
) : (
395
434
< div className = { styles [ 'timeline-input-wrapper' ] } >
396
435
< div className = { `${ styles [ 'timeline-input' ] } ${ styles [ 'msg' ] } ` } onClick = { handleStartEditing } >
0 commit comments