[Material Design Lite] スクロールが出来ない。など、ハマッたポイントと解決策

Material Design Lite (マテリアルデザインライト)でWordPressのテーマを作り直しました。使い方は公式サイトに詳しいですが、いくつかハマったところをメモしておきます。

開発終了?

https://github.com/google/material-design-lite/releases/tag/v1.3.0にあるように、新たにMaterial Designを採用するのであれば、Material Components for the Webを利用したほうがよさそうです。

環境
  • Material Design Lite1.2.1
  • jQuery3.1.1

グリッドデザイン

Material Design LiteグリッドデザインFlexを使用しています。HTMLに指定のクラスを追加するだけで、レスポンシブなグリッドデザインが簡単に出来上がります。また、特定デバイスでの表示・非表示や、マークアップとは異なる順序でのカラム表示も、クラス追加で簡単にできます。

<div class="mdl-grid">
    <div class="mdl-cell mdl-cell--6-col mdl-cell--8-col-tablet">6 (8 tablet)</div>
    <div class="mdl-cell mdl-cell--4-col mdl-cell--6-col-tablet">4 (6 tablet)</div>
    <div class="mdl-cell mdl-cell--2-col mdl-cell--4-col-phone">2 (4 phone)</div>
</div>

ブレークポイントとカラム数

各デバイス(画面幅)のブレークポイントと一行の最大カラム数は以下となっています。(Ver 1.2.1)

デバイス カラム数 ブレークポイント
phone 4 max-width: 479px
tablet 8 (min-width: 480px) and (max-width: 839px)
desktop 12 min-width: 840px

ブレークポイントに合わせた Mixin(SCSS)をつくる

CSSのメディアクエリ簡略化のため、Mixinを作りました。

$bp-pc: 840px;
$bp-tablet_max: 839px;
$bp-tablet_min: 480px;
$bp-sp: 479px;

@mixin media($media-width)
{
    @if $media-width == pc {
        @media only screen and (min-width: $bp-pc) {
            @content;
        }
    }
    @else if $media-width == tablet {
        @media only screen and (min-width: $bp-tablet_min) and (max-width: $bp-tablet_max) {
            @content;
        }
    }
    @else if $media-width == sp {
        @media only screen and (max-width: $bp-sp) {
            @content;
        }
    }
}

ページ内スクロール

当ページサイドバーの目次がそうですが、ページ内リンクの移動にスクロールを用いています。そのためのコードを下のように書いたのですが、ピクリとも動いてくれません。またoffset().topの挙動もおかしい(数値が変動する)ようです。

offset().topの値がおかしい?
let targetY = $('selector').offset().top;
$('html,body').animate({scrollTop: targetY}, duration);

以前のテーマでは動作に問題が無かったので、Material Design Liteに原因がありそうです。

html,bodyのスクロールがうまく動かない

Layoutページのサンプルでは、以下のようにHTMLマークアップがされています。(material.jsによる要素追加後)

<body>
    <div class="mdl-layout__container">
        <div class="mdl-layout mdl-js-layout mdl-layout--fixed-drawer">
            <header class="mdl-layout__header mdl-layout__header--transparent">
                <div class="mdl-layout__header-row">
                    ...
                </div>
            </header>
            <main class="mdl-layout__content">
                <div class="page-content"><!-- Your content goes here --></div>
            </main>
        </div>
    </div>
</body>

続いてCSSを見ます。

html {
    width: 100%;
    height: 100%;
    -ms-touch-action: manipulation;
    touch-action: manipulation; }

body {
    width: 100%;
    min-height: 100%;
    margin: 0; }

.mdl-layout__container {
    position: absolute;
    width: 100%;
    height: 100%; }

.mdl-layout {
  width: 100%;
  height: 100%;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
  -webkit-flex-direction: column;
      -ms-flex-direction: column;
          flex-direction: column;
  overflow-y: auto;
  overflow-x: hidden;
  position: relative;
  -webkit-overflow-scrolling: touch; }

.mdl-layout__content {
    -ms-flex: 0 1 auto;
    position: relative;
    display: inline-block;
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-flex-grow: 1;
        -ms-flex-positive: 1;
            flex-grow: 1;
    z-index: 1;
    -webkit-overflow-scrolling: touch; }
    .mdl-layout--fixed-drawer > .mdl-layout__content {
      margin-left: 240px; }
    .mdl-layout__container.has-scrolling-header .mdl-layout__content {
      overflow: visible; }
    @media screen and (max-width: 1024px) {
      .mdl-layout--fixed-drawer > .mdl-layout__content {
        margin-left: 0; }
      .mdl-layout__container.has-scrolling-header .mdl-layout__content {
        overflow-y: auto;
        overflow-x: hidden; } }

htmlbody.mdl-layout__container.mdl-layoutらがウィンドウ幅固定になっています。

従って、$(window).height()(ウィンドウ縦幅)が1000px、.mdl-layout__contentの縦幅が2000pxだった場合...
htmlbody.mdl-layout__container.mdl-layoutの縦幅は1000px、
.mdl-layout__contentの縦幅が2000pxとなります。

その際、ウィンドウ幅を超えた.mdl-layout__contentの扱いは、ヘッダー固定の有無によって変わります。ヘッダー固定時はoverflow-y: auto;が、非固定時はoverflow: visible;が適応されます。つまり、ヘッダー固定時は.mdl-layout__content、非固定時は.mdl-layoutのスクロールによってウィンドウ幅を超えた.mdl-layout__contentの視認が可能になります。

offset().topの取得値がおかしい原因はここにあったようです。

offset().topで取得値がズレる原因

以下、ヘッダー非固定を想定し、.mdl-layoutをスクロールする前提で進めます。

ウィンドウが画面幅固定でスクロールされないため、$(window).scrollTop()は常に0となります。ページ読込み後のウィンドウをスクロールしていない状態であれば、.mdl-layoutのポジションとdocument(offsetの基準)は0で重なっており、offset().topは想定通りの値を返してくれます。問題はページを少しでもスクロールした後です。例えば、ページ最下部までスクロールした後に、指定要素のoffset().topを取得すると、値はマイナスを示します。原因は、ウィンドウではなく.mdl-layoutをスクロールさせるため、指定要素がウィンドウ外(上部)へと突き抜けた状態になるからです。

offset().topを正確に取得する

ページスクロール後に、指定要素の正確な位置を取得したい場合は$('selector').offset().topではなく
$('.mdl-layout').scrollTop() + $('selector').offset().topとします。

スクロールを正確に

以上を踏まえて、Material Design Liteでページ内スクロールをする場合のコードは以下となります。

let targetY = $('.mdl-layout').scrollTop() + $('selector').offset().top;

$('.mdl-layout').animate({
    scrollTop: targetY
    }, duration);

これによって意図通りにスクロールできました。

アドセンス、SNSボタンが表示されない

SNSボタンなど、iframeを使用したコンテンツが表示直後に消えてしまいます。原因と解決策はMaterial Desgin Lightでいろいろ困る。iframeとかスクロールとかに詳しく書かれています。私はドロワーメニューを使用する予定はないので、mdl-layoutmdl-js-layoutmdl-layout__contentのクラスを使用しないことにしました。これによって、offset().topの問題も発生しなくなりました。

Material Design 関連サイト

まとめ

グリッド、ボタンなどのマテリアルなアニメーション。これらが簡単に再現できるのは魅力ですが、独特の挙動が厄介でした。煩わしさを考慮するとBootstrap 4正式公開後も使用するかは微妙なところです。

参考

CONTENTS
  1. グリッドデザイン
    1. ブレークポイントとカラム数
    2. ブレークポイントに合わせた Mixin(SCSS)をつくる
  2. ページ内スクロール
    1. html,bodyのスクロールがうまく動かない
    2. offset().topで取得値がズレる原因
    3. offset().topを正確に取得する
    4. スクロールを正確に
  3. アドセンス、SNSボタンが表示されない
  4. Material Design 関連サイト
  5. まとめ
  6. 参考