Skip to content

Commit 13af74b

Browse files
committed
WIP: attempt to use MutationObserver to observe table rows
1 parent d4cccb7 commit 13af74b

File tree

1 file changed

+51
-38
lines changed

1 file changed

+51
-38
lines changed

src/cdk-experimental/popover-edit/table-directives.ts

Lines changed: 51 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,26 @@ import {FocusTrap} from '@angular/cdk/a11y';
99
import {OverlayRef, OverlaySizeConfig, PositionStrategy} from '@angular/cdk/overlay';
1010
import {TemplatePortal} from '@angular/cdk/portal';
1111
import {
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';
2524
import {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';
3829
import {CELL_SELECTOR, EDIT_PANE_CLASS, EDIT_PANE_SELECTOR, ROW_SELECTOR} from './constants';
3930
import {EditEventDispatcher, HoverContentState} from './edit-event-dispatcher';
31+
import {EditRef} from './edit-ref';
4032
import {EditServices} from './edit-services';
4133
import {FocusDispatcher} from './focus-dispatcher';
4234
import {
@@ -45,8 +37,6 @@ import {
4537
FocusEscapeNotifierFactory,
4638
} from './focus-escape-notifier';
4739
import {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

Comments
 (0)