如何在JavaScript中实现一个仅用键盘就能实现无限滚动的功能?

前端开发 2026-07-08

真的很难用语言准确表达我想要的东西,所以让我让图片来多讲点儿吧。

这是我现在拥有的:

当前情况

当我滚动到底部时,我希望出现的效果如下:

我想要的效果

我不想在侧边出现滚动条。理想情况下,这个盒子的边框在视口中保持固定尺寸。当我添加更多条目时,尺寸会改变——这并非我所期望,因此如果能解决这个问题,也请帮忙。

我已经提供了HTML和 JS,方便你自己来尝试:

JS(其中大部分直接来自某位在前一个问题中的解法。我相信你能找到原始来源)

const ul = document.getElementById('list');
var liSelected;
var index = 0;

document.addEventListener('keydown', function(event) {
  var len = ul.getElementsByTagName('li').length - 1;

  // DOWN ARROW
  if (event.which === 40) {
    index++;

    if (liSelected) {
      removeClass(liSelected, 'selected');
      next = ul.getElementsByTagName('li')[index];

      if (typeof next !== undefined && index <= len) {
        liSelected = next;
      } else {
        index = 0;
        liSelected = ul.getElementsByTagName('li')[0];
      }

      addClass(liSelected, 'selected');
      console.log(index);
    } else {
      index = 0;
      liSelected = ul.getElementsByTagName('li')[0];
      addClass(liSelected, 'selected');
    }
  }
  // UP ARROW
  else if (event.which === 38) {
    if (liSelected) {
      removeClass(liSelected, 'selected');
      index--;
      next = ul.getElementsByTagName('li')[index];

      if (typeof next !== undefined && index >= 0) {
        liSelected = next;
      } else {
        index = len;
        liSelected = ul.getElementsByTagName('li')[len];
      }

      addClass(liSelected, 'selected');
    } else {
      index = 0;
      liSelected = ul.getElementsByTagName('li')[len];
      addClass(liSelected, 'selected');
    }
  }
}, false);

function removeClass(el, className) {
  if (el.classList) {
    el.classList.remove(className);
  } else {
    el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
  }
};

function addClass(el, className) {
  if (el.classList) {
    el.classList.add(className);
  } else {
    el.className += ' ' + className;
  }
};
#main-box {
  text-align: left;
  border: solid 2px;
  margin-left: 30px;
  margin-right: 30px;
  /*padding-bottom: 20%;*/
}

ul {
  list-style-type: none;
  margin: 10px;
  ;
  padding: 2px;
}

li {
  padding: 0;
  margin: 0;
  width: 99%;
}

li.selected {
  background-color: #ff0;
  color: #000;
}
<div id="main-box">
  <ul id="list">
    <!--<li class="selected"><span>Option 1</span></li>-->
    <li><span>Option 2</span></li>
    <li><span>Option 3</span></li>
    <li><span>Option 4</span></li>
    <li><span>Option 5</span></li>
    <li><span>Option 6</span></li>
    <li><span>Option 7</span></li>
    <li><span>Option 8</span></li>
    <li><span>Option 9</span></li>
    <li class="selected"><span>Option 10</span></li>
  </ul>
</div>

解决方案

这是我对这项任务的理解:

  • 将UL的 overflow设置为hidden
  • 添加:tabindex="0" 属性到父元素,以捕获焦点,并随之处理键盘事件
  • 让LI可以通过点击进行选择
  • 上/下箭头循环选择上一个/下一个
// Utility functions
const el = (sel, par = document) => par.querySelector(sel);
const els = (sel, par = document) => [...par.querySelectorAll(sel)];
const mod = (n, m) => (n % m + m) % m; //stackoverflow.com/a/71167019/383904

// Task select loop on arrows up/down
const elBox = el("#main-box");
const elUL = el("#list", elBox)
const elsItems = els("li", elUL);
let i = 0; // Current selected index

const selectItem = (i) => {
  i = mod(i, elsItems.length);
  el("li.selected", elUL)?.classList.remove("selected");
  elsItems[i].classList.add("selected");
  elsItems[i].scrollIntoView({
    behavior: "smooth",
    block: "center",
    inline: "center"
  });
};

addEventListener("keydown", (ev) => {
  if (document.activeElement !== elBox) return;
  if (ev.key === "ArrowUp") {
    selectItem(--i);
    ev.preventDefault(); // prevent entire page scroll
  } else if (ev.key === "ArrowDown") {
    selectItem(++i);
    ev.preventDefault(); // prevent entire page scroll
  }
});

addEventListener("click", ({target, key}) => {
  const elItem = target.closest("#main-box li");
  if (!elItem) return;
  i = elsItems.indexOf(elItem);
  selectItem(i);
});

// Initialize!
selectItem(0); // 0 = first one
* {
  margin: 0;
  box-sizing: border-box;
}

#main-box {
  border: solid 2px #000;
  padding: 1em;
}

#list {
  list-style: none;
  height: 5lh;
  overflow: hidden;
  padding: 0;
  outline: none;

  li {
    cursor: pointer;

    &.selected {
      background-color: #ff0;
    }
  }
}
<div id="main-box" tabindex="0">
  <ul id="list">
    <li><span>Option 1</span></li>
    <li><span>Option 2</span></li>
    <li><span>Option 3</span></li>
    <li><span>Option 4</span></li>
    <li><span>Option 5</span></li>
    <li><span>Option 6</span></li>
    <li><span>Option 7</span></li>
    <li><span>Option 8</span></li>
    <li><span>Option 9</span></li>
    <li><span>Option 10</span></li>
  </ul>
</div>

下面是另一个示例:不使用变量 i 来跟踪当前选中列表项的索引,而改用 prev/nextElementSibiling

// Utility functions
const el = (sel, par = document) => par.querySelector(sel);
const els = (sel, par = document) => [...par.querySelectorAll(sel)];

// Task: Select loop on arrows up/down
const elUL = el("#list")
const elsItems = els("li", elUL);

const selectItem = (elItem) => {
  el(".selected", elUL)?.classList.remove("selected");
  elItem.classList.add("selected");
  elItem.scrollIntoView({ behavior: "smooth", block: "center"});
};

const keyActions = {
  ArrowUp: () => selectItem(el(".selected", elUL).previousElementSibling ?? elsItems[elsItems.length - 1]),
  ArrowDown: () => selectItem(el(".selected", elUL).nextElementSibling ?? elsItems[0]),
};

addEventListener("keydown", (ev) => {
  if (document.activeElement !== elUL) return;
  ev.preventDefault(); // prevent entire page scroll
  keyActions[ev.key]?.();
});

addEventListener("click", ({target, key}) => {
  const elItem = target.closest("#main-box li");
  elItem && selectItem(elItem);
});

// Initialize!
selectItem(elsItems[0]);
* {
  margin: 0;
  box-sizing: border-box;
}

#main-box {
  border: solid 2px #000;
  padding: 1em;
}

#list {
  list-style: none;
  height: 5lh;
  overflow: hidden;
  padding: 0;
  outline: none;

  li {
    cursor: pointer;
    transition: background-color 0.2s;

    &.selected {
      background-color: #ff0;
    }
  }
}
<div id="main-box">
  <ul id="list" tabindex="0">
    <li><span>Option 1 (click to focus here and use arrows up/dn)</span></li>
    <li><span>Option 2</span></li>
    <li><span>Option 3</span></li>
    <li><span>Option 4</span></li>
    <li><span>Option 5</span></li>
    <li><span>Option 6</span></li>
    <li><span>Option 7</span></li>
    <li><span>Option 8</span></li>
    <li><span>Option 9</span></li>
    <li><span>Option 10</span></li>
  </ul>
</div>

注意:

鉴于这些奇怪的任务要求,上述示例在触控设备上可能效果不佳。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章