Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best approach to hide an absolutely positioned <div> when horizontally scrolling?

I have an example of my current work here: https://jsfiddle.net/pv5xroLc/

My problem is that when the table in my example is fully scrolled to the right, the faded gradient still covers part of my table even though it cannot be scrolled further, thus it makes the last column harder to read. I am wondering what the best solution to hiding this gradient is while still making it clear that the table can be scrolled horizontally (this is going to appear on mobile).

Currently, my html structure is as follows:

<div class="fader">
    <div class="scrollable">
       *content*
    </div>
</div>

The .fader element has an ::after pseudo-element which contains the "fader" on it, which is an absolutely positioned element with the linear-gradient I'm using to indicate that the element can be scrolled horizontally. The .scrollable element is a horizontally scrolling element that holds my table.


I currently have two solutions I have considered:

  1. Add a listener to check when the scrollbar has reached the right side (like this example), then hide or fade out the gradient. The problem with this is that I have more than one of these faded tables on the page, and I'm not sure what the most effective way to setup these listeners would be. I'm using Vue.js with this, so I'm not sure if this could/should be a directive or just a listener set on the page for each of these tables.
  2. Add some blank space to the right of the table, so you could scroll a little bit past the end of the actual table and the gradient would just blend into the background. I've tried adding padding and margins to both the table and the .scrollable element but it does not add any extra space after the table.

If anyone has suggestions for what they think I should do, it would be greatly appreciated.

like image 443
Nick G Avatar asked Jan 11 '19 16:01

Nick G


2 Answers

If I needed to implement this functionality, I would make a Vue component that would take in the table content (or any content) in a slot and then listen to the scroll event of the .scrollable div, adding or removing the faded ::after content if the div was scrolled all the way to the right.

Here's an example:

Vue.component('fader', {
  template: `
    <div class="fader" :class="{ 'scrolled-right': isScrolledRight }">
      <div class="scrollable" ref="scrollable">
        <slot></slot>
      </div>
    </div>
  `,
  data() {
    return {
      isScrolledRight: false,
    }
  },
  methods: {
    onScroll(event) {
      this.updateIsScrolledRight(event.target);
    },
    updateIsScrolledRight({ scrollLeft, offsetWidth, scrollWidth }) {
      this.isScrolledRight = (scrollLeft + offsetWidth) === scrollWidth;
    }
  },
  mounted() {
    this.$refs.scrollable.addEventListener('scroll', this.onScroll);
    this.updateIsScrolledRight(this.$refs.scrollable);
  },
  destroyed() {
    this.$refs.scrollable.removeEventListeneer('scroll', this.onScroll);
  }
}) 

.fader.scrolled-right::after {
  opacity: 0;
}

Here's how the component works:

  • A ref property is added to the .scrollable div so that it can be easily referenced in the component's script.
  • An onScroll method is attached to the scroll event of the scrollable ref when the component is mounted and removed when the component is destroyed.
  • The onScroll method calls an updateIsScrolledRight method, passing it the scroll event's target (the .scrollable div).
  • The updateIsScrolledRight method looks at the scrollLeft, offsetWidth, and scrollWidth properties of the element passed as the parameter to determine if the element is scrolled all the way to the right and sets an isScrolledRight property to true if so and false if not.
  • The root div of the component has a bound :class attribute which will add the scrolled-right class to the div if the value of isScrolledRight is true.
  • The .scrolled-right class sets the div's ::after content to have opacity: 0;.
  • The updateIsScrolledRight method is also called in the mounted hook so that, if the content in the <slot> happens to not be wide enough to need a scrollbar, the fade will be removed in that case as well.

Here's a full working example:

Vue.component('fader', {
  template: `
    <div class="fader" :class="{ 'scrolled-right': isScrolledRight }">
      <div class="scrollable" ref="scrollable">
        <slot></slot>
      </div>
    </div>
  `,
  data() {
    return {
      isScrolledRight: false,
    }
  },
  methods: {
    onScroll(event) { 
      this.updateIsScrolledRight(event.target);
    },
    updateIsScrolledRight({ scrollLeft, offsetWidth, scrollWidth }) {
      this.isScrolledRight = (scrollLeft + offsetWidth) === scrollWidth;
    }
  },
  mounted() {
    this.$refs.scrollable.addEventListener('scroll', this.onScroll);
    this.updateIsScrolledRight(this.$refs.scrollable);
  },
  destroyed() {
    this.$refs.scrollable.removeEventListeneer('scroll', this.onScroll);
  }
})

new Vue({
  el: "#app",
})
.fader {
  position: relative;
  width: 90%;
  margin-left: 46px;
}

.fader::after {
  content: "";
  position: absolute;
  z-index: 1;
  top: 0;
  right: -1px;
  bottom: 15px;
  pointer-events: none;
  background: linear-gradient(to right, rgba(255, 255, 255, 0.1), white);
  width: 10%;
  opacity: 1;
  transition: opacity .2s ease-out;
}

.fader .scrollable {
  white-space: nowrap;
  overflow-x: scroll;
  position: relative;
}
  
.fader.scrolled-right::after {
  opacity: 0;
}

.breakdown-title {
  font-size: 14px;
  font-weight: 700;
  text-align: center;
  margin: 8px auto;
}

table {
  font-size: 12px;
  margin: auto;
  color: #000;
  width: 100%;
  table-layout: fixed;
}

table thead {
  color: #fff;
  background-color: #da291c;
}

table thead th {
  width: 75px;
  text-align: right;
}

table thead th:first-of-type {
  width: 120px;
  padding-left: 4px;
}

table thead th:last-of-type {
  width: 80px;
  padding-right: 4px;
}

table tbody tr:nth-of-type(odd) {
  background-color: #fce9e8;
}

table tbody td {
  width: 75px;
  text-align: right;
}
 
table tbody td:first-of-type {
  width: 120px;
  text-align: left;
  padding-left: 4px;
}

table tbody td:last-of-type {
  width: 80px;
  padding-right: 4px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <fader>
    <div class="breakdown-title">Total Revenue Bonus</div>
    <table>
      <thead>
        <tr>
          <th></th>
          <th>Oct</th>
          <th>Nov</th>
          <th>Dec</th>
          <th>Jan</th>
          <th>Feb</th>
          <th>Mar</th>
          <th>Apr</th>
          <th>May</th>
          <th>Jun</th>
          <th>Jul</th>
          <th>Aug</th>
          <th>Sep</th>
          <th>Year End</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>YTD Target</td>
          <td>$1,325,705</td>
          <td>$2,651,410</td>
          <td>$3,977,115</td>
          <td>$5,302,821</td>
          <td>$6,628,526</td>
          <td>$7,954,231</td>
          <td>$9,279,936</td>
          <td>$10,605,642</td>
          <td>$11,931,347</td>
          <td>$13,257,052</td>
          <td>$14,582,757</td>
          <td>$15,908,463</td>
          <td>$15,908,463</td>
        </tr>
        <tr>
          <td>YTD Actual</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
        </tr>
        <tr>
          <td>% to Target</td>
          <td>2%</td>
          <td>1%</td>
          <td>1%</td>
          <td>0%</td>
          <td>0%</td>
          <td>0%</td>
          <td>0%</td>
          <td>0%</td>
          <td>0%</td>
          <td>0%</td>
          <td>0%</td>
          <td>0%</td>
          <td>0%</td>
        </tr>
      </tbody>
    </table>
  </fader>
</div>
like image 194
thanksd Avatar answered Nov 20 '22 10:11

thanksd


(I'd rather comment on the OP than answer, but it won't let me start a comment yet because I'm too new so apologies for that.)

I've dealt with stuff like this before, and my quick hack following your #2 solution idea above would be to add another <th> after <th>Year End</th>, then style that with the appropriate width to match the gradient fade. You could then decide whether to also put in blank <td>s below it.

ALSO I noticed that your white gradient has a pretty solid white line where it starts (right above "$9") - you can smooth that out in line 15/16 of your SCSS by adding more reference points (it took me a while to figure this out back in the day, just including in case it's helpful):

background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.2), white);

width: 30%;


  1. Current hard line to white:

current with hard line

  1. Softer transition to white:

softer transition line

like image 35
DriveItLikeYouStoleIt Avatar answered Nov 20 '22 12:11

DriveItLikeYouStoleIt