position: sticky;

  1. ​最近滚动祖先原则​​:
    • 粘滞元素会相对于其DOM树中最近的滚动容器定位
    • 如果没有设置滚动容器,则相对于视口定位
  2. ​常见定位上下文​​:
    • 具有overflow属性的容器(autoscrollhiddenclip
    • 文档正文(<body>
    • 视口本身(如果没有找到其他滚动容器)
  3. ​排除无效容器​​:
    • 设置了overflow: visible的容器不会限制粘滞元素
    • 设置了 position: static 的容器不会影响粘滞元素

DOM 不方便调整时,换种思路实现:

// sticky 模拟
const tableBodyDOM = document.querySelector(`#${TABLE_DOM_ID} .dpt-table-body`) as HTMLDivElement | null;
const headerDOM = document.querySelector(`#${HEADER_DOM_ID}`) as HTMLDivElement | null;
const divDOM = document.createElement('div');
const headerHeight = headerDOM?.getBoundingClientRect().height || 0;
const handleScroll = () => {
    if (!tableBodyDOM || !headerDOM) {
        return;
    }
    const scrollTop = tableBodyDOM.scrollTop;
    if (scrollTop <= headerHeight) {
        divDOM.style.marginBottom = `-${headerHeight - scrollTop}px`;
        headerDOM.style.setProperty('--header-offset', `-${scrollTop}px`);
    } else {
        divDOM.style.marginBottom = `0`;
        headerDOM.style.setProperty('--header-offset', `-${headerHeight}px`);
    }
};
if (headerDOM && tableBodyDOM) {
    const canScroll = tableBodyDOM.scrollHeight > Math.ceil(tableBodyDOM.clientHeight);
    if (canScroll) {
        divDOM.style.width = '100%';
        divDOM.style.height = `${headerHeight}px`;
        divDOM.style.marginBottom = `-${headerHeight}px`;
        tableBodyDOM.insertBefore(divDOM, tableBodyDOM.firstChild);
        tableBodyDOM.addEventListener('scroll', handleScroll);
    } else {
        headerDOM?.style.setProperty('--header-offset', `0`);
    }
}
 
return () => {
    if (divDOM) {
        if (typeof divDOM.remove === 'function') {
            divDOM.remove();
        } else if (tableBodyDOM && divDOM.parentNode === tableBodyDOM) {
            tableBodyDOM.removeChild(divDOM);
        }
    }
    headerDOM?.style.setProperty('--header-offset', `0`);
    tableBodyDOM?.removeEventListener('scroll', handleScroll);
};