Skip to content
Merged
8 changes: 6 additions & 2 deletions flow-typed/environments/bom.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// flow-typed signature: 09630545c584c3b212588a2390c257d0
// flow-typed version: baae4b8bcc/bom/flow_>=v0.261.x
// flow-typed signature: b6e924fee0c3aabc47b3e089cc9ff0a7
// flow-typed version: 74bef415fd/bom/flow_>=v0.261.x

/* BOM */

Expand Down Expand Up @@ -37,6 +37,9 @@ declare interface Crypto {
T: Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | BigInt64Array | BigUint64Array
>(typedArray: T) => T;
randomUUID: () => string;
subtle: {
digest(algorithm: string, data: Uint8Array): Promise<ArrayBuffer>
},
}
declare var crypto: Crypto;

Expand Down Expand Up @@ -826,6 +829,7 @@ declare class SharedWorker extends EventTarget {
declare function importScripts(...urls: Array<string | TrustedScriptURL>): void;

declare class WorkerGlobalScope extends EventTarget {
// $FlowExpectedError[incompatible-variance]
self: this;
location: WorkerLocation;
navigator: WorkerNavigator;
Expand Down
69 changes: 32 additions & 37 deletions flow-typed/environments/dom.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// flow-typed signature: c126209a0e678d121655f21a36219841
// flow-typed version: f998bd7c46/dom/flow_>=v0.261.x
// flow-typed signature: cf23703830440a242ae9f9850dc4b882
// flow-typed version: 7c14103836/dom/flow_>=v0.261.x

/* Files */

Expand Down Expand Up @@ -154,41 +154,34 @@ type StorageEventTypes = 'storage';
type SecurityPolicyViolationEventTypes = 'securitypolicyviolation';
type USBConnectionEventTypes = 'connect' | 'disconnect';
type ToggleEventTypes = 'beforetoggle' | 'toggle';

type AddEventListenerOptionsOrUseCapture =
| boolean
| $ReadOnly<{
capture?: boolean,
once?: boolean,
passive?: boolean,
signal?: AbortSignal,
}>;
type EventListenerOptionsOrUseCapture =
| boolean
| $ReadOnly<{
capture?: boolean,
}>;
type EventListenerOptionsOrUseCapture = boolean | {
capture?: boolean,
once?: boolean,
passive?: boolean,
signal?: AbortSignal,
...
};

declare class EventTarget {
addEventListener(type: MouseEventTypes, listener: MouseEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: FocusEventTypes, listener: FocusEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: KeyboardEventTypes, listener: KeyboardEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: InputEventTypes, listener: InputEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: TouchEventTypes, listener: TouchEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: WheelEventTypes, listener: WheelEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: AbortProgressEventTypes, listener: AbortProgressEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: ProgressEventTypes, listener: ProgressEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: DragEventTypes, listener: DragEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: PointerEventTypes, listener: PointerEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: AnimationEventTypes, listener: AnimationEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: ClipboardEventTypes, listener: ClipboardEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: TransitionEventTypes, listener: TransitionEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: MessageEventTypes, listener: MessageEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: BeforeUnloadEventTypes, listener: BeforeUnloadEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: StorageEventTypes, listener: StorageEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: SecurityPolicyViolationEventTypes, listener: SecurityPolicyViolationEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: USBConnectionEventTypes, listener: USBConnectionEventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: string, listener: EventListener, optionsOrUseCapture?: AddEventListenerOptionsOrUseCapture): void;
addEventListener(type: MouseEventTypes, listener: MouseEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: FocusEventTypes, listener: FocusEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: KeyboardEventTypes, listener: KeyboardEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: InputEventTypes, listener: InputEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: TouchEventTypes, listener: TouchEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: WheelEventTypes, listener: WheelEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: AbortProgressEventTypes, listener: AbortProgressEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: ProgressEventTypes, listener: ProgressEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: DragEventTypes, listener: DragEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: PointerEventTypes, listener: PointerEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: AnimationEventTypes, listener: AnimationEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: ClipboardEventTypes, listener: ClipboardEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: TransitionEventTypes, listener: TransitionEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: MessageEventTypes, listener: MessageEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: BeforeUnloadEventTypes, listener: BeforeUnloadEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: StorageEventTypes, listener: StorageEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: SecurityPolicyViolationEventTypes, listener: SecurityPolicyViolationEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: USBConnectionEventTypes, listener: USBConnectionEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
addEventListener(type: string, listener: EventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;

removeEventListener(type: MouseEventTypes, listener: MouseEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
removeEventListener(type: FocusEventTypes, listener: FocusEventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture): void;
Expand Down Expand Up @@ -837,11 +830,11 @@ declare class TaskSignal extends AbortSignal {
+priority: number;
}

type SchedulerPostTaskOptions = {
type SchedulerPostTaskOptions = {|
priority?: "user-blocking" | "user-visible" | "background",
signal?: TaskSignal | AbortSignal,
delay?: number,
};
|};

declare class Scheduler {
postTask<T>(
Expand Down Expand Up @@ -973,6 +966,7 @@ declare class HTMLCollection<+Elem: Element> {
length: number;
item(nameOrIndex?: any, optionalIndex?: any): Elem | null;
namedItem(name: string): Elem | null;
// $FlowExpectedError[incompatible-variance]
[index: number | string]: Elem;
}

Expand Down Expand Up @@ -1396,6 +1390,7 @@ declare class Element extends Node mixins mixin$Animatable {
scrollLeft: number;
scrollTop: number;
scrollWidth: number;
role: string | null;
+tagName: string;

// TODO: a lot more ARIA properties
Expand Down
7 changes: 5 additions & 2 deletions flow-typed/environments/html.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// flow-typed signature: 760aeea3b9b767e808097fe22b68a20f
// flow-typed version: 8584579196/html/flow_>=v0.261.x
// flow-typed signature: b876b0c754b533b8b6d83f6166e66d8e
// flow-typed version: 84d934abce/html/flow_>=v0.261.x

/* DataTransfer */

Expand Down Expand Up @@ -808,6 +808,7 @@ declare class HTMLImageElement extends HTMLElement {
isMap: boolean;
naturalHeight: number; // readonly
naturalWidth: number; // readonly
fetchPriority: "high" | "low" | "auto";
sizes: string;
src: string;
srcset: string;
Expand Down Expand Up @@ -1280,6 +1281,7 @@ declare class HTMLLinkElement extends HTMLElement {
href: string;
hreflang: string;
media: string;
fetchPriority: "high" | "low" | "auto";
rel: string;
sizes: DOMTokenList;
type: string;
Expand All @@ -1292,6 +1294,7 @@ declare class HTMLScriptElement extends HTMLElement {
charset: string;
crossOrigin?: string;
defer: boolean;
fetchPriority: "high" | "low" | "auto";
// flowlint unsafe-getters-setters:off
get src(): string;
set src(value: string | TrustedScriptURL): void;
Expand Down
6 changes: 3 additions & 3 deletions flow-typed/environments/jsx.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// flow-typed signature: e51684b7f9618ccda34e09816ddb01da
// flow-typed version: bb4cb83b7a/jsx/flow_>=v0.261.x
// flow-typed signature: 4e0586c675a57bbe33b81cc8cedd32ba
// flow-typed version: 284fb57107/jsx/flow_>=v0.261.x

// https://www.w3.org/TR/uievents-key/#keys-modifier
type ModifierKey =
Expand Down Expand Up @@ -27,7 +27,7 @@ declare class SyntheticEvent<+T: EventTarget = EventTarget, +E: Event = Event> {
isDefaultPrevented(): boolean;
isPropagationStopped(): boolean;
isTrusted: boolean;
nativeEvent: E;
+nativeEvent: E;
persist(): void;
preventDefault(): void;
stopPropagation(): void;
Expand Down
2 changes: 1 addition & 1 deletion packages/lexical-link/flow/LexicalLink.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export type ClickableLinkConfig = {
disabled: boolean;
}

type AddEventListenerOptions = Exclude<AddEventListenerOptionsOrUseCapture, boolean>;
type AddEventListenerOptions = Exclude<EventListenerOptionsOrUseCapture, boolean>;

declare export function registerClickableLink(
editor: LexicalEditor,
Expand Down
6 changes: 3 additions & 3 deletions packages/lexical-playground/src/nodes/StickyComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ import {useEffect, useLayoutEffect, useRef} from 'react';
import {createWebsocketProvider} from '../collaboration';
import {$isStickyNode} from './StickyNode';

type Positioning = {
interface Positioning {
isDragging: boolean;
offsetX: number;
offsetY: number;
rootElementRect: null | ClientRect;
rootElementRect: null | DOMRect;
x: number;
y: number;
};
}

function positionSticky(
stickyElem: HTMLElement,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const ACTIVE_RESIZER_COLOR = '#76b6ff';
function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
const targetRef = useRef<HTMLElement | null>(null);
const resizerRef = useRef<HTMLDivElement | null>(null);
const tableRectRef = useRef<ClientRect | null>(null);
const tableRectRef = useRef<DOMRect | null>(null);
const [hasTable, setHasTable] = useState(false);

const pointerStartPosRef = useRef<PointerPosition | null>(null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import {createRectsFromDOMRange} from '@lexical/selection';
import {dedupeSelectionRects} from '@lexical/utils';
import {describe, expect, it, onTestFinished} from 'vitest';

/**
* Characterizes the documented KNOWN LIMITATION of dedupeSelectionRects' keep-smaller
* rule (see its docstring): for OVERLAPPING inline content it can drop a wider rect
* that covers real selected area, under-painting the part it uniquely covered.
*
* Uses real browser layout (Chromium / Firefox / WebKit) and the real
* createRectsFromDOMRange + dedupeSelectionRects. The construction uses
* explicit-width inline boxes and a fixed sub-pixel transform so the geometry is
* identical across platforms (no dependence on font glyph metrics), and the
* assertions are structural (no hard-coded pixel thresholds): the root is a real,
* laid-out element so getBoundingClientRect / getComputedStyle are real; only
* `editor` is a getRootElement shim, which is all the helper reads off it.
*/

const ROOT_WIDTH = 600;

function setupRoot(): HTMLDivElement {
const root = document.createElement('div');
root.style.position = 'absolute';
root.style.left = '0px';
root.style.top = '0px';
root.style.width = `${ROOT_WIDTH}px`;
root.style.padding = '0px';
root.style.margin = '0px';
root.style.font = '16px/1 monospace';
document.body.style.margin = '0px';
document.body.appendChild(root);
return root;
}

// Containment predicate matching dedupeSelectionRects (1px tolerance).
function contains(a: DOMRect, b: DOMRect): boolean {
return (
b.left >= a.left - 1 &&
b.top >= a.top - 1 &&
b.right <= a.right + 1 &&
b.bottom <= a.bottom + 1
);
}

const sameRect = (a: DOMRect, b: DOMRect): boolean =>
Math.abs(a.left - b.left) < 1 &&
Math.abs(a.right - b.right) < 1 &&
Math.abs(a.top - b.top) < 1 &&
Math.abs(a.bottom - b.bottom) < 1;

function selectAll(root: HTMLElement): Range {
const r = document.createRange();
r.selectNodeContents(root);
return r;
}

describe('dedupeSelectionRects under-paints real content for overlapping inline boxes', () => {
it('drops the wider rect that uniquely covers part of the selection', () => {
const root = setupRoot();
onTestFinished(() => root.remove());

// A wide run of text, then an inline box pulled back over it with a negative
// margin so it overlaps the run, and raised 0.5px (vertical-align) so its top
// sits a hair above the text run's. The selection's client rects follow the
// glyphs, so the text run is a genuinely wide rect; the raise defeats
// createRectsFromDOMRange's asymmetric overlap guard (`prevRect.top <= cur.top`),
// so the wide text rect AND the contained box rect both survive; keep-smaller
// then drops the wide one. (The box stands in for any overlapping inline content,
// e.g. a baseline-shifted inline decorator.) Assertions are structural — no
// pixel thresholds — so they hold whatever the monospace glyph width is.
root.innerHTML =
`<p style="margin:0">aaaaaaaaaaaaaaaa` +
`<span style="display:inline-block;width:12px;height:18px;margin-left:-100px;vertical-align:0.5px;background:rgba(255,0,0,.4)">N</span>` +
`</p>`;
void root.offsetHeight;

const range = selectAll(root);
const survivors = createRectsFromDOMRange(
{getRootElement: () => root},
range,
);

// createRectsFromDOMRange leaves a same-row contained pair: a wider rect that
// strictly contains a narrower survivor.
let wide: DOMRect | undefined;
let narrow: DOMRect | undefined;
for (const a of survivors) {
for (const b of survivors) {
if (
a !== b &&
contains(a, b) &&
a.width > b.width + 2 &&
!contains(b, a)
) {
wide = a;
narrow = b;
}
}
}
expect(
wide && narrow,
'createRectsFromDOMRange left a same-row contained pair (wider ⊇ narrower)',
).toBeTruthy();
if (!wide || !narrow) return;

// dedupeSelectionRects drops the wider rect (keep-smaller)...
const deduped = dedupeSelectionRects(survivors);
expect(
deduped.some(r => sameRect(r, wide)),
'the wider rect was dropped by keep-smaller dedupe',
).toBe(false);

// ...and nothing left covers the wide rect's left edge, so the area it uniquely
// covered (left of the pulled-back box) goes unpainted — the under-paint.
const probeX = wide.left + 1;
const probeY = wide.top + wide.height / 2;
const covered = deduped.some(
r =>
r.left <= probeX &&
r.right >= probeX &&
r.top <= probeY &&
r.bottom >= probeY,
);
expect(
covered,
'the wide rect left edge is left unpainted after dedupe',
).toBe(false);
});
});
4 changes: 2 additions & 2 deletions packages/lexical-selection/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ export function createDOMRange(
* @returns The selectionRects as an array.
*/
export function createRectsFromDOMRange(
editor: LexicalEditor,
editor: Pick<LexicalEditor, 'getRootElement'>,
range: Range,
): ClientRect[] {
): DOMRect[] {
const rootElement = editor.getRootElement();

if (rootElement === null) {
Expand Down
3 changes: 3 additions & 0 deletions packages/lexical-utils/flow/LexicalUtils.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ declare export function $findMatchingParent(
findFn: (LexicalNode) => boolean,
): LexicalNode | null;
declare export function mergeRegister(...func: (() => void)[]): () => void;
declare export function dedupeSelectionRects(
rects: Iterable<ClientRect>,
): ClientRect[];
declare export function markSelection(
editor: LexicalEditor,
onReposition?: (node: HTMLElement[]) => void,
Expand Down
Loading
Loading