Search code examples
javascriptreactjsnext.jstailwind-css

Positioning element according to coordinates


const renderTimeSlots = () => {
    const timeSlots = [];
    for (let i = parseInt(workStartsAt); i <= parseInt(workEndsAt); i++) {
      if (i !== 0) {
        timeSlots.push(
          <div className="flex flex-row cursor-pointer">
            <p key={i} className="text-slate-400 h-[150px]">{i < 10 ? "0" + i : i.toString()}:00</p>
            <div className="w-[90%] h-[1px] bg-slate-400 mt-2.5 ml-3"></div>
          </div>
        );
      }
    }

    return timeSlots;
};


const handlePosClick = (e: any) => {
    setTimeSlotItems([...timeSlotItems, { date: currentSelectedDate, div: <div onClick={handleOpen} className="w-[20%] h-[60px] bg-red-400 rounded-[1rem] absolute cursor-pointer" style={{ left: e.clientX + "px", top: (e.clientY - 20) + "px" }}></div> }])
}

<>
<div className="w-[72vw] -mr-[18vw] -mt-3 shadow-[0_3px_10px_rgb(0,0,0,0.2)] pl-5 pt-3 ml-5">
    <p className="text-black font-bold">{moment(currentSelectedDate.fullDate).subtract(1, 'day').format('ddd DD MMMM YYYY')}</p>

    <div className="flex flex-col h-[93%]" onClick={handlePosClick}>
      {renderTimeSlots()}
    </div>
</div>

{timeSlotItems.map((item: any) => {
  if (item.date == currentSelectedDate) return item.div
})}
</>

I have this code where it renders the times (I'll give an example of 07am to 07pm. And wherever I click, I want it do add a div. When I am not scrolled down, it works properly, but as soon as I scroll even a little bit, it adds the div a lot higher than where I clicked

When I scroll I want it to add the items where I clicked


Solution

  • In your placement logic, you are using clientX and clientY of the MouseEvent:

    { left: e.clientX + "px", top: (e.clientY - 20) + "px" }
    

    But if we look at what these clientX/Y properties are, according to the MDN documentation:

    The clientY read-only property of the MouseEvent interface provides the vertical coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page).

    For example, clicking on the top edge of the viewport will always result in a mouse event with a clientY value of 0, regardless of whether the page is scrolled vertically.

    So, when you click when the webpage is scrolled, clientX/Y takes the distance from the top of the viewport, but top positions the element relative to the nearest position parent, <body> in this case. This causes the mismatch in position.

    Instead, consider using the pageX/Y properties of the MouseEvent object:

    { left: e.pageX + "px", top: (e.pageY - 20) + "px" }
    

    As per the MDN documentation:

    The pageY read-only property of the MouseEvent interface returns the Y (vertical) coordinate in pixels of the event relative to the whole document. This property takes into account any vertical scrolling of the page.

    const workStartsAt = 7;
    const workEndsAt = 19;
    
    const handleOpen = () => {};
    
    const renderTimeSlots = () => {
      const timeSlots = [];
      for (let i = parseInt(workStartsAt); i <= parseInt(workEndsAt); i++) {
        if (i !== 0) {
          timeSlots.push(
            <div className="flex flex-row cursor-pointer">
              <p key={i} className="text-slate-400 h-[150px]">
                {i < 10 ? '0' + i : i.toString()}:00
              </p>
              <div className="w-[90%] h-[1px] bg-slate-400 mt-2.5 ml-3"></div>
            </div>
          );
        }
      }
    
      return timeSlots;
    };
    
    function App() {
      const [timeSlotItems, setTimeSlotItems] = React.useState([]);
      const currentSelectedDate = '20240404';
    
      const handlePosClick = (e) => {
        setTimeSlotItems([
          ...timeSlotItems,
          {
            date: currentSelectedDate,
            div: (
              <div
                onClick={handleOpen}
                className="w-[20%] h-[60px] bg-red-400 rounded-[1rem] absolute cursor-pointer"
                style={{ left: e.pageX + 'px', top: e.pageY - 20 + 'px' }}
              ></div>
            ),
          },
        ]);
      };
    
      return (
        <React.Fragment>
          <div className="w-[72vw] -mr-[18vw] -mt-3 shadow-[0_3px_10px_rgb(0,0,0,0.2)] pl-5 pt-3 ml-5">
            <p className="text-black font-bold">
              {moment(currentSelectedDate.fullDate)
                .subtract(1, 'day')
                .format('ddd DD MMMM YYYY')}
            </p>
    
            <div className="flex flex-col h-[93%]" onClick={handlePosClick}>
              {renderTimeSlots()}
            </div>
          </div>
    
          {timeSlotItems.map((item) => {
            if (item.date == currentSelectedDate) return item.div;
          })}
        </React.Fragment>
      );
    }
    
    ReactDOM.createRoot(document.getElementById('app')).render(<App/>);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js" integrity="sha512-QVs8Lo43F9lSuBykadDb0oSXDL/BbZ588urWVCRwSIoewQv/Ewg1f84mK3U790bZ0FfhFa1YSQUmIhG+pIRKeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js" integrity="sha512-6a1107rTlA4gYpgHAqbwLAtxmWipBdJFcq8y5S/aTge3Bp+VAklABm2LO+Kg51vOWR9JMZq1Ovjl5tpluNpTeQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js" integrity="sha512-QoJS4DOhdmG8kbbHkxmB/rtPdN62cGWXAdAFWWJPvUFF1/zxcPSdAnn4HhYZSIlVoLVEJ0LesfNlusgm2bPfnA==" crossorigin="anonymous"></script>
    <script src="https://cdn.tailwindcss.com/3.4.3"></script>
    
    <div id="app"></div>