Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,11 @@ export function DecoratorBoundarySelectionPlugin(): null {
// which on Windows and Linux grabs the mouse so the page never sees the matching `pointerup`.
// The flag would then stay set with no drag in flight, and every later arrival — a click, a
// Shift+Arrow extend — would be treated as one, leaving the DOM selection stranded on points
// Lexical cannot resolve.
// Lexical cannot resolve. The button is duck-typed rather than read through `instanceof
// PointerEvent`: not every host defines that global (jsdom before 27 has none), and a press that
// carries no button at all still counts as a primary one.
const markDown = (event: Event) => {
if (event instanceof PointerEvent && event.button !== 0) return;
if ("button" in event && event.button !== 0) return;
isPointerDown.current = true;
};
// BOTH ends are listened for on the DOCUMENT in the capture phase, and the release on
Expand Down
2 changes: 1 addition & 1 deletion packages/platform/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18290,7 +18290,7 @@ function IC() {
const [e] = ce(), t = Z(!1), r = Z(!1);
return z(() => {
const n = (s) => {
s instanceof PointerEvent && s.button !== 0 || (t.current = !0);
"button" in s && s.button !== 0 || (t.current = !0);
}, i = () => {
t.current = !1, r.current && (r.current = !1, e.update(() => {
const s = w();
Expand Down
2 changes: 1 addition & 1 deletion packages/platform/dist/index.js.map

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -159,20 +159,24 @@ function selectionPoints(editor: LexicalEditor): {
/**
* Hold the pointer button down the way a drag does. `pointerdown` goes to the root element, which is
* where a press inside the editor lands; the plugin leaves the browser's DOM selection alone only for
* as long as this is held.
* as long as this is held. The default is a plain `Event`, which carries no `button` at all — the
* shape every host that implements no `PointerEvent` dispatches.
*/
async function pressPointer(editor: LexicalEditor): Promise<void> {
async function pressPointer(
editor: LexicalEditor,
event: Event = new Event("pointerdown", { bubbles: true }),
): Promise<void> {
const root = editor.getRootElement();
if (!root) throw new Error("the editor rendered no root element");
await act(async () => {
root.dispatchEvent(new Event("pointerdown", { bubbles: true }));
root.dispatchEvent(event);
});
}

/**
* Let the pointer button up. `pointerup` goes to the DOCUMENT, because a drag begun in the editor can
* be released anywhere, and that is where the plugin listens for it. jsdom implements no
* `PointerEvent`, so a plain `Event` carries the type — the plugin reads nothing else off it.
* be released anywhere, and that is where the plugin listens for it. A plain `Event` carries the
* type — the plugin reads nothing else off a release.
*/
async function releasePointer(editor: LexicalEditor): Promise<void> {
const root = editor.getRootElement();
Expand Down Expand Up @@ -382,6 +386,43 @@ describe("a selection landing inside a read-only construct's decorator glyphs",
expect(domSelectionPoints().anchorOffset).toBe(0);
});

it("writes the DOM straight away after a secondary-button press", async () => {
const { lexical } = await mountStandardViewEditor(figureUsj);
const dom = figureDom(lexical);

// A right-click can open the platform's native context menu, which swallows the matching
// `pointerup`; a press that counted as a drag would then hold the raw points indefinitely.
await pressPointer(lexical, new PointerEvent("pointerdown", { bubbles: true, button: 2 }));
await dragSelect(dom.opener, 2, dom.trailing, 3);

expect(domSelectionPoints().anchorNode).toBe(dom.figure);
expect(domSelectionPoints().anchorOffset).toBe(0);
});

it("treats a press as a drag in a host that defines no `PointerEvent`", async () => {
// Hosts such as jsdom before 27 have no `PointerEvent` global, so the press listener must not
// name one. Deleting the global raises the same `ReferenceError` such a host does.
const host: { PointerEvent?: unknown } = globalThis;
const hostPointerEvent = Object.getOwnPropertyDescriptor(host, "PointerEvent");
delete host.PointerEvent;
try {
const { lexical } = await mountStandardViewEditor(figureUsj);
const dom = figureDom(lexical);
await pressPointer(lexical);

await dragSelect(dom.opener, 2, dom.trailing, 3);

expect(domSelectionPoints()).toEqual({
anchorNode: dom.opener,
anchorOffset: 2,
focusNode: dom.trailing,
focusOffset: 3,
});
} finally {
if (hostPointerEvent) Object.defineProperty(host, "PointerEvent", hostPointerEvent);
}
});

it("survives an event-less update once the DOM holds the boundary form", async () => {
const { lexical } = await mountStandardViewEditor(figureUsj);
const dom = figureDom(lexical);
Expand Down
Loading