【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 Lite
1.2.1
jQuery
3.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ページのサンプルでは、以下のようにマークアップがされています。(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; } }

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

従って

  • $(window).height()(ウィンドウ縦幅)が1000px
  • .mdl-layout__contentの縦幅が2000pxだった場合...

html body .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-layout mdl-js-layout mdl-layout__contentのクラスを使用しないことにしました。これによって、offset().topの問題も発生しなくなりました。

Material Design 関連サイト

終わりに

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