Skip to content
Open
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
1 change: 1 addition & 0 deletions api-goldens/element-ng/wizard/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { TranslatableString } from '@siemens/element-translate-ng/translate';

// @public (undocumented)
export class SiWizardComponent {
constructor();
back(delta?: number): void;
readonly backText: _angular_core.InputSignal<_siemens_element_translate_ng_translate.TranslatableString>;
readonly cancelText: _angular_core.InputSignal<_siemens_element_translate_ng_translate.TranslatableString>;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 12 additions & 10 deletions projects/element-ng/wizard/si-wizard.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ $wizard-vertical-divider-border-color: variables.$element-ui-4;
max-inline-size: $wizard-vertical-max-inline-size;

.step {
grid-template-columns: 24px 1fr;
grid-template-columns: 1.5rem 1fr;
grid-template-rows: auto auto auto;

align-items: center;
Expand Down Expand Up @@ -169,7 +169,7 @@ $wizard-vertical-divider-border-color: variables.$element-ui-4;
&:focus-visible {
.step-icon {
@include variables.make-outline-focus-inside();
border-radius: 12px;
border-radius: 0.75rem;

&.number-step {
outline: none;
Expand Down Expand Up @@ -270,8 +270,8 @@ $wizard-vertical-divider-border-color: variables.$element-ui-4;
}

.wizard-btn-container {
max-inline-size: 50px;
min-inline-size: 40px;
min-inline-size: 8ch;
flex-shrink: 1;
text-align: center;
cursor: pointer;

Expand All @@ -289,18 +289,19 @@ $wizard-vertical-divider-border-color: variables.$element-ui-4;
}

.circle {
min-inline-size: 18px;
min-block-size: 18px;
border-radius: 9px;
min-inline-size: 1.125rem;
min-block-size: 1.125rem;
border-radius: 0.75rem;
Comment thread
spliffone marked this conversation as resolved.
border-width: 1px;
border-style: solid;
border-color: currentColor;
color: currentColor;
}

.number-step {
min-inline-size: 24px;
min-block-size: 24px;
min-inline-size: 1.5rem;
min-block-size: 1.5rem;
text-align: center;
}

.wizard-footer {
Expand All @@ -318,7 +319,8 @@ $wizard-vertical-divider-border-color: variables.$element-ui-4;
margin-inline-start: auto;
}
}
@container (max-width: 400px) {

@container (max-width: 25em) {
.wizard-footer-inner {
flex-direction: column;
align-items: stretch;
Expand Down
5 changes: 3 additions & 2 deletions projects/element-ng/wizard/si-wizard.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,12 @@ describe('SiWizardComponent', () => {
let element: HTMLElement;

beforeEach(async () => {
await TestBed.compileComponents();
fixture = TestBed.createComponent(TestHostComponent);
hostComponent = fixture.componentInstance;
component = fixture.componentInstance.wizard();
element = fixture.nativeElement.querySelector('si-wizard');
fixture.detectChanges();
await fixture.whenStable();
});

it('stepCount should match number of steps', () => {
Expand Down Expand Up @@ -272,7 +273,7 @@ describe('SiWizardComponent', () => {
it('should calculate visible items', async () => {
hostComponent.generateSteps(10);
await fixture.whenStable();
expect(element.querySelectorAll('.container-steps .step').length).toBe(7);
expect(element.querySelectorAll('.container-steps .step').length).toBeGreaterThanOrEqual(7);
element.querySelector<HTMLElement>('.next')!.click();
});

Expand Down
67 changes: 62 additions & 5 deletions projects/element-ng/wizard/si-wizard.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import {
computed,
contentChildren,
ElementRef,
inject,
input,
linkedSignal,
output,
signal,
untracked,
viewChild
} from '@angular/core';
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
import {
elementCancel,
elementChecked,
Expand All @@ -27,10 +29,15 @@ import {
elementRight4,
elementWarningFilled
} from '@siemens/element-icons';
import { WebComponentContentChildren } from '@siemens/element-ng/common';
import { TextMeasureService, WebComponentContentChildren } from '@siemens/element-ng/common';
import { addIcons, SiIconComponent } from '@siemens/element-ng/icon';
import { SiResizeObserverDirective } from '@siemens/element-ng/resize-observer';
import { SiTranslatePipe, t } from '@siemens/element-translate-ng/translate';
import {
injectSiTranslateService,
SiTranslatePipe,
t
} from '@siemens/element-translate-ng/translate';
import { switchMap } from 'rxjs';

import { SiWizardStepComponent } from './si-wizard-step.component';

Expand Down Expand Up @@ -61,6 +68,16 @@ interface StepMetadata {
}
})
export class SiWizardComponent {
/** em-based multipliers relative to the step title font-size. */
private static readonly minStepWidthEm = 6;
private static readonly maxStepWidthEm = 14;
private static readonly defaultStepWidthEm = 11;
private static readonly fallbackFontSize = 14;
/** Fixed horizontal padding of the step title (px-6 = 2 × 16px). */
private static readonly stepPadding = 32;
private readonly translateService = injectSiTranslateService();
private readonly textMeasureService = inject(TextMeasureService);

protected readonly containerSteps = viewChild<ElementRef<HTMLDivElement>>('containerSteps');

/**
Expand Down Expand Up @@ -219,7 +236,10 @@ export class SiWizardComponent {
protected readonly showCompletionPage = signal(false);
/** The list of visible steps. */
protected readonly activeSteps = computed(() => this.computeVisibleSteps());

private readonly headingKeys = computed(() => this.steps().map(s => s.heading()));
private readonly maxStepWidth = signal(
SiWizardComponent.defaultStepWidthEm * SiWizardComponent.fallbackFontSize
);
private readonly _index = linkedSignal(() => {
const currentStep = this._currentStep();
const currentStepIndex = currentStep ? this.steps().indexOf(currentStep) : 0;
Expand Down Expand Up @@ -288,6 +308,17 @@ export class SiWizardComponent {
});
});

constructor() {
toObservable(this.headingKeys)
.pipe(
switchMap(keys => this.translateService.translateAsync(keys)),
takeUntilDestroyed()
)
.subscribe(translations =>
this.maxStepWidth.set(this.measureMaxTextWidth(Object.values(translations)))
);
}

protected activateStep(event: Event, stepIndex: number): void {
event.preventDefault();
if (this.stepsMetadata()[stepIndex].canActivate) {
Expand Down Expand Up @@ -374,6 +405,30 @@ export class SiWizardComponent {
this._index.set(this.steps().indexOf(step));
}

private measureMaxTextWidth(texts: string[]): number {
const titleEl =
this.containerSteps()?.nativeElement.querySelector<HTMLElement>('.title') ?? undefined;
const fontSize = titleEl
? parseFloat(getComputedStyle(titleEl).fontSize)
: SiWizardComponent.fallbackFontSize;
const defaultWidth = SiWizardComponent.defaultStepWidthEm * fontSize;

if (texts.length === 0) {
return defaultWidth;
}

const minWidth = SiWizardComponent.minStepWidthEm * fontSize;
const maxWidth = SiWizardComponent.maxStepWidthEm * fontSize;

// Only take texts into account which aren't much shorter than the longest text.
const maxCharLength = Math.max(...texts.map(text => text.length));
const candidates = texts.filter(text => text.length >= maxCharLength * 0.8);
const maxTextWidth = Math.max(
...candidates.map(text => this.textMeasureService.measureText(text, titleEl))
);
return Math.min(Math.max(maxTextWidth + SiWizardComponent.stepPadding, minWidth), maxWidth);
}

Comment on lines +408 to +431
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is it worth the complexity? I mean we could just assume a ch (or even rem for simplicity) based max-width and work with that instead of the hard-coded 150px

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

If we directly replace the pixel values with rem or ch, small container sizes become problematic — a larger font size can cause only a single step to be visible at a time. To address this, we introduce the additional complexity of a solution that also handles smaller containers.

See also https://code.siemens.com/simpl/simpl-element/-/work_items/2464

@dr-itz but if you believe it it isn't worth it - I adjust the code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hmm..displaying more items when we can for small sizes is really nice, but it doesn't seem to work correctly yet:

Screenshot 2026-04-21 at 13 36 04
  • wrapping of the number (can be ok I guess)
  • step 5 is misaligned

vs before:

Screenshot 2026-04-21 at 13 36 09

protected updateVisibleSteps(): void {
const newVisibleSteps = this.calculateVisibleStepCount();
if (newVisibleSteps !== this.visibleSteps()) {
Expand All @@ -392,10 +447,12 @@ export class SiWizardComponent {
containerSteps.nativeElement.clientHeight -
parseInt(computedStyle.paddingBlockStart) -
parseInt(computedStyle.paddingBlockEnd);
return Math.max(Math.floor(clientHeight / 48), 1);
const stepEl = containerSteps.nativeElement.querySelector('.step');
const stepHeight = stepEl?.getBoundingClientRect().height ?? 48;
return Math.max(Math.floor(clientHeight / stepHeight), 1);
} else {
const clientWidth = containerSteps.nativeElement.clientWidth;
return Math.max(Math.floor(clientWidth / 150), 1);
return Math.max(Math.floor(clientWidth / this.maxStepWidth()), 1);
}
}

Expand Down