diff --git a/.changeset/empty-coins-dig.md b/.changeset/empty-coins-dig.md new file mode 100644 index 0000000000..bb8bcdae36 --- /dev/null +++ b/.changeset/empty-coins-dig.md @@ -0,0 +1,7 @@ +--- +'@swisspost/design-system-styles': major +'@swisspost/design-system-components': minor +'@swisspost/design-system-documentation': patch +--- + +Removed the HTML stepper component and replaced it with new `post-stepper` and `post-stepper-item` web components. diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html index 5ffb87877a..26a81835e2 100644 --- a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html +++ b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html @@ -140,3 +140,18 @@

Post Tooltip

Hi there 👋 + +
+

Post Stepper

+ + Step 1 + Step 2 + Step 3 + Step 4 + +
diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts index ebc96d67a3..7262c74683 100644 --- a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts +++ b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts @@ -21,6 +21,8 @@ import { PostTabs, PostTooltip, PostTooltipTrigger, + PostStepper, + PostStepperItem, } from 'components'; @Component({ @@ -49,6 +51,8 @@ import { PostTabs, PostTooltip, PostTooltipTrigger, + PostStepper, + PostStepperItem, ], }) export class HomeComponent { diff --git a/packages/components/cypress/e2e/stepper.cy.ts b/packages/components/cypress/e2e/stepper.cy.ts new file mode 100644 index 0000000000..e3ec22f525 --- /dev/null +++ b/packages/components/cypress/e2e/stepper.cy.ts @@ -0,0 +1,194 @@ +describe('stepper', { baseUrl: null }, () => { + beforeEach(() => { + cy.visit('./cypress/fixtures/post-stepper.test.html'); + }); + + afterEach(() => { + cy.reload(); + }); + + it('should render the post-stepper component', () => { + cy.get('post-stepper').should('exist'); + }); + + // Errors + + it('should log an error if the current label is missing', () => { + cy.window().then(win => { + cy.spy(win.console, 'error').as('consoleError'); + }); + cy.get('post-stepper').invoke('attr', 'current-label', null); + cy.get('@consoleError').should('be.called'); + }); + + it('should log an error if the completed label is missing', () => { + cy.window().then(win => { + cy.spy(win.console, 'error').as('consoleError'); + }); + cy.get('post-stepper').invoke('attr', 'completed-label', null); + cy.get('@consoleError').should('be.called'); + }); + + it('should log an error if the mobile step label does not contain #index', () => { + cy.window().then(win => { + cy.spy(win.console, 'error').as('consoleError'); + }); + cy.get('post-stepper').invoke('attr', 'active-step-label', 'Step:'); + cy.get('@consoleError').should('be.called'); + }); + + it('should log an error if the mobile step label is missing', () => { + cy.window().then(win => { + cy.spy(win.console, 'error').as('consoleError'); + }); + cy.get('post-stepper').invoke('attr', 'active-step-label', null); + cy.get('@consoleError').should('be.called'); + }); + + // Dynamic classes + + it('should set all inactive classes when current-index is negative', () => { + cy.get('post-stepper') + .invoke('attr', 'current-index', -1) + .wait(100) + .find('post-stepper-item') + .then($items => { + expect($items.filter('.stepper-item-inactive').length).to.equal(5); + }); + }); + + it('should set first step as active when current-index is 0', () => { + cy.get('post-stepper') + .invoke('attr', 'current-index', 0) + .wait(100) + .find('post-stepper-item') + .then($items => { + expect($items[0]).to.have.class('stepper-item-current'); + expect($items.filter('.stepper-item-inactive').length).to.equal(4); + }); + }); + + it('should set third step as active when current-index is 2', () => { + cy.get('post-stepper') + .invoke('attr', 'current-index', 2) + .wait(100) + .find('post-stepper-item') + .then($items => { + expect($items[2]).to.have.class('stepper-item-current'); + expect($items.filter('.stepper-item-completed').length).to.equal(2); + expect($items.filter('.stepper-item-inactive').length).to.equal(2); + }); + }); + + it('should set all steps as complete but the last when current-index is last element', () => { + cy.get('post-stepper') + .invoke('attr', 'current-index', 4) + .wait(100) + .find('post-stepper-item') + .then($items => { + expect($items[4]).to.have.class('stepper-item-current'); + expect($items.filter('.stepper-item-completed').length).to.equal(4); + expect($items.filter('.stepper-item-inactive').length).to.equal(0); + }); + }); + + it('should set all steps as complete when current-index is bigger than the steps length', () => { + cy.get('post-stepper') + .invoke('attr', 'current-index', 5) + .wait(100) + .find('post-stepper-item') + .then($items => { + expect($items.filter('.stepper-item-current').length).to.equal(0); + expect($items.filter('.stepper-item-completed').length).to.equal(5); + expect($items.filter('.stepper-item-inactive').length).to.equal(0); + }); + }); + + // Accessibility and labels + + it('should set aria-current="step" on the current step', () => { + cy.get('post-stepper') + .invoke('attr', 'current-index', 1) + .wait(100) + .find('post-stepper-item') + .eq(1) + .should('have.attr', 'aria-current', 'step'); + }); + + it('should set aria-live="polite" on the current step', () => { + cy.get('post-stepper') + .invoke('attr', 'current-index', 1) + .wait(100) + .find('post-stepper-item') + .eq(1) + .should('have.attr', 'aria-live', 'polite'); + }); + + it('should not have aria-live="polite" on future step', () => { + cy.get('post-stepper') + .invoke('attr', 'current-index', 1) + .wait(100) + .find('post-stepper-item') + .eq(2) + .should('not.have.attr', 'aria-live'); + }); + + it('should set current label on the current step', () => { + cy.get('post-stepper') + .invoke('attr', 'current-index', 1) + .wait(100) + .find('post-stepper-item') + .eq(1) + .find('.step-hidden-label') + .should('have.text', 'Current step:'); + }); + + it('should set completed label on completed step', () => { + cy.get('post-stepper') + .invoke('attr', 'current-index', 1) + .wait(100) + .find('post-stepper-item') + .eq(0) + .find('.step-hidden-label') + .should('have.text', 'Completed step:'); + }); + + it('should set correct mobile label on stepper', () => { + cy.get('post-stepper') + .invoke('attr', 'current-index', 1) + .wait(100) + .find('.active-step') + .should('have.text', 'Step 2 : Step 2'); + }); + + // Dynamically added/removed steps + + it('should add correct class when a new step is added dynamically', () => { + cy.get('post-stepper').find('post-stepper-item').should('have.length', 5); + cy.get('post-stepper').then($stepper => { + $stepper[0].appendChild(document.createElement('post-stepper-item')); + cy.get('post-stepper') + .wait(100) + .find('post-stepper-item') + .should('have.length', 6) + .last() + .should('have.class', 'stepper-item-inactive'); + }); + }); + + it('should throw an error if there is only one step', () => { + cy.window().then(win => { + cy.spy(win.console, 'error').as('consoleError'); + }); + cy.get('post-stepper').find('post-stepper-item').should('have.length', 5); + + cy.get('post-stepper').then($stepper => { + const stepper = $stepper[0]; + const allButFirstStep = stepper.querySelectorAll('post-stepper-item:not(:first-child)'); + allButFirstStep.forEach(step => step.remove()); + }); + + cy.get('post-stepper').find('post-stepper-item').should('have.length', 1); + cy.get('@consoleError').should('be.called'); + }); +}); diff --git a/packages/components/cypress/fixtures/post-stepper.test.html b/packages/components/cypress/fixtures/post-stepper.test.html new file mode 100644 index 0000000000..951fc8a2d9 --- /dev/null +++ b/packages/components/cypress/fixtures/post-stepper.test.html @@ -0,0 +1,26 @@ + + + + + + Document + + + + + + Step 1 + Step 2 + Step 3 + Step 4 + Step 5 + + + diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index c61dd61335..737f376abd 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -477,6 +477,27 @@ export namespace Components { */ "stars": number; } + interface PostStepper { + /** + * Label for the "Step N:" indicator for mobile view. Use `#index` as a placeholder — it will be replaced with the current step number at runtime. + */ + "activeStepLabel": string; + /** + * "Completed step" label for accessibility + */ + "completedLabel": string; + /** + * Defines the currently active step + * @default -1 + */ + "currentIndex": number; + /** + * "Current step" label for accessibility + */ + "currentLabel": string; + } + interface PostStepperItem { + } interface PostTabHeader { /** * The name of the panel controlled by the tab header. @@ -877,6 +898,18 @@ declare global { prototype: HTMLPostRatingElement; new (): HTMLPostRatingElement; }; + interface HTMLPostStepperElement extends Components.PostStepper, HTMLStencilElement { + } + var HTMLPostStepperElement: { + prototype: HTMLPostStepperElement; + new (): HTMLPostStepperElement; + }; + interface HTMLPostStepperItemElement extends Components.PostStepperItem, HTMLStencilElement { + } + var HTMLPostStepperItemElement: { + prototype: HTMLPostStepperItemElement; + new (): HTMLPostStepperItemElement; + }; interface HTMLPostTabHeaderElement extends Components.PostTabHeader, HTMLStencilElement { } var HTMLPostTabHeaderElement: { @@ -955,6 +988,8 @@ declare global { "post-popover-trigger": HTMLPostPopoverTriggerElement; "post-popovercontainer": HTMLPostPopovercontainerElement; "post-rating": HTMLPostRatingElement; + "post-stepper": HTMLPostStepperElement; + "post-stepper-item": HTMLPostStepperItemElement; "post-tab-header": HTMLPostTabHeaderElement; "post-tab-panel": HTMLPostTabPanelElement; "post-tabs": HTMLPostTabsElement; @@ -1374,6 +1409,27 @@ declare namespace LocalJSX { */ "stars"?: number; } + interface PostStepper { + /** + * Label for the "Step N:" indicator for mobile view. Use `#index` as a placeholder — it will be replaced with the current step number at runtime. + */ + "activeStepLabel": string; + /** + * "Completed step" label for accessibility + */ + "completedLabel": string; + /** + * Defines the currently active step + * @default -1 + */ + "currentIndex"?: number; + /** + * "Current step" label for accessibility + */ + "currentLabel": string; + } + interface PostStepperItem { + } interface PostTabHeader { /** * The name of the panel controlled by the tab header. @@ -1471,6 +1527,8 @@ declare namespace LocalJSX { "post-popover-trigger": PostPopoverTrigger; "post-popovercontainer": PostPopovercontainer; "post-rating": PostRating; + "post-stepper": PostStepper; + "post-stepper-item": PostStepperItem; "post-tab-header": PostTabHeader; "post-tab-panel": PostTabPanel; "post-tabs": PostTabs; @@ -1519,6 +1577,8 @@ declare module "@stencil/core" { "post-popover-trigger": LocalJSX.PostPopoverTrigger & JSXBase.HTMLAttributes; "post-popovercontainer": LocalJSX.PostPopovercontainer & JSXBase.HTMLAttributes; "post-rating": LocalJSX.PostRating & JSXBase.HTMLAttributes; + "post-stepper": LocalJSX.PostStepper & JSXBase.HTMLAttributes; + "post-stepper-item": LocalJSX.PostStepperItem & JSXBase.HTMLAttributes; "post-tab-header": LocalJSX.PostTabHeader & JSXBase.HTMLAttributes; "post-tab-panel": LocalJSX.PostTabPanel & JSXBase.HTMLAttributes; "post-tabs": LocalJSX.PostTabs & JSXBase.HTMLAttributes; diff --git a/packages/components/src/components/post-popover/readme.md b/packages/components/src/components/post-popover/readme.md index f9e1110c89..7acc272cb6 100644 --- a/packages/components/src/components/post-popover/readme.md +++ b/packages/components/src/components/post-popover/readme.md @@ -1,5 +1,7 @@ # post-popover + + diff --git a/packages/components/src/components/post-stepper-item/post-stepper-item.scss b/packages/components/src/components/post-stepper-item/post-stepper-item.scss new file mode 100644 index 0000000000..19fe286343 --- /dev/null +++ b/packages/components/src/components/post-stepper-item/post-stepper-item.scss @@ -0,0 +1,258 @@ +@use '@swisspost/design-system-styles/mixins/icons'; +@use '@swisspost/design-system-styles/mixins/utilities'; +@use '@swisspost/design-system-styles/mixins/media'; +@use '@swisspost/design-system-styles/functions/tokens'; +@use '@swisspost/design-system-styles/tokens/components'; +@use '@swisspost/design-system-styles/tokens/elements'; + +tokens.$default-map: components.$post-stepper; + +@include icons.custom-property('success-solid'); + +post-stepper-item { + grid-row: 1; + position: relative; + padding-inline-start: 0; + + &:not(:first-child) { + padding-inline-start: calc(#{tokens.get('stepper-indicator')} / 2); + } + + &:not(:last-child) { + padding-inline-end: calc(#{tokens.get('stepper-indicator')} / 2); + grid-column: span 2; + } + + // progress bar + &::before, + &::after { + content: ''; + display: block; + position: absolute; + inset-block-start: calc((#{tokens.get('stepper-indicator')} - 2px) / 2); + height: 2px; + background-color: tokens.get('stepper-connector-next'); + inset-inline: 0; + } + + &:not(:first-child, :last-child) { + &::before { + inset-inline-end: 50%; + } + + &::after { + inset-inline-start: 50%; + } + } + + &.stepper-item-current:not(:last-child)::after, + &.stepper-item-current:first-child::before, + &.stepper-item-current + *:last-child::after, + &.stepper-item-current + *::before { + background-color: tokens.get('stepper-connector-active'); + } + + &.stepper-item-current:not(:first-child)::before, + &.stepper-item-current:last-child::after, + &.stepper-item-completed::after, + &.stepper-item-completed::before, + &.stepper-item-completed + *::before { + background-color: tokens.get('stepper-connector-completed'); + z-index: 1; + } +} + +.stepper-item-content { + // stylelint-disable-next-line value-no-vendor-prefix + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + position: relative; + z-index: 2; + overflow: hidden; + text-decoration: none; + color: tokens.get('stepper-text-disabled-fg'); + width: fit-content; + word-break: break-word; + font-size: tokens.get('stepper-step-label-font-size'); + + post-stepper-item:not(:first-child, :last-child) > & { + margin-inline: auto; + text-align: center; + } + + post-stepper-item:last-child > & { + margin-inline-start: auto; + margin-inline-end: calc(40px / -2); // negative margin matching the container padding + text-align: end; + } + + // Completed steps + .stepper-item-completed > & { + color: tokens.get('stepper-text-enabled-fg'); + } + + // Current step + .stepper-item-current > & { + font-weight: tokens.get('stepper-step-label-selected-font-weight'); + color: tokens.get('stepper-text-enabled-fg'); + + &::before { + font-weight: tokens.get('body-font-weight', elements.$post-body); + color: tokens.get('stepper-enabled-fg'); + } + } +} + +// step indicator +.stepper-item-content::before { + counter-increment: step-index; + content: counter(step-index); + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + height: tokens.get('stepper-indicator'); + width: tokens.get('stepper-indicator'); + box-sizing: border-box; + margin-block-end: tokens.get('stepper-gap-text-start'); + color: tokens.get('stepper-enabled-fg'); + background-color: tokens.get('stepper-enabled-bg'); + border: tokens.get('stepper-border-width') solid tokens.get('stepper-enabled-stroke'); + border-radius: tokens.get('stepper-border-radius-round'); + text-indent: initial; + + post-stepper-item:not(:first-child, :last-child) > & { + margin-inline: auto; + } + + post-stepper-item:last-child > & { + margin-inline-start: auto; + } + + // Completed steps + .stepper-item-completed > & { + color: transparent; // Hides the number on completed step + background-color: tokens.get('stepper-completed-stroke'); + border-color: tokens.get('stepper-completed-stroke'); + } + + // Future steps + .stepper-item-inactive > & { + color: tokens.get('stepper-disabled-fg'); + background-color: tokens.get('stepper-disabled-bg'); + border-color: tokens.get('stepper-disabled-stroke'); + } +} + +$checkIconSize: calc( + #{tokens.get('stepper-indicator')} - (#{tokens.get('stepper-border-width')} * 2) +); + +// check icon +.stepper-item-content::after { + display: block; + position: absolute; + inset-block-start: tokens.get('stepper-border-width'); + z-index: 1; + background-color: tokens.get('stepper-enabled-fg'); + @include icons.mask-image('success-solid'); + width: $checkIconSize; + height: $checkIconSize; + color: tokens.get('stepper-completed-bg'); + + post-stepper-item:first-child > & { + inset-inline-start: tokens.get('stepper-border-width'); + } + + post-stepper-item:not(:first-child, :last-child) > & { + inset-inline-start: calc(50% - ($checkIconSize / 2)); + } + + post-stepper-item:last-child > & { + inset-inline-end: tokens.get('stepper-border-width'); + } + + // show only for completed steps + .stepper-item-completed > & { + content: ''; + } +} + +// smaller screens +@include media.max(md) { + post-stepper-item { + flex-grow: 1; + margin-inline: 0; + padding: 0 !important; + + &::before { + content: unset; + } + + &::after { + inset-inline-start: 0 !important; + } + + &:last-child { + flex: 0 0 32px; + + &::after { + content: unset; + } + } + .stepper-item-content { + margin-inline: 0 !important; + + > span { + display: none; + } + } + } +} + +@include utilities.high-contrast-mode { + post-stepper-item { + &::before, + &::after { + background-color: CanvasText !important; + } + + &.stepper-item-current:not(:first-child)::before, + &.stepper-item-current:last-child::after, + &.stepper-item-completed::after, + &.stepper-item-completed::before, + &.stepper-item-completed + *::before { + background-color: Highlight !important; + } + } + + .stepper-item-content { + &::before { + forced-color-adjust: none; + } + + .stepper-item-current > &::before { + background-color: Canvas; + color: CanvasText; + } + + .stepper-item-completed > &::before, + .stepper-item-inactive > &::before { + border-color: Canvas; + background-color: CanvasText; + } + + .stepper-item-inactive > &::before { + color: Canvas; + } + + &::after { + color: Canvas; + } + } +} + +.visually-hidden { + @include utilities.visuallyhidden(); +} diff --git a/packages/components/src/components/post-stepper-item/post-stepper-item.tsx b/packages/components/src/components/post-stepper-item/post-stepper-item.tsx new file mode 100644 index 0000000000..584b310fef --- /dev/null +++ b/packages/components/src/components/post-stepper-item/post-stepper-item.tsx @@ -0,0 +1,22 @@ +import { Component, h, Host } from '@stencil/core'; +import { version } from '@root/package.json'; + +@Component({ + tag: 'post-stepper-item', + styleUrl: 'post-stepper-item.scss', + shadow: false, +}) +export class PostStepperItem { + render() { + return ( + + + + + + + + + ); + } +} diff --git a/packages/components/src/components/post-stepper-item/readme.md b/packages/components/src/components/post-stepper-item/readme.md new file mode 100644 index 0000000000..70033f179c --- /dev/null +++ b/packages/components/src/components/post-stepper-item/readme.md @@ -0,0 +1,8 @@ +# post-stepper-item + + + + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/components/src/components/post-stepper/post-stepper.scss b/packages/components/src/components/post-stepper/post-stepper.scss new file mode 100644 index 0000000000..6e6f82f79b --- /dev/null +++ b/packages/components/src/components/post-stepper/post-stepper.scss @@ -0,0 +1,40 @@ +@use '@swisspost/design-system-styles/mixins/utilities'; +@use '@swisspost/design-system-styles/functions/tokens'; +@use '@swisspost/design-system-styles/tokens/components'; +@use '@swisspost/design-system-styles/mixins/media'; + +tokens.$default-map: components.$post-stepper; + +ol { + @include utilities.reset-list; + // start a counter for the step numbers + counter-reset: step-index; + + display: grid; + position: relative; + overflow: hidden; + + // the first column is half a step wide to make sure the separators are the same size even on small screens + grid-template-columns: calc(40px / 2); + + // all other columns are 1 fraction of the available space ase we don't know the number of steps + grid-auto-columns: minmax(0, 1fr); + + // we use a padding and negative margin on the last step for the same reason we need the first column + padding-inline-end: calc(40px / 2); + + @include media.max(md) { + display: flex; + padding-inline-end: 0; + } +} + +.active-step { + font-size: tokens.get('stepper-step-label-font-size'); + font-weight: tokens.get('stepper-step-label-selected-font-weight'); + color: tokens.get('stepper-text-enabled-fg'); + + @include media.min(md) { + display: none; + } +} diff --git a/packages/components/src/components/post-stepper/post-stepper.tsx b/packages/components/src/components/post-stepper/post-stepper.tsx new file mode 100644 index 0000000000..2d4094818d --- /dev/null +++ b/packages/components/src/components/post-stepper/post-stepper.tsx @@ -0,0 +1,154 @@ +import { Component, Element, h, Host, Prop, State, Watch } from '@stencil/core'; +import { version } from '@root/package.json'; +import { checkRequiredAndPattern, checkRequiredAndType } from '@/utils'; + +@Component({ + tag: 'post-stepper', + styleUrl: 'post-stepper.scss', + shadow: true, +}) +export class PostStepper { + @Element() host: HTMLPostStepperElement; + + private stepItems: NodeListOf; + + /** + * Active step label is for visual purposes on mobile only + */ + @State() mobileActiveStepLabel: string; + + /** + * Active step name is for visual purposes on mobile only + */ + @State() mobileActiveStepName: string; + + /** + * "Current step" label for accessibility + */ + @Prop({ reflect: true }) currentLabel!: string; + + @Watch('currentLabel') + validateCurrentLabel() { + checkRequiredAndType(this, 'currentLabel', 'string'); + } + + /** + * "Completed step" label for accessibility + */ + @Prop({ reflect: true }) completedLabel!: string; + + @Watch('completedLabel') + validateCompletedLabel() { + checkRequiredAndType(this, 'completedLabel', 'string'); + } + + /** + * Label for the "Step N:" indicator for mobile view. + * Use `#index` as a placeholder — it will be replaced with the current step number at runtime. + */ + @Prop({ reflect: true }) activeStepLabel!: string; + + @Watch('activeStepLabel') + validateActiveStepLabel() { + checkRequiredAndPattern(this, 'activeStepLabel', /#index\b/); + this.updateActiveStepLabel(); + } + + /** + * Defines the currently active step + */ + @Prop() currentIndex: number = -1; + + @Watch('currentIndex') + validateCurrentIndex() { + checkRequiredAndType(this, 'currentIndex', 'number'); + this.updateSteps(); + } + + componentDidLoad() { + this.validateCompletedLabel(); + this.validateCurrentLabel(); + this.validateActiveStepLabel(); + + // Wait for slotchange + setTimeout(() => { + this.validateCurrentIndex(); + }); + } + + private updateActiveStepLabel() { + if (this.activeStepLabel) { + const labelTemplate = this.activeStepLabel; + this.mobileActiveStepLabel = labelTemplate.replace(/#index/g, `${this.currentIndex + 1}`); + this.updateMobileActiveStepVisibility(); + } + } + + private updateSteps() { + this.stepItems = this.host.querySelectorAll('post-stepper-item'); + + if (!this.stepItems || this.stepItems.length < 3) { + console.error('The post-stepper component should have at least three steps.'); + return; + } + + this.updateActiveStepLabel(); + + this.stepItems.forEach((el, i) => { + if (this.currentIndex === i) { + this.mobileActiveStepName = el.querySelector('.label').innerHTML; + } + + // Update "post-stepper-item" classes to show correct status + el.classList.toggle('stepper-item-completed', this.currentIndex > i); + el.classList.toggle('stepper-item-current', this.currentIndex === i); + el.classList.toggle('stepper-item-inactive', this.currentIndex < i); + + // Update accessibility label depending on status (Completed/Current/-) + const hiddenLabel = el.querySelector('.step-hidden-label'); + if (hiddenLabel) { + let labelText = ''; + + if (this.currentIndex > i) { + labelText = `${this.completedLabel}:`; + } else if (this.currentIndex === i) { + labelText = `${this.currentLabel}:`; + } + + hiddenLabel.textContent = labelText; + } + + // Update accessibility aria attributes + if (this.currentIndex === i) { + el.setAttribute('aria-current', 'step'); + el.setAttribute('aria-live', 'polite'); + } else { + el.removeAttribute('aria-current'); + el.removeAttribute('aria-live'); + } + }); + + this.updateMobileActiveStepVisibility(); + } + + private updateMobileActiveStepVisibility() { + if (this.currentIndex >= this.stepItems.length || this.currentIndex < 0) { + this.mobileActiveStepLabel = ''; + this.mobileActiveStepName = ''; + } + } + + render() { + return ( + +
    + this.updateSteps()}> +
+ +
+ ); + } +} diff --git a/packages/components/src/components/post-stepper/readme.md b/packages/components/src/components/post-stepper/readme.md new file mode 100644 index 0000000000..2f6f7ecfd8 --- /dev/null +++ b/packages/components/src/components/post-stepper/readme.md @@ -0,0 +1,20 @@ +# post-stepper + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ------------------------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------- | +| `activeStepLabel` _(required)_ | `active-step-label` | Label for the "Step N:" indicator for mobile view. Use `#index` as a placeholder — it will be replaced with the current step number at runtime. | `string` | `undefined` | +| `completedLabel` _(required)_ | `completed-label` | "Completed step" label for accessibility | `string` | `undefined` | +| `currentIndex` | `current-index` | Defines the currently active step | `number` | `-1` | +| `currentLabel` _(required)_ | `current-label` | "Current step" label for accessibility | `string` | `undefined` | + + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/documentation/src/stories/components/stepper/stepper.docs.mdx b/packages/documentation/src/stories/components/stepper/stepper.docs.mdx index ef3a9dee62..d18b8adf4a 100644 --- a/packages/documentation/src/stories/components/stepper/stepper.docs.mdx +++ b/packages/documentation/src/stories/components/stepper/stepper.docs.mdx @@ -1,5 +1,4 @@ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks'; -import StylesPackageImport from '@/shared/styles-package-import.mdx'; import meta, * as StepperStories from './stepper.stories'; import PackageTag from '@/shared/package-tag'; @@ -8,15 +7,15 @@ import PackageTag from '@/shared/package-tag';
# Stepper - + +
- +
- The stepped progression component provides an interactive visual overview of a process. It shows - at a glance the amount of steps a user is required to go through, and serves as a guide for each - step, indicating its status. + A stepper guides users through any linear, multistep process by showing the user their completed, + current, and future steps in an interactive manner.
@@ -24,30 +23,7 @@ import PackageTag from '@/shared/package-tag'; - - -## Examples - -### Navigational Stepper - -You can use a stepper to allow users to navigate to specific steps. -To do so, use `a` elements for the `.stepper-link` of the steps you want to be navigable, and use `span` for the steps you want not to be navigable. -Then, wrap the stepper inside a `