@@ -9,34 +9,26 @@ import {FocusTrap} from '@angular/cdk/a11y';
99import { OverlayRef , OverlaySizeConfig , PositionStrategy } from '@angular/cdk/overlay' ;
1010import { TemplatePortal } from '@angular/cdk/portal' ;
1111import {
12- afterRender ,
1312 AfterViewInit ,
1413 Directive ,
1514 ElementRef ,
1615 EmbeddedViewRef ,
16+ inject ,
17+ ListenerOptions ,
1718 NgZone ,
1819 OnDestroy ,
20+ Renderer2 ,
1921 TemplateRef ,
2022 ViewContainerRef ,
21- inject ,
22- Renderer2 ,
23- ListenerOptions ,
2423} from '@angular/core' ;
2524import { merge , Observable , Subject } from 'rxjs' ;
26- import {
27- debounceTime ,
28- filter ,
29- map ,
30- mapTo ,
31- share ,
32- startWith ,
33- takeUntil ,
34- throttleTime ,
35- withLatestFrom ,
36- } from 'rxjs/operators' ;
25+ import { filter , map , mapTo , share , startWith , takeUntil , throttleTime } from 'rxjs/operators' ;
3726
27+ import { _bindEventWithOptions } from '@angular/cdk/platform' ;
28+ import { toSignal } from '@angular/core/rxjs-interop' ;
3829import { CELL_SELECTOR , EDIT_PANE_CLASS , EDIT_PANE_SELECTOR , ROW_SELECTOR } from './constants' ;
3930import { EditEventDispatcher , HoverContentState } from './edit-event-dispatcher' ;
31+ import { EditRef } from './edit-ref' ;
4032import { EditServices } from './edit-services' ;
4133import { FocusDispatcher } from './focus-dispatcher' ;
4234import {
@@ -45,8 +37,6 @@ import {
4537 FocusEscapeNotifierFactory ,
4638} from './focus-escape-notifier' ;
4739import { closest } from './polyfill' ;
48- import { EditRef } from './edit-ref' ;
49- import { _bindEventWithOptions } from '@angular/cdk/platform' ;
5040
5141/**
5242 * Describes the number of columns before and after the originating cell that the
@@ -80,11 +70,29 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
8070
8171 protected readonly destroyed = new Subject < void > ( ) ;
8272
83- private _rendered = new Subject ( ) ;
73+ private _editingOrFocusedSignal = toSignal ( this . editEventDispatcher . editingOrFocused ) ;
74+
75+ private _rowMutationObserver = globalThis . MutationObserver
76+ ? new globalThis . MutationObserver ( mutations => {
77+ if ( mutations . some ( m => this . _isRowMutation ( m ) ) ) {
78+ // Optimization: ignore dom changes while focus is within the table as we already
79+ // ensure that rows above and below the focused/active row are tabbable.
80+ if ( this . _editingOrFocusedSignal ( ) == null ) {
81+ this . editEventDispatcher . allRows . next (
82+ this . elementRef . nativeElement . querySelectorAll ( ROW_SELECTOR ) ,
83+ ) ;
84+ }
85+ }
86+ } )
87+ : null ;
8488
8589 constructor ( ) {
86- afterRender ( ( ) => {
87- this . _rendered . next ( ) ;
90+ // Keep track of rows within the table. This is used to know which rows with hover content
91+ // are first or last in the table. They are kept focusable in case focus enters from above
92+ // or below the table.
93+ this . _rowMutationObserver ?. observe ( this . elementRef . nativeElement , {
94+ childList : true ,
95+ subtree : true ,
8896 } ) ;
8997 }
9098
@@ -95,7 +103,29 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
95103 ngOnDestroy ( ) : void {
96104 this . destroyed . next ( ) ;
97105 this . destroyed . complete ( ) ;
98- this . _rendered . complete ( ) ;
106+ this . _rowMutationObserver ?. disconnect ( ) ;
107+ }
108+
109+ private _isRowMutation ( mutation : MutationRecord ) : boolean {
110+ for ( let i = 0 ; i < mutation . addedNodes . length ; i ++ ) {
111+ const el = mutation . addedNodes [ i ] ;
112+ if ( ! ( el instanceof HTMLElement ) ) {
113+ continue ;
114+ }
115+ if ( el . matches ( ROW_SELECTOR ) ) {
116+ return true ;
117+ }
118+ }
119+ for ( let i = 0 ; i < mutation . removedNodes . length ; i ++ ) {
120+ const el = mutation . removedNodes [ i ] ;
121+ if ( ! ( el instanceof HTMLElement ) ) {
122+ continue ;
123+ }
124+ if ( el . matches ( ROW_SELECTOR ) ) {
125+ return true ;
126+ }
127+ }
128+ return false ;
99129 }
100130
101131 private _observableFromEvent < T extends Event > (
@@ -150,23 +180,6 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
150180 . pipe ( mapTo ( null ) , share ( ) , takeUntil ( this . destroyed ) )
151181 . subscribe ( this . editEventDispatcher . focused ) ;
152182
153- // Keep track of rows within the table. This is used to know which rows with hover content
154- // are first or last in the table. They are kept focusable in case focus enters from above
155- // or below the table.
156- this . _rendered
157- . pipe (
158- // Avoid some timing inconsistencies since Angular v19.
159- debounceTime ( 0 ) ,
160- // Optimization: ignore dom changes while focus is within the table as we already
161- // ensure that rows above and below the focused/active row are tabbable.
162- withLatestFrom ( this . editEventDispatcher . editingOrFocused ) ,
163- filter ( ( [ _ , activeRow ] ) => activeRow == null ) ,
164- map ( ( ) => element . querySelectorAll ( ROW_SELECTOR ) ) ,
165- share ( ) ,
166- takeUntil ( this . destroyed ) ,
167- )
168- . subscribe ( this . editEventDispatcher . allRows ) ;
169-
170183 this . _observableFromEvent < KeyboardEvent > ( element , 'keydown' )
171184 . pipe (
172185 filter ( event => event . key === 'Enter' ) ,
0 commit comments