Skip to content

Commit 6b9decb

Browse files
authored
feat(material/stepper): add a prefix section to the horizontal stepper header (#32184)
Adds a prefix section to the header of the horizontal stepper header.
1 parent e87a66b commit 6b9decb

File tree

5 files changed

+116
-14
lines changed

5 files changed

+116
-14
lines changed

goldens/material/stepper/index.api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export class MatStepper extends CdkStepper implements AfterViewInit, AfterConten
119119
// (undocumented)
120120
_getAnimationDuration(): string;
121121
headerPosition: 'top' | 'bottom';
122+
readonly headerPrefix: i0.InputSignal<TemplateRef<unknown> | null>;
122123
_iconOverrides: Record<string, TemplateRef<MatStepperIconContext>>;
123124
_icons: QueryList<MatStepperIcon>;
124125
// (undocumented)
@@ -135,7 +136,7 @@ export class MatStepper extends CdkStepper implements AfterViewInit, AfterConten
135136
readonly steps: QueryList<MatStep>;
136137
_steps: QueryList<MatStep>;
137138
// (undocumented)
138-
static ɵcmp: i0.ɵɵComponentDeclaration<MatStepper, "mat-stepper, mat-vertical-stepper, mat-horizontal-stepper, [matStepper]", ["matStepper", "matVerticalStepper", "matHorizontalStepper"], { "disableRipple": { "alias": "disableRipple"; "required": false; }; "color": { "alias": "color"; "required": false; }; "labelPosition": { "alias": "labelPosition"; "required": false; }; "headerPosition": { "alias": "headerPosition"; "required": false; }; "animationDuration": { "alias": "animationDuration"; "required": false; }; }, { "animationDone": "animationDone"; }, ["_steps", "_icons"], ["*"], true, never>;
139+
static ɵcmp: i0.ɵɵComponentDeclaration<MatStepper, "mat-stepper, mat-vertical-stepper, mat-horizontal-stepper, [matStepper]", ["matStepper", "matVerticalStepper", "matHorizontalStepper"], { "disableRipple": { "alias": "disableRipple"; "required": false; }; "color": { "alias": "color"; "required": false; }; "labelPosition": { "alias": "labelPosition"; "required": false; }; "headerPosition": { "alias": "headerPosition"; "required": false; }; "headerPrefix": { "alias": "headerPrefix"; "required": false; "isSignal": true; }; "animationDuration": { "alias": "animationDuration"; "required": false; }; }, { "animationDone": "animationDone"; }, ["_steps", "_icons"], ["*"], true, never>;
139140
// (undocumented)
140141
static ɵfac: i0.ɵɵFactoryDeclaration<MatStepper, never>;
141142
}

src/material/stepper/stepper.html

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,16 @@
1111
@switch (orientation) {
1212
@case ('horizontal') {
1313
<div class="mat-horizontal-stepper-wrapper">
14-
<div
15-
aria-orientation="horizontal"
16-
class="mat-horizontal-stepper-header-container"
17-
role="tablist">
18-
@for (step of steps; track step) {
19-
<ng-container
20-
[ngTemplateOutlet]="stepTemplate"
21-
[ngTemplateOutletContext]="{step}"/>
22-
@if (!$last) {
23-
<div class="mat-stepper-horizontal-line"></div>
24-
}
25-
}
26-
</div>
14+
@if (headerPrefix()) {
15+
<div class="mat-horizontal-stepper-header-wrapper">
16+
<ng-container [ngTemplateOutlet]="headerPrefix()"/>
17+
<ng-container [ngTemplateOutlet]="horizontalStepsTemplate"
18+
[ngTemplateOutletContext]="{steps}"/>
19+
</div>
20+
} @else {
21+
<ng-container [ngTemplateOutlet]="horizontalStepsTemplate"
22+
[ngTemplateOutletContext]="{steps}"/>
23+
}
2724

2825
<div class="mat-horizontal-content-container">
2926
@for (step of steps; track step) {
@@ -44,6 +41,10 @@
4441

4542
@case ('vertical') {
4643
<div class="mat-vertical-stepper-wrapper">
44+
@if (headerPrefix()) {
45+
<ng-container [ngTemplateOutlet]="headerPrefix()"/>
46+
}
47+
4748
@for (step of steps; track step) {
4849
<div class="mat-step">
4950
<ng-container
@@ -102,3 +103,19 @@
102103
[disableRipple]="disableRipple || !step.isNavigable()"
103104
[color]="step.color || color"/>
104105
</ng-template>
106+
107+
<ng-template #horizontalStepsTemplate let-steps="steps">
108+
<div
109+
aria-orientation="horizontal"
110+
class="mat-horizontal-stepper-header-container"
111+
role="tablist">
112+
@for (step of steps; track step) {
113+
<ng-container
114+
[ngTemplateOutlet]="stepTemplate"
115+
[ngTemplateOutletContext]="{step}"/>
116+
@if (!$last) {
117+
<div class="mat-stepper-horizontal-line"></div>
118+
}
119+
}
120+
</div>
121+
</ng-template>

src/material/stepper/stepper.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,16 @@ $fallbacks: m3-stepper.get-tokens();
1919
background: token-utils.slot(stepper-container-color, $fallbacks);
2020
}
2121

22+
.mat-horizontal-stepper-header-wrapper {
23+
align-items: center;
24+
display: flex;
25+
}
26+
2227
.mat-horizontal-stepper-header-container {
2328
white-space: nowrap;
2429
display: flex;
2530
align-items: center;
31+
flex-grow: 1;
2632

2733
.mat-stepper-label-position-bottom & {
2834
align-items: flex-start;

src/material/stepper/stepper.spec.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,6 +1561,44 @@ describe('MatStepper', () => {
15611561
expect(fixture.componentInstance.index).toBe(0);
15621562
});
15631563
});
1564+
1565+
describe('stepper with header prefix', () => {
1566+
it('should render the horizontal prefix content before the header', () => {
1567+
const fixture = createComponent(HorizontalStepperWithHeaderPrefix);
1568+
fixture.detectChanges();
1569+
1570+
const stepperHeaderWrapper = fixture.nativeElement.querySelector(
1571+
'.mat-horizontal-stepper-header-wrapper',
1572+
);
1573+
1574+
expect(stepperHeaderWrapper.children.length).toBe(2);
1575+
1576+
const stepperHeaderWrapperChildrenTags = Array.from(
1577+
stepperHeaderWrapper.children as HTMLElement[],
1578+
).map((child: HTMLElement) => child.tagName);
1579+
const stepperHeaderPrefix = stepperHeaderWrapper.children[0];
1580+
1581+
expect(stepperHeaderWrapperChildrenTags).toEqual(['H2', 'DIV']);
1582+
expect(stepperHeaderPrefix.textContent).toContain('This is a header prefix');
1583+
});
1584+
1585+
it('should render the vertical prefix content before the first step', () => {
1586+
const fixture = createComponent(VerticalStepperWithHeaderPrefix);
1587+
fixture.detectChanges();
1588+
1589+
const stepperWrapper = fixture.nativeElement.querySelector('.mat-vertical-stepper-wrapper');
1590+
1591+
expect(stepperWrapper.children.length).toBe(4);
1592+
1593+
const stepperHeaderWrapperChildrenTags = Array.from(
1594+
stepperWrapper.children as HTMLElement[],
1595+
).map((child: HTMLElement) => child.tagName);
1596+
const stepperHeaderPrefix = stepperWrapper.children[0];
1597+
1598+
expect(stepperHeaderWrapperChildrenTags).toEqual(['H2', 'DIV', 'DIV', 'DIV']);
1599+
expect(stepperHeaderPrefix.textContent).toContain('This is a header prefix');
1600+
});
1601+
});
15641602
});
15651603

15661604
/** Asserts that keyboard interaction works correctly. */
@@ -2258,3 +2296,39 @@ class HorizontalStepperWithDelayedStep {
22582296
class StepperWithTwoWayBindingOnSelectedIndex {
22592297
index: number = 0;
22602298
}
2299+
2300+
@Component({
2301+
template: `
2302+
<mat-stepper [headerPrefix]="stepHeaderPrefix" linear>
2303+
<mat-step label="One"></mat-step>
2304+
<mat-step label="Two"></mat-step>
2305+
<mat-step label="Three"></mat-step>
2306+
</mat-stepper>
2307+
2308+
<ng-template #stepHeaderPrefix>
2309+
<h2>This is a header prefix</h2>
2310+
</ng-template>
2311+
`,
2312+
imports: [MatStepperModule],
2313+
})
2314+
class HorizontalStepperWithHeaderPrefix {
2315+
@ViewChild(MatStepper) stepper: MatStepper;
2316+
}
2317+
2318+
@Component({
2319+
template: `
2320+
<mat-stepper [headerPrefix]="stepHeaderPrefix" orientation="vertical" linear>
2321+
<mat-step label="One"></mat-step>
2322+
<mat-step label="Two"></mat-step>
2323+
<mat-step label="Three"></mat-step>
2324+
</mat-stepper>
2325+
2326+
<ng-template #stepHeaderPrefix>
2327+
<h2>This is a header prefix</h2>
2328+
</ng-template>
2329+
`,
2330+
imports: [MatStepperModule],
2331+
})
2332+
class VerticalStepperWithHeaderPrefix {
2333+
@ViewChild(MatStepper) stepper: MatStepper;
2334+
}

src/material/stepper/stepper.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
EventEmitter,
1919
inject,
2020
Input,
21+
input,
2122
NgZone,
2223
OnDestroy,
2324
Output,
@@ -187,6 +188,9 @@ export class MatStepper extends CdkStepper implements AfterViewInit, AfterConten
187188
@Input()
188189
headerPosition: 'top' | 'bottom' = 'top';
189190

191+
/** The content prefix to use in the stepper header. */
192+
readonly headerPrefix = input<TemplateRef<unknown> | null>(null);
193+
190194
/** Consumer-specified template-refs to be used to override the header icons. */
191195
_iconOverrides: Record<string, TemplateRef<MatStepperIconContext>> = {};
192196

0 commit comments

Comments
 (0)