隠れコンテンツのheightミスによるスクロール妨害のデモ

 一部のマストドンでmp3動画(映像はマストドンが付加)を含む投稿の下の投稿に移動した後にmp3動画の投稿に戻れないバグがあったので、それに似せたデモ。実際の原因は別にあるが、同じ対処法が使えた。

 

 下のコードでscroller内に見えなくなったarticleは内側のboxを削除し、articleタグにheightを記載する。scroller内に見えている場合はboxを生成し直し、articleタグにheightは削除される。各articleのheightはboxが存在する時に計算され170pxであるが、隠れてboxが存在しない場合も170pxが保たれる。ただし、「コンテンツ #2」だけは隠れている時のheightを修正できて、大きくすると、「コンテンツ #2」が隠れた後に「コンテンツ #2」を再表示しにくくなる。そのような時の対処法として「overflow-anchor: none;」がある。ただし、マストドンの方はスムーズにスクロールするが、このデモでは、揺れながらスクロールされる。

test20251213-1.html

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>スクロール妨害の再現と対処策のデモ</title>
<style>
    /* スクロールコンテナ */
    .scroller {
        height: 350px;
        overflow-y: scroll;
        border: 2px solid #3498db;
        position: relative;
    }
    /* リスト全体 */
    .list-container {
        width: 100%;
        /* overflow-anchor の設定はJSで動的に行います */
    }
    /* 個別アイテム (articleタグ) */
    .item {
        padding: 15px;
        box-sizing: border-box;
        font-size: 18px;
        border-bottom: 1px solid #ddd;
        display: flex; 
        align-items: center;
        justify-content: center;
        flex-direction: column; 
    }
    /* コンテンツ内の四角いボックス */
    .box {
        width: 100px;
        height: 100px;
        margin-top: 10px; 
        background-color: #f39c12; 
        border: 2px solid #e67e22;
        border-radius: 5px;
    }
    /* アイテムの色分け */
    .item[data-index="1"] { background-color: #f7f3f9; border-left: 5px solid #8e44ad; }
    .item[data-index="2"] { background-color: #e8f8f5; border-left: 5px solid #1abc9c; } 
    .item[data-index="3"] { background-color: #f9fcf2; border-left: 5px solid #2ecc71; }
    .item[data-index="4"] { background-color: #f0f8ff; border-left: 5px solid #3498db; }
    .item[data-index="5"] { background-color: #fdf6f0; border-left: 5px solid #e67e22; }
    .item[data-index="6"] { background-color: #fceceb; border-left: 5px solid #e74c3c; }

    /* スクロールステータス表示エリア */
    #scrollStatus {
        margin-top: 15px;
        margin-left: 20px;
        padding: 10px;
        border: 1px solid #7f8c8d;
        background-color: #ecf0f1;
        font-family: monospace;
        font-weight: bold;
    }
    #anchorControl {
        margin-top: 10px;
        padding: 10px;
        border: 1px dashed #7f8c8d;
        background-color: #f8f9fa;
    }
    input[type=checkbox] {
        transform: scale(1.5);
    }
</style>
</head>
<body>

<h1>スクロール妨害の再現と対処策のデモ</h1>

<div id="anchorControl">
    <div>
        <label for="heightInput">
            コンテンツ #2 が隠れている時のheight (px): 
            <input type="number" id="heightInput" value="170" min="1" style="width: 80px;"><br>
            (見えている時は170px。300pxに増やすと上へのスクロール妨害発生)
        </label>
    </div>
    <div style="margin-top: 10px;">
        <label for="anchorToggle">
            <input type="checkbox" id="anchorToggle"> 
            対処策:overflow-anchor: none; を有効にする
        </label>
        <span id="anchorStatus"></span>
    </div>
</div>

<div class="scroller" id="scroller">
    <div class="list-container" id="listContainer">
        <article class="item" data-index="1">コンテンツ #1 <div class="box"></div></article>
        <article class="item" data-index="2">コンテンツ #2 <div class="box"></div></article>
        <article class="item" data-index="3">コンテンツ #3 <div class="box"></div></article>
        <article class="item" data-index="4">コンテンツ #4 <div class="box"></div></article>
        <article class="item" data-index="5">コンテンツ #5 <div class="box"></div></article>
        <article class="item" data-index="6">コンテンツ #6 <div class="box"></div></article>
    </div>
</div>

<div id="scrollStatus">
    現在のスクロール位置 (scrollTop): 0.00 px
</div>

<script>
    const SCROLL_CONTAINER = document.getElementById('scroller');
    const LIST_CONTAINER = document.getElementById('listContainer');
    const SCROLL_STATUS = document.getElementById('scrollStatus');
    const ANCHOR_TOGGLE = document.getElementById('anchorToggle');
    const ANCHOR_STATUS = document.getElementById('anchorStatus');
    const HEIGHT_INPUT = document.getElementById('heightInput'); // 💡 高さ入力フィールド
    const items = document.querySelectorAll('.item');
    const itemHeightsCache = {};

    // --- 0. スクロールアンカリング切替ロジック ---
    function toggleAnchor() {
        if (ANCHOR_TOGGLE.checked) {
            LIST_CONTAINER.style.overflowAnchor = 'none';
            ANCHOR_STATUS.textContent = '[現在: 有効]';
            ANCHOR_STATUS.style.color = 'green';
        } else {
            LIST_CONTAINER.style.overflowAnchor = 'auto'; 
            ANCHOR_STATUS.textContent = '[現在: 無効]';
            ANCHOR_STATUS.style.color = 'red';
        }
    }

    // --- 1. 初期化と高さ測定 ---
    function initializeHeights() {
        items.forEach(item => {
            const measuredHeight = item.offsetHeight;
            itemHeightsCache[item.dataset.index] = measuredHeight;
            item.firstChild.textContent = `コンテンツ #${item.dataset.index} (実測高: ${measuredHeight}px)`;
        });

        toggleAnchor(); 
    }

    // --- 2. スクロールロジック ---
    function applyScrollLogic() {
        const scrollTop = SCROLL_CONTAINER.scrollTop;
        const scrollerBottom = scrollTop + SCROLL_CONTAINER.offsetHeight;

        // スクロール位置の表示を更新
        SCROLL_STATUS.innerHTML = `現在のスクロール位置 (scrollTop): <strong>${scrollTop.toFixed(2)} px</strong>`;

        items.forEach(item => {
            const itemTop = item.offsetTop;
            const itemIndex = item.dataset.index;
            let cachedHeight = itemHeightsCache[itemIndex]; 

            // --- #2 の固定高さをフォームの値から取得 ---
            if (itemIndex === "2") {
                // フォームから現在の高さを取得し、数値に変換
                const inputHeight = parseInt(HEIGHT_INPUT.value);
                cachedHeight = isNaN(inputHeight) || inputHeight < 1 ? itemHeightsCache[itemIndex] : inputHeight;
            }
            // --------------------------------------------------------

            const currentHeight = item.style.height ? parseInt(item.style.height) : item.offsetHeight;
            const isVisible = (itemTop < scrollerBottom) && (itemTop + currentHeight > scrollTop);

            const box = item.querySelector('.box');
            const fixedHeightStyle = `height: ${cachedHeight}px;`;

            if (isVisible) {
                // A. 画面内に表示されている場合 (height: auto)

                item.removeAttribute('style'); 

                if (!box) {
                    const newBox = document.createElement('div');
                    newBox.className = 'box';
                    item.appendChild(newBox);
                }

            } else {
                // B. 画面外に隠れている場合 (上部/下部問わず height: 固定)

                if (item.getAttribute('style') !== fixedHeightStyle) {
                    item.setAttribute('style', fixedHeightStyle);
                }

                if (box) {
                    box.remove();
                }
            }
        });
    }

    // --- 3. イベントリスナーと実行 ---
    SCROLL_CONTAINER.addEventListener('scroll', applyScrollLogic);
    ANCHOR_TOGGLE.addEventListener('change', toggleAnchor); 

    // 💡 高さの入力値が変わったらロジックを再実行 (リフローさせる)
    HEIGHT_INPUT.addEventListener('input', applyScrollLogic);

    // 初期化処理を実行
    window.onload = function() {
        initializeHeights();
        applyScrollLogic(); 
    };
</script>

</body>
</html> 

コメント