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. はじめに
このブログのテンプレートは僕が2015年から開発を続けています。ブログのサイドバーをスクロールによしなに追従させる SidebarFixer.js を間もなく追加して、機能追加を続けてきました。
当初はスクロールイベントに反応するだけ(position:sticky
相当)でしたが、マウスホイールイベントでサイドバーだけをスライド出来るようにしました。最後にサイドバー内のフォーカス要素がビューポート内に入るようにしました。
1-1. スペック
CSS ポジショニングに使うプロパティ
CSS のプロパティ | IE | Presto | Gecko | Webkit |
---|---|---|---|---|
transform3D (*1), transform (*2) | 9 | ? | ? | Chrome 2, Safari 4 |
position:fixed | 7 | 9 | 1 | Chrome 1, Safari ~3.x(*3) |
position:absolute | 5 | 7.5 | 0.6 | ? |
position:relative | 7(*4) |
transform3D
を使わないとアニメーションが著しくガタつく環境があります。Android 3.1transform3D
をサポートするがバグの為に描画が乱れる環境にはtransform
を使います。IE, Chrome 2。- 初期の WebKit では
transform
した要素ではoffsetTop
の値が不正です。これらのブラウザでも正しい値の取れるgetBoundingClientRect
が未実装の Safari 3.x と Chrome 1はposition:fixed
を使います。(2022/12/03) position:relative
は Opera 7.23 以下用。
サポートするイベント
イベント | IE | Presto | Gecko | Webkit | |
---|---|---|---|---|---|
スクロール | scroll | 5 | 7.5 | 1.0~1.1, 1.8 | ? |
fallback | 7.0(*2) | 0.6(*2) | |||
ホイール | wheel | 9 | 17 | Chrome 31, Safari 7 | |
mousewheel | 6 | 9 | Chrome 1, Safari 3 | ||
MozMousePixelScroll | 1.9.1 | ||||
DOMMouseScroll | 0.9.7 | ||||
フォーカス | focusin | 6 | 11.6(*3) | 52 | Chrome 15, Safari 5.1 |
DOMFocusIn | 8(*3) | ? | |||
focus + capture phase | 7(*3) | 0.6(*4) | ? | ||
fallback | 5(*1) |
- IE5.5 以下は
document.activeElement
を監視してフォーカスの変化を検出する。(2022/11/19 追記) ScrollEvent
をサポートしない Gecko 0.9.x 以下、Gecko 1.2~1.7, Opera 7.2x以下はwindow.pageYOffset
の変化をタイマーで監視する。(2022/11/29 追記)- Opera 12.xまでは、タブフォーカスしたい要素に
tab-index
属性が必要です。タブフォーカスさせたくない要素はtab-index="-1"
を削除します。(2022/12/03) - Gecko 0.9.4以下はタブフォーカスが不十分で、階層が離れている要素へフォーカス移動しません。パッチを追加して本機能のタブフォーカスも動作するようになりました。(2022/12/03)
1-2. 最新の Chrome で意図しない挙動に遭遇
この木曜日に、最新の Chrome で意図したように動かないことに気づいた時はビビりました。(僕は Firefox をメインのブラウザにしています。)
Chrome と Firefox で focus
イベントによって発生するフォーカス要素への自動スクロールが異なることが直接の原因だったと思います。しかし問題を見逃してきた大元には、場当たり的に機能追加を続けてきた失敗があると考えました。
ビューポート、コンテナ、サイドバーの位置関係の全ての組み合わせを列挙して、もれなく処理を定義して文書化することをサボったのでした。
1-3. 「コンテナの可視部分」について
ようやく腰を据えて、各要素の位置関係のパターンを網羅する図を描きました。この図を眺めながら各イベントに対する処理を文書化して、処理の漏れが無いか確認していきました。
この作業に取り組んだところ「コンテナの可視部分」というキーワードでうまく整理できることが分かりました。
この記事でコンテナとは、メインカラムとサイドバー(サブカラム)を包む <div>
を指します。可視部分とはビューポートのこと。コンテナの可視部分とはビューポートとコンテナの重なり合う部分です。
1-4. 実装にあたって留意事項
ビューポートのリサイズによって、コンテナの高さとサイドバーの高さも変わりうる。またメインカラムとサイドバーの高さの逆転も起こりうる。
2. ドキュメントのスクロールでの動作
position:sticky
っぽい動作をします。サイドバーがコンテナの可視部分より高い場合には、常にサイドバーのどこかしらでサブカラム可視部分を覆うようにスクロールに追従します。
2-1. 動作の詳細
- サイドバーの高さ ≧ メインコンテンツの高さ
- ゼロ位置
- コンテナはビューポートの外
- ビューポートのトップより上ならば、サイドバーはコンテナの地へ
- ビューポートのボトムより下ならば、ゼロ位置
- サイドバーの高さ ≦ コンテナの可視部分の高さ
- サイドバーの天をコンテナの可視部分の天に揃える
- サイドバーの高さ > コンテナの可視部分の高さ
- サイドバーの天がコンテナの可視部分の天より下なら、サイドバーの天をコンテナの可視部分の天に揃える
- サイドバーの地がコンテナの可視部分の地より上なら、サイドバーの地をコンテナの可視部分の地に揃える
- スクロール量 < コンテナのy + サイドバーの高さ - コンテナの可視部分の高さ
- ゼロ位置
- これ以外
- サイドバーの地はコンテナの可視部分の地
2-2. TODO
サイドバーのコンテンツの先頭にページ内索引があってリンク集などが続く場合、なるべく索引をビューポートに入れ続けたい。そこでサイドバーの地がコンテナの地と揃うのは、コンテナの地が画面に入っている時とする。
逆に、サイドバーのコンテンツに軽重が無い場合、素早くサイドバー全体を見渡せるように先の動作となる。(4.4.3~4.4.4)
3. サイドバー上でのマウスホイール動作
文書自体のスクロールをキャンセルして、ホイールスクロール量分だけサイドバーだけがスライドします。文書をスクロールしなくてもサイドバーを見渡すことが出来るので、サイドバーにページ内索引等のメインカラムと連携するコンテンツがある場合には、特に活きる機能です。
3-1. 動作の詳細
- サイドバーの高さ ≦ コンテナの可視部分の高さ
mousewheel
イベントをキャンセルしない
- サイドバーの高さ > コンテナの可視部分の高さ
- サイドバーの天の最小値は、コンテナの可視部分の地 - コンテナのy - サイドバーの高さ
- サイドバーの天の最大値は、コンテナの可視部分の天 - コンテナのy
4. Tabキーでのリンク要素へのフォーカス移動
サイドバー内のリンク要素等への focus
イベントに反応して、要素が画面内に入るように調整します。フォーカスを得た要素が画面外にある場合に起る自動スクロールはブラウザ間で異なりますが、これを共通のルールで調整することで文書の閲覧性を一定にします。
4-1. 動作の詳細
- コンテナはビューポートの外(
focus
イベント直後のスクロールが起きていないケース、未確認)- ビューポートの高さ ≦ サイドバー下のフォーカスを得た要素の高さなら、コンテナのyにスクロールする
- コンテナはビューポートの上側なら、コンテナの地 - 要素の高さにスクロールする
- コンテナはビューポートの下側なら、コンテナのy + 要素の高さ - ビューポートの高さにスクロールする
window.scroll()
の呼び出しに対して、同期か非同期でスクロールイベントが起きる場合を想定したコードにする。 - サイドバーの高さ ≦ コンテナの可視部分の高さ
可視部分の天と要素の天を合わせるサイドバーの天をコンテナの可視部分の天に揃える(2022/04/10)
- サイドバー下のフォーカスを得た要素の高さ ≦ コンテナの可視部分の高さ
- 要素の天をコンテナの可視部分の天に合わせると、サイドバー下に隙間が出来る場合、可視部分の地と要素の地を合わせる
- 完全に入っている場合
- サイドバー上に隙間が出来る場合、可視部分の天と要素の天を合わせる
- これ以外の場合、何もしない
- 要素の天がコンテナの可視部分に入っている場合、可視部分の地と要素の地を合わせる
- 要素の地がコンテナの可視部分に入っている場合、可視部分の天と要素の天を合わせる
- 要素はコンテナの可視部分より上、可視部分の天と要素の天を合わせる
- 要素はコンテナの可視部分より下、可視部分の地と要素の地を合わせる
- これ以外
- コンテナの可視部分の天と要素の天を合わせる
4-2. リンクへのフォーカスを Vivaldi で有効化する
Chronium ベースのブラウザである Vivaldi はデフォルトでリンク要素へのフォーカス移動が無効になっています。
ウェブページ > ウェブページのフォーカス > すべてのコントロールとリンクをフォーカスする にチェックを付けます。
4-3. リンクへのフォーカスを Safari 3.1.2 で有効化する
Safari 3.1.2 はデフォルトでリンク要素へのフォーカス移動が無効になっています。
編集 > 設定 > 詳細 > Tab キーを押したときに Web ページ上の各項目を強調表示 にチェックを付けます。
5. その他の動作
- シングルカラムに変化したのを検出してサイドバーの CSS-P をリセットする
- Opera でスモールスクリーンモードを有効にすると画面サイズに関わらず、handheld メディア用の CSS が適用されるので、ビューポートのサイズを参照するコードでは不適切。メインカラムとサイドバーの
offsetTop
が一致するか、で判定する。(2020/05/30 追記)
- Opera でスモールスクリーンモードを有効にすると画面サイズに関わらず、handheld メディア用の CSS が適用されるので、ビューポートのサイズを参照するコードでは不適切。メインカラムとサイドバーの
resize
イベント- スクロール位置を再取得してサイドバーの位置を調整する
window.onblur
イベント何か処理が必要か?不明不要に思う(2022/12/13)