Skip to content

Commit 9bceaef

Browse files
authored
refactor(stepper): Stepper and step refactor (#2136)
* refactor(stepper): Stepper and step refactor - Sync stepper and steps through Lit context - Absracted state logic to a separate file - Moved common types and animations to a common folder - Updated tests and stories to reflect the changes
1 parent fa49ac2 commit 9bceaef

10 files changed

Lines changed: 1796 additions & 1854 deletions

File tree

src/components/stepper/animations.ts renamed to src/components/stepper/common/animations.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { EaseOut } from '../../animations/easings.js';
1+
import { EaseOut } from '../../../animations/easings.js';
22
import {
33
type AnimationReferenceMetadata,
44
animation,
5-
} from '../../animations/types.js';
5+
} from '../../../animations/types.js';
66
import type {
77
HorizontalTransitionAnimation,
88
StepperVerticalAnimation,
9-
} from '../types.js';
9+
} from '../../types.js';
1010

1111
const baseOptions: KeyframeAnimationOptions = {
1212
duration: 320,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { createContext } from '@lit/context';
2+
import type IgcStepperComponent from '../stepper.js';
3+
import type { StepperState } from './state.js';
4+
5+
export type StepperContext = {
6+
stepper: IgcStepperComponent;
7+
state: StepperState;
8+
};
9+
10+
export const STEPPER_CONTEXT = createContext<StepperContext>(
11+
Symbol('stepper-context')
12+
);
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import type { PropertyValues } from 'lit';
2+
import type IgcStepComponent from '../step.js';
3+
4+
type StepState = {
5+
linearDisabled: boolean;
6+
previousCompleted: boolean;
7+
visited: boolean;
8+
};
9+
10+
class StepperState {
11+
private readonly _state = new WeakMap<IgcStepComponent, StepState>();
12+
13+
private _steps: IgcStepComponent[] = [];
14+
private _activeStep?: IgcStepComponent;
15+
16+
public linear = false;
17+
18+
//#region Collection accessors
19+
20+
/** Returns all registered step components. */
21+
public get steps(): readonly IgcStepComponent[] {
22+
return this._steps;
23+
}
24+
25+
/** Returns the currently active step. */
26+
public get activeStep(): IgcStepComponent | undefined {
27+
return this._activeStep;
28+
}
29+
30+
/** Returns all steps that are currently accessible (not disabled or linear-disabled). */
31+
public get accessibleSteps(): IgcStepComponent[] {
32+
return this._steps.filter((step) => this.isAccessible(step));
33+
}
34+
35+
//#endregion
36+
37+
//#region Per-step state
38+
39+
/**
40+
* Sets the state of a given step component.
41+
*
42+
* If the step already has an existing state, it merges the new state with the existing one.
43+
* If the step does not have an existing state, it initializes it with default values and then applies the new state.
44+
*
45+
* After updating the state, it requests an update on the step component to reflect the changes in the UI.
46+
*/
47+
public set(step: IgcStepComponent, state: Partial<StepState>): void {
48+
this.has(step)
49+
? this._state.set(step, { ...this.get(step)!, ...state })
50+
: this._state.set(step, {
51+
linearDisabled: false,
52+
previousCompleted: false,
53+
visited: false,
54+
...state,
55+
});
56+
57+
step.requestUpdate();
58+
}
59+
60+
/** Checks if a given step component has an associated state. */
61+
public has(step: IgcStepComponent): boolean {
62+
return this._state.has(step);
63+
}
64+
65+
/** Retrieves the state of a given step component. */
66+
public get(step: IgcStepComponent): StepState | undefined {
67+
return this._state.get(step);
68+
}
69+
70+
/** Deletes the state of a given step component. */
71+
public delete(step: IgcStepComponent): boolean {
72+
return this._state.delete(step);
73+
}
74+
75+
/**
76+
* Determines if a given step component is accessible based on its `disabled` state
77+
* and the `linearDisabled` state from the stepper state management.
78+
*/
79+
public isAccessible(step: IgcStepComponent): boolean {
80+
return !(step.disabled || this.get(step)?.linearDisabled);
81+
}
82+
83+
//#endregion
84+
85+
//#region Active step management
86+
87+
/** Updates the registered steps collection. */
88+
public setSteps(steps: IgcStepComponent[]): void {
89+
this._steps = steps;
90+
}
91+
92+
/** Changes the active step, deactivating the previous one and marking the new one as visited. */
93+
public changeActiveStep(step: IgcStepComponent): void {
94+
if (step === this._activeStep) {
95+
return;
96+
}
97+
98+
if (this._activeStep) {
99+
this._activeStep.active = false;
100+
}
101+
step.active = true;
102+
this.set(step, { visited: true });
103+
this._activeStep = step;
104+
}
105+
106+
/** Activates the first non-disabled step. */
107+
public activateFirstStep(): void {
108+
const step = this._steps.find((s) => !s.disabled);
109+
if (step) {
110+
this.changeActiveStep(step);
111+
}
112+
}
113+
114+
/** Returns the next or previous accessible step relative to the active step. */
115+
public getAdjacentStep(next = true): IgcStepComponent | undefined {
116+
const steps = this.accessibleSteps;
117+
const activeIndex = steps.indexOf(this._activeStep!);
118+
119+
if (activeIndex === -1) {
120+
return undefined;
121+
}
122+
123+
return next ? steps[activeIndex + 1] : steps[activeIndex - 1];
124+
}
125+
126+
//#endregion
127+
128+
//#region State synchronization
129+
130+
/** Synchronizes the `active` and `previousCompleted` state across all steps. */
131+
public syncState(): void {
132+
for (const [index, step] of this._steps.entries()) {
133+
step.active = this._activeStep === step;
134+
135+
if (index > 0) {
136+
this.set(step, {
137+
previousCompleted: this._steps[index - 1].complete,
138+
});
139+
}
140+
}
141+
}
142+
143+
/**
144+
* Sets the visited state for all steps based on the current active step and the linear mode.
145+
*/
146+
public setVisitedState(value: boolean): void {
147+
const activeIndex = this._steps.indexOf(this._activeStep!);
148+
this.linear = value;
149+
150+
for (const [index, step] of this._steps.entries()) {
151+
this.set(step, { visited: index <= activeIndex });
152+
}
153+
154+
this.setLinearState();
155+
}
156+
157+
/** Computes and applies the linear-disabled state for all steps. */
158+
public setLinearState(): void {
159+
if (!this.linear) {
160+
for (const step of this._steps) {
161+
this.set(step, { linearDisabled: false });
162+
}
163+
return;
164+
}
165+
166+
const invalidIndex = this._steps.findIndex(
167+
(step) => !(step.disabled || step.optional) && step.invalid
168+
);
169+
170+
if (invalidIndex > -1) {
171+
for (const [index, step] of this._steps.entries()) {
172+
this.set(step, { linearDisabled: index > invalidIndex });
173+
}
174+
} else {
175+
for (const step of this._steps) {
176+
this.set(step, { linearDisabled: false });
177+
}
178+
}
179+
}
180+
181+
/** Handles step property changes, updating active step tracking and re-syncing state. */
182+
public onStepPropertyChanged(
183+
step: IgcStepComponent,
184+
changed: PropertyValues<IgcStepComponent>
185+
): void {
186+
if (changed.has('active') && step.active) {
187+
this.changeActiveStep(step);
188+
}
189+
this.syncState();
190+
this.setLinearState();
191+
}
192+
193+
/** Processes a change in the steps collection, resolving the active step and syncing state. */
194+
public stepsChanged(): void {
195+
const lastActiveStep = this._steps.findLast((step) => step.active);
196+
197+
if (lastActiveStep) {
198+
this.changeActiveStep(lastActiveStep);
199+
} else {
200+
this.activateFirstStep();
201+
}
202+
203+
this.syncState();
204+
this.setLinearState();
205+
}
206+
207+
/** Resets all step states and activates the first step. */
208+
public reset(): void {
209+
for (const step of this._steps) {
210+
this.delete(step);
211+
}
212+
213+
this.activateFirstStep();
214+
this.setLinearState();
215+
}
216+
217+
//#endregion
218+
}
219+
220+
/**
221+
* Creates a new instance of the StepperState class, which manages the state of steps in a stepper component.
222+
*/
223+
function createStepperState(): StepperState {
224+
return new StepperState();
225+
}
226+
227+
export { createStepperState };
228+
export type { StepperState };

0 commit comments

Comments
 (0)