diff --git a/goldens/cdk/stepper/index.api.md b/goldens/cdk/stepper/index.api.md index 9295ce4417e2..a26d32c85c00 100644 --- a/goldens/cdk/stepper/index.api.md +++ b/goldens/cdk/stepper/index.api.md @@ -28,16 +28,22 @@ export class CdkStep implements OnChanges { get completed(): boolean; set completed(value: boolean); // (undocumented) - _completedOverride: boolean | null; + _completedOverride: i0.WritableSignal; content: TemplateRef; // (undocumented) _displayDefaultIndicatorType: boolean; - editable: boolean; + get editable(): boolean; + set editable(value: boolean); errorMessage: string; get hasError(): boolean; set hasError(value: boolean); - interacted: boolean; + readonly index: i0.WritableSignal; + readonly indicatorType: i0.Signal; + get interacted(): boolean; + set interacted(value: boolean); readonly interactedStream: EventEmitter; + readonly isNavigable: i0.Signal; + readonly isSelected: i0.Signal; label: string; // (undocumented) _markAsInteracted(): void; @@ -55,7 +61,8 @@ export class CdkStep implements OnChanges { reset(): void; select(): void; _showError(): boolean; - state: StepState; + get state(): StepState; + set state(value: StepState); stepControl: AbstractControl; stepLabel: CdkStepLabel; // (undocumented) @@ -97,7 +104,6 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { protected _elementRef: ElementRef; _getAnimationDirection(index: number): StepContentPositionState; _getFocusIndex(): number | null; - _getIndicatorType(index: number, state?: StepState): StepState; _getStepContentId(i: number): string; _getStepLabelId(i: number): string; linear: boolean; diff --git a/goldens/material/stepper/index.api.md b/goldens/material/stepper/index.api.md index c4ecf404a356..b511e57b2aae 100644 --- a/goldens/material/stepper/index.api.md +++ b/goldens/material/stepper/index.api.md @@ -82,7 +82,6 @@ export class MatStepHeader extends CdkStepHeader implements AfterViewInit, OnDes // (undocumented) _getDefaultTextForState(state: StepState): string; _getHostElement(): HTMLElement; - _getIconContext(): MatStepperIconContext; iconOverrides: { [key: string]: TemplateRef; }; @@ -138,8 +137,6 @@ export class MatStepper extends CdkStepper implements AfterViewInit, AfterConten // (undocumented) ngOnDestroy(): void; _stepHeader: QueryList; - // (undocumented) - _stepIsNavigable(index: number, step: MatStep): boolean; readonly steps: QueryList; _steps: QueryList; // (undocumented) diff --git a/src/cdk/a11y/key-manager/list-key-manager.ts b/src/cdk/a11y/key-manager/list-key-manager.ts index 55796cbe1d01..c52ebb9697e8 100644 --- a/src/cdk/a11y/key-manager/list-key-manager.ts +++ b/src/cdk/a11y/key-manager/list-key-manager.ts @@ -39,7 +39,7 @@ export type ListKeyManagerModifierKey = 'altKey' | 'ctrlKey' | 'metaKey' | 'shif * of items, it will set the active item correctly when arrow events occur. */ export class ListKeyManager { - private _activeItemIndex = -1; + private _activeItemIndex = signal(-1); private _activeItem = signal(null); private _wrap = false; private _typeaheadSubscription = Subscription.EMPTY; @@ -209,7 +209,7 @@ export class ListKeyManager { this.updateActiveItem(item); if (this._activeItem() !== previousActiveItem) { - this.change.next(this._activeItemIndex); + this.change.next(this._activeItemIndex()); } } @@ -279,7 +279,7 @@ export class ListKeyManager { case PAGE_UP: if (this._pageUpAndDown.enabled && isModifierAllowed) { - const targetIndex = this._activeItemIndex - this._pageUpAndDown.delta; + const targetIndex = this._activeItemIndex() - this._pageUpAndDown.delta; this._setActiveItemByIndex(targetIndex > 0 ? targetIndex : 0, 1); break; } else { @@ -288,7 +288,7 @@ export class ListKeyManager { case PAGE_DOWN: if (this._pageUpAndDown.enabled && isModifierAllowed) { - const targetIndex = this._activeItemIndex + this._pageUpAndDown.delta; + const targetIndex = this._activeItemIndex() + this._pageUpAndDown.delta; const itemsLength = this._getItemsArray().length; this._setActiveItemByIndex(targetIndex < itemsLength ? targetIndex : itemsLength - 1, -1); break; @@ -312,7 +312,7 @@ export class ListKeyManager { /** Index of the currently active item. */ get activeItemIndex(): number | null { - return this._activeItemIndex; + return this._activeItemIndex(); } /** The active item. */ @@ -337,12 +337,12 @@ export class ListKeyManager { /** Sets the active item to the next enabled item in the list. */ setNextItemActive(): void { - this._activeItemIndex < 0 ? this.setFirstItemActive() : this._setActiveItemByDelta(1); + this._activeItemIndex() < 0 ? this.setFirstItemActive() : this._setActiveItemByDelta(1); } /** Sets the active item to a previous enabled item in the list. */ setPreviousItemActive(): void { - this._activeItemIndex < 0 && this._wrap + this._activeItemIndex() < 0 && this._wrap ? this.setLastItemActive() : this._setActiveItemByDelta(-1); } @@ -366,7 +366,7 @@ export class ListKeyManager { // Explicitly check for `null` and `undefined` because other falsy values are valid. this._activeItem.set(activeItem == null ? null : activeItem); - this._activeItemIndex = index; + this._activeItemIndex.set(index); this._typeahead?.setCurrentSelectedItemIndex(index); } @@ -398,7 +398,7 @@ export class ListKeyManager { const items = this._getItemsArray(); for (let i = 1; i <= items.length; i++) { - const index = (this._activeItemIndex + delta * i + items.length) % items.length; + const index = (this._activeItemIndex() + delta * i + items.length) % items.length; const item = items[index]; if (!this._skipPredicateFn(item)) { @@ -414,7 +414,7 @@ export class ListKeyManager { * it encounters either end of the list, it will stop and not wrap. */ private _setActiveInDefaultMode(delta: -1 | 1): void { - this._setActiveItemByIndex(this._activeItemIndex + delta, delta); + this._setActiveItemByIndex(this._activeItemIndex() + delta, delta); } /** @@ -456,8 +456,8 @@ export class ListKeyManager { if (activeItem) { const newIndex = newItems.indexOf(activeItem); - if (newIndex > -1 && newIndex !== this._activeItemIndex) { - this._activeItemIndex = newIndex; + if (newIndex > -1 && newIndex !== this._activeItemIndex()) { + this._activeItemIndex.set(newIndex); this._typeahead?.setCurrentSelectedItemIndex(newIndex); } } diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index 66f99022eea9..06d82f8af560 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -32,6 +32,8 @@ import { booleanAttribute, numberAttribute, inject, + signal, + computed, } from '@angular/core'; import { ControlContainer, @@ -135,7 +137,13 @@ export class CdkStep implements OnChanges { @Input() stepControl: AbstractControl; /** Whether user has attempted to move away from the step. */ - interacted = false; + get interacted(): boolean { + return this._interacted(); + } + set interacted(value: boolean) { + this._interacted.set(value); + } + private _interacted = signal(false); /** Emits when the user has attempted to move away from the step. */ @Output('interacted') @@ -157,10 +165,24 @@ export class CdkStep implements OnChanges { @Input('aria-labelledby') ariaLabelledby: string; /** State of the step. */ - @Input() state: StepState; + @Input() + get state(): StepState { + return this._state()!; + } + set state(value: StepState) { + this._state.set(value); + } + private _state = signal(undefined); /** Whether the user can return to this step once it has been marked as completed. */ - @Input({transform: booleanAttribute}) editable: boolean = true; + @Input({transform: booleanAttribute}) + get editable(): boolean { + return this._editable()!; + } + set editable(value: boolean) { + this._editable.set(value); + } + private _editable = signal(true); /** Whether the completion of step is optional. */ @Input({transform: booleanAttribute}) optional: boolean = false; @@ -168,29 +190,72 @@ export class CdkStep implements OnChanges { /** Whether step is marked as completed. */ @Input({transform: booleanAttribute}) get completed(): boolean { - return this._completedOverride == null ? this._getDefaultCompleted() : this._completedOverride; + const override = this._completedOverride(); + const interacted = this._interacted(); + + if (override != null) { + return override; + } + + return interacted && (!this.stepControl || this.stepControl.valid); } set completed(value: boolean) { - this._completedOverride = value; + this._completedOverride.set(value); } - _completedOverride: boolean | null = null; + _completedOverride = signal(null); - private _getDefaultCompleted() { - return this.stepControl ? this.stepControl.valid && this.interacted : this.interacted; - } + /** Current index of the step within the stepper. */ + readonly index = signal(-1); + + /** Whether the step is selected. */ + readonly isSelected = computed(() => this._stepper.selectedIndex === this.index()); + + /** Type of indicator that should be shown for the step. */ + readonly indicatorType = computed(() => { + const selected = this.isSelected(); + const completed = this.completed; + const defaultState = this._state() ?? STEP_STATE.NUMBER; + const editable = this._editable(); + + if (this._showError() && this.hasError && !selected) { + return STEP_STATE.ERROR; + } + + if (this._displayDefaultIndicatorType) { + if (!completed || selected) { + return STEP_STATE.NUMBER; + } + return editable ? STEP_STATE.EDIT : STEP_STATE.DONE; + } else { + if (completed && !selected) { + return STEP_STATE.DONE; + } else if (completed && selected) { + return defaultState; + } + return editable && selected ? STEP_STATE.EDIT : defaultState; + } + }); + + /** Whether the user can navigate to the step. */ + readonly isNavigable = computed(() => { + const isSelected = this.isSelected(); + const isCompleted = this.completed; + return isCompleted || isSelected || !this._stepper.linear; + }); /** Whether step has an error. */ @Input({transform: booleanAttribute}) get hasError(): boolean { - return this._customError == null ? this._getDefaultError() : this._customError; + const customError = this._customError(); + return customError == null ? this._getDefaultError() : customError; } set hasError(value: boolean) { - this._customError = value; + this._customError.set(value); } - private _customError: boolean | null = null; + private _customError = signal(null); private _getDefaultError() { - return this.stepControl && this.stepControl.invalid && this.interacted; + return this.interacted && !!this.stepControl?.invalid; } constructor(...args: unknown[]); @@ -208,14 +273,14 @@ export class CdkStep implements OnChanges { /** Resets the step to its initial state. Note that this includes resetting form data. */ reset(): void { - this.interacted = false; + this._interacted.set(false); - if (this._completedOverride != null) { - this._completedOverride = false; + if (this._completedOverride() != null) { + this._completedOverride.set(false); } - if (this._customError != null) { - this._customError = false; + if (this._customError() != null) { + this._customError.set(false); } if (this.stepControl) { @@ -234,8 +299,8 @@ export class CdkStep implements OnChanges { } _markAsInteracted() { - if (!this.interacted) { - this.interacted = true; + if (!this._interacted()) { + this._interacted.set(true); this.interactedStream.emit(this); } } @@ -244,7 +309,7 @@ export class CdkStep implements OnChanges { _showError(): boolean { // We want to show the error state either if the user opted into/out of it using the // global options, or if they've explicitly set it through the `hasError` input. - return this._stepperOptions.showError ?? this._customError != null; + return this._stepperOptions.showError ?? this._customError() != null; } } @@ -281,7 +346,7 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { /** The index of the selected step. */ @Input({transform: numberAttribute}) get selectedIndex(): number { - return this._selectedIndex; + return this._selectedIndex(); } set selectedIndex(index: number) { if (this._steps) { @@ -290,21 +355,21 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { throw Error('cdkStepper: Cannot assign out-of-bounds value to `selectedIndex`.'); } - if (this._selectedIndex !== index) { + if (this.selectedIndex !== index) { this.selected?._markAsInteracted(); if ( !this._anyControlsInvalidOrPending(index) && - (index >= this._selectedIndex || this.steps.toArray()[index].editable) + (index >= this.selectedIndex || this.steps.toArray()[index].editable) ) { this._updateSelectedItemIndex(index); } } } else { - this._selectedIndex = index; + this._selectedIndex.set(index); } } - private _selectedIndex = 0; + private _selectedIndex = signal(0); /** The step that is selected. */ @Input() @@ -347,6 +412,7 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { .pipe(startWith(this._steps), takeUntil(this._destroyed)) .subscribe((steps: QueryList) => { this.steps.reset(steps.filter(step => step._stepper === this)); + this.steps.forEach((step, index) => step.index.set(index)); this.steps.notifyOnChanges(); }); } @@ -393,26 +459,26 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { .pipe(startWith(this._layoutDirection()), takeUntil(this._destroyed)) .subscribe(direction => this._keyManager?.withHorizontalOrientation(direction)); - this._keyManager.updateActiveItem(this._selectedIndex); + this._keyManager.updateActiveItem(this.selectedIndex); // No need to `takeUntil` here, because we're the ones destroying `steps`. this.steps.changes.subscribe(() => { if (!this.selected) { - this._selectedIndex = Math.max(this._selectedIndex - 1, 0); + this._selectedIndex.set(Math.max(this.selectedIndex - 1, 0)); } }); // The logic which asserts that the selected index is within bounds doesn't run before the // steps are initialized, because we don't how many steps there are yet so we may have an // invalid index on init. If that's the case, auto-correct to the default so we don't throw. - if (!this._isValidIndex(this._selectedIndex)) { - this._selectedIndex = 0; + if (!this._isValidIndex(this.selectedIndex)) { + this._selectedIndex.set(0); } // For linear step and selected index is greater than zero, // set all the previous steps to interacted so that we can navigate to previous steps. - if (this.linear && this._selectedIndex > 0) { - const visitedSteps = this.steps.toArray().slice(0, this._selectedIndex); + if (this.linear && this.selectedIndex > 0) { + const visitedSteps = this.steps.toArray().slice(0, this._selectedIndex()); for (const step of visitedSteps) { step._markAsInteracted(); @@ -430,12 +496,12 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { /** Selects and focuses the next step in list. */ next(): void { - this.selectedIndex = Math.min(this._selectedIndex + 1, this.steps.length - 1); + this.selectedIndex = Math.min(this._selectedIndex() + 1, this.steps.length - 1); } /** Selects and focuses the previous step in list. */ previous(): void { - this.selectedIndex = Math.max(this._selectedIndex - 1, 0); + this.selectedIndex = Math.max(this._selectedIndex() - 1, 0); } /** Resets the stepper to its initial state. Note that this includes clearing form data. */ @@ -462,7 +528,7 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { /** Returns position state of the step with the given index. */ _getAnimationDirection(index: number): StepContentPositionState { - const position = index - this._selectedIndex; + const position = index - this._selectedIndex(); if (position < 0) { return this._layoutDirection() === 'rtl' ? 'next' : 'previous'; } else if (position > 0) { @@ -471,60 +537,20 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { return 'current'; } - /** Returns the type of icon to be displayed. */ - _getIndicatorType(index: number, state: StepState = STEP_STATE.NUMBER): StepState { - const step = this.steps.toArray()[index]; - const isCurrentStep = this._isCurrentStep(index); - - return step._displayDefaultIndicatorType - ? this._getDefaultIndicatorLogic(step, isCurrentStep) - : this._getGuidelineLogic(step, isCurrentStep, state); - } - - private _getDefaultIndicatorLogic(step: CdkStep, isCurrentStep: boolean): StepState { - if (step._showError() && step.hasError && !isCurrentStep) { - return STEP_STATE.ERROR; - } else if (!step.completed || isCurrentStep) { - return STEP_STATE.NUMBER; - } else { - return step.editable ? STEP_STATE.EDIT : STEP_STATE.DONE; - } - } - - private _getGuidelineLogic( - step: CdkStep, - isCurrentStep: boolean, - state: StepState = STEP_STATE.NUMBER, - ): StepState { - if (step._showError() && step.hasError && !isCurrentStep) { - return STEP_STATE.ERROR; - } else if (step.completed && !isCurrentStep) { - return STEP_STATE.DONE; - } else if (step.completed && isCurrentStep) { - return state; - } else if (step.editable && isCurrentStep) { - return STEP_STATE.EDIT; - } else { - return state; - } - } - - private _isCurrentStep(index: number) { - return this._selectedIndex === index; - } - /** Returns the index of the currently-focused step header. */ - _getFocusIndex() { - return this._keyManager ? this._keyManager.activeItemIndex : this._selectedIndex; + _getFocusIndex(): number | null { + return this._keyManager ? this._keyManager.activeItemIndex : this._selectedIndex(); } private _updateSelectedItemIndex(newIndex: number): void { const stepsArray = this.steps.toArray(); + const selectedIndex = this._selectedIndex(); + this.selectionChange.emit({ selectedIndex: newIndex, - previouslySelectedIndex: this._selectedIndex, + previouslySelectedIndex: selectedIndex, selectedStep: stepsArray[newIndex], - previouslySelectedStep: stepsArray[this._selectedIndex], + previouslySelectedStep: stepsArray[selectedIndex], }); // If focus is inside the stepper, move it to the next header, otherwise it may become @@ -537,8 +563,8 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { : this._keyManager.updateActiveItem(newIndex); } - this._selectedIndex = newIndex; - this.selectedIndexChange.emit(this._selectedIndex); + this._selectedIndex.set(newIndex); + this.selectedIndexChange.emit(newIndex); this._stateChanged(); } @@ -569,7 +595,7 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { const isIncomplete = control ? control.invalid || control.pending || !step.interacted : !step.completed; - return isIncomplete && !step.optional && !step._completedOverride; + return isIncomplete && !step.optional && !step._completedOverride(); }); } diff --git a/src/material/list/list-base.ts b/src/material/list/list-base.ts index eaa942ed0043..401ba7856318 100644 --- a/src/material/list/list-base.ts +++ b/src/material/list/list-base.ts @@ -19,6 +19,7 @@ import { OnDestroy, QueryList, Injector, + signal, } from '@angular/core'; import { _animationsDisabled, @@ -64,12 +65,12 @@ export abstract class MatListBase { */ @Input() get disabled(): boolean { - return this._disabled; + return this._disabled(); } set disabled(value: BooleanInput) { - this._disabled = coerceBooleanProperty(value); + this._disabled.set(coerceBooleanProperty(value)); } - private _disabled = false; + private _disabled = signal(false); protected _defaultOptions = inject(MAT_LIST_CONFIG, {optional: true}); } @@ -149,12 +150,12 @@ export abstract class MatListItemBase implements AfterViewInit, OnDestroy, Rippl /** Whether the list-item is disabled. */ @Input() get disabled(): boolean { - return this._disabled || !!this._listBase?.disabled; + return this._disabled() || !!this._listBase?.disabled; } set disabled(value: BooleanInput) { - this._disabled = coerceBooleanProperty(value); + this._disabled.set(coerceBooleanProperty(value)); } - private _disabled = false; + private _disabled = signal(false); private _subscriptions = new Subscription(); private _rippleRenderer: RippleRenderer | null = null; diff --git a/src/material/list/selection-list.spec.ts b/src/material/list/selection-list.spec.ts index 911dd94d077d..d83c25fa6589 100644 --- a/src/material/list/selection-list.spec.ts +++ b/src/material/list/selection-list.spec.ts @@ -10,7 +10,6 @@ import { ChangeDetectionStrategy, Component, DebugElement, - provideCheckNoChangesConfig, QueryList, ViewChildren, } from '@angular/core'; @@ -43,21 +42,6 @@ describe('MatSelectionList without forms', () => { let listOptions: DebugElement[]; let selectionList: DebugElement; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - providers: [provideCheckNoChangesConfig({exhaustive: false})], - imports: [ - MatListModule, - SelectionListWithListOptions, - SelectionListWithCheckboxPositionAfter, - SelectionListWithListDisabled, - SelectionListWithOnlyOneOption, - SelectionListWithIndirectChildOptions, - SelectionListWithSelectedOptionAndValue, - ], - }); - })); - beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(SelectionListWithListOptions); fixture.detectChanges(); @@ -1277,23 +1261,6 @@ describe('MatSelectionList without forms', () => { }); describe('MatSelectionList with forms', () => { - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - providers: [provideCheckNoChangesConfig({exhaustive: false})], - imports: [ - MatListModule, - FormsModule, - ReactiveFormsModule, - SelectionListWithModel, - SelectionListWithFormControl, - SelectionListWithPreselectedOption, - SelectionListWithPreselectedOptionAndModel, - SelectionListWithPreselectedFormControlOnPush, - SelectionListWithCustomComparator, - ], - }); - })); - describe('and ngModel', () => { let fixture: ComponentFixture; let selectionListDebug: DebugElement; diff --git a/src/material/list/selection-list.ts b/src/material/list/selection-list.ts index ca819bd7e0f4..9a8d186c4cea 100644 --- a/src/material/list/selection-list.ts +++ b/src/material/list/selection-list.ts @@ -30,6 +30,7 @@ import { ViewEncapsulation, forwardRef, inject, + signal, } from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import {ThemePalette} from '../core'; @@ -265,18 +266,18 @@ export class MatSelectionList */ @Input() override get disabled(): boolean { - return this._selectionListDisabled; + return this._selectionListDisabled(); } override set disabled(value: BooleanInput) { // Update the disabled state of this list. Write to `this._selectionListDisabled` instead of // `super.disabled`. That is to avoid closure compiler compatibility issues with assigning to // a super property. - this._selectionListDisabled = coerceBooleanProperty(value); - if (this._selectionListDisabled) { + this._selectionListDisabled.set(coerceBooleanProperty(value)); + if (this._selectionListDisabled()) { this._keyManager?.setActiveItem(-1); } } - private _selectionListDisabled = false; + private _selectionListDisabled = signal(false); /** Implemented as part of ControlValueAccessor. */ registerOnChange(fn: (value: any) => void): void { diff --git a/src/material/menu/menu.spec.ts b/src/material/menu/menu.spec.ts index b2f150e3d139..2dd79464239a 100644 --- a/src/material/menu/menu.spec.ts +++ b/src/material/menu/menu.spec.ts @@ -22,7 +22,6 @@ import { Input, OnDestroy, Output, - provideCheckNoChangesConfig, QueryList, signal, TemplateRef, @@ -1744,7 +1743,6 @@ describe('MatMenu', () => { direction = 'ltr'; TestBed.resetTestingModule().configureTestingModule({ providers: [ - provideCheckNoChangesConfig({exhaustive: false}), { provide: Directionality, useValue: { @@ -1907,7 +1905,7 @@ describe('MatMenu', () => { .withContext('Expected two open menus') .toBe(2); - items[1].componentInstance.disabled = true; + fixture.componentInstance.secondItemDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); @@ -1932,7 +1930,7 @@ describe('MatMenu', () => { const item = fixture.debugElement.query(By.directive(MatMenuItem))!; - item.componentInstance.disabled = true; + fixture.componentInstance.firstItemDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); @@ -2728,8 +2726,9 @@ class CustomMenu { - + @if (showLazy) {