fix(stepper): add disabled state to prevent navigation and provide visual feedback
This commit is contained in:
@@ -6,7 +6,11 @@
|
|||||||
} @else {
|
} @else {
|
||||||
<div class="checkout-page">
|
<div class="checkout-page">
|
||||||
<div class="checkout-page__stepper-col">
|
<div class="checkout-page__stepper-col">
|
||||||
<app-stepper #stepper [initialStepIndex]="checkoutStepIndex()">
|
<app-stepper
|
||||||
|
#stepper
|
||||||
|
[initialStepIndex]="checkoutStepIndex()"
|
||||||
|
[disabled]="hasSubmittedTransfer()"
|
||||||
|
>
|
||||||
<app-step label="Datos" [isValid]="isStep1Valid()">
|
<app-step label="Datos" [isValid]="isStep1Valid()">
|
||||||
<app-checkout-data-step
|
<app-checkout-data-step
|
||||||
[form]="form"
|
[form]="form"
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
<div class="stepper-container">
|
<div class="stepper-container">
|
||||||
|
|
||||||
<!-- Horizontal indicator bar -->
|
<!-- Horizontal indicator bar -->
|
||||||
<div class="stepper-header">
|
<div class="stepper-header" [class.is-disabled]="disabled()" [attr.aria-disabled]="disabled()">
|
||||||
@for (step of steps(); track step; let i = $index; let last = $last) {
|
@for (step of steps(); track step; let i = $index; let last = $last) {
|
||||||
<div
|
<div
|
||||||
class="stepper-header__item"
|
class="stepper-header__item"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'is-active': currentStepIndex() === i,
|
'is-active': currentStepIndex() === i,
|
||||||
'is-completed': currentStepIndex() > i
|
'is-completed': currentStepIndex() > i,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<!-- Connecting line before (except first) -->
|
<!-- Connecting line before (except first) -->
|
||||||
@if (i > 0) {
|
@if (i > 0) {
|
||||||
<div class="stepper-header__line" [ngClass]="{ 'is-completed': currentStepIndex() > i - 1 }"></div>
|
<div
|
||||||
|
class="stepper-header__line"
|
||||||
|
[ngClass]="{ 'is-completed': currentStepIndex() > i - 1 }"
|
||||||
|
></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Circle indicator -->
|
<!-- Circle indicator -->
|
||||||
<div
|
<div
|
||||||
class="stepper-header__circle"
|
class="stepper-header__circle"
|
||||||
[attr.aria-label]="step.label()"
|
[attr.aria-label]="step.label()"
|
||||||
[class.is-clickable]="currentStepIndex() > i"
|
[class.is-clickable]="!disabled() && currentStepIndex() > i"
|
||||||
(click)="goToStep(i)"
|
(click)="goToStep(i)"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
@@ -33,5 +35,4 @@
|
|||||||
<div class="stepper-body">
|
<div class="stepper-body">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,14 @@
|
|||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&.is-disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
.stepper-header__circle {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { StepComponent } from './step.component';
|
|||||||
@Component({
|
@Component({
|
||||||
imports: [StepperComponent, StepComponent],
|
imports: [StepperComponent, StepComponent],
|
||||||
template: `
|
template: `
|
||||||
<app-stepper #stepper>
|
<app-stepper #stepper [disabled]="stepperDisabled()">
|
||||||
<app-step label="Step 1" [isValid]="step1Valid()">
|
<app-step label="Step 1" [isValid]="step1Valid()">
|
||||||
<div id="content-1">Content 1</div>
|
<div id="content-1">Content 1</div>
|
||||||
</app-step>
|
</app-step>
|
||||||
@@ -20,6 +20,7 @@ class TestHostComponent {
|
|||||||
@ViewChild('stepper') stepper!: StepperComponent;
|
@ViewChild('stepper') stepper!: StepperComponent;
|
||||||
step1Valid = signal(true);
|
step1Valid = signal(true);
|
||||||
step2Valid = signal(true);
|
step2Valid = signal(true);
|
||||||
|
stepperDisabled = signal(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('StepperComponent & StepComponent', () => {
|
describe('StepperComponent & StepComponent', () => {
|
||||||
@@ -137,4 +138,28 @@ describe('StepperComponent & StepComponent', () => {
|
|||||||
expect(component.stepper.currentStepIndex()).toBe(0);
|
expect(component.stepper.currentStepIndex()).toBe(0);
|
||||||
expect(fixture.nativeElement.querySelector('#content-1')).not.toBeNull();
|
expect(fixture.nativeElement.querySelector('#content-1')).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('blocks all navigation and shows visual feedback while disabled', async () => {
|
||||||
|
const { fixture, component } = await setup();
|
||||||
|
component.stepperDisabled.set(true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
component.stepper.next();
|
||||||
|
expect(component.stepper.currentStepIndex()).toBe(0);
|
||||||
|
|
||||||
|
component.stepperDisabled.set(false);
|
||||||
|
fixture.detectChanges();
|
||||||
|
component.stepper.next();
|
||||||
|
expect(component.stepper.currentStepIndex()).toBe(1);
|
||||||
|
|
||||||
|
component.stepperDisabled.set(true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
component.stepper.previous();
|
||||||
|
component.stepper.goToStep(0);
|
||||||
|
|
||||||
|
expect(component.stepper.currentStepIndex()).toBe(1);
|
||||||
|
const header = fixture.nativeElement.querySelector('.stepper-header');
|
||||||
|
expect(header.classList.contains('is-disabled')).toBe(true);
|
||||||
|
expect(header.getAttribute('aria-disabled')).toBe('true');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,9 +18,12 @@ import { NgClass } from '@angular/common';
|
|||||||
export class StepperComponent {
|
export class StepperComponent {
|
||||||
readonly steps = contentChildren(StepComponent);
|
readonly steps = contentChildren(StepComponent);
|
||||||
readonly initialStepIndex = input(0);
|
readonly initialStepIndex = input(0);
|
||||||
|
readonly disabled = input(false);
|
||||||
readonly currentStepIndex = linkedSignal(() => this.initialStepIndex());
|
readonly currentStepIndex = linkedSignal(() => this.initialStepIndex());
|
||||||
|
|
||||||
next() {
|
next() {
|
||||||
|
if (this.disabled()) return;
|
||||||
|
|
||||||
const currentSteps = this.steps();
|
const currentSteps = this.steps();
|
||||||
const currentIndex = this.currentStepIndex();
|
const currentIndex = this.currentStepIndex();
|
||||||
if (currentIndex < currentSteps.length - 1) {
|
if (currentIndex < currentSteps.length - 1) {
|
||||||
@@ -32,6 +35,8 @@ export class StepperComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
previous() {
|
previous() {
|
||||||
|
if (this.disabled()) return;
|
||||||
|
|
||||||
const currentIndex = this.currentStepIndex();
|
const currentIndex = this.currentStepIndex();
|
||||||
if (currentIndex > 0) {
|
if (currentIndex > 0) {
|
||||||
this.currentStepIndex.set(currentIndex - 1);
|
this.currentStepIndex.set(currentIndex - 1);
|
||||||
@@ -39,6 +44,8 @@ export class StepperComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
goToStep(index: number) {
|
goToStep(index: number) {
|
||||||
|
if (this.disabled()) return;
|
||||||
|
|
||||||
const targetIndex = index;
|
const targetIndex = index;
|
||||||
// Only allow navigating to completed steps or the current one
|
// Only allow navigating to completed steps or the current one
|
||||||
if (targetIndex < this.currentStepIndex()) {
|
if (targetIndex < this.currentStepIndex()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user