Skip to content
Merged
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
3 changes: 2 additions & 1 deletion goldens/material/stepper/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export class MatStepper extends CdkStepper implements AfterViewInit, AfterConten
// (undocumented)
_getAnimationDuration(): string;
headerPosition: 'top' | 'bottom';
readonly headerPrefix: i0.InputSignal<TemplateRef<unknown> | null>;
_iconOverrides: Record<string, TemplateRef<MatStepperIconContext>>;
_icons: QueryList<MatStepperIcon>;
// (undocumented)
Expand All @@ -135,7 +136,7 @@ export class MatStepper extends CdkStepper implements AfterViewInit, AfterConten
readonly steps: QueryList<MatStep>;
_steps: QueryList<MatStep>;
// (undocumented)
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>;
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>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatStepper, never>;
}
Expand Down
43 changes: 30 additions & 13 deletions src/material/stepper/stepper.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,16 @@
@switch (orientation) {
@case ('horizontal') {
<div class="mat-horizontal-stepper-wrapper">
<div
aria-orientation="horizontal"
class="mat-horizontal-stepper-header-container"
role="tablist">
@for (step of steps; track step) {
<ng-container
[ngTemplateOutlet]="stepTemplate"
[ngTemplateOutletContext]="{step}"/>
@if (!$last) {
<div class="mat-stepper-horizontal-line"></div>
}
}
</div>
@if (headerPrefix()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we could solve this with content projection. E.g. have <ng-content select="matStepperPrefix"/> here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had tried that initially but it required adding a wrapper around the header content and it broke some projects internally whose styling depended on the exact underlying structure.

<div class="mat-horizontal-stepper-header-wrapper">
<ng-container [ngTemplateOutlet]="headerPrefix()"/>
<ng-container [ngTemplateOutlet]="horizontalStepsTemplate"
[ngTemplateOutletContext]="{steps}"/>
</div>
} @else {
<ng-container [ngTemplateOutlet]="horizontalStepsTemplate"
[ngTemplateOutletContext]="{steps}"/>
}

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

@case ('vertical') {
<div class="mat-vertical-stepper-wrapper">
@if (headerPrefix()) {
<ng-container [ngTemplateOutlet]="headerPrefix()"/>
}

@for (step of steps; track step) {
<div class="mat-step">
<ng-container
Expand Down Expand Up @@ -102,3 +103,19 @@
[disableRipple]="disableRipple || !step.isNavigable()"
[color]="step.color || color"/>
</ng-template>

<ng-template #horizontalStepsTemplate let-steps="steps">
<div
aria-orientation="horizontal"
class="mat-horizontal-stepper-header-container"
role="tablist">
@for (step of steps; track step) {
<ng-container
[ngTemplateOutlet]="stepTemplate"
[ngTemplateOutletContext]="{step}"/>
@if (!$last) {
<div class="mat-stepper-horizontal-line"></div>
}
}
</div>
</ng-template>
6 changes: 6 additions & 0 deletions src/material/stepper/stepper.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,16 @@ $fallbacks: m3-stepper.get-tokens();
background: token-utils.slot(stepper-container-color, $fallbacks);
}

.mat-horizontal-stepper-header-wrapper {
align-items: center;
display: flex;
}

.mat-horizontal-stepper-header-container {
white-space: nowrap;
display: flex;
align-items: center;
flex-grow: 1;

.mat-stepper-label-position-bottom & {
align-items: flex-start;
Expand Down
74 changes: 74 additions & 0 deletions src/material/stepper/stepper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1561,6 +1561,44 @@ describe('MatStepper', () => {
expect(fixture.componentInstance.index).toBe(0);
});
});

describe('stepper with header prefix', () => {
it('should render the horizontal prefix content before the header', () => {
const fixture = createComponent(HorizontalStepperWithHeaderPrefix);
fixture.detectChanges();

const stepperHeaderWrapper = fixture.nativeElement.querySelector(
'.mat-horizontal-stepper-header-wrapper',
);

expect(stepperHeaderWrapper.children.length).toBe(2);

const stepperHeaderWrapperChildrenTags = Array.from(
stepperHeaderWrapper.children as HTMLElement[],
).map((child: HTMLElement) => child.tagName);
const stepperHeaderPrefix = stepperHeaderWrapper.children[0];

expect(stepperHeaderWrapperChildrenTags).toEqual(['H2', 'DIV']);
expect(stepperHeaderPrefix.textContent).toContain('This is a header prefix');
});

it('should render the vertical prefix content before the first step', () => {
const fixture = createComponent(VerticalStepperWithHeaderPrefix);
fixture.detectChanges();

const stepperWrapper = fixture.nativeElement.querySelector('.mat-vertical-stepper-wrapper');

expect(stepperWrapper.children.length).toBe(4);

const stepperHeaderWrapperChildrenTags = Array.from(
stepperWrapper.children as HTMLElement[],
).map((child: HTMLElement) => child.tagName);
const stepperHeaderPrefix = stepperWrapper.children[0];

expect(stepperHeaderWrapperChildrenTags).toEqual(['H2', 'DIV', 'DIV', 'DIV']);
expect(stepperHeaderPrefix.textContent).toContain('This is a header prefix');
});
});
});

/** Asserts that keyboard interaction works correctly. */
Expand Down Expand Up @@ -2258,3 +2296,39 @@ class HorizontalStepperWithDelayedStep {
class StepperWithTwoWayBindingOnSelectedIndex {
index: number = 0;
}

@Component({
template: `
<mat-stepper [headerPrefix]="stepHeaderPrefix" linear>
<mat-step label="One"></mat-step>
<mat-step label="Two"></mat-step>
<mat-step label="Three"></mat-step>
</mat-stepper>

<ng-template #stepHeaderPrefix>
<h2>This is a header prefix</h2>
</ng-template>
`,
imports: [MatStepperModule],
})
class HorizontalStepperWithHeaderPrefix {
@ViewChild(MatStepper) stepper: MatStepper;
}

@Component({
template: `
<mat-stepper [headerPrefix]="stepHeaderPrefix" orientation="vertical" linear>
<mat-step label="One"></mat-step>
<mat-step label="Two"></mat-step>
<mat-step label="Three"></mat-step>
</mat-stepper>

<ng-template #stepHeaderPrefix>
<h2>This is a header prefix</h2>
</ng-template>
`,
imports: [MatStepperModule],
})
class VerticalStepperWithHeaderPrefix {
@ViewChild(MatStepper) stepper: MatStepper;
}
4 changes: 4 additions & 0 deletions src/material/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
EventEmitter,
inject,
Input,
input,
NgZone,
OnDestroy,
Output,
Expand Down Expand Up @@ -187,6 +188,9 @@ export class MatStepper extends CdkStepper implements AfterViewInit, AfterConten
@Input()
headerPosition: 'top' | 'bottom' = 'top';

/** The content prefix to use in the stepper header. */
readonly headerPrefix = input<TemplateRef<unknown> | null>(null);

/** Consumer-specified template-refs to be used to override the header icons. */
_iconOverrides: Record<string, TemplateRef<MatStepperIconContext>> = {};

Expand Down
Loading