@lexical/table 0.47.0 → 0.47.1-nightly.20260713.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2497,6 +2497,27 @@ function $getTableNodeByKeyOrThrow(key) {
2497
2497
  const isPointerDownOnEvent = event => {
2498
2498
  return (event.buttons & 1) === 1;
2499
2499
  };
2500
+
2501
+ // Distance (px) from a scroll container edge at which drag auto-scroll kicks
2502
+ // in, and the maximum scroll delta applied per animation frame.
2503
+ const AUTO_SCROLL_EDGE_ZONE = 40;
2504
+ const AUTO_SCROLL_MAX_STEP = 18;
2505
+
2506
+ // Given a pointer position and the start/end edges of a scroll container on one
2507
+ // axis, return the signed per-frame scroll delta: negative near the start edge,
2508
+ // positive near the end edge, 0 while outside both edge zones. The delta ramps
2509
+ // up the deeper the pointer is into the zone (and is capped once it reaches or
2510
+ // passes the edge).
2511
+ function autoScrollStep(pos, start, end) {
2512
+ const speed = depth => Math.max(1, Math.ceil(Math.min(AUTO_SCROLL_EDGE_ZONE, depth) / AUTO_SCROLL_EDGE_ZONE * AUTO_SCROLL_MAX_STEP));
2513
+ if (pos <= start + AUTO_SCROLL_EDGE_ZONE) {
2514
+ return -speed(start + AUTO_SCROLL_EDGE_ZONE - pos);
2515
+ }
2516
+ if (pos >= end - AUTO_SCROLL_EDGE_ZONE) {
2517
+ return speed(pos - (end - AUTO_SCROLL_EDGE_ZONE));
2518
+ }
2519
+ return 0;
2520
+ }
2500
2521
  function isHTMLTableElement(el) {
2501
2522
  return lexical.isHTMLElement(el) && el.nodeName === 'TABLE';
2502
2523
  }
@@ -2588,22 +2609,177 @@ function $handleTableClick(editor, event, selectedDOMCell, tableElement, tableOb
2588
2609
  tableObserver.$setAnchorCellForSelection(startingCell);
2589
2610
  });
2590
2611
  }
2591
- const onPointerUp = () => {
2612
+ let lastClientX = event.clientX;
2613
+ let lastClientY = event.clientY;
2614
+ let autoScrollRafId = null;
2615
+ const stopSelecting = () => {
2592
2616
  tableObserver.isSelecting = false;
2617
+ if (autoScrollRafId !== null) {
2618
+ editorWindow.cancelAnimationFrame(autoScrollRafId);
2619
+ autoScrollRafId = null;
2620
+ }
2593
2621
  editorWindow.removeEventListener('pointerup', onPointerUp);
2594
2622
  editorWindow.removeEventListener('pointermove', onPointerMove);
2595
2623
  };
2624
+
2625
+ // Resolve the table cell under the given viewport coordinates via the
2626
+ // table's own root so elementsFromPoint isn't retargeted; narrow with the
2627
+ // type guards rather than casting so the detached-table case (Node) falls
2628
+ // through to no hit-test.
2629
+ const resolveFocusCellFromPoint = (clientX, clientY) => {
2630
+ const tableRoot = tableElement.getRootNode();
2631
+ if (!lexical.isDOMDocumentNode(tableRoot) && !lexical.isDOMShadowRoot(tableRoot)) {
2632
+ return null;
2633
+ }
2634
+ for (const el of tableRoot.elementsFromPoint(clientX, clientY)) {
2635
+ const cell = getDOMCellInTableFromTarget(tableElement, el);
2636
+ if (cell) {
2637
+ return cell;
2638
+ }
2639
+ }
2640
+ return null;
2641
+ };
2642
+ const applyFocusCell = (focusCell, override) => {
2643
+ // Fallback: set anchor if still missing (handles race conditions)
2644
+ if (tableObserver.anchorCell === null) {
2645
+ editor.update(() => {
2646
+ tableObserver.$setAnchorCellForSelection(focusCell);
2647
+ });
2648
+ }
2649
+ if (tableObserver.focusCell === null || focusCell.elem !== tableObserver.focusCell.elem) {
2650
+ tableObservers.setNextFocus({
2651
+ focusCell,
2652
+ override,
2653
+ tableKey: tableObserver.tableNodeKey
2654
+ });
2655
+ editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
2656
+ }
2657
+ };
2658
+
2659
+ // Walk up from the table to the nearest ancestor that can actually scroll
2660
+ // on the requested axis (the scrollable-tables wrapper for 'x'). Returns
2661
+ // null when none is found, in which case the caller may fall back to the
2662
+ // window.
2663
+ const findScrollContainer = axis => {
2664
+ for (let el = tableElement.parentElement; el; el = el.parentElement) {
2665
+ const canScroll = axis === 'x' ? el.scrollWidth > el.clientWidth : el.scrollHeight > el.clientHeight;
2666
+ if (canScroll) {
2667
+ const style = editorWindow.getComputedStyle(el);
2668
+ const overflow = axis === 'x' ? style.overflowX : style.overflowY;
2669
+ if (overflow === 'auto' || overflow === 'scroll') {
2670
+ return el;
2671
+ }
2672
+ }
2673
+ }
2674
+ return null;
2675
+ };
2676
+
2677
+ // Scroll `container` (or the window when null) on `axis` if the pointer is
2678
+ // within the edge zone. Returns whether it actually scrolled.
2679
+ const scrollAxis = (container, pos, axis) => {
2680
+ let start;
2681
+ let end;
2682
+ if (container === null) {
2683
+ start = 0;
2684
+ end = axis === 'x' ? editorWindow.innerWidth : editorWindow.innerHeight;
2685
+ } else {
2686
+ const rect = container.getBoundingClientRect();
2687
+ start = axis === 'x' ? rect.left : rect.top;
2688
+ end = axis === 'x' ? rect.right : rect.bottom;
2689
+ }
2690
+ const step = autoScrollStep(pos, start, end);
2691
+ if (step === 0) {
2692
+ return false;
2693
+ }
2694
+ if (container === null) {
2695
+ const before = axis === 'x' ? editorWindow.scrollX : editorWindow.scrollY;
2696
+ editorWindow.scrollBy(axis === 'x' ? step : 0, axis === 'x' ? 0 : step);
2697
+ return (axis === 'x' ? editorWindow.scrollX : editorWindow.scrollY) !== before;
2698
+ }
2699
+ if (axis === 'x') {
2700
+ const before = container.scrollLeft;
2701
+ container.scrollLeft += step;
2702
+ return container.scrollLeft !== before;
2703
+ }
2704
+ const before = container.scrollTop;
2705
+ container.scrollTop += step;
2706
+ return container.scrollTop !== before;
2707
+ };
2708
+
2709
+ // Clamp the last pointer position into the visible bounds of the scroll
2710
+ // container(s) so a pointer dragged past an edge still hit-tests onto the
2711
+ // newly-revealed cell instead of empty space beyond the table.
2712
+ const clampHitPoint = (hContainer, vContainer) => {
2713
+ let x = lastClientX;
2714
+ let y = lastClientY;
2715
+ if (hContainer === null) {
2716
+ x = Math.min(Math.max(x, 1), editorWindow.innerWidth - 1);
2717
+ } else {
2718
+ const rect = hContainer.getBoundingClientRect();
2719
+ x = Math.min(Math.max(x, rect.left + 1), rect.right - 1);
2720
+ }
2721
+ if (vContainer === null) {
2722
+ y = Math.min(Math.max(y, 1), editorWindow.innerHeight - 1);
2723
+ } else {
2724
+ const rect = vContainer.getBoundingClientRect();
2725
+ y = Math.min(Math.max(y, rect.top + 1), rect.bottom - 1);
2726
+ }
2727
+ return [x, y];
2728
+ };
2729
+ const isNearScrollEdge = () => {
2730
+ const hContainer = findScrollContainer('x');
2731
+ if (hContainer !== null) {
2732
+ const rect = hContainer.getBoundingClientRect();
2733
+ if (autoScrollStep(lastClientX, rect.left, rect.right) !== 0) {
2734
+ return true;
2735
+ }
2736
+ }
2737
+ const vContainer = findScrollContainer('y');
2738
+ const vStart = vContainer === null ? 0 : vContainer.getBoundingClientRect().top;
2739
+ const vEnd = vContainer === null ? editorWindow.innerHeight : vContainer.getBoundingClientRect().bottom;
2740
+ return autoScrollStep(lastClientY, vStart, vEnd) !== 0;
2741
+ };
2742
+ const tickAutoScroll = () => {
2743
+ autoScrollRafId = null;
2744
+ if (!tableObserver.isSelecting) {
2745
+ return;
2746
+ }
2747
+ const hContainer = findScrollContainer('x');
2748
+ const vContainer = findScrollContainer('y');
2749
+ // Only the table's own wrapper scrolls horizontally; pages don't
2750
+ // auto-scroll sideways. Vertically we fall back to the window.
2751
+ const scrolledX = hContainer !== null && scrollAxis(hContainer, lastClientX, 'x');
2752
+ const scrolledY = scrollAxis(vContainer, lastClientY, 'y');
2753
+ if (scrolledX || scrolledY) {
2754
+ const [hitX, hitY] = clampHitPoint(hContainer, vContainer);
2755
+ const focusCell = resolveFocusCellFromPoint(hitX, hitY);
2756
+ if (focusCell) {
2757
+ applyFocusCell(focusCell, false);
2758
+ }
2759
+ autoScrollRafId = editorWindow.requestAnimationFrame(tickAutoScroll);
2760
+ }
2761
+ };
2762
+ const maybeStartAutoScroll = () => {
2763
+ // Touch taps don't initiate table selection, so they shouldn't scroll.
2764
+ if (autoScrollRafId !== null || tableObserver.pointerType === 'touch' || !isNearScrollEdge()) {
2765
+ return;
2766
+ }
2767
+ autoScrollRafId = editorWindow.requestAnimationFrame(tickAutoScroll);
2768
+ };
2769
+ const onPointerUp = () => {
2770
+ stopSelecting();
2771
+ };
2596
2772
  const onPointerMove = moveEvent => {
2597
2773
  if (!isPointerDownOnEvent(moveEvent) && tableObserver.isSelecting) {
2598
- tableObserver.isSelecting = false;
2599
- editorWindow.removeEventListener('pointerup', onPointerUp);
2600
- editorWindow.removeEventListener('pointermove', onPointerMove);
2774
+ stopSelecting();
2601
2775
  return;
2602
2776
  }
2603
2777
  const moveTarget = lexical.getComposedEventTarget(moveEvent);
2604
2778
  if (!lexical.isDOMNode(moveTarget)) {
2605
2779
  return;
2606
2780
  }
2781
+ lastClientX = moveEvent.clientX;
2782
+ lastClientY = moveEvent.clientY;
2607
2783
  let focusCell = null;
2608
2784
  // In firefox the moveEvent.target may be captured so we must always
2609
2785
  // consult the coordinates #7245
@@ -2611,37 +2787,14 @@ function $handleTableClick(editor, event, selectedDOMCell, tableElement, tableOb
2611
2787
  if (override) {
2612
2788
  focusCell = getDOMCellInTableFromTarget(tableElement, moveTarget);
2613
2789
  } else {
2614
- // Resolve via the table's own root so elementsFromPoint isn't
2615
- // retargeted; narrow with the type guards rather than casting so the
2616
- // detached-table case (Node) falls through to no hit-test.
2617
- const tableRoot = tableElement.getRootNode();
2618
- if (!lexical.isDOMDocumentNode(tableRoot) && !lexical.isDOMShadowRoot(tableRoot)) {
2619
- return;
2620
- }
2621
- for (const el of tableRoot.elementsFromPoint(moveEvent.clientX, moveEvent.clientY)) {
2622
- focusCell = getDOMCellInTableFromTarget(tableElement, el);
2623
- if (focusCell) {
2624
- break;
2625
- }
2626
- }
2790
+ focusCell = resolveFocusCellFromPoint(moveEvent.clientX, moveEvent.clientY);
2627
2791
  }
2628
2792
  if (focusCell) {
2629
- const anchorCell = focusCell;
2630
- // Fallback: set anchor if still missing (handles race conditions)
2631
- if (tableObserver.anchorCell === null) {
2632
- editor.update(() => {
2633
- tableObserver.$setAnchorCellForSelection(anchorCell);
2634
- });
2635
- }
2636
- if (tableObserver.focusCell === null || focusCell.elem !== tableObserver.focusCell.elem) {
2637
- tableObservers.setNextFocus({
2638
- focusCell,
2639
- override,
2640
- tableKey: tableObserver.tableNodeKey
2641
- });
2642
- editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
2643
- }
2793
+ applyFocusCell(focusCell, override);
2644
2794
  }
2795
+ // Keep the selection reachable when dragging toward/past an edge of a
2796
+ // scrollable table (#7153).
2797
+ maybeStartAutoScroll();
2645
2798
  };
2646
2799
  editorWindow.addEventListener('pointerup', onPointerUp, tableObserver.listenerOptions);
2647
2800
  editorWindow.addEventListener('pointermove', onPointerMove, tableObserver.listenerOptions);
@@ -2495,6 +2495,27 @@ function $getTableNodeByKeyOrThrow(key) {
2495
2495
  const isPointerDownOnEvent = event => {
2496
2496
  return (event.buttons & 1) === 1;
2497
2497
  };
2498
+
2499
+ // Distance (px) from a scroll container edge at which drag auto-scroll kicks
2500
+ // in, and the maximum scroll delta applied per animation frame.
2501
+ const AUTO_SCROLL_EDGE_ZONE = 40;
2502
+ const AUTO_SCROLL_MAX_STEP = 18;
2503
+
2504
+ // Given a pointer position and the start/end edges of a scroll container on one
2505
+ // axis, return the signed per-frame scroll delta: negative near the start edge,
2506
+ // positive near the end edge, 0 while outside both edge zones. The delta ramps
2507
+ // up the deeper the pointer is into the zone (and is capped once it reaches or
2508
+ // passes the edge).
2509
+ function autoScrollStep(pos, start, end) {
2510
+ const speed = depth => Math.max(1, Math.ceil(Math.min(AUTO_SCROLL_EDGE_ZONE, depth) / AUTO_SCROLL_EDGE_ZONE * AUTO_SCROLL_MAX_STEP));
2511
+ if (pos <= start + AUTO_SCROLL_EDGE_ZONE) {
2512
+ return -speed(start + AUTO_SCROLL_EDGE_ZONE - pos);
2513
+ }
2514
+ if (pos >= end - AUTO_SCROLL_EDGE_ZONE) {
2515
+ return speed(pos - (end - AUTO_SCROLL_EDGE_ZONE));
2516
+ }
2517
+ return 0;
2518
+ }
2498
2519
  function isHTMLTableElement(el) {
2499
2520
  return isHTMLElement(el) && el.nodeName === 'TABLE';
2500
2521
  }
@@ -2586,22 +2607,177 @@ function $handleTableClick(editor, event, selectedDOMCell, tableElement, tableOb
2586
2607
  tableObserver.$setAnchorCellForSelection(startingCell);
2587
2608
  });
2588
2609
  }
2589
- const onPointerUp = () => {
2610
+ let lastClientX = event.clientX;
2611
+ let lastClientY = event.clientY;
2612
+ let autoScrollRafId = null;
2613
+ const stopSelecting = () => {
2590
2614
  tableObserver.isSelecting = false;
2615
+ if (autoScrollRafId !== null) {
2616
+ editorWindow.cancelAnimationFrame(autoScrollRafId);
2617
+ autoScrollRafId = null;
2618
+ }
2591
2619
  editorWindow.removeEventListener('pointerup', onPointerUp);
2592
2620
  editorWindow.removeEventListener('pointermove', onPointerMove);
2593
2621
  };
2622
+
2623
+ // Resolve the table cell under the given viewport coordinates via the
2624
+ // table's own root so elementsFromPoint isn't retargeted; narrow with the
2625
+ // type guards rather than casting so the detached-table case (Node) falls
2626
+ // through to no hit-test.
2627
+ const resolveFocusCellFromPoint = (clientX, clientY) => {
2628
+ const tableRoot = tableElement.getRootNode();
2629
+ if (!isDOMDocumentNode(tableRoot) && !isDOMShadowRoot(tableRoot)) {
2630
+ return null;
2631
+ }
2632
+ for (const el of tableRoot.elementsFromPoint(clientX, clientY)) {
2633
+ const cell = getDOMCellInTableFromTarget(tableElement, el);
2634
+ if (cell) {
2635
+ return cell;
2636
+ }
2637
+ }
2638
+ return null;
2639
+ };
2640
+ const applyFocusCell = (focusCell, override) => {
2641
+ // Fallback: set anchor if still missing (handles race conditions)
2642
+ if (tableObserver.anchorCell === null) {
2643
+ editor.update(() => {
2644
+ tableObserver.$setAnchorCellForSelection(focusCell);
2645
+ });
2646
+ }
2647
+ if (tableObserver.focusCell === null || focusCell.elem !== tableObserver.focusCell.elem) {
2648
+ tableObservers.setNextFocus({
2649
+ focusCell,
2650
+ override,
2651
+ tableKey: tableObserver.tableNodeKey
2652
+ });
2653
+ editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
2654
+ }
2655
+ };
2656
+
2657
+ // Walk up from the table to the nearest ancestor that can actually scroll
2658
+ // on the requested axis (the scrollable-tables wrapper for 'x'). Returns
2659
+ // null when none is found, in which case the caller may fall back to the
2660
+ // window.
2661
+ const findScrollContainer = axis => {
2662
+ for (let el = tableElement.parentElement; el; el = el.parentElement) {
2663
+ const canScroll = axis === 'x' ? el.scrollWidth > el.clientWidth : el.scrollHeight > el.clientHeight;
2664
+ if (canScroll) {
2665
+ const style = editorWindow.getComputedStyle(el);
2666
+ const overflow = axis === 'x' ? style.overflowX : style.overflowY;
2667
+ if (overflow === 'auto' || overflow === 'scroll') {
2668
+ return el;
2669
+ }
2670
+ }
2671
+ }
2672
+ return null;
2673
+ };
2674
+
2675
+ // Scroll `container` (or the window when null) on `axis` if the pointer is
2676
+ // within the edge zone. Returns whether it actually scrolled.
2677
+ const scrollAxis = (container, pos, axis) => {
2678
+ let start;
2679
+ let end;
2680
+ if (container === null) {
2681
+ start = 0;
2682
+ end = axis === 'x' ? editorWindow.innerWidth : editorWindow.innerHeight;
2683
+ } else {
2684
+ const rect = container.getBoundingClientRect();
2685
+ start = axis === 'x' ? rect.left : rect.top;
2686
+ end = axis === 'x' ? rect.right : rect.bottom;
2687
+ }
2688
+ const step = autoScrollStep(pos, start, end);
2689
+ if (step === 0) {
2690
+ return false;
2691
+ }
2692
+ if (container === null) {
2693
+ const before = axis === 'x' ? editorWindow.scrollX : editorWindow.scrollY;
2694
+ editorWindow.scrollBy(axis === 'x' ? step : 0, axis === 'x' ? 0 : step);
2695
+ return (axis === 'x' ? editorWindow.scrollX : editorWindow.scrollY) !== before;
2696
+ }
2697
+ if (axis === 'x') {
2698
+ const before = container.scrollLeft;
2699
+ container.scrollLeft += step;
2700
+ return container.scrollLeft !== before;
2701
+ }
2702
+ const before = container.scrollTop;
2703
+ container.scrollTop += step;
2704
+ return container.scrollTop !== before;
2705
+ };
2706
+
2707
+ // Clamp the last pointer position into the visible bounds of the scroll
2708
+ // container(s) so a pointer dragged past an edge still hit-tests onto the
2709
+ // newly-revealed cell instead of empty space beyond the table.
2710
+ const clampHitPoint = (hContainer, vContainer) => {
2711
+ let x = lastClientX;
2712
+ let y = lastClientY;
2713
+ if (hContainer === null) {
2714
+ x = Math.min(Math.max(x, 1), editorWindow.innerWidth - 1);
2715
+ } else {
2716
+ const rect = hContainer.getBoundingClientRect();
2717
+ x = Math.min(Math.max(x, rect.left + 1), rect.right - 1);
2718
+ }
2719
+ if (vContainer === null) {
2720
+ y = Math.min(Math.max(y, 1), editorWindow.innerHeight - 1);
2721
+ } else {
2722
+ const rect = vContainer.getBoundingClientRect();
2723
+ y = Math.min(Math.max(y, rect.top + 1), rect.bottom - 1);
2724
+ }
2725
+ return [x, y];
2726
+ };
2727
+ const isNearScrollEdge = () => {
2728
+ const hContainer = findScrollContainer('x');
2729
+ if (hContainer !== null) {
2730
+ const rect = hContainer.getBoundingClientRect();
2731
+ if (autoScrollStep(lastClientX, rect.left, rect.right) !== 0) {
2732
+ return true;
2733
+ }
2734
+ }
2735
+ const vContainer = findScrollContainer('y');
2736
+ const vStart = vContainer === null ? 0 : vContainer.getBoundingClientRect().top;
2737
+ const vEnd = vContainer === null ? editorWindow.innerHeight : vContainer.getBoundingClientRect().bottom;
2738
+ return autoScrollStep(lastClientY, vStart, vEnd) !== 0;
2739
+ };
2740
+ const tickAutoScroll = () => {
2741
+ autoScrollRafId = null;
2742
+ if (!tableObserver.isSelecting) {
2743
+ return;
2744
+ }
2745
+ const hContainer = findScrollContainer('x');
2746
+ const vContainer = findScrollContainer('y');
2747
+ // Only the table's own wrapper scrolls horizontally; pages don't
2748
+ // auto-scroll sideways. Vertically we fall back to the window.
2749
+ const scrolledX = hContainer !== null && scrollAxis(hContainer, lastClientX, 'x');
2750
+ const scrolledY = scrollAxis(vContainer, lastClientY, 'y');
2751
+ if (scrolledX || scrolledY) {
2752
+ const [hitX, hitY] = clampHitPoint(hContainer, vContainer);
2753
+ const focusCell = resolveFocusCellFromPoint(hitX, hitY);
2754
+ if (focusCell) {
2755
+ applyFocusCell(focusCell, false);
2756
+ }
2757
+ autoScrollRafId = editorWindow.requestAnimationFrame(tickAutoScroll);
2758
+ }
2759
+ };
2760
+ const maybeStartAutoScroll = () => {
2761
+ // Touch taps don't initiate table selection, so they shouldn't scroll.
2762
+ if (autoScrollRafId !== null || tableObserver.pointerType === 'touch' || !isNearScrollEdge()) {
2763
+ return;
2764
+ }
2765
+ autoScrollRafId = editorWindow.requestAnimationFrame(tickAutoScroll);
2766
+ };
2767
+ const onPointerUp = () => {
2768
+ stopSelecting();
2769
+ };
2594
2770
  const onPointerMove = moveEvent => {
2595
2771
  if (!isPointerDownOnEvent(moveEvent) && tableObserver.isSelecting) {
2596
- tableObserver.isSelecting = false;
2597
- editorWindow.removeEventListener('pointerup', onPointerUp);
2598
- editorWindow.removeEventListener('pointermove', onPointerMove);
2772
+ stopSelecting();
2599
2773
  return;
2600
2774
  }
2601
2775
  const moveTarget = getComposedEventTarget(moveEvent);
2602
2776
  if (!isDOMNode(moveTarget)) {
2603
2777
  return;
2604
2778
  }
2779
+ lastClientX = moveEvent.clientX;
2780
+ lastClientY = moveEvent.clientY;
2605
2781
  let focusCell = null;
2606
2782
  // In firefox the moveEvent.target may be captured so we must always
2607
2783
  // consult the coordinates #7245
@@ -2609,37 +2785,14 @@ function $handleTableClick(editor, event, selectedDOMCell, tableElement, tableOb
2609
2785
  if (override) {
2610
2786
  focusCell = getDOMCellInTableFromTarget(tableElement, moveTarget);
2611
2787
  } else {
2612
- // Resolve via the table's own root so elementsFromPoint isn't
2613
- // retargeted; narrow with the type guards rather than casting so the
2614
- // detached-table case (Node) falls through to no hit-test.
2615
- const tableRoot = tableElement.getRootNode();
2616
- if (!isDOMDocumentNode(tableRoot) && !isDOMShadowRoot(tableRoot)) {
2617
- return;
2618
- }
2619
- for (const el of tableRoot.elementsFromPoint(moveEvent.clientX, moveEvent.clientY)) {
2620
- focusCell = getDOMCellInTableFromTarget(tableElement, el);
2621
- if (focusCell) {
2622
- break;
2623
- }
2624
- }
2788
+ focusCell = resolveFocusCellFromPoint(moveEvent.clientX, moveEvent.clientY);
2625
2789
  }
2626
2790
  if (focusCell) {
2627
- const anchorCell = focusCell;
2628
- // Fallback: set anchor if still missing (handles race conditions)
2629
- if (tableObserver.anchorCell === null) {
2630
- editor.update(() => {
2631
- tableObserver.$setAnchorCellForSelection(anchorCell);
2632
- });
2633
- }
2634
- if (tableObserver.focusCell === null || focusCell.elem !== tableObserver.focusCell.elem) {
2635
- tableObservers.setNextFocus({
2636
- focusCell,
2637
- override,
2638
- tableKey: tableObserver.tableNodeKey
2639
- });
2640
- editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
2641
- }
2791
+ applyFocusCell(focusCell, override);
2642
2792
  }
2793
+ // Keep the selection reachable when dragging toward/past an edge of a
2794
+ // scrollable table (#7153).
2795
+ maybeStartAutoScroll();
2643
2796
  };
2644
2797
  editorWindow.addEventListener('pointerup', onPointerUp, tableObserver.listenerOptions);
2645
2798
  editorWindow.addEventListener('pointermove', onPointerMove, tableObserver.listenerOptions);