Skip to content

[lexical-react][lexical-playground] Bug Fix: Keep the arrow key highlight when a typeahead menu scrolls under the mouse - #9391

Open
Thanatos9404 wants to merge 1 commit into
meta:mainfrom
Thanatos9404:fix/typeahead-hover-keeps-keyboard-highlight
Open

Thanatos9404 wants to merge 1 commit into
meta:mainfrom
Thanatos9404:fix/typeahead-hover-keeps-keyboard-highlight

Conversation

@Thanatos9404

Copy link
Copy Markdown
Contributor

Description

Pressing ArrowDown or ArrowUp in a typeahead menu scrolls the highlighted option into view. With the mouse resting over the menu, the scroll slides another option under it, the browser fires mouseenter there, and the highlight jumps back to it.

The default menu now moves the highlight on mousemove, and only when the pointer position has changed, because WebKit also fires a mousemove after the scroll at the position the pointer never left. If the arrow keys come first, the first mousemove only records the position. Taps still highlight and select, since touch sends a mousemove before the click. Wheel scrolling and typing a filter no longer move the highlight until the mouse moves. The playground's li:hover style is removed so that only the highlighted row is grey. Menus with their own menuRenderFn are unchanged.

Closes #4296

Test plan

Before

$ pnpm exec vitest --project unit --no-watch packages/lexical-react/src/__tests__/unit/LexicalMenu.test.tsx
         × keeps the keyboard highlight when the list scrolls under a still pointer
         × keeps the ArrowDown highlight when the menu opened under a still pointer
         × keeps the ArrowUp highlight when the menu opened under a still pointer
AssertionError: expected 2 to be 1 // Object.is equality
AssertionError: expected 2 to be 1 // Object.is equality
AssertionError: expected 1 to be 2 // Object.is equality
      Tests  3 failed | 19 passed (22)

In the playground, resting the mouse on the third "/" option and pressing ArrowDown ten times (local Playwright script, highlighted index after each press):

chromium 153.0.8010.12: 3, 4, 5, 3, 4, 5, 6, 4, 5, 6
firefox 155.0: 3, 4, 5, 3, 4, 5, 6, 4, 5, 6
webkit 26.6: 3, 4, 5, 3, 4, 5, 6, 4, 5, 6

After

$ pnpm exec vitest --project unit --no-watch packages/lexical-react/src/__tests__/unit/LexicalMenu.test.tsx
      Tests  22 passed (22)

$ pnpm exec vitest --project unit --no-watch packages/lexical-react packages/lexical-playground
 Test Files  74 passed (74)
      Tests  674 passed (674)
chromium 153.0.8010.12: 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
firefox 155.0: 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
webkit 26.6: 3, 4, 5, 6, 7, 8, 9, 10, 11, 12

With mousemove but no position check, WebKit still jumped (3, 4, 2, 3, 4, 5, 3, 4, 5, 6). When the menu opens under a mouse that never moves, the highlight also follows the keys (1 to 10 in all three). Moving the mouse still highlights the option under it, and clicking an option without moving or tapping one with touch emulation still inserts it.

tsc, tsc-test, flow full-check (0 errors), and eslint and prettier --list-different on the changed files pass. The ComponentPicker, Mentions and DraggableBlock e2e specs pass in Chromium; the Firefox and WebKit failures I saw there also fail or flake on main. All of this ran on Windows with Node 24.21.0, so WebKit is Playwright's Windows build rather than Safari, and touch was only emulated.

…ight when a typeahead menu scrolls under the mouse

## Description

Pressing ArrowDown or ArrowUp in a typeahead menu scrolls the highlighted
option into view. With the mouse resting over the menu, the scroll slides
another option under it, the browser fires mouseenter there, and the
highlight jumps back to it.

The default menu now moves the highlight on mousemove, and only when the
pointer position has changed, because WebKit also fires a mousemove after the
scroll at the position the pointer never left. If the arrow keys come first,
the first mousemove only records the position. Taps still highlight and
select, since touch sends a mousemove before the click. Wheel scrolling and
typing a filter no longer move the highlight until the mouse moves. The
playground's `li:hover` style is removed so that only the highlighted row is
grey. Menus with their own `menuRenderFn` are unchanged.

Closes meta#4296

## Test plan

### Before

```
$ pnpm exec vitest --project unit --no-watch packages/lexical-react/src/__tests__/unit/LexicalMenu.test.tsx
         × keeps the keyboard highlight when the list scrolls under a still pointer
         × keeps the ArrowDown highlight when the menu opened under a still pointer
         × keeps the ArrowUp highlight when the menu opened under a still pointer
AssertionError: expected 2 to be 1 // Object.is equality
AssertionError: expected 2 to be 1 // Object.is equality
AssertionError: expected 1 to be 2 // Object.is equality
      Tests  3 failed | 19 passed (22)
```

In the playground, resting the mouse on the third "/" option and pressing
ArrowDown ten times (local Playwright script, highlighted index after each
press):

```
chromium 153.0.8010.12: 3, 4, 5, 3, 4, 5, 6, 4, 5, 6
firefox 155.0: 3, 4, 5, 3, 4, 5, 6, 4, 5, 6
webkit 26.6: 3, 4, 5, 3, 4, 5, 6, 4, 5, 6
```

### After

```
$ pnpm exec vitest --project unit --no-watch packages/lexical-react/src/__tests__/unit/LexicalMenu.test.tsx
      Tests  22 passed (22)

$ pnpm exec vitest --project unit --no-watch packages/lexical-react packages/lexical-playground
 Test Files  74 passed (74)
      Tests  674 passed (674)
```

```
chromium 153.0.8010.12: 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
firefox 155.0: 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
webkit 26.6: 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
```

With mousemove but no position check, WebKit still jumped (3, 4, 2, 3, 4,
5, 3, 4, 5, 6). When the menu opens under a mouse that never moves, the
highlight also follows the keys (1 to 10 in all three). Moving the mouse
still highlights the option under it, and clicking an option without moving
or tapping one with touch emulation still inserts it.

`tsc`, `tsc-test`, `flow full-check` (0 errors), and `eslint` and
`prettier --list-different` on the changed files pass. The ComponentPicker,
Mentions and DraggableBlock e2e specs pass in Chromium; the Firefox and
WebKit failures I saw there also fail or flake on `main`. All of this ran on
Windows with Node 24.21.0, so WebKit is Playwright's Windows build rather
than Safari, and touch was only emulated.
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
lexical Ready Ready Preview Oct 20, 2026 7:53am UTC
lexical-playground Ready Ready Preview Oct 20, 2026 7:53am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 20, 2026
@pkg-pr-new

pkg-pr-new Bot commented Oct 20, 2026

Copy link
Copy Markdown
lexical

npm i https://pkg.pr.new/lexical@9391
pnpm add https://pkg.pr.new/lexical@9391
yarn add https://pkg.pr.new/lexical@9391.tgz

@lexical/a11y

npm i https://pkg.pr.new/@lexical/a11y@9391
pnpm add https://pkg.pr.new/@lexical/a11y@9391
yarn add https://pkg.pr.new/@lexical/a11y@9391.tgz

@lexical/clipboard

npm i https://pkg.pr.new/@lexical/clipboard@9391
pnpm add https://pkg.pr.new/@lexical/clipboard@9391
yarn add https://pkg.pr.new/@lexical/clipboard@9391.tgz

@lexical/code

npm i https://pkg.pr.new/@lexical/code@9391
pnpm add https://pkg.pr.new/@lexical/code@9391
yarn add https://pkg.pr.new/@lexical/code@9391.tgz

@lexical/code-core

npm i https://pkg.pr.new/@lexical/code-core@9391
pnpm add https://pkg.pr.new/@lexical/code-core@9391
yarn add https://pkg.pr.new/@lexical/code-core@9391.tgz

@lexical/code-prism

npm i https://pkg.pr.new/@lexical/code-prism@9391
pnpm add https://pkg.pr.new/@lexical/code-prism@9391
yarn add https://pkg.pr.new/@lexical/code-prism@9391.tgz

@lexical/code-shiki

npm i https://pkg.pr.new/@lexical/code-shiki@9391
pnpm add https://pkg.pr.new/@lexical/code-shiki@9391
yarn add https://pkg.pr.new/@lexical/code-shiki@9391.tgz

@lexical/compiler

npm i https://pkg.pr.new/@lexical/compiler@9391
pnpm add https://pkg.pr.new/@lexical/compiler@9391
yarn add https://pkg.pr.new/@lexical/compiler@9391.tgz

@lexical/devtools-core

npm i https://pkg.pr.new/@lexical/devtools-core@9391
pnpm add https://pkg.pr.new/@lexical/devtools-core@9391
yarn add https://pkg.pr.new/@lexical/devtools-core@9391.tgz

@lexical/dragon

npm i https://pkg.pr.new/@lexical/dragon@9391
pnpm add https://pkg.pr.new/@lexical/dragon@9391
yarn add https://pkg.pr.new/@lexical/dragon@9391.tgz

@lexical/eslint-plugin

npm i https://pkg.pr.new/@lexical/eslint-plugin@9391
pnpm add https://pkg.pr.new/@lexical/eslint-plugin@9391
yarn add https://pkg.pr.new/@lexical/eslint-plugin@9391.tgz

@lexical/extension

npm i https://pkg.pr.new/@lexical/extension@9391
pnpm add https://pkg.pr.new/@lexical/extension@9391
yarn add https://pkg.pr.new/@lexical/extension@9391.tgz

@lexical/fast-check

npm i https://pkg.pr.new/@lexical/fast-check@9391
pnpm add https://pkg.pr.new/@lexical/fast-check@9391
yarn add https://pkg.pr.new/@lexical/fast-check@9391.tgz

@lexical/file

npm i https://pkg.pr.new/@lexical/file@9391
pnpm add https://pkg.pr.new/@lexical/file@9391
yarn add https://pkg.pr.new/@lexical/file@9391.tgz

@lexical/hashtag

npm i https://pkg.pr.new/@lexical/hashtag@9391
pnpm add https://pkg.pr.new/@lexical/hashtag@9391
yarn add https://pkg.pr.new/@lexical/hashtag@9391.tgz

@lexical/headless

npm i https://pkg.pr.new/@lexical/headless@9391
pnpm add https://pkg.pr.new/@lexical/headless@9391
yarn add https://pkg.pr.new/@lexical/headless@9391.tgz

@lexical/history

npm i https://pkg.pr.new/@lexical/history@9391
pnpm add https://pkg.pr.new/@lexical/history@9391
yarn add https://pkg.pr.new/@lexical/history@9391.tgz

@lexical/html

npm i https://pkg.pr.new/@lexical/html@9391
pnpm add https://pkg.pr.new/@lexical/html@9391
yarn add https://pkg.pr.new/@lexical/html@9391.tgz

@lexical/internal

npm i https://pkg.pr.new/@lexical/internal@9391
pnpm add https://pkg.pr.new/@lexical/internal@9391
yarn add https://pkg.pr.new/@lexical/internal@9391.tgz

@lexical/link

npm i https://pkg.pr.new/@lexical/link@9391
pnpm add https://pkg.pr.new/@lexical/link@9391
yarn add https://pkg.pr.new/@lexical/link@9391.tgz

@lexical/list

npm i https://pkg.pr.new/@lexical/list@9391
pnpm add https://pkg.pr.new/@lexical/list@9391
yarn add https://pkg.pr.new/@lexical/list@9391.tgz

@lexical/mark

npm i https://pkg.pr.new/@lexical/mark@9391
pnpm add https://pkg.pr.new/@lexical/mark@9391
yarn add https://pkg.pr.new/@lexical/mark@9391.tgz

@lexical/markdown

npm i https://pkg.pr.new/@lexical/markdown@9391
pnpm add https://pkg.pr.new/@lexical/markdown@9391
yarn add https://pkg.pr.new/@lexical/markdown@9391.tgz

@lexical/mdast

npm i https://pkg.pr.new/@lexical/mdast@9391
pnpm add https://pkg.pr.new/@lexical/mdast@9391
yarn add https://pkg.pr.new/@lexical/mdast@9391.tgz

@lexical/offset

npm i https://pkg.pr.new/@lexical/offset@9391
pnpm add https://pkg.pr.new/@lexical/offset@9391
yarn add https://pkg.pr.new/@lexical/offset@9391.tgz

@lexical/overflow

npm i https://pkg.pr.new/@lexical/overflow@9391
pnpm add https://pkg.pr.new/@lexical/overflow@9391
yarn add https://pkg.pr.new/@lexical/overflow@9391.tgz

@lexical/plain-text

npm i https://pkg.pr.new/@lexical/plain-text@9391
pnpm add https://pkg.pr.new/@lexical/plain-text@9391
yarn add https://pkg.pr.new/@lexical/plain-text@9391.tgz

@lexical/react

npm i https://pkg.pr.new/@lexical/react@9391
pnpm add https://pkg.pr.new/@lexical/react@9391
yarn add https://pkg.pr.new/@lexical/react@9391.tgz

@lexical/rich-text

npm i https://pkg.pr.new/@lexical/rich-text@9391
pnpm add https://pkg.pr.new/@lexical/rich-text@9391
yarn add https://pkg.pr.new/@lexical/rich-text@9391.tgz

@lexical/selection

npm i https://pkg.pr.new/@lexical/selection@9391
pnpm add https://pkg.pr.new/@lexical/selection@9391
yarn add https://pkg.pr.new/@lexical/selection@9391.tgz

@lexical/table

npm i https://pkg.pr.new/@lexical/table@9391
pnpm add https://pkg.pr.new/@lexical/table@9391
yarn add https://pkg.pr.new/@lexical/table@9391.tgz

@lexical/tailwind

npm i https://pkg.pr.new/@lexical/tailwind@9391
pnpm add https://pkg.pr.new/@lexical/tailwind@9391
yarn add https://pkg.pr.new/@lexical/tailwind@9391.tgz

@lexical/text

npm i https://pkg.pr.new/@lexical/text@9391
pnpm add https://pkg.pr.new/@lexical/text@9391
yarn add https://pkg.pr.new/@lexical/text@9391.tgz

@lexical/utils

npm i https://pkg.pr.new/@lexical/utils@9391
pnpm add https://pkg.pr.new/@lexical/utils@9391
yarn add https://pkg.pr.new/@lexical/utils@9391.tgz

@lexical/yjs

npm i https://pkg.pr.new/@lexical/yjs@9391
pnpm add https://pkg.pr.new/@lexical/yjs@9391
yarn add https://pkg.pr.new/@lexical/yjs@9391.tgz

commit: b46d3d4

This branch was successfully deployed

2 active deployments
Preview – lexical — b46d3d4b Deployed Oct 20, 2026 by vercel[bot]
Preview – lexical-playground — b46d3d4b Deployed Oct 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

1 participant