スキップしてメイン コンテンツに移動

Webページのサイドバーをいい感じにstickyする、マウスホイールする!タブフォーカスでスクロールする!SidebarFixerを実装した記録

スペック」の表を分割して読みやすくしました。(2022/12/13)

タブフォーカスのパッチを修正して Opera 7.xでも動作するようになったため、本機能の下限ブラウザも変わりました。tabNavigation.js で Gecko 0.9.4以下のタブフォーカスの未実装を補った為、本機能の下限ブラウザも変わりました。(2022/12/03)

ScrollEvent について追記。position:fixed は Opera 9以上。(2022/11/29)

その後も JavaScript が無効の際に position:sticky でよしなにする更新を行っています。(2022/11/12)

5. その他の動作の「1. シングルカラムに変化したのを検出してサイドバーの CSS-P をリセットする」にリンクを追記しました。(2022/05/30)

4-1. 2. の動作を変更しました。(2022/04/10)

僕の開発している Web 文書用のサイドバーをよしなにするスクリプトについてです。同様のスクリプトを実装したい方には結構参考になると思います。

  1. はじめに
    1. スペック
    2. 最新のChromeで意図しない挙動に遭遇
    3. 「コンテナの可視部分」について
    4. 実装にあたって留意事項
  2. ドキュメントのスクロールでの動作
    1. 動作の詳細
    2. TODO
  3. サイドバー上でのマウスホイール動作
    1. 動作の詳細
  4. Tabキーでのリンク要素へのフォーカス移動
    1. 動作の詳細
    2. リンクへのフォーカスを Vivaldi で有効化する
    3. リンクへのフォーカスを Safari 3.1.2 で有効化する
  5. その他の動作

1. はじめに

このブログのテンプレートは僕が2015年から開発を続けています。ブログのサイドバーをスクロールによしなに追従させる SidebarFixer.js を間もなく追加して、機能追加を続けてきました。

当初はスクロールイベントに反応するだけ(position:sticky 相当)でしたが、マウスホイールイベントでサイドバーだけをスライド出来るようにしました。最後にサイドバー内のフォーカス要素がビューポート内に入るようにしました。

1-1. スペック

CSS ポジショニングに使うプロパティ

CSS のプロパティIEPrestoGeckoWebkit
transform3D(*1), transform(*2)9??Chrome 2, Safari 4
position:fixed791Chrome 1, Safari ~3.x(*3)
position:absolute57.50.6?
position:relative7(*4)
  1. transform3D を使わないとアニメーションが著しくガタつく環境があります。Android 3.1
  2. transform3D をサポートするがバグの為に描画が乱れる環境には transform を使います。IE, Chrome 2。
  3. 初期の WebKit では transform した要素では offsetTop の値が不正です。これらのブラウザでも正しい値の取れる getBoundingClientRect が未実装の Safari 3.x と Chrome 1は position:fixed を使います。(2022/12/03)
  4. position:relative は Opera 7.23 以下用。

サポートするイベント

イベントIEPrestoGeckoWebkit
スクロールscroll57.51.0~1.1, 1.8?
fallback7.0(*2)0.6(*2)
ホイール wheel917Chrome 31, Safari 7
mousewheel69Chrome 1, Safari 3
MozMousePixelScroll1.9.1
DOMMouseScroll0.9.7
フォーカス focusin611.6(*3)52Chrome 15, Safari 5.1
DOMFocusIn8(*3)?
focus + capture phase7(*3)0.6(*4)?
fallback5(*1)
  1. IE5.5 以下は document.activeElement を監視してフォーカスの変化を検出する。(2022/11/19 追記)
  2. ScrollEvent をサポートしない Gecko 0.9.x 以下、Gecko 1.2~1.7, Opera 7.2x以下は window.pageYOffset の変化をタイマーで監視する。(2022/11/29 追記)
  3. Opera 12.xまでは、タブフォーカスしたい要素に tab-index 属性が必要です。タブフォーカスさせたくない要素は tab-index="-1" を削除します。(2022/12/03)
  4. Gecko 0.9.4以下はタブフォーカスが不十分で、階層が離れている要素へフォーカス移動しません。パッチを追加して本機能のタブフォーカスも動作するようになりました。(2022/12/03)

1-2. 最新の Chrome で意図しない挙動に遭遇

この木曜日に、最新の Chrome で意図したように動かないことに気づいた時はビビりました。(僕は Firefox をメインのブラウザにしています。)

Chrome と Firefox で focus イベントによって発生するフォーカス要素への自動スクロールが異なることが直接の原因だったと思います。しかし問題を見逃してきた大元には、場当たり的に機能追加を続けてきた失敗があると考えました。

ビューポート、コンテナ、サイドバーの位置関係の全ての組み合わせを列挙して、もれなく処理を定義して文書化することをサボったのでした。

1-3. 「コンテナの可視部分」について

ようやく腰を据えて、各要素の位置関係のパターンを網羅する図を描きました。この図を眺めながら各イベントに対する処理を文書化して、処理の漏れが無いか確認していきました。

この作業に取り組んだところ「コンテナの可視部分」というキーワードでうまく整理できることが分かりました。

この記事でコンテナとは、メインカラムとサイドバー(サブカラム)を包む <div> を指します。可視部分とはビューポートのこと。コンテナの可視部分とはビューポートとコンテナの重なり合う部分です。

1-4. 実装にあたって留意事項

ビューポートのリサイズによって、コンテナの高さとサイドバーの高さも変わりうる。またメインカラムとサイドバーの高さの逆転も起こりうる。

2カラムレイアウトからシングルカラムに変化しうる。

2. ドキュメントのスクロールでの動作

position:sticky っぽい動作をします。サイドバーがコンテナの可視部分より高い場合には、常にサイドバーのどこかしらでサブカラム可視部分を覆うようにスクロールに追従します。

2-1. 動作の詳細

  1. サイドバーの高さ ≧ メインコンテンツの高さ
    • ゼロ位置
  2. コンテナはビューポートの外
    1. ビューポートのトップより上ならば、サイドバーはコンテナの地へ
    2. ビューポートのボトムより下ならば、ゼロ位置
  3. サイドバーの高さ ≦ コンテナの可視部分の高さ
    • サイドバーの天をコンテナの可視部分の天に揃える
  4. サイドバーの高さ > コンテナの可視部分の高さ
    1. サイドバーの天がコンテナの可視部分の天より下なら、サイドバーの天をコンテナの可視部分の天に揃える
    2. サイドバーの地がコンテナの可視部分の地より上なら、サイドバーの地をコンテナの可視部分の地に揃える
    3. スクロール量 < コンテナのy + サイドバーの高さ - コンテナの可視部分の高さ
      • ゼロ位置
    4. これ以外
      • サイドバーの地はコンテナの可視部分の地

2-2. TODO

サイドバーのコンテンツの先頭にページ内索引があってリンク集などが続く場合、なるべく索引をビューポートに入れ続けたい。そこでサイドバーの地がコンテナの地と揃うのは、コンテナの地が画面に入っている時とする。

逆に、サイドバーのコンテンツに軽重が無い場合、素早くサイドバー全体を見渡せるように先の動作となる。(4.4.3~4.4.4)

この動作を切り替えられるようにする。

3. サイドバー上でのマウスホイール動作

文書自体のスクロールをキャンセルして、ホイールスクロール量分だけサイドバーだけがスライドします。文書をスクロールしなくてもサイドバーを見渡すことが出来るので、サイドバーにページ内索引等のメインカラムと連携するコンテンツがある場合には、特に活きる機能です。

3-1. 動作の詳細

  1. サイドバーの高さ ≦ コンテナの可視部分の高さ
    • mousewheel イベントをキャンセルしない
  2. サイドバーの高さ > コンテナの可視部分の高さ
    • サイドバーの天の最小値は、コンテナの可視部分の地 - コンテナのy - サイドバーの高さ
    • サイドバーの天の最大値は、コンテナの可視部分の天 - コンテナのy

4. Tabキーでのリンク要素へのフォーカス移動

サイドバー内のリンク要素等への focus イベントに反応して、要素が画面内に入るように調整します。フォーカスを得た要素が画面外にある場合に起る自動スクロールはブラウザ間で異なりますが、これを共通のルールで調整することで文書の閲覧性を一定にします。

4-1. 動作の詳細

  1. コンテナはビューポートの外(focus イベント直後のスクロールが起きていないケース、未確認)
    • ビューポートの高さ ≦ サイドバー下のフォーカスを得た要素の高さなら、コンテナのyにスクロールする
    • コンテナはビューポートの上側なら、コンテナの地 - 要素の高さにスクロールする
    • コンテナはビューポートの下側なら、コンテナのy + 要素の高さ - ビューポートの高さにスクロールする
    window.scroll() の呼び出しに対して、同期か非同期でスクロールイベントが起きる場合を想定したコードにする。
  2. サイドバーの高さ ≦ コンテナの可視部分の高さ
    • 可視部分の天と要素の天を合わせる サイドバーの天をコンテナの可視部分の天に揃える(2022/04/10)
  3. サイドバー下のフォーカスを得た要素の高さ ≦ コンテナの可視部分の高さ
    1. 要素の天をコンテナの可視部分の天に合わせると、サイドバー下に隙間が出来る場合、可視部分の地と要素の地を合わせる
    2. 完全に入っている場合
      • サイドバー上に隙間が出来る場合、可視部分の天と要素の天を合わせる
      • これ以外の場合、何もしない
    3. 要素の天がコンテナの可視部分に入っている場合、可視部分の地と要素の地を合わせる
    4. 要素の地がコンテナの可視部分に入っている場合、可視部分の天と要素の天を合わせる
    5. 要素はコンテナの可視部分より上、可視部分の天と要素の天を合わせる
    6. 要素はコンテナの可視部分より下、可視部分の地と要素の地を合わせる
  4. これ以外
    • コンテナの可視部分の天と要素の天を合わせる

4-2. リンクへのフォーカスを Vivaldi で有効化する

Chronium ベースのブラウザである Vivaldi はデフォルトでリンク要素へのフォーカス移動が無効になっています。

ウェブページ > ウェブページのフォーカス > すべてのコントロールとリンクをフォーカスする にチェックを付けます。

4-3. リンクへのフォーカスを Safari 3.1.2 で有効化する

Safari 3.1.2 はデフォルトでリンク要素へのフォーカス移動が無効になっています。

編集 > 設定 > 詳細 > Tab キーを押したときに Web ページ上の各項目を強調表示 にチェックを付けます。

5. その他の動作

  1. シングルカラムに変化したのを検出してサイドバーの CSS-P をリセットする
  2. resize イベント
    • スクロール位置を再取得してサイドバーの位置を調整する
  3. window.onblur イベント
    • 何か処理が必要か?不明 不要に思う(2022/12/13)