Skip to content

Commit 9a303b9

Browse files
authored
Merge branch 'angular:main' into main
2 parents a873e3c + 6b3a371 commit 9a303b9

File tree

36 files changed

+232
-151
lines changed

36 files changed

+232
-151
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
"@angular-devkit/build-angular": "^19.0.0-next.10",
7474
"@angular-devkit/core": "^19.0.0-next.10",
7575
"@angular-devkit/schematics": "^19.0.0-next.10",
76-
"@angular/bazel": "https://github.com/angular/bazel-builds.git#9e6140d1eef8ddf7113d00738f603e9cc3c310f1",
76+
"@angular/bazel": "https://github.com/angular/bazel-builds.git#d9a8ea4f9e62cb475eff89519426a38631b2704d",
7777
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#74e0e7b090c6e16056290836b2d936ca7820b86f",
7878
"@angular/build": "^19.0.0-next.10",
7979
"@angular/cli": "^19.0.0-next.10",

src/bazel-tsconfig-build.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
"importHelpers": true,
2222
"strictBindCallApply": true,
2323
"esModuleInterop": true,
24-
// Keep the below in sync with https://github.com/angular/angular/blob/f9b821f07d8dba57a6a7e5fc127dc096247424aa/packages/bazel/src/ng_module/ng_module.bzl#L214
25-
"useDefineForClassFields": false,
2624
"newLine": "lf",
2725
// Bazel either uses "umd" or "esnext". We replicate this here for IDE support.
2826
// https://github.com/bazelbuild/rules_typescript/blob/master/internal/common/tsconfig.bzl#L199

src/cdk-experimental/popover-edit/popover-edit.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ describe('CDK Popover Edit', () => {
403403
});
404404
fixture = TestBed.createComponent<BaseTestComponent>(componentClass);
405405
component = fixture.componentInstance;
406+
component.renderData();
406407
fixture.detectChanges();
407408
tick(10);
408409
fixture.detectChanges();

src/cdk-experimental/selection/selection-module.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
*/
88

99
import {CdkTableModule} from '@angular/cdk/table';
10-
import {CommonModule} from '@angular/common';
1110
import {NgModule} from '@angular/core';
1211

1312
import {CdkRowSelection} from './row-selection';
@@ -18,7 +17,6 @@ import {CdkSelectionToggle} from './selection-toggle';
1817

1918
@NgModule({
2019
imports: [
21-
CommonModule,
2220
CdkTableModule,
2321
CdkSelection,
2422
CdkSelectionToggle,

src/cdk/table/sticky-styler.ts

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ import {StickyPositioningListener} from './sticky-position-listener';
1616

1717
export type StickyDirection = 'top' | 'bottom' | 'left' | 'right';
1818

19+
interface UpdateStickyColumnsParams {
20+
rows: HTMLElement[];
21+
stickyStartStates: boolean[];
22+
stickyEndStates: boolean[];
23+
}
24+
1925
/**
2026
* List of all possible directions that can be used for sticky positioning.
2127
* @docs-private
@@ -27,6 +33,12 @@ export const STICKY_DIRECTIONS: StickyDirection[] = ['top', 'bottom', 'left', 'r
2733
* @docs-private
2834
*/
2935
export class StickyStyler {
36+
private _elemSizeCache = new WeakMap<HTMLElement, {width: number; height: number}>();
37+
private _resizeObserver = globalThis?.ResizeObserver
38+
? new globalThis.ResizeObserver(entries => this._updateCachedSizes(entries))
39+
: null;
40+
private _updatedStickyColumnsParamsToReplay: UpdateStickyColumnsParams[] = [];
41+
private _stickyColumnsReplayTimeout: number | null = null;
3042
private _cachedCellWidths: number[] = [];
3143
private readonly _borderCellCss: Readonly<{[d in StickyDirection]: string}>;
3244

@@ -68,6 +80,10 @@ export class StickyStyler {
6880
* @param stickyDirections The directions that should no longer be set as sticky on the rows.
6981
*/
7082
clearStickyPositioning(rows: HTMLElement[], stickyDirections: StickyDirection[]) {
83+
if (stickyDirections.includes('left') || stickyDirections.includes('right')) {
84+
this._removeFromStickyColumnReplayQueue(rows);
85+
}
86+
7187
const elementsToClear: HTMLElement[] = [];
7288
for (const row of rows) {
7389
// If the row isn't an element (e.g. if it's an `ng-container`),
@@ -100,13 +116,23 @@ export class StickyStyler {
100116
* in this index position should be stuck to the end of the row.
101117
* @param recalculateCellWidths Whether the sticky styler should recalculate the width of each
102118
* column cell. If `false` cached widths will be used instead.
119+
* @param replay Whether to enqueue this call for replay after a ResizeObserver update.
103120
*/
104121
updateStickyColumns(
105122
rows: HTMLElement[],
106123
stickyStartStates: boolean[],
107124
stickyEndStates: boolean[],
108125
recalculateCellWidths = true,
126+
replay = true,
109127
) {
128+
if (replay) {
129+
this._updateStickyColumnReplayQueue({
130+
rows: [...rows],
131+
stickyStartStates: [...stickyStartStates],
132+
stickyEndStates: [...stickyEndStates],
133+
});
134+
}
135+
110136
if (
111137
!rows.length ||
112138
!this._isBrowser ||
@@ -213,7 +239,7 @@ export class StickyStyler {
213239
? (Array.from(row.children) as HTMLElement[])
214240
: [row];
215241

216-
const height = row.getBoundingClientRect().height;
242+
const height = this._retrieveElementSize(row).height;
217243
stickyOffset += height;
218244
stickyCellHeights[rowIndex] = height;
219245
}
@@ -366,8 +392,8 @@ export class StickyStyler {
366392
const cellWidths: number[] = [];
367393
const firstRowCells = row.children;
368394
for (let i = 0; i < firstRowCells.length; i++) {
369-
let cell: HTMLElement = firstRowCells[i] as HTMLElement;
370-
cellWidths.push(cell.getBoundingClientRect().width);
395+
const cell = firstRowCells[i] as HTMLElement;
396+
cellWidths.push(this._retrieveElementSize(cell).width);
371397
}
372398

373399
this._cachedCellWidths = cellWidths;
@@ -411,4 +437,103 @@ export class StickyStyler {
411437

412438
return positions;
413439
}
440+
441+
/**
442+
* Retreives the most recently observed size of the specified element from the cache, or
443+
* meaures it directly if not yet cached.
444+
*/
445+
private _retrieveElementSize(element: HTMLElement): {width: number; height: number} {
446+
const cachedSize = this._elemSizeCache.get(element);
447+
if (cachedSize) {
448+
return cachedSize;
449+
}
450+
451+
const clientRect = element.getBoundingClientRect();
452+
const size = {width: clientRect.width, height: clientRect.height};
453+
454+
if (!this._resizeObserver) {
455+
return size;
456+
}
457+
458+
this._elemSizeCache.set(element, size);
459+
this._resizeObserver.observe(element, {box: 'border-box'});
460+
return size;
461+
}
462+
463+
/**
464+
* Conditionally enqueue the requested sticky update and clear previously queued updates
465+
* for the same rows.
466+
*/
467+
private _updateStickyColumnReplayQueue(params: UpdateStickyColumnsParams) {
468+
this._removeFromStickyColumnReplayQueue(params.rows);
469+
470+
// No need to replay if a flush is pending.
471+
if (this._stickyColumnsReplayTimeout) {
472+
return;
473+
}
474+
475+
this._updatedStickyColumnsParamsToReplay.push(params);
476+
}
477+
478+
/** Remove updates for the specified rows from the queue. */
479+
private _removeFromStickyColumnReplayQueue(rows: HTMLElement[]) {
480+
const rowsSet = new Set(rows);
481+
for (const update of this._updatedStickyColumnsParamsToReplay) {
482+
update.rows = update.rows.filter(row => !rowsSet.has(row));
483+
}
484+
this._updatedStickyColumnsParamsToReplay = this._updatedStickyColumnsParamsToReplay.filter(
485+
update => !!update.rows.length,
486+
);
487+
}
488+
489+
/** Update _elemSizeCache with the observed sizes. */
490+
private _updateCachedSizes(entries: ResizeObserverEntry[]) {
491+
let needsColumnUpdate = false;
492+
for (const entry of entries) {
493+
const newEntry = entry.borderBoxSize?.length
494+
? {
495+
width: entry.borderBoxSize[0].inlineSize,
496+
height: entry.borderBoxSize[0].blockSize,
497+
}
498+
: {
499+
width: entry.contentRect.width,
500+
height: entry.contentRect.height,
501+
};
502+
503+
if (
504+
newEntry.width !== this._elemSizeCache.get(entry.target as HTMLElement)?.width &&
505+
isCell(entry.target)
506+
) {
507+
needsColumnUpdate = true;
508+
}
509+
510+
this._elemSizeCache.set(entry.target as HTMLElement, newEntry);
511+
}
512+
513+
if (needsColumnUpdate && this._updatedStickyColumnsParamsToReplay.length) {
514+
if (this._stickyColumnsReplayTimeout) {
515+
clearTimeout(this._stickyColumnsReplayTimeout);
516+
}
517+
518+
this._stickyColumnsReplayTimeout = setTimeout(() => {
519+
for (const update of this._updatedStickyColumnsParamsToReplay) {
520+
this.updateStickyColumns(
521+
update.rows,
522+
update.stickyStartStates,
523+
update.stickyEndStates,
524+
true,
525+
false,
526+
);
527+
}
528+
this._updatedStickyColumnsParamsToReplay = [];
529+
this._stickyColumnsReplayTimeout = null;
530+
}, 0);
531+
}
532+
}
533+
}
534+
535+
function isCell(element: Element) {
536+
return ['cdk-cell', 'cdk-header-cell', 'cdk-footer-cell'].some(klass =>
537+
element.classList.contains(klass),
538+
);
414539
}

src/material-experimental/selection/selection-module.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
// TODO(yifange): Move the table-specific code to a separate module from the other selection
1010
// behaviors once we move it out of experimental.
11-
import {CommonModule} from '@angular/common';
1211
import {NgModule} from '@angular/core';
1312
import {MatTableModule} from '@angular/material/table';
1413
import {MatCheckboxModule} from '@angular/material/checkbox';
@@ -20,7 +19,6 @@ import {MatRowSelection} from './row-selection';
2019

2120
@NgModule({
2221
imports: [
23-
CommonModule,
2422
MatTableModule,
2523
MatCheckboxModule,
2624
MatSelectAll,

src/material/autocomplete/module.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import {NgModule} from '@angular/core';
1010
import {MatCommonModule, MatOptionModule} from '@angular/material/core';
11-
import {CommonModule} from '@angular/common';
1211
import {CdkScrollableModule} from '@angular/cdk/scrolling';
1312
import {OverlayModule} from '@angular/cdk/overlay';
1413
import {MatAutocomplete} from './autocomplete';
@@ -23,7 +22,6 @@ import {MatAutocompleteOrigin} from './autocomplete-origin';
2322
OverlayModule,
2423
MatOptionModule,
2524
MatCommonModule,
26-
CommonModule,
2725
MatAutocomplete,
2826
MatAutocompleteTrigger,
2927
MatAutocompleteOrigin,

src/material/button/button-base.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
215215

216216
/** Shared host configuration for buttons using the `<a>` tag. */
217217
export const MAT_ANCHOR_HOST = {
218+
// Note that this is basically a noop on anchors,
219+
// but it appears that some internal apps depend on it.
218220
'[attr.disabled]': '_getDisabledAttribute()',
219221
'[class.mat-mdc-button-disabled]': 'disabled',
220222
'[class.mat-mdc-button-disabled-interactive]': 'disabledInteractive',
@@ -224,7 +226,7 @@ export const MAT_ANCHOR_HOST = {
224226
// consistency with the `mat-button` applied on native buttons where even
225227
// though they have an index, they're not tabbable.
226228
'[attr.tabindex]': 'disabled && !disabledInteractive ? -1 : tabIndex',
227-
'[attr.aria-disabled]': '_getDisabledAttribute()',
229+
'[attr.aria-disabled]': '_getAriaDisabled()',
228230
// MDC automatically applies the primary theme color to the button, but we want to support
229231
// an unthemed version. If color is undefined, apply a CSS class that makes it easy to
230232
// select and style this "theme".
@@ -267,6 +269,9 @@ export class MatAnchorBase extends MatButtonBase implements OnInit, OnDestroy {
267269
};
268270

269271
protected override _getAriaDisabled() {
270-
return this.ariaDisabled == null ? this.disabled : this.ariaDisabled;
272+
if (this.ariaDisabled != null) {
273+
return this.ariaDisabled;
274+
}
275+
return this.disabled || null;
271276
}
272277
}

src/material/button/button.spec.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,13 +316,15 @@ describe('MatButton', () => {
316316
describe('interactive disabled buttons', () => {
317317
let fixture: ComponentFixture<TestApp>;
318318
let button: HTMLButtonElement;
319+
let anchor: HTMLAnchorElement;
319320

320321
beforeEach(() => {
321322
fixture = TestBed.createComponent(TestApp);
322323
fixture.componentInstance.isDisabled = true;
323324
fixture.changeDetectorRef.markForCheck();
324325
fixture.detectChanges();
325-
button = fixture.debugElement.query(By.css('button'))!.nativeElement;
326+
button = fixture.nativeElement.querySelector('button');
327+
anchor = fixture.nativeElement.querySelector('a');
326328
});
327329

328330
it('should set a class when allowing disabled interactivity', () => {
@@ -354,6 +356,29 @@ describe('MatButton', () => {
354356

355357
expect(button.hasAttribute('disabled')).toBe(false);
356358
});
359+
360+
it('should set aria-disabled on anchor when disabledInteractive is enabled', () => {
361+
fixture.componentInstance.isDisabled = false;
362+
fixture.changeDetectorRef.markForCheck();
363+
fixture.detectChanges();
364+
expect(anchor.hasAttribute('aria-disabled')).toBe(false);
365+
expect(anchor.hasAttribute('disabled')).toBe(false);
366+
expect(anchor.classList).not.toContain('mat-mdc-button-disabled-interactive');
367+
368+
fixture.componentInstance.isDisabled = true;
369+
fixture.changeDetectorRef.markForCheck();
370+
fixture.detectChanges();
371+
expect(anchor.getAttribute('aria-disabled')).toBe('true');
372+
expect(anchor.hasAttribute('disabled')).toBe(true);
373+
expect(anchor.classList).not.toContain('mat-mdc-button-disabled-interactive');
374+
375+
fixture.componentInstance.disabledInteractive = true;
376+
fixture.changeDetectorRef.markForCheck();
377+
fixture.detectChanges();
378+
expect(anchor.getAttribute('aria-disabled')).toBe('true');
379+
expect(anchor.hasAttribute('disabled')).toBe(false);
380+
expect(anchor.classList).toContain('mat-mdc-button-disabled-interactive');
381+
});
357382
});
358383
});
359384

src/material/card/module.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {CommonModule} from '@angular/common';
109
import {NgModule} from '@angular/core';
1110
import {MatCommonModule} from '@angular/material/core';
1211
import {
@@ -44,7 +43,7 @@ const CARD_DIRECTIVES = [
4443
];
4544

4645
@NgModule({
47-
imports: [MatCommonModule, CommonModule, ...CARD_DIRECTIVES],
46+
imports: [MatCommonModule, ...CARD_DIRECTIVES],
4847
exports: [CARD_DIRECTIVES, MatCommonModule],
4948
})
5049
export class MatCardModule {}

0 commit comments

Comments
 (0)