Skip to content
Draft
Show file tree
Hide file tree
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
8 changes: 4 additions & 4 deletions src/material/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ChangeDetectorRef,
Component,
ContentChildren,
DestroyRef,
ElementRef,
EventEmitter,
InjectionToken,
Expand All @@ -35,7 +36,7 @@ import {
} from '../core';
import {_IdGenerator, ActiveDescendantKeyManager} from '@angular/cdk/a11y';
import {Platform} from '@angular/cdk/platform';
import {Subscription} from 'rxjs';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';

/** Event object that is emitted when an autocomplete option is selected. */
export class MatAutocompleteSelectedEvent {
Expand Down Expand Up @@ -116,7 +117,7 @@ export class MatAutocomplete implements AfterContentInit, OnDestroy {
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
protected _defaults = inject<MatAutocompleteDefaultOptions>(MAT_AUTOCOMPLETE_DEFAULT_OPTIONS);
protected _animationsDisabled = _animationsDisabled();
private _activeOptionChanges = Subscription.EMPTY;
private readonly _destroyRef = inject(DestroyRef);

/** Manages active item in option list based on key events. */
_keyManager: ActiveDescendantKeyManager<MatOption>;
Expand Down Expand Up @@ -266,7 +267,7 @@ export class MatAutocomplete implements AfterContentInit, OnDestroy {
this._keyManager = new ActiveDescendantKeyManager<MatOption>(this.options)
.withWrap()
.skipPredicate(this._skipPredicate);
this._activeOptionChanges = this._keyManager.change.subscribe(index => {
this._keyManager.change.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(index => {
if (this.isOpen) {
this.optionActivated.emit({source: this, option: this.options.toArray()[index] || null});
}
Expand All @@ -278,7 +279,6 @@ export class MatAutocomplete implements AfterContentInit, OnDestroy {

ngOnDestroy() {
this._keyManager?.destroy();
this._activeOptionChanges.unsubscribe();
}

/**
Expand Down
7 changes: 3 additions & 4 deletions src/material/bottom-sheet/bottom-sheet-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import {
ViewEncapsulation,
inject,
} from '@angular/core';
import {Subscription} from 'rxjs';
import {CdkPortalOutlet} from '@angular/cdk/portal';
import {_animationsDisabled} from '../core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';

const ENTER_ANIMATION = '_mat-bottom-sheet-enter';
const EXIT_ANIMATION = '_mat-bottom-sheet-exit';
Expand Down Expand Up @@ -53,7 +53,6 @@ const EXIT_ANIMATION = '_mat-bottom-sheet-exit';
imports: [CdkPortalOutlet],
})
export class MatBottomSheetContainer extends CdkDialogContainer implements OnDestroy {
private _breakpointSubscription: Subscription;
protected _animationsDisabled = _animationsDisabled();

/** The state of the bottom sheet animations. */
Expand All @@ -75,8 +74,9 @@ export class MatBottomSheetContainer extends CdkDialogContainer implements OnDes

const breakpointObserver = inject(BreakpointObserver);

this._breakpointSubscription = breakpointObserver
breakpointObserver
.observe([Breakpoints.Medium, Breakpoints.Large, Breakpoints.XLarge])
.pipe(takeUntilDestroyed())
.subscribe(() => {
const classList = (this._elementRef.nativeElement as HTMLElement).classList;

Expand Down Expand Up @@ -121,7 +121,6 @@ export class MatBottomSheetContainer extends CdkDialogContainer implements OnDes

override ngOnDestroy() {
super.ngOnDestroy();
this._breakpointSubscription.unsubscribe();
this._destroyed = true;
}

Expand Down
6 changes: 3 additions & 3 deletions src/material/chips/chip-grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ import {
import {_ErrorStateTracker, ErrorStateMatcher} from '../core';
import {MatFormFieldControl} from '../form-field';
import {merge, Observable, Subject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
import {MatChipEvent} from './chip';
import {MatChipRow} from './chip-row';
import {MatChipSet} from './chip-set';
import {MatChipTextControl} from './chip-text-control';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';

/** Change event object that is emitted when the chip grid value has changed. */
export class MatChipGridChange {
Expand Down Expand Up @@ -277,13 +277,13 @@ export class MatChipGrid
}

ngAfterContentInit() {
this.chipBlurChanges.pipe(takeUntil(this._destroyed)).subscribe(() => {
this.chipBlurChanges.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(() => {
this._blur();
this.stateChanges.next();
});

merge(this.chipFocusChanges, this._chips.changes)
.pipe(takeUntil(this._destroyed))
.pipe(takeUntilDestroyed(this._destroyRef))
.subscribe(() => this.stateChanges.next());
}

Expand Down
33 changes: 16 additions & 17 deletions src/material/chips/chip-listbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@ import {
forwardRef,
inject,
Input,
OnDestroy,
Output,
QueryList,
ViewEncapsulation,
} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {Observable} from 'rxjs';
import {startWith, takeUntil} from 'rxjs/operators';
import {startWith} from 'rxjs/operators';
import {TAB} from '@angular/cdk/keycodes';
import {MatChip, MatChipEvent} from './chip';
import {MatChipOption, MatChipSelectionChange} from './chip-option';
import {MatChipSet} from './chip-set';
import {MatChipAction} from './chip-action';
import {MAT_CHIPS_DEFAULT_OPTIONS} from './tokens';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';

/** Change event object that is emitted when the chip listbox value has changed. */
export class MatChipListboxChange {
Expand Down Expand Up @@ -82,10 +82,7 @@ export const MAT_CHIP_LISTBOX_CONTROL_VALUE_ACCESSOR: any = {
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MatChipListbox
extends MatChipSet
implements AfterContentInit, OnDestroy, ControlValueAccessor
{
export class MatChipListbox extends MatChipSet implements AfterContentInit, ControlValueAccessor {
/**
* Function when touched. Set as part of ControlValueAccessor implementation.
* @docs-private
Expand Down Expand Up @@ -199,18 +196,20 @@ export class MatChipListbox
override _chips: QueryList<MatChipOption> = undefined!;

ngAfterContentInit() {
this._chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => {
if (this.value !== undefined) {
Promise.resolve().then(() => {
this._setSelectionByValue(this.value, false);
});
}
// Update listbox selectable/multiple properties on chips
this._syncListboxProperties();
});
this._chips.changes
.pipe(startWith(null), takeUntilDestroyed(this._destroyRef))
.subscribe(() => {
if (this.value !== undefined) {
Promise.resolve().then(() => {
this._setSelectionByValue(this.value, false);
});
}
// Update listbox selectable/multiple properties on chips
this._syncListboxProperties();
});

this.chipBlurChanges.pipe(takeUntil(this._destroyed)).subscribe(() => this._blur());
this.chipSelectionChanges.pipe(takeUntil(this._destroyed)).subscribe(event => {
this.chipBlurChanges.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(() => this._blur());
this.chipSelectionChanges.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(event => {
if (!this.multiple) {
this._chips.forEach(chip => {
if (chip !== event.source) {
Expand Down
82 changes: 42 additions & 40 deletions src/material/chips/chip-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ import {
booleanAttribute,
numberAttribute,
inject,
DestroyRef,
} from '@angular/core';
import {Observable, Subject, merge} from 'rxjs';
import {startWith, switchMap, takeUntil} from 'rxjs/operators';
import {Observable, merge} from 'rxjs';
import {startWith, switchMap} from 'rxjs/operators';
import {MatChip, MatChipEvent} from './chip';
import {MatChipAction, MatChipContent} from './chip-action';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';

/**
* Basic container component for the MatChip component.
Expand All @@ -53,16 +55,14 @@ export class MatChipSet implements AfterViewInit, OnDestroy {
protected _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
protected _changeDetectorRef = inject(ChangeDetectorRef);
private _dir = inject(Directionality, {optional: true});
protected readonly _destroyRef = inject(DestroyRef);

/** Index of the last destroyed chip that had focus. */
private _lastDestroyedFocusedChipIndex: number | null = null;

/** Used to manage focus within the chip list. */
protected _keyManager: FocusKeyManager<MatChipAction>;

/** Subject that emits when the component has been destroyed. */
protected _destroyed = new Subject<void>();

/** Role to use if it hasn't been overwritten by the user. */
protected _defaultRole = 'presentation';

Expand Down Expand Up @@ -146,8 +146,6 @@ export class MatChipSet implements AfterViewInit, OnDestroy {
ngOnDestroy() {
this._keyManager?.destroy();
this._chipActions.destroy();
this._destroyed.next();
this._destroyed.complete();
}

/** Checks whether any of the chips is focused. */
Expand Down Expand Up @@ -249,7 +247,7 @@ export class MatChipSet implements AfterViewInit, OnDestroy {

// Keep the manager active index in sync so that navigation picks
// up from the current chip if the user clicks into the list directly.
this.chipFocusChanges.pipe(takeUntil(this._destroyed)).subscribe(({chip}) => {
this.chipFocusChanges.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(({chip}) => {
const action = chip._getSourceAction(document.activeElement as Element);

if (action) {
Expand All @@ -258,7 +256,7 @@ export class MatChipSet implements AfterViewInit, OnDestroy {
});

this._dir?.change
.pipe(takeUntil(this._destroyed))
.pipe(takeUntilDestroyed(this._destroyRef))
.subscribe(direction => this._keyManager.withHorizontalOrientation(direction));
}

Expand All @@ -273,42 +271,46 @@ export class MatChipSet implements AfterViewInit, OnDestroy {

/** Listens to changes in the chip set and syncs up the state of the individual chips. */
private _trackChipSetChanges() {
this._chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => {
if (this.disabled) {
// Since this happens after the content has been
// checked, we need to defer it to the next tick.
Promise.resolve().then(() => this._syncChipsState());
}
this._chips.changes
.pipe(startWith(null), takeUntilDestroyed(this._destroyRef))
.subscribe(() => {
if (this.disabled) {
// Since this happens after the content has been
// checked, we need to defer it to the next tick.
Promise.resolve().then(() => this._syncChipsState());
}

this._redirectDestroyedChipFocus();
});
this._redirectDestroyedChipFocus();
});
}

/** Starts tracking the destroyed chips in order to capture the focused one. */
private _trackDestroyedFocusedChip() {
this.chipDestroyedChanges.pipe(takeUntil(this._destroyed)).subscribe((event: MatChipEvent) => {
// If the focused chip is destroyed, save its index so that we can move focus to the next
// chip. We only save the index here, rather than move the focus immediately, because we want
// to wait until the chip is removed from the chip list before focusing the next one. This
// allows us to keep focus on the same index if the chip gets swapped out.
const chipArray = this._chips.toArray();
const chipIndex = chipArray.indexOf(event.chip);
const hasFocus = event.chip._hasFocus();
const wasLastFocused =
event.chip._hadFocusOnRemove &&
this._keyManager.activeItem &&
event.chip._getActions().includes(this._keyManager.activeItem);

// Note that depending on the timing, the chip might've already lost focus by the
// time we check this. We need the `wasLastFocused` as a fallback to detect such cases.
// In `wasLastFocused` we also need to ensure that the chip actually had focus when it was
// deleted so that we don't steal away the user's focus after they've moved on from the chip.
const shouldMoveFocus = hasFocus || wasLastFocused;

if (this._isValidIndex(chipIndex) && shouldMoveFocus) {
this._lastDestroyedFocusedChipIndex = chipIndex;
}
});
this.chipDestroyedChanges
.pipe(takeUntilDestroyed(this._destroyRef))
.subscribe((event: MatChipEvent) => {
// If the focused chip is destroyed, save its index so that we can move focus to the next
// chip. We only save the index here, rather than move the focus immediately, because we want
// to wait until the chip is removed from the chip list before focusing the next one. This
// allows us to keep focus on the same index if the chip gets swapped out.
const chipArray = this._chips.toArray();
const chipIndex = chipArray.indexOf(event.chip);
const hasFocus = event.chip._hasFocus();
const wasLastFocused =
event.chip._hadFocusOnRemove &&
this._keyManager.activeItem &&
event.chip._getActions().includes(this._keyManager.activeItem);

// Note that depending on the timing, the chip might've already lost focus by the
// time we check this. We need the `wasLastFocused` as a fallback to detect such cases.
// In `wasLastFocused` we also need to ensure that the chip actually had focus when it was
// deleted so that we don't steal away the user's focus after they've moved on from the chip.
const shouldMoveFocus = hasFocus || wasLastFocused;

if (this._isValidIndex(chipIndex) && shouldMoveFocus) {
this._lastDestroyedFocusedChipIndex = chipIndex;
}
});
}

/**
Expand Down
16 changes: 8 additions & 8 deletions src/material/datepicker/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
inject,
} from '@angular/core';
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '../core';
import {Subject, Subscription} from 'rxjs';
import {Subject} from 'rxjs';
import {MatCalendarUserEvent, MatCalendarCellClassFunction} from './calendar-body';
import {createMissingDateImplError} from './datepicker-errors';
import {MatDatepickerIntl} from './datepicker-intl';
Expand All @@ -44,6 +44,7 @@ import {_IdGenerator, CdkMonitorFocus} from '@angular/cdk/a11y';
import {_CdkPrivateStyleLoader, _VisuallyHiddenLoader} from '@angular/cdk/private';
import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform';
import {MatTooltip} from '../tooltip';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';

/**
* Possible views for the calendar.
Expand Down Expand Up @@ -270,8 +271,6 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
/** A portal containing the header component type for this calendar. */
_calendarHeaderPortal: Portal<any>;

private _intlChanges: Subscription;

/**
* Used for scheduling that focus should be moved to the active cell on the next tick.
* We need to schedule it, rather than do it immediately, because we have to wait
Expand Down Expand Up @@ -433,10 +432,12 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
}
}

this._intlChanges = inject(MatDatepickerIntl).changes.subscribe(() => {
this._changeDetectorRef.markForCheck();
this.stateChanges.next();
});
inject(MatDatepickerIntl)
.changes.pipe(takeUntilDestroyed())
.subscribe(() => {
this._changeDetectorRef.markForCheck();
this.stateChanges.next();
});
}

ngAfterContentInit() {
Expand All @@ -455,7 +456,6 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
}

ngOnDestroy() {
this._intlChanges.unsubscribe();
this.stateChanges.complete();
}

Expand Down
Loading
Loading