Search code examples
htmlcss

How to trigger :hover effect on multiple overlapped tags X-axis/Rows and Y-axies/column markers


example of real layout

I have been playing with a pure html-css gantt-styled chart. Still a lot to go.

But I can't find the solution to trigger "hover" on the time-axis vertical divs, which are position absolute, and are below all the contents drawn by each row.

Hovering the rows works as expected and also on their child-bars, but obstructs overlapped elements to be triggered.

I quickly made this shortened example on fiddle https://jsfiddle.net/diegoj2020/52snopkb/17/

My objective is to highlight the date where the mouse is pointing at.

I am trying to avoid javascript for layouting or styling. I would like to find the pure-css method if any.

In the fiddle example, the expected behavior is to also highlight the vertical bars when the mouse sits on any row.

fiddle example

Any help would be very appreciated.

/*
The attempt her eis to highlight the row, the column, and the insight bar/object, all of 3 at the same time when the mouse is over.
*/
* {
  margin: 0;
  padding: 0;
}

xy-plot {
  position: absolute;
  width: 95%;
  border: 1px solid red;
  padding: 5px;
  margin: 10px;
}

cols {
  position: absolute;
  display: block;
  left: 100px;
  right: 10px;
  height: 100%;
}

rows {
  position: relative;
  display: block;
  margin-top: 30px;
}

day {
  box-sizing: border-box;
  display: inline-block;
  background-color: #2222;
  ;
  width: calc(100% / 365);
  height: 100%;
}

day:nth-child(even) {
  background-color: #5552;
}

task {
  display: block;
  position: relative;
}

taskbar {
  display: inline-block;
  position: absolute;
  left: 250px;
  width: 100px;
  background-color: red;
  height: 100%;
  border-radius: 8px;
}

task:hover {
  background-color: darkgray;
}

day:hover {
  background-color: darkgray;
}

taskbar:hover {
  background-color: black;
}
<xy-plot>
  <cols>
    <script>
      // populate columns time-axis
      document.write('<day></day>'.repeat(365));
    </script>
  </cols>
  <rows>
    <script>
      // populate rows and inside elements
      document.write('<task>A Task<taskbar></taskbar></task>'.repeat(10));
    </script>
  </rows>
</xy-plot>


Solution

  • The heart of your question is the ability for a mouse hover to apply to all elements in the z-index at the pointer location.

    enter image description here

    CSS does not support this. By default, :hover is only triggered on the element in front, or with the highest z-index, at the current pointer location.

    screenshot showing desired scenario

    body {
      margin: 1em;
    }
    
    .d {
      display: inline-block;
      margin-right: 3em;
    }
    
    .d > * {
      width: 6em;
      aspect-ratio: 1;
      opacity: 0.7;
    }
    
    .d > :first-child {
      background: red;
    }
    
    .d > :last-child {
      background: green;
      margin-top: -3em;
      margin-left: 3em;
    }
    
    .d > :hover {
      outline: 5px solid black;
    }
    
    .d2 > :last-child {
      pointer-events: none;  
    }
    <div class="d">
      <div></div>
      <div></div>
    </div>

    The only control CSS supports is the ability to hide an element from all mouse interaction by styling it with pointer-events: none. This effectively makes the element invisible to the mouse pointer, so :hover will trigger on the next-highest element in the z-order. (Of course, this won’t be of any help in your scenario.)

    screenshot showing hover applying to next element when front element is styled with pointer-events: none

    body {
      margin: 1em;
    }
    
    .d {
      display: inline-block;
      margin-right: 3em;
    }
    
    .d > * {
      width: 6em;
      aspect-ratio: 1;
      opacity: 0.7;
    }
    
    .d > :first-child {
      background: red;
    }
    
    .d > :last-child {
      background: green;
      margin-top: -3em;
      margin-left: 3em;
      pointer-events: none;  
    }
    
    .d > :hover {
      outline: 5px solid black;
    }
    <div class="d">
      <div></div>
      <div></div>
    </div>

    To solve it you will need to use script, because the DOM provides the method document.elementsFromPoint(x, y) which returns all elements at the specified coordinates. Set up a mousemove event listener on the parent element which calls this method.

    const d = document.querySelector('.d')
    
    const hasHoverClass = () => {
      return Array.from(d.querySelectorAll('.hover'))
    }
    
    const removeHover = arr => {
      arr.forEach(a => {
        a.classList.remove('hover')
      })
    }
    
    d.addEventListener('mousemove', evt => {
      const hovCls = hasHoverClass()
      const moused = document.elementsFromPoint(evt.clientX, evt.clientY)
    
      moused.forEach(a => {           // for each element under the mouse
        const i = hovCls.indexOf(a)   // is it already styled hovered?
        if (i == -1)                  // no ...
          a.classList.add('hover')    // ... so style it hovered
        else                          // yes ...
          hovCls.splice(i,1)          // ... so leave it alone
      })
      removeHover(hovCls)             // clear the hover from the rest
    })
    
    d.addEventListener('mouseout', evt => {
      removeHover(hasHoverClass())
    })
    body {
      margin: 1em;
    }
    
    .d {
      display: inline-block;
      margin-right: 3em;
    }
    
    .d > * {
      width: 6em;
      aspect-ratio: 1;
      opacity: 0.7;
    }
    
    .d > :first-child {
      background: red;
    }
    
    .d > :last-child {
      background: green;
      margin-top: -3em;
      margin-left: 3em;
    }
    
    .d > .hover {
      outline: 5px solid black;
    }
    <div id="x" class="d">
      <div id="y"></div>
      <div id="z"></div>
    </div>

    Now take that logic and apply it to your example, as follows:

    enter image description here

    const d = document.querySelector('xy-plot')
    const tags = ['DAY','TASK','TASKBAR']
    
    const hasHoverClass = () => {
      return Array.from(d.querySelectorAll('.hover'))
    }
    
    const removeHover = arr => {
      arr.forEach(a => {
        a.classList.remove('hover')
      })
    }
    
    d.addEventListener('mousemove', evt => {
      const hovCls = hasHoverClass()
      const moused = document.elementsFromPoint(evt.clientX, evt.clientY).filter(a => {return tags.includes(a.nodeName)})
    
      moused.forEach(a => {           // for each element under the mouse
        const i = hovCls.indexOf(a)   // is it already styled hovered?
        if (i == -1)                  // no ...
          a.classList.add('hover')    // ... so style it hovered
        else                          // yes ...
          hovCls.splice(i,1)          // ... so leave it alone
      })
      removeHover(hovCls)             // clear the hover from the rest
    })
    
    d.addEventListener('mouseout', evt => {
      removeHover(hasHoverClass())
    })
    * {
      margin: 0;
      padding: 0;
    }
    
    xy-plot {
      position: absolute;
      width: 95%;
      border: 1px solid red;
      padding: 5px;
      margin: 10px;
    }
    
    cols {
      position: absolute;
      display: block;
      left: 100px;
      right: 10px;
      height: 100%;
    }
    
    rows {
      position: relative;
      display: block;
      margin-top: 30px;
    }
    
    day {
      box-sizing: border-box;
      display: inline-block;
      background-color: #2222;
      width: calc(100% / 100);
      height: 100%;
    }
    
    day:nth-child(even) {
      background-color: #5552;
    }
    
    task {
      display: block;
      position: relative;
    }
    
    taskbar {
      display: inline-block;
      position: absolute;
      left: 250px;
      width: 100px;
      background-color: #ff00008f;
      height: 100%;
      border-radius: 8px;
    }
    
    day.hover, task.hover {
      background-color: yellow;
    }
    
    taskbar.hover {
      background-color: pink;
    }
    <xy-plot>
      <cols> 
        <script>
          // populate columns time-axis
          document.write('<day></day>'.repeat(100)); 
        </script> 
      </cols>
      <rows> 
        <script> 
          // populate rows and inside elements
          document.write('<task>A Task<taskbar></taskbar></task>'.repeat(10)); 
        </script> 
      </rows>
    </xy-plot>