ヘッダー固定のリストコンポーネントの実装

# 始めに
以下のようなヘッダーを固定にした一覧を表示するコンポーネントを作った際に色々ハマったことがあるので備忘録として残したいと思います。

![Oct-14-2020 17-15-46.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187768/cc6bb4cd-43d1-6097-3f62-6de4be804130.gif)

# 一番単純な実装
一番単純なのはheaderに`position: sticky;`をつけることです。

```vue:stickyを使ったStickyListコンポーネント
<template lang="pug">
.sticky-list
  .sticky-list__header
    slot(name="header")
  .sticky-list__content
    slot(name="content")
</template>

<style lang="scss" scoped>
.sticky-list {
  position: relative;
  width: 100%;
  height: 100%;
  overflow-y: auto;
  
  &__header {
    position: sticky;
    top: 0;
    left: 0;
  }
}
</style>
```

呼び出しがわは以下のような感じでやると一番最初に表示したGIFアニメのようなものができます。


```vue:呼び出し側
<template lang="pug">
StickyList
  template(v-slot:header)
    .header
      .header__column.-item1 番号
      .header__column.-item2 項目2
      .header__column.-item3 項目3
  template(v-slot:content)
    .content
      template(v-for="index in 30")
        content__item
          .content__item__column.-item1 {{ index }}
          .content__item__column.-item2 内容2
          .content__item__column.-item3 内容3
</template>

<style lang="scss" scoped>
// 各カラム幅はmixinで設定しておく
@mixin list-width() {
  &.-item1 {
    flex: 0 0 50px;
  }

  &.-item2 {
    flex: 1 1 0;
  }

  &.-item3 {
    flex: 2 2 0;
  }
}

.header {
  display: flex;
  padding: 10px;
  background-color: #fff;
  border-bottom: solid 1px #ccc;
  
  &__column {
    @include list-width();
  }
}

.content {
  &__item {
    display: flex;
    padding: 10px;

    & + & {
      border-top: solid 1px #ccc;
    }

    &__column {
      @include list-width();
    }
  }
}
</style>
```

## 問題点
基本的にはこれで問題ないですが、親要素がflexを使っている場合、iOS Safariだと上手くいかない場合があるようです😓

+ [position:stickyの要素が、flexbox内で機能しない（safari）](https://teratail.com/questions/164794)

## サンプルコード
サンプルコードは以下になります。CodePenではVueファイルは書けないので別な書き方になっていますが基本は同じです。

<p class="codepen" data-height="437" data-theme-id="dark" data-default-tab="html,result" data-user="wintyo" data-slug-hash="xxOwBbB" style="height: 437px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="StickyList（stickyで実装）">
  <span>See the Pen <a href="https://codepen.io/wintyo/pen/xxOwBbB">
  StickyList（stickyで実装）</a> by wintyo (<a href="https://codepen.io/wintyo">@wintyo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

flexで書いたパターンも用意していますので、iPhoneがある方はこちらから動作を確認してみると上手く動いていないことが分かると思います。
https://codepen.io/wintyo/full/xxOwBbB

# コンテンツ内だけスクロールさせる
前項のやり方では上手くいかない時があるので、愚直にコンテンツ内だけスクロールするようなコンポーネントにしてみます。
この際にコンテンツ側だけスクロールバーが出てしまうため、その幅分ヘッダーにpaddingを入れて幅を合わせます。

```vue:コンテンツ内だけスクロールさせる
<template lang="pug">
.sticky-list
  .sticky-list__header(
    :style="{ paddingRight: `${$data.scrollBarWidth}px` }"
  )
    slot(name="header")
  .sticky-list__content(
    ref="elContent"
  )
    slot(name="content")
</template>

<script>
export default Vue.extend({
  data() {
    return {
      scrollBarWidth: 0,
    };
  },
  mounted() {
    const { elContent } = this.$refs;
    // スクロールバーの幅を計算する
    this.$data.scrollBarWidth = elContent.offsetWidth - elContent.clientWidth;
  }
});
</script>

<style lang="scss" scoped>
.sticky-list {
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
  
  &__content {
    flex: 1 1 0;
    overflow-x: hidden;
    overflow-y: scroll;
  }
}
</style>
```

実行すると以下のようなGIFアニメになります。

![Oct-14-2020 17-04-47.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187768/1e7cffaf-57dc-b301-0cba-b60809a03be8.gif)

## サンプルコード
このやり方のサンプルコードは以下になります。

<p class="codepen" data-height="425" data-theme-id="dark" data-default-tab="html,result" data-user="wintyo" data-slug-hash="BazoMrj" style="height: 425px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Sticky List（paddingで実装）">
  <span>See the Pen <a href="https://codepen.io/wintyo/pen/BazoMrj">
  Sticky List（paddingで実装）</a> by wintyo (<a href="https://codepen.io/wintyo">@wintyo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

# 終わりに
以上がヘッダー固定のリストコンポーネントの実装でした。基本的には`position: sticky`で済ませられるならそれにしたいですが、意外と罠がありましたので、確実にやる場合は２つ目のやり方を参考にするといいと思います。