偶遇H5之tabs滚动固定且根据当前显示内容激活对应tab

724 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

前言

最近公司运营部门给提了个H5活动页的需求,需要实现页面滚动固定tab、tab切换和页面滚动两者直接的联动效果,而项目中使用的UI组件库无法满足需求。这里记录一下实现方法以及实现过程中遇到的问题。

思路

拆分需求:

  1. 根据页面滚动固定tab-list
  2. 滚动页面时根据当前显示的内容区激活对应的tab
  3. 点击切换tab时页面滚到相应内容区

解决思路:

  1. 针对第一点,可以通过监听页面的scroll事件,判断当前页面的scrollTop是否超过tab-list与页面顶部的距离。
    // 获取页面已滚动的距离
    const scrollTop = document.body.scrollTop || document.documentElement.scrollTop || window.pageYOffset;
    // 获取tab-list与页面顶部的距离
    const headDom = document.querySelector('XXX');
    const tabTop = headDom.getBoundingClientRect().height;
    // 比较两者的大小
    if (scrollTop > tabTop) {
        // 固定tab-list
    } else {
        // 取消固定
    }
  1. 针对第二点,同样可以在页面滚动的事件中进行处理。通过判断各内容区与页面顶部的距离和tab-list高度来解决。注: 在使用内容区和tab-list高度进行判断当前应该激活哪个tab时,判断顺序要从下往上,因为当底下的某一内容区刚满足条件时,它上面的内容区已经超出可视区了。
  // 获取tab-list的高度
  const tabHeight = document.querySelector('#tabs').offsetHeight;
  // 获取各内容区的实例
  const cont2 = // 获取各内容区的实例;
  const cont3 = // 获取各内容区的实例;
  // 判断各内容区顶部距离和tab-list高度
  if(cont3.getBoundingClientRect().top <= tabHeight) {
    // 激活对应tab
  } else if(cont2.getBoundingClientRect().top <= tabHeight) {
    // 激活对应tab
  } else {
    // 激活对应tab
  }

针对第三点

第三点算是实现这个需求时碰到的一个坑点?(应该算是坑吧),一开始的想法是通过点击切换tab时,使用scrollIntoView将对应的内容区元素滚动到顶部。

但是因为tab-list是通过fixed定位的,与内容器不在一个文档流,导致使用scrollIntoView会将内容区直接顶到窗口顶部,部分内容会被tab-list覆盖。

元素的顶端将和其所在滚动区的可视区域的顶端对齐

取决于其它元素的布局情况,此元素可能不会完全滚动到顶端或底端。

---- 摘取自 MDN-ScrollIntoView

最后通过修改页面的scrollTop解决该问题,需要计算每个内容区显示在可视区时需要滚动的距离。

      // 获取内容区元素实例
      const cont1 = this.$refs['cont-1'];
      const cont2 = this.$refs['cont-2'];
      // 获取内容区高度
      const cont1H = cont1.offsetHeight;
      const cont2H = cont2.offsetHeight;
      /*
        根据要切换的tab设置相应的scrollTop
        200 是顶部logo的高度,可以动态去获取
      */
      if(tab === '1') {
        // cont1.scrollIntoView();
        document.documentElement.scrollTop = 200;
      } else if(tab === '2') {
        // cont2.scrollIntoView();
        document.documentElement.scrollTop = 200 + cont1H;
      } else {
        // cont3.scrollIntoView();
        document.documentElement.scrollTop = 200 + cont1H + cont2H;
      }

最终效果

tab-demo (1).gif

代码

结构和样式只是为了演示最终效果。

<template>
  <div class="demo-wrap">
    <!-- header -->
    <div class="head-logo">
      这是顶部LOGO展示区
    </div>

    <!-- tab-list -->
    <div
      id="tabs"
      :class="['tab-list', { 'fix-tab': isFixTab }]"
      @click="changeTab"
    >
      <div
        v-for="tab in tabList"
        :key="tab.value"
        :data-tab="tab.value"
        :class="['tab-item', { active: activeTab === tab.value }]"
      >
        <span :data-tab="tab.value" class="tab-name">{{ tab.label }}</span>
      </div>
    </div>

    <!-- content -->
    <div ref="cont-wrap" :class="['content-wrap', {'fix-tab-gap': isFixTab}]">
      <div ref="cont-1" class="content content-1">这是1号内容区</div>
      <div ref="cont-2" class="content content-2">这是2号内容区</div>
      <div ref="cont-3" class="content content-3">这是3号内容区</div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // tab-list
      tabList: [
        {
          value: '1',
          label: '1号'
        },
        {
          value: '2',
          label: '2号'
        },
        {
          value: '3',
          label: '3号'
        }
      ],
      // 是否需要固定tab-list
      isFixTab: false,
      // 当前激活的tab
      activeTab: '1'
    };
  },
  mounted() {
    window.addEventListener('scroll', this.handleScroll);
  },
  beforeDestroy() {
    window.removeEventListener('scroll', this.handleScroll);
  },
  methods: {
    // 点击切换tab
    changeTab(e) {
      const tab = e.target.dataset.tab;
      if (!tab || tab === this.activeTab) {
        return;
      }
      this.activeTab = tab;
      this.changeScrollTop(tab);
    },
    // 切换tab时将对应内容区显示在可视区
    changeScrollTop(tab) {
      // 获取内容区元素实例
      const cont1 = this.$refs['cont-1'];
      const cont2 = this.$refs['cont-2'];
      // 获取内容区高度
      const cont1H = cont1.offsetHeight;
      const cont2H = cont2.offsetHeight;
      /*
        根据要切换的tab设置相应的scrollTop
        200 是顶部logo的高度,可以动态去获取
      */
      if(tab === '1') {
        // cont1.scrollIntoView();
        document.documentElement.scrollTop = 200;
      } else if(tab === '2') {
        // cont2.scrollIntoView();
        document.documentElement.scrollTop = 200 + cont1H;
      } else {
        // cont3.scrollIntoView();
        document.documentElement.scrollTop = 200 + cont1H + cont2H;
      }
    },
    // 页面滚动事件
    handleScroll() {
      this.handleFixTab();
      this.handleContentScroll();
    },
    // 页面滚动时,根据当前在可视区窗口显示的内容区激活对应的tab
    handleContentScroll() {
      const tabHeight = document.querySelector('#tabs').offsetHeight;
      const cont2 = this.$refs['cont-2'];
      const cont3 = this.$refs['cont-3'];
      if(cont3.getBoundingClientRect().top <= tabHeight) {
        this.activeTab = '3'
      } else if(cont2.getBoundingClientRect().top <= tabHeight) {
        this.activeTab = '2'
      } else {
        this.activeTab = '1'
      }
    },
    // 判定是否要固定tab-list, 根据页面滚动距离是否大于顶部logo的高度
    handleFixTab() {
      const scrollTop =
        document.body.scrollTop ||
        document.documentElement.scrollTop ||
        window.pageYOffset;
      const headDom = document.querySelector('.head-logo');
      const tabTop = headDom.getBoundingClientRect().height;
      if (scrollTop > tabTop) {
        this.isFixTab = true;
      } else {
        this.isFixTab = false;
      }
    }
  }
};
</script>

<style lang="less" scoped>
.demo-wrap {
  position: relative;
  margin-bottom: 300px;
  .head-logo {
    height: 200px;
    line-height: 200px;
    text-align: center;
    background: black;
    color: #fff;
  }
  .tab-list {
    display: flex;
    background: #fff;
    &.fix-tab {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
    }
    .tab-item {
      width: 33%;
      text-align: center;
      height: 50px;
      line-height: 50px;
      font-size: 18px;
      font-weight: bold;
      &.active {
        color: #faab0a;
        .tab-name {
          border-bottom-color: #faab0a;
        }
      }
      .tab-name {
        padding-bottom: 6px;
        border-bottom: 4px solid transparent;
        border-radius: 4px;
      }
    }
  }
  .content-wrap {
    &.fix-tab-gap {
      margin-top: 50px;
    }
    .content {
      width: 100%;
      height: 400px;
      color: #fff;
      font-size: 24px;
      line-height: 400px;
      text-align: center;
      &-1 {
        background: red;
      }
      &-2 {
        background: green;
      }
      &-3 {
        background: blue;
      }
    }
  }
}
</style>