Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(cdk-experimental/popover-edit): use MutationObserver to detect table rows #30713

Merged
merged 1 commit into from
Mar 28, 2025
Merged
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
54 changes: 43 additions & 11 deletions src/cdk-experimental/popover-edit/table-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ import {FocusTrap} from '@angular/cdk/a11y';
import {OverlayRef, OverlaySizeConfig, PositionStrategy} from '@angular/cdk/overlay';
import {TemplatePortal} from '@angular/cdk/portal';
import {
afterRender,
afterNextRender,
AfterViewInit,
Directive,
ElementRef,
EmbeddedViewRef,
inject,
ListenerOptions,
NgZone,
OnDestroy,
Renderer2,
TemplateRef,
ViewContainerRef,
inject,
Renderer2,
ListenerOptions,
} from '@angular/core';
import {merge, Observable, Subject} from 'rxjs';
import {
Expand All @@ -35,8 +35,10 @@ import {
withLatestFrom,
} from 'rxjs/operators';

import {_bindEventWithOptions} from '@angular/cdk/platform';
import {CELL_SELECTOR, EDIT_PANE_CLASS, EDIT_PANE_SELECTOR, ROW_SELECTOR} from './constants';
import {EditEventDispatcher, HoverContentState} from './edit-event-dispatcher';
import {EditRef} from './edit-ref';
import {EditServices} from './edit-services';
import {FocusDispatcher} from './focus-dispatcher';
import {
Expand All @@ -45,8 +47,6 @@ import {
FocusEscapeNotifierFactory,
} from './focus-escape-notifier';
import {closest} from './polyfill';
import {EditRef} from './edit-ref';
import {_bindEventWithOptions} from '@angular/cdk/platform';

/**
* Describes the number of columns before and after the originating cell that the
Expand All @@ -61,6 +61,23 @@ export interface CdkPopoverEditColspan {
/** Used for rate-limiting mousemove events. */
const MOUSE_MOVE_THROTTLE_TIME_MS = 10;

function hasRowElement(nl: NodeList) {
for (let i = 0; i < nl.length; i++) {
const el = nl[i];
if (!(el instanceof HTMLElement)) {
continue;
}
if (el.matches(ROW_SELECTOR)) {
return true;
}
}
return false;
}

function isRowMutation(mutation: MutationRecord): boolean {
return hasRowElement(mutation.addedNodes) || hasRowElement(mutation.removedNodes);
}

/**
* A directive that must be attached to enable editability on a table.
* It is responsible for setting up delegated event handlers and providing the
Expand All @@ -80,11 +97,25 @@ export class CdkEditable implements AfterViewInit, OnDestroy {

protected readonly destroyed = new Subject<void>();

private _rendered = new Subject();
private _rowsRendered = new Subject();

private _rowMutationObserver = globalThis.MutationObserver
? new globalThis.MutationObserver(mutations => {
if (mutations.some(isRowMutation)) {
this._rowsRendered.next();
}
})
: null;

constructor() {
afterRender(() => {
this._rendered.next();
// TODO: consider a design where instead of polling for row changes we just use
// afterRenderEffect + a signal of the rows.
afterNextRender(() => {
this._rowsRendered.next();
this._rowMutationObserver?.observe(this.elementRef.nativeElement, {
childList: true,
subtree: true,
});
});
}

Expand All @@ -95,7 +126,7 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
ngOnDestroy(): void {
this.destroyed.next();
this.destroyed.complete();
this._rendered.complete();
this._rowMutationObserver?.disconnect();
}

private _observableFromEvent<T extends Event>(
Expand Down Expand Up @@ -153,9 +184,10 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
// Keep track of rows within the table. This is used to know which rows with hover content
// are first or last in the table. They are kept focusable in case focus enters from above
// or below the table.
this._rendered
this._rowsRendered
.pipe(
// Avoid some timing inconsistencies since Angular v19.
// TODO: see if we can remove this now that we're using MutationObserver.
debounceTime(0),
// Optimization: ignore dom changes while focus is within the table as we already
// ensure that rows above and below the focused/active row are tabbable.
Expand Down
Loading