diff --git a/dev/stepper.html b/dev/stepper.html new file mode 100644 index 00000000000..d71fefceb3e --- /dev/null +++ b/dev/stepper.html @@ -0,0 +1,166 @@ + + + + + + vaadin-stepper + + + + + + +

Stepper Examples

+ +
+

Vertical Stepper (Default)

+ + + + + + +
+ +
+

Horizontal Stepper

+ + + + + + +
+ +
+

Small Stepper

+ + + + + + +
+ +
+

Horizontal Small Stepper

+ + + + + + +
+ +
+

Stepper with Error State

+ + + + + + +
+ +
+

Disabled Steps

+ + + + + +
+ +
+

Steps without Links

+ + + + + + +
+ +
+

Interactive Stepper

+ + + + + + + +
+ + + + + + + +
+
+ + + + \ No newline at end of file diff --git a/packages/stepper/package.json b/packages/stepper/package.json new file mode 100644 index 00000000000..1266e680572 --- /dev/null +++ b/packages/stepper/package.json @@ -0,0 +1,52 @@ +{ + "name": "@vaadin/stepper", + "version": "25.0.0-dev", + "publishConfig": { + "access": "public" + }, + "description": "Web component for step-by-step process", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/vaadin/web-components.git", + "directory": "packages/stepper" + }, + "author": "Vaadin Ltd", + "homepage": "https://vaadin.com/components", + "bugs": { + "url": "https://github.com/vaadin/web-components/issues" + }, + "main": "vaadin-stepper.js", + "module": "vaadin-stepper.js", + "type": "module", + "files": [ + "src", + "vaadin-*.d.ts", + "vaadin-*.js", + "web-types.json", + "web-types.lit.json" + ], + "web-types": [ + "web-types.json", + "web-types.lit.json" + ], + "keywords": [ + "Vaadin", + "stepper", + "step", + "progress", + "wizard", + "web-components", + "web-component" + ], + "dependencies": { + "@vaadin/a11y-base": "25.0.0-alpha16", + "@vaadin/component-base": "25.0.0-alpha16", + "@vaadin/vaadin-themable-mixin": "25.0.0-alpha16", + "lit": "^3.0.0" + }, + "devDependencies": { + "@vaadin/chai-plugins": "25.0.0-alpha16", + "@vaadin/testing-helpers": "^2.0.0" + } +} \ No newline at end of file diff --git a/packages/stepper/src/styles/vaadin-step-styles.js b/packages/stepper/src/styles/vaadin-step-styles.js new file mode 100644 index 00000000000..509021cecf1 --- /dev/null +++ b/packages/stepper/src/styles/vaadin-step-styles.js @@ -0,0 +1,213 @@ +/** + * @license + * Copyright (c) 2017 - 2025 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import { css } from 'lit'; + +export const stepStyles = css` + :host { + display: block; + position: relative; + min-width: 0; + font-family: var(--lumo-font-family); + } + + :host([hidden]) { + display: none !important; + } + + :host([disabled]) { + pointer-events: none; + opacity: 0.5; + } + + /* Content wrapper */ + a, + div { + display: flex; + align-items: center; + gap: var(--lumo-space-m); + padding: var(--lumo-space-s); + text-decoration: none; + color: inherit; + cursor: pointer; + outline: none; + transition: color 0.2s; + } + + :host([small]) a, + :host([small]) div { + gap: var(--lumo-space-s); + } + + div { + cursor: default; + } + + /* Indicator (circle) */ + [part='indicator'] { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: var(--lumo-size-m); + height: var(--lumo-size-m); + border: 2px solid var(--lumo-contrast-30pct); + border-radius: 50%; + background: var(--lumo-base-color); + font-size: var(--lumo-font-size-s); + font-weight: 500; + color: var(--lumo-secondary-text-color); + transition: all 0.2s; + } + + :host([small]) [part='indicator'] { + width: var(--lumo-size-xs); + height: var(--lumo-size-xs); + font-size: var(--lumo-font-size-xs); + } + + /* Active state */ + :host([active]) [part='indicator'] { + border-color: var(--lumo-primary-color); + color: var(--lumo-primary-color); + } + + /* Completed state */ + :host([completed]) [part='indicator'] { + background: var(--lumo-primary-color); + border-color: var(--lumo-primary-color); + color: var(--lumo-primary-contrast-color); + } + + /* Error state */ + :host([error]) [part='indicator'] { + background: var(--lumo-error-color); + border-color: var(--lumo-error-color); + color: var(--lumo-error-contrast-color); + } + + /* Content */ + [part='content'] { + display: flex; + flex-direction: column; + overflow: hidden; + flex: 1; + } + + /* Label */ + [part='label'] { + font-weight: 500; + color: var(--lumo-body-text-color); + } + + :host([small]) [part='label'] { + font-size: var(--lumo-font-size-s); + } + + :host([active]) [part='label'] { + color: var(--lumo-primary-color); + } + + :host([completed]) [part='label'] { + color: var(--lumo-body-text-color); + } + + :host([error]) [part='label'] { + color: var(--lumo-error-color); + } + + :host(:not([active]):not([completed]):not([error])) [part='label'] { + color: var(--lumo-secondary-text-color); + } + + /* Description */ + [part='description'] { + font-size: var(--lumo-font-size-s); + color: var(--lumo-secondary-text-color); + margin-top: var(--lumo-space-xs); + } + + :host([small]) [part='description'] { + font-size: var(--lumo-font-size-xs); + } + + /* Horizontal orientation specific styles */ + :host([orientation='horizontal']) [part='label'], + :host([orientation='horizontal']) [part='description'] { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + /* Connector line */ + [part='connector'] { + position: absolute; + background: var(--lumo-contrast-30pct); + } + + /* Vertical connector */ + :host([orientation='vertical']) [part='connector'] { + position: absolute; + left: calc(var(--lumo-size-m) / 2 + var(--lumo-space-s) - 1px); + top: calc(var(--lumo-size-m) + var(--lumo-space-s)); + width: 2px; + height: var(--lumo-space-l); + } + + :host([orientation='vertical'][small]) [part='connector'] { + left: calc(var(--lumo-size-xs) / 2 + var(--lumo-space-s) - 1px); + top: calc(var(--lumo-size-xs) + var(--lumo-space-s)); + height: var(--lumo-space-m); + } + + /* Horizontal connector */ + :host([orientation='horizontal']) [part='connector'] { + top: 50%; + left: 100%; + right: calc(var(--lumo-space-l) * -1); + height: 2px; + transform: translateY(-50%); + } + + /* Hide connector for last step */ + :host([last]) [part='connector'] { + display: none; + } + + /* Hover effects */ + a:hover [part='indicator']:not(:host([disabled]) [part='indicator']) { + border-color: var(--lumo-primary-color-50pct); + } + + a:hover [part='label']:not(:host([disabled]) [part='label']) { + color: var(--lumo-primary-text-color); + } + + /* Focus styles */ + a:focus-visible { + outline: 2px solid var(--lumo-primary-color); + outline-offset: 2px; + border-radius: var(--lumo-border-radius-m); + } + + /* Icons in indicator */ + .checkmark, + .error-icon, + .step-number { + line-height: 1; + } + + /* RTL support */ + :host([dir='rtl']) [part='connector'] { + left: auto; + right: calc(var(--lumo-size-m) / 2); + transform: translateX(50%); + } + + :host([dir='rtl'][orientation='horizontal']) [part='connector'] { + left: calc(var(--lumo-space-l) * -1); + right: 100%; + } +`; diff --git a/packages/stepper/src/styles/vaadin-stepper-styles.js b/packages/stepper/src/styles/vaadin-stepper-styles.js new file mode 100644 index 00000000000..9ca60aec9a5 --- /dev/null +++ b/packages/stepper/src/styles/vaadin-stepper-styles.js @@ -0,0 +1,60 @@ +/** + * @license + * Copyright (c) 2017 - 2025 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import { css } from 'lit'; + +export const stepperStyles = css` + :host { + display: block; + font-family: var(--lumo-font-family); + } + + :host([hidden]) { + display: none !important; + } + + [part='nav'] { + display: block; + } + + [part='list'] { + display: flex; + flex-direction: column; + gap: var(--lumo-space-l); + list-style: none; + margin: 0; + padding: 0; + counter-reset: step; + } + + /* Horizontal orientation */ + :host([orientation='horizontal']) [part='list'] { + flex-direction: row; + align-items: center; + } + + /* Small theme */ + :host([theme~='small']) [part='list'] { + gap: var(--lumo-space-m); + } + + /* Counter for step numbers */ + ::slotted(vaadin-step) { + counter-increment: step; + } + + /* Responsive behavior */ + @media (max-width: 1023px) { + :host([orientation='horizontal']) [part='list'] { + flex-direction: column; + align-items: stretch; + } + } + + /* RTL support */ + :host([dir='rtl']) [part='list'] { + direction: rtl; + } +`; diff --git a/packages/stepper/src/vaadin-step.js b/packages/stepper/src/vaadin-step.js new file mode 100644 index 00000000000..fca4cb446e2 --- /dev/null +++ b/packages/stepper/src/vaadin-step.js @@ -0,0 +1,370 @@ +/** + * @license + * Copyright (c) 2017 - 2025 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import { html, LitElement } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { DisabledMixin } from '@vaadin/a11y-base/src/disabled-mixin.js'; +import { defineCustomElement } from '@vaadin/component-base/src/define.js'; +import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js'; +import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js'; +import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js'; +import { matchPaths } from '@vaadin/component-base/src/url-utils.js'; +import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js'; +import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; +import { stepStyles } from './styles/vaadin-step-styles.js'; + +/** + * `` is a Web Component for displaying a single step in a stepper. + * + * ```html + * Step 1 + * ``` + * + * ### Styling + * + * The following shadow DOM parts are available for styling: + * + * Part name | Description + * -------------|---------------- + * `indicator` | The step indicator (circle with number/icon) + * `content` | The content wrapper containing label and description + * `label` | The step label + * `description`| The step description + * `connector` | The connector line to the next step + * + * The following state attributes are available for styling: + * + * Attribute | Description + * -------------|------------- + * `disabled` | Set when the element is disabled + * `completed` | Set when the step is completed + * `error` | Set when the step has an error + * `active` | Set when the step is active + * `last` | Set when this is the last step + * `orientation`| The orientation of the parent stepper (horizontal or vertical) + * `small` | Set when using small size variant + * + * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation. + * + * @customElement + * @extends HTMLElement + * @mixes DisabledMixin + * @mixes DirMixin + * @mixes ElementMixin + * @mixes ThemableMixin + */ +class Step extends DisabledMixin(DirMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(LitElement)))))) { + static get is() { + return 'vaadin-step'; + } + + static get styles() { + return stepStyles; + } + + static get properties() { + return { + /** + * The URL to navigate to + */ + href: { + type: String, + }, + + /** + * The target of the link + */ + target: { + type: String, + }, + + /** + * The label text + */ + label: { + type: String, + }, + + /** + * The description text + */ + description: { + type: String, + }, + + /** + * The state of the step + * @type {string} + */ + state: { + type: String, + value: 'inactive', + reflectToAttribute: true, + observer: '_stateChanged', + }, + + /** + * Whether to exclude the item from client-side routing + * @type {boolean} + * @attr {boolean} router-ignore + */ + routerIgnore: { + type: Boolean, + value: false, + }, + + /** + * The orientation from parent stepper + * @type {string} + * @private + */ + _orientation: { + type: String, + value: 'vertical', + reflectToAttribute: true, + attribute: 'orientation', + }, + + /** + * Whether this is the last step + * @type {boolean} + * @private + */ + _last: { + type: Boolean, + value: false, + reflectToAttribute: true, + attribute: 'last', + }, + + /** + * Whether using small size variant + * @type {boolean} + * @private + */ + _small: { + type: Boolean, + value: false, + reflectToAttribute: true, + attribute: 'small', + }, + + /** + * The step number + * @type {number} + * @private + */ + _stepNumber: { + type: Number, + value: 0, + }, + + /** + * Whether the step's href matches the current page + * @type {boolean} + */ + current: { + type: Boolean, + value: false, + readOnly: true, + reflectToAttribute: true, + }, + + /** + * Whether the step is completed + * @type {boolean} + */ + completed: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + + /** + * Whether the step has an error + * @type {boolean} + */ + error: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + + /** + * Whether the step is active + * @type {boolean} + */ + active: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + }; + } + + constructor() { + super(); + this.__boundUpdateCurrent = this.__updateCurrent.bind(this); + } + + /** @protected */ + render() { + const hasLink = !!this.href && !this.disabled; + const showConnector = !this._last; + + return html` + ${hasLink + ? html` + + ${this._renderContent()} + + ` + : html`
${this._renderContent()}
`} + ${showConnector ? html`` : ''} + `; + } + + /** @private */ + _renderContent() { + return html` + + + ${this.label || html``} + ${this.description ? html`${this.description}` : ''} + + `; + } + + /** @protected */ + firstUpdated() { + super.firstUpdated(); + + if (!this.hasAttribute('role')) { + this.setAttribute('role', 'listitem'); + } + } + + /** @protected */ + updated(props) { + super.updated(props); + + if (props.has('href')) { + this.__updateCurrent(); + } + } + + /** @protected */ + connectedCallback() { + super.connectedCallback(); + this.__updateCurrent(); + + window.addEventListener('popstate', this.__boundUpdateCurrent); + window.addEventListener('vaadin-navigated', this.__boundUpdateCurrent); + } + + /** @protected */ + disconnectedCallback() { + super.disconnectedCallback(); + + window.removeEventListener('popstate', this.__boundUpdateCurrent); + window.removeEventListener('vaadin-navigated', this.__boundUpdateCurrent); + } + + /** + * @param {boolean} last + * @private + */ + _setLast(last) { + this._last = last; + } + + /** + * @param {string} orientation + * @private + */ + _setOrientation(orientation) { + this._orientation = orientation; + } + + /** + * @param {boolean} small + * @private + */ + _setSmall(small) { + this._small = small; + } + + /** + * @param {number} stepNumber + * @private + */ + _setStepNumber(stepNumber) { + this._stepNumber = stepNumber; + } + + /** @private */ + _stateChanged() { + this._updateStateAttributes(); + } + + /** @private */ + __updateCurrent() { + if (!this.href) { + this._setCurrent(false); + return; + } + + const browserPath = `${location.pathname}${location.search}`; + const isCurrent = matchPaths(browserPath, this.href); + this._setCurrent(isCurrent); + + // Set active state if current + if (isCurrent) { + this.active = true; + this.completed = false; + this.error = false; + } + } + + /** @private */ + _updateStateAttributes() { + // Clear all states first + this.active = false; + this.completed = false; + this.error = false; + + // Set the appropriate state + switch (this.state) { + case 'active': + this.active = true; + break; + case 'completed': + this.completed = true; + break; + case 'error': + this.error = true; + break; + default: + // inactive state - all flags remain false + break; + } + } +} + +defineCustomElement(Step); + +export { Step }; diff --git a/packages/stepper/src/vaadin-stepper.js b/packages/stepper/src/vaadin-stepper.js new file mode 100644 index 00000000000..bacf28de0b7 --- /dev/null +++ b/packages/stepper/src/vaadin-stepper.js @@ -0,0 +1,195 @@ +/** + * @license + * Copyright (c) 2017 - 2025 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import './vaadin-step.js'; +import { html, LitElement } from 'lit'; +import { defineCustomElement } from '@vaadin/component-base/src/define.js'; +import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js'; +import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js'; +import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js'; +import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; +import { stepperStyles } from './styles/vaadin-stepper-styles.js'; + +/** + * `` is a Web Component for displaying a step-by-step process. + * + * ```html + * + * Step 1 + * Step 2 + * Step 3 + * + * ``` + * + * ### Styling + * + * The following shadow DOM parts are available for styling: + * + * Part name | Description + * -----------|---------------- + * `list` | The ordered list element containing steps + * + * The following attributes are available for styling: + * + * Attribute | Description + * --------------|------------- + * `orientation` | The orientation of the stepper (horizontal or vertical) + * `theme` | Can be set to `small` for compact size + * + * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation. + * + * @customElement + * @extends HTMLElement + * @mixes ElementMixin + * @mixes ThemableMixin + */ +class Stepper extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(LitElement)))) { + static get is() { + return 'vaadin-stepper'; + } + + static get styles() { + return stepperStyles; + } + + static get properties() { + return { + /** + * The orientation of the stepper + * @type {string} + * @attr {string} orientation + */ + orientation: { + type: String, + value: 'vertical', + reflectToAttribute: true, + }, + + /** + * The list of steps + * @type {!Array} + * @private + */ + _steps: { + type: Array, + }, + }; + } + + constructor() { + super(); + this._steps = []; + } + + /** @protected */ + render() { + return html` + + `; + } + + /** @protected */ + firstUpdated() { + super.firstUpdated(); + + if (!this.hasAttribute('role')) { + this.setAttribute('role', 'navigation'); + } + + this.setAttribute('aria-label', 'Progress'); + } + + /** @protected */ + updated(props) { + super.updated(props); + + if (props.has('orientation')) { + this._updateStepsOrientation(); + } + } + + /** @private */ + _onSlotChange() { + const slot = this.shadowRoot.querySelector('slot'); + const steps = slot.assignedElements().filter((el) => el.localName === 'vaadin-step'); + + this._steps = steps; + + // Update step properties + steps.forEach((step, index) => { + const isLast = index === steps.length - 1; + step._setLast(isLast); + step._setStepNumber(index + 1); + step._setOrientation(this.orientation); + + // Check if step has small theme + const theme = this.getAttribute('theme'); + const hasSmallTheme = theme && theme.includes('small'); + step._setSmall(hasSmallTheme); + }); + } + + /** @private */ + _updateStepsOrientation() { + if (this._steps) { + this._steps.forEach((step) => { + step._setOrientation(this.orientation); + }); + } + } + + /** + * Sets the state of a specific step + * @param {string} state - The state to set ('active', 'completed', 'error', 'inactive') + * @param {number} stepIndex - The index of the step to update + */ + setStepState(state, stepIndex) { + if (this._steps && this._steps[stepIndex]) { + this._steps[stepIndex].state = state; + this._steps[stepIndex].requestUpdate(); + } + } + + /** + * Marks steps up to the specified index as completed + * @param {number} untilIndex - Complete steps up to this index (exclusive) + */ + completeStepsUntil(untilIndex) { + if (this._steps) { + this._steps.forEach((step, index) => { + if (index < untilIndex) { + step.state = 'completed'; + } + }); + } + } + + /** + * Gets the current active step + * @return {Step|null} The active step or null if none is active + */ + getActiveStep() { + return this._steps.find((step) => step.active) || null; + } + + /** + * Resets all steps to inactive state + */ + reset() { + if (this._steps) { + this._steps.forEach((step) => { + step.state = 'inactive'; + }); + } + } +} + +defineCustomElement(Stepper); + +export { Stepper }; diff --git a/packages/stepper/test/dom/__snapshots__/stepper.test.snap.js b/packages/stepper/test/dom/__snapshots__/stepper.test.snap.js new file mode 100644 index 00000000000..70a1d4d5abc --- /dev/null +++ b/packages/stepper/test/dom/__snapshots__/stepper.test.snap.js @@ -0,0 +1,487 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["vaadin-stepper stepper host default"] = +` + + + + + + + +`; +/* end snapshot vaadin-stepper stepper host default */ + +snapshots["vaadin-stepper stepper host horizontal"] = +` + + + + + + + +`; +/* end snapshot vaadin-stepper stepper host horizontal */ + +snapshots["vaadin-stepper stepper host small theme"] = +` + + + + + + + +`; +/* end snapshot vaadin-stepper stepper host small theme */ + +snapshots["vaadin-stepper stepper shadow default"] = +` +`; +/* end snapshot vaadin-stepper stepper shadow default */ + +snapshots["vaadin-step step host default with href"] = +` + +`; +/* end snapshot vaadin-step step host default with href */ + +snapshots["vaadin-step step host default without href"] = +` + +`; +/* end snapshot vaadin-step step host default without href */ + +snapshots["vaadin-step step host with description"] = +` + +`; +/* end snapshot vaadin-step step host with description */ + +snapshots["vaadin-step step host active state"] = +` + +`; +/* end snapshot vaadin-step step host active state */ + +snapshots["vaadin-step step host completed state"] = +` + +`; +/* end snapshot vaadin-step step host completed state */ + +snapshots["vaadin-step step host error state"] = +` + +`; +/* end snapshot vaadin-step step host error state */ + +snapshots["vaadin-step step host disabled"] = +` + +`; +/* end snapshot vaadin-step step host disabled */ + +snapshots["vaadin-step step host with target"] = +` + +`; +/* end snapshot vaadin-step step host with target */ + +snapshots["vaadin-step step host router-ignore"] = +` + +`; +/* end snapshot vaadin-step step host router-ignore */ + +snapshots["vaadin-step step host focused"] = +` + +`; +/* end snapshot vaadin-step step host focused */ + +snapshots["vaadin-step step host focus-ring"] = +` + +`; +/* end snapshot vaadin-step step host focus-ring */ + +snapshots["vaadin-step step host last step"] = +` + +`; +/* end snapshot vaadin-step step host last step */ + +snapshots["vaadin-step step host horizontal orientation"] = +` + +`; +/* end snapshot vaadin-step step host horizontal orientation */ + +snapshots["vaadin-step step host small size"] = +` + +`; +/* end snapshot vaadin-step step host small size */ + +snapshots["vaadin-step step host with step number"] = +` + +`; +/* end snapshot vaadin-step step host with step number */ + +snapshots["vaadin-step step host current"] = +` + +`; +/* end snapshot vaadin-step step host current */ + +snapshots["vaadin-step step shadow default with href"] = +` + + + + Test Step + + + + +`; +/* end snapshot vaadin-step step shadow default with href */ + +snapshots["vaadin-step step shadow default without href"] = +`
+ + + + Test Step + + +
+ +`; +/* end snapshot vaadin-step step shadow default without href */ + +snapshots["vaadin-step step shadow with description"] = +`
+ + + + Test Step + + + Step description + + +
+ +`; +/* end snapshot vaadin-step step shadow with description */ + +snapshots["vaadin-step step shadow completed state"] = +`
+ + + + Completed Step + + +
+ +`; +/* end snapshot vaadin-step step shadow completed state */ + +snapshots["vaadin-step step shadow error state"] = +`
+ + + + Error Step + + +
+ +`; +/* end snapshot vaadin-step step shadow error state */ + +snapshots["vaadin-step step shadow disabled"] = +`
+ + + + Disabled Step + + +
+ +`; +/* end snapshot vaadin-step step shadow disabled */ + +snapshots["vaadin-step step shadow last step"] = +`
+ + + + Last Step + + +
+ +`; +/* end snapshot vaadin-step step shadow last step */ + diff --git a/packages/stepper/test/dom/stepper.test.js b/packages/stepper/test/dom/stepper.test.js new file mode 100644 index 00000000000..940809c3b2e --- /dev/null +++ b/packages/stepper/test/dom/stepper.test.js @@ -0,0 +1,189 @@ +import { expect } from '@vaadin/chai-plugins'; +import { sendKeys } from '@vaadin/test-runner-commands'; +import { fixtureSync } from '@vaadin/testing-helpers'; +import '../../src/vaadin-stepper.js'; +import '../../src/vaadin-step.js'; + +describe('vaadin-stepper', () => { + let stepper; + + describe('stepper host', () => { + beforeEach(() => { + stepper = fixtureSync(` + + + + + + `); + }); + + it('default', async () => { + await expect(stepper).dom.to.equalSnapshot(); + }); + + it('horizontal', async () => { + stepper.orientation = 'horizontal'; + await expect(stepper).dom.to.equalSnapshot(); + }); + + it('small theme', async () => { + stepper.setAttribute('theme', 'small'); + await expect(stepper).dom.to.equalSnapshot(); + }); + }); + + describe('stepper shadow', () => { + beforeEach(() => { + stepper = fixtureSync(` + + + + + + `); + }); + + it('default', async () => { + await expect(stepper).shadowDom.to.equalSnapshot(); + }); + }); +}); + +describe('vaadin-step', () => { + let step; + + describe('step host', () => { + it('default with href', async () => { + step = fixtureSync(''); + await expect(step).dom.to.equalSnapshot(); + }); + + it('default without href', async () => { + step = fixtureSync(''); + await expect(step).dom.to.equalSnapshot(); + }); + + it('with description', async () => { + step = fixtureSync(''); + await expect(step).dom.to.equalSnapshot(); + }); + + it('active state', async () => { + step = fixtureSync(''); + await expect(step).dom.to.equalSnapshot(); + }); + + it('completed state', async () => { + step = fixtureSync(''); + await expect(step).dom.to.equalSnapshot(); + }); + + it('error state', async () => { + step = fixtureSync(''); + await expect(step).dom.to.equalSnapshot(); + }); + + it('disabled', async () => { + step = fixtureSync(''); + await expect(step).dom.to.equalSnapshot(); + }); + + it('with target', async () => { + step = fixtureSync(''); + await expect(step).dom.to.equalSnapshot(); + }); + + it('router-ignore', async () => { + step = fixtureSync(''); + await expect(step).dom.to.equalSnapshot(); + }); + + it('focused', async () => { + step = fixtureSync(''); + step.focus(); + await expect(step).dom.to.equalSnapshot(); + }); + + it('focus-ring', async () => { + step = fixtureSync(''); + await sendKeys({ press: 'Tab' }); + await expect(step).dom.to.equalSnapshot(); + }); + + it('last step', async () => { + step = fixtureSync(''); + step._setLast(true); + await expect(step).dom.to.equalSnapshot(); + }); + + it('horizontal orientation', async () => { + step = fixtureSync( + '', + ); + step._setOrientation('horizontal'); + await expect(step).dom.to.equalSnapshot(); + }); + + it('small size', async () => { + step = fixtureSync(''); + step._setSmall(true); + await expect(step).dom.to.equalSnapshot(); + }); + + it('with step number', async () => { + step = fixtureSync(''); + step._setStepNumber(3); + await expect(step).dom.to.equalSnapshot(); + }); + + it('current', async () => { + step = fixtureSync(''); + step._setCurrent(true); + await expect(step).dom.to.equalSnapshot(); + }); + }); + + describe('step shadow', () => { + it('default with href', async () => { + step = fixtureSync(''); + step._setStepNumber(1); + await expect(step).shadowDom.to.equalSnapshot(); + }); + + it('default without href', async () => { + step = fixtureSync(''); + step._setStepNumber(1); + await expect(step).shadowDom.to.equalSnapshot(); + }); + + it('with description', async () => { + step = fixtureSync(''); + step._setStepNumber(1); + await expect(step).shadowDom.to.equalSnapshot(); + }); + + it('completed state', async () => { + step = fixtureSync(''); + await expect(step).shadowDom.to.equalSnapshot(); + }); + + it('error state', async () => { + step = fixtureSync(''); + await expect(step).shadowDom.to.equalSnapshot(); + }); + + it('disabled', async () => { + step = fixtureSync(''); + step._setStepNumber(1); + await expect(step).shadowDom.to.equalSnapshot(); + }); + + it('last step', async () => { + step = fixtureSync(''); + step._setLast(true); + step._setStepNumber(4); + await expect(step).shadowDom.to.equalSnapshot(); + }); + }); +}); diff --git a/packages/stepper/test/stepper.test.js b/packages/stepper/test/stepper.test.js new file mode 100644 index 00000000000..24867599bd6 --- /dev/null +++ b/packages/stepper/test/stepper.test.js @@ -0,0 +1,391 @@ +import { expect } from '@vaadin/chai-plugins'; +import { fixtureSync, nextFrame, nextRender } from '@vaadin/testing-helpers'; +import '../src/vaadin-stepper.js'; +import '../src/vaadin-step.js'; + +describe('vaadin-stepper', () => { + let stepper; + + describe('basic', () => { + beforeEach(() => { + stepper = fixtureSync(` + + + + + + `); + }); + + it('should have correct tag name', () => { + expect(stepper.localName).to.equal('vaadin-stepper'); + }); + + it('should have navigation role', () => { + expect(stepper.getAttribute('role')).to.equal('navigation'); + }); + + it('should have aria-label', () => { + expect(stepper.getAttribute('aria-label')).to.equal('Progress'); + }); + + it('should have vertical orientation by default', () => { + expect(stepper.orientation).to.equal('vertical'); + expect(stepper.getAttribute('orientation')).to.equal('vertical'); + }); + + it('should update steps array on slot change', async () => { + await nextFrame(); + expect(stepper._steps).to.have.lengthOf(3); + expect(stepper._steps[0].label).to.equal('Step 1'); + expect(stepper._steps[1].label).to.equal('Step 2'); + expect(stepper._steps[2].label).to.equal('Step 3'); + }); + + it('should mark last step', async () => { + await nextFrame(); + expect(stepper._steps[0].hasAttribute('last')).to.be.false; + expect(stepper._steps[1].hasAttribute('last')).to.be.false; + expect(stepper._steps[2].hasAttribute('last')).to.be.true; + }); + + it('should set step numbers', async () => { + await nextFrame(); + expect(stepper._steps[0]._stepNumber).to.equal(1); + expect(stepper._steps[1]._stepNumber).to.equal(2); + expect(stepper._steps[2]._stepNumber).to.equal(3); + }); + }); + + describe('orientation', () => { + beforeEach(() => { + stepper = fixtureSync(` + + + + + `); + }); + + it('should set horizontal orientation', () => { + expect(stepper.orientation).to.equal('horizontal'); + expect(stepper.getAttribute('orientation')).to.equal('horizontal'); + }); + + it('should update step orientation', async () => { + await nextFrame(); + expect(stepper._steps[0].getAttribute('orientation')).to.equal('horizontal'); + expect(stepper._steps[1].getAttribute('orientation')).to.equal('horizontal'); + }); + + it('should update orientation when changed', async () => { + await nextFrame(); + stepper.orientation = 'vertical'; + await nextFrame(); + expect(stepper._steps[0].getAttribute('orientation')).to.equal('vertical'); + expect(stepper._steps[1].getAttribute('orientation')).to.equal('vertical'); + }); + }); + + describe('theme', () => { + beforeEach(() => { + stepper = fixtureSync(` + + + + + `); + }); + + it('should apply small theme to steps', async () => { + await nextFrame(); + expect(stepper._steps[0].hasAttribute('small')).to.be.true; + expect(stepper._steps[1].hasAttribute('small')).to.be.true; + }); + }); + + describe('step states', () => { + beforeEach(() => { + stepper = fixtureSync(` + + + + + + `); + }); + + it('should set step state', async () => { + await nextFrame(); + stepper.setStepState('active', 0); + await nextRender(); + expect(stepper._steps[0].state).to.equal('active'); + expect(stepper._steps[0].hasAttribute('active')).to.be.true; + }); + + it('should complete steps until index', async () => { + await nextFrame(); + stepper.completeStepsUntil(2); + expect(stepper._steps[0].state).to.equal('completed'); + expect(stepper._steps[1].state).to.equal('completed'); + expect(stepper._steps[2].state).to.equal('inactive'); + }); + + it('should get active step', async () => { + await nextFrame(); + stepper.setStepState('active', 1); + await nextRender(); + const activeStep = stepper.getActiveStep(); + expect(activeStep).to.equal(stepper._steps[1]); + }); + + it('should return null when no active step', async () => { + await nextFrame(); + const activeStep = stepper.getActiveStep(); + expect(activeStep).to.be.null; + }); + + it('should reset all steps', async () => { + await nextFrame(); + stepper.setStepState('active', 0); + stepper.setStepState('completed', 1); + stepper.setStepState('error', 2); + + stepper.reset(); + + expect(stepper._steps[0].state).to.equal('inactive'); + expect(stepper._steps[1].state).to.equal('inactive'); + expect(stepper._steps[2].state).to.equal('inactive'); + }); + }); +}); + +describe('vaadin-step', () => { + let step; + + describe('basic', () => { + beforeEach(() => { + step = fixtureSync(``); + }); + + it('should have correct tag name', () => { + expect(step.localName).to.equal('vaadin-step'); + }); + + it('should have listitem role', () => { + expect(step.getAttribute('role')).to.equal('listitem'); + }); + + it('should render label', () => { + expect(step.label).to.equal('Test Step'); + }); + + it('should render description', () => { + expect(step.description).to.equal('Test description'); + }); + + it('should have inactive state by default', () => { + expect(step.state).to.equal('inactive'); + }); + }); + + describe('with href', () => { + beforeEach(() => { + step = fixtureSync(``); + }); + + it('should have href', () => { + expect(step.href).to.equal('/test'); + }); + + it('should render link element', () => { + const link = step.shadowRoot.querySelector('a'); + expect(link).to.exist; + expect(link.getAttribute('href')).to.equal('/test'); + }); + + it('should support target attribute', async () => { + step.target = '_blank'; + await nextFrame(); + const link = step.shadowRoot.querySelector('a'); + expect(link.getAttribute('target')).to.equal('_blank'); + }); + + it('should support router-ignore', async () => { + step.routerIgnore = true; + await nextFrame(); + const link = step.shadowRoot.querySelector('a'); + expect(link.hasAttribute('router-ignore')).to.be.true; + }); + }); + + describe('without href', () => { + beforeEach(() => { + step = fixtureSync(``); + }); + + it('should render div element', () => { + const div = step.shadowRoot.querySelector('div'); + expect(div).to.exist; + const link = step.shadowRoot.querySelector('a'); + expect(link).to.not.exist; + }); + }); + + describe('states', () => { + beforeEach(() => { + step = fixtureSync(``); + }); + + it('should set active state', async () => { + step.state = 'active'; + await nextRender(); + expect(step.hasAttribute('active')).to.be.true; + expect(step.hasAttribute('completed')).to.be.false; + expect(step.hasAttribute('error')).to.be.false; + }); + + it('should set completed state', async () => { + step.state = 'completed'; + await nextRender(); + expect(step.hasAttribute('completed')).to.be.true; + expect(step.hasAttribute('active')).to.be.false; + expect(step.hasAttribute('error')).to.be.false; + }); + + it('should set error state', async () => { + step.state = 'error'; + await nextRender(); + expect(step.hasAttribute('error')).to.be.true; + expect(step.hasAttribute('active')).to.be.false; + expect(step.hasAttribute('completed')).to.be.false; + }); + + it('should show checkmark for completed state', async () => { + step.state = 'completed'; + await nextFrame(); + const indicator = step.shadowRoot.querySelector('[part="indicator"]'); + expect(indicator.querySelector('.checkmark')).to.exist; + }); + + it('should show error icon for error state', async () => { + step.state = 'error'; + await nextFrame(); + const indicator = step.shadowRoot.querySelector('[part="indicator"]'); + expect(indicator.querySelector('.error-icon')).to.exist; + }); + + it('should show step number for other states', async () => { + step._setStepNumber(3); + await nextFrame(); + const indicator = step.shadowRoot.querySelector('[part="indicator"]'); + expect(indicator.querySelector('.step-number').textContent).to.equal('3'); + }); + }); + + describe('disabled', () => { + beforeEach(() => { + step = fixtureSync(``); + }); + + it('should be disabled', () => { + expect(step.disabled).to.be.true; + }); + + it('should have disabled attribute', () => { + expect(step.hasAttribute('disabled')).to.be.true; + }); + + it('should render as div when disabled', () => { + const div = step.shadowRoot.querySelector('div'); + expect(div).to.exist; + const link = step.shadowRoot.querySelector('a'); + expect(link).to.not.exist; + }); + }); + + describe('current page detection', () => { + let originalLocation; + + beforeEach(() => { + originalLocation = window.location.href; + window.history.pushState({}, '', '/test-page'); + step = fixtureSync(``); + }); + + afterEach(() => { + window.history.pushState({}, '', originalLocation); + }); + + it('should detect current page', () => { + expect(step.current).to.be.true; + expect(step.active).to.be.true; + }); + + it('should set aria-current for current page', async () => { + await nextRender(); + const link = step.shadowRoot.querySelector('a'); + expect(link.getAttribute('aria-current')).to.equal('step'); + }); + + it('should update on navigation', async () => { + window.history.pushState({}, '', '/other-page'); + window.dispatchEvent(new PopStateEvent('popstate')); + await nextFrame(); + expect(step.current).to.be.false; + }); + }); + + describe('orientation', () => { + beforeEach(() => { + step = fixtureSync( + ``, + ); + }); + + it('should have vertical orientation by default', () => { + expect(step.getAttribute('orientation')).to.equal('vertical'); + }); + + it('should update orientation', async () => { + step._setOrientation('horizontal'); + await nextRender(); + expect(step.getAttribute('orientation')).to.equal('horizontal'); + }); + }); + + describe('size', () => { + beforeEach(() => { + step = fixtureSync(``); + }); + + it('should not be small by default', () => { + expect(step.hasAttribute('small')).to.be.false; + }); + + it('should set small size', async () => { + step._setSmall(true); + await nextRender(); + + expect(step.hasAttribute('small')).to.be.true; + }); + }); + + describe('connector', () => { + beforeEach(() => { + step = fixtureSync(``); + }); + + it('should show connector by default', () => { + const connector = step.shadowRoot.querySelector('[part="connector"]'); + expect(connector).to.exist; + }); + + it('should hide connector for last step', async () => { + step._setLast(true); + await nextFrame(); + const connector = step.shadowRoot.querySelector('[part="connector"]'); + expect(connector).to.not.exist; + }); + }); +}); diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/all-states.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/all-states.png new file mode 100644 index 00000000000..d71c6a96e84 Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/all-states.png differ diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/disabled.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/disabled.png new file mode 100644 index 00000000000..f2617b3129b Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/disabled.png differ diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/focus-ring.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/focus-ring.png new file mode 100644 index 00000000000..dd7e4818b66 Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/focus-ring.png differ diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-default.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-default.png new file mode 100644 index 00000000000..071b9597367 Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-default.png differ diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-small.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-small.png new file mode 100644 index 00000000000..3086249a2e8 Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-small.png differ diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-with-descriptions.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-with-descriptions.png new file mode 100644 index 00000000000..4c4c9d835f9 Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-with-descriptions.png differ diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/hover.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/hover.png new file mode 100644 index 00000000000..dd7e4818b66 Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/hover.png differ diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/no-href.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/no-href.png new file mode 100644 index 00000000000..51b488f99fe Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/no-href.png differ diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/rtl-horizontal.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/rtl-horizontal.png new file mode 100644 index 00000000000..04dbfc6f1f9 Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/rtl-horizontal.png differ diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/rtl-vertical.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/rtl-vertical.png new file mode 100644 index 00000000000..83b8d7bfe61 Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/rtl-vertical.png differ diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-default.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-default.png new file mode 100644 index 00000000000..395b70d1634 Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-default.png differ diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-no-descriptions.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-no-descriptions.png new file mode 100644 index 00000000000..43e3c40261e Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-no-descriptions.png differ diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-small.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-small.png new file mode 100644 index 00000000000..de3374fa7b7 Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-small.png differ diff --git a/packages/stepper/test/visual/base/stepper.test.js b/packages/stepper/test/visual/base/stepper.test.js new file mode 100644 index 00000000000..3c1379fea4c --- /dev/null +++ b/packages/stepper/test/visual/base/stepper.test.js @@ -0,0 +1,245 @@ +import { resetMouse, sendKeys, sendMouseToElement } from '@vaadin/test-runner-commands'; +import { fixtureSync } from '@vaadin/testing-helpers'; +import { visualDiff } from '@web/test-runner-visual-regression'; +import '../../../src/vaadin-stepper.js'; +import '../../../src/vaadin-step.js'; + +describe('stepper', () => { + let div, element; + + afterEach(async () => { + await resetMouse(); + }); + + describe('vertical', () => { + it('default', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'vertical-default'); + }); + + it('small', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'vertical-small'); + }); + + it('without-descriptions', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'vertical-no-descriptions'); + }); + }); + + describe('horizontal', () => { + it('default', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '800px'; + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'horizontal-default'); + }); + + it('small', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '800px'; + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'horizontal-small'); + }); + + it('with-descriptions', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '1000px'; + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'horizontal-with-descriptions'); + }); + }); + + describe('states', () => { + it('all-states', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'all-states'); + }); + + it('hover', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + element = fixtureSync( + ` + + + + `, + div, + ); + const step = element.querySelector('vaadin-step[href="/step3"]'); + await sendMouseToElement({ type: 'move', element: step }); + await visualDiff(div, 'hover'); + }); + + it('focus-ring', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + element = fixtureSync( + ` + + + + `, + div, + ); + await sendKeys({ press: 'Tab' }); + await visualDiff(div, 'focus-ring'); + }); + + it('disabled', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + element = fixtureSync( + ` + + + + `, + div, + ); + await visualDiff(div, 'disabled'); + }); + }); + + describe('without-links', () => { + it('no-href', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'no-href'); + }); + }); + + describe('RTL', () => { + it('rtl-vertical', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + div.setAttribute('dir', 'rtl'); + element = fixtureSync( + ` + + + + `, + div, + ); + await visualDiff(div, 'rtl-vertical'); + }); + + it('rtl-horizontal', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '800px'; + div.setAttribute('dir', 'rtl'); + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'rtl-horizontal'); + }); + }); +}); diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/all-states.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/all-states.png new file mode 100644 index 00000000000..57ff5e2ab72 Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/all-states.png differ diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/disabled.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/disabled.png new file mode 100644 index 00000000000..3a124fb4092 Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/disabled.png differ diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/focus-ring.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/focus-ring.png new file mode 100644 index 00000000000..53741cb86a7 Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/focus-ring.png differ diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-default.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-default.png new file mode 100644 index 00000000000..4ca0710f006 Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-default.png differ diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-small.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-small.png new file mode 100644 index 00000000000..a9d56c4cfce Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-small.png differ diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-with-descriptions.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-with-descriptions.png new file mode 100644 index 00000000000..0d1a3346e02 Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-with-descriptions.png differ diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/hover.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/hover.png new file mode 100644 index 00000000000..1637ed23dbc Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/hover.png differ diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/no-href.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/no-href.png new file mode 100644 index 00000000000..d2e59c197d8 Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/no-href.png differ diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/rtl-horizontal.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/rtl-horizontal.png new file mode 100644 index 00000000000..c1f77d32bda Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/rtl-horizontal.png differ diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/rtl-vertical.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/rtl-vertical.png new file mode 100644 index 00000000000..4f084c7e3c1 Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/rtl-vertical.png differ diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-default.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-default.png new file mode 100644 index 00000000000..b238f3d883c Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-default.png differ diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-no-descriptions.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-no-descriptions.png new file mode 100644 index 00000000000..0121004a547 Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-no-descriptions.png differ diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-small.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-small.png new file mode 100644 index 00000000000..2a3abb6a324 Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-small.png differ diff --git a/packages/stepper/test/visual/lumo/stepper.test.js b/packages/stepper/test/visual/lumo/stepper.test.js new file mode 100644 index 00000000000..2904b2ae3b1 --- /dev/null +++ b/packages/stepper/test/visual/lumo/stepper.test.js @@ -0,0 +1,246 @@ +import { resetMouse, sendKeys, sendMouseToElement } from '@vaadin/test-runner-commands'; +import { fixtureSync } from '@vaadin/testing-helpers'; +import { visualDiff } from '@web/test-runner-visual-regression'; +import '@vaadin/vaadin-lumo-styles/props.css'; +import '../../../vaadin-stepper.js'; +import '../../../vaadin-step.js'; + +describe('stepper', () => { + let div, element; + + afterEach(async () => { + await resetMouse(); + }); + + describe('vertical', () => { + it('default', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'vertical-default'); + }); + + it('small', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'vertical-small'); + }); + + it('without-descriptions', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'vertical-no-descriptions'); + }); + }); + + describe('horizontal', () => { + it('default', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '800px'; + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'horizontal-default'); + }); + + it('small', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '800px'; + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'horizontal-small'); + }); + + it('with-descriptions', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '1000px'; + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'horizontal-with-descriptions'); + }); + }); + + describe('states', () => { + it('all-states', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'all-states'); + }); + + it('hover', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + element = fixtureSync( + ` + + + + `, + div, + ); + const step = element.querySelector('vaadin-step[href="/step3"]'); + await sendMouseToElement({ type: 'move', element: step }); + await visualDiff(div, 'hover'); + }); + + it('focus-ring', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + element = fixtureSync( + ` + + + + `, + div, + ); + await sendKeys({ press: 'Tab' }); + await visualDiff(div, 'focus-ring'); + }); + + it('disabled', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + element = fixtureSync( + ` + + + + `, + div, + ); + await visualDiff(div, 'disabled'); + }); + }); + + describe('without-links', () => { + it('no-href', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'no-href'); + }); + }); + + describe('RTL', () => { + it('rtl-vertical', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '400px'; + div.setAttribute('dir', 'rtl'); + element = fixtureSync( + ` + + + + `, + div, + ); + await visualDiff(div, 'rtl-vertical'); + }); + + it('rtl-horizontal', async () => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + div.style.width = '800px'; + div.setAttribute('dir', 'rtl'); + element = fixtureSync( + ` + + + + + `, + div, + ); + await visualDiff(div, 'rtl-horizontal'); + }); + }); +}); diff --git a/packages/stepper/vaadin-step.d.ts b/packages/stepper/vaadin-step.d.ts new file mode 100644 index 00000000000..2ea025ec924 --- /dev/null +++ b/packages/stepper/vaadin-step.d.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright (c) 2017 - 2025 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import { DisabledMixin } from '@vaadin/a11y-base/src/disabled-mixin.js'; +import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js'; +import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js'; +import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; + +export type StepState = 'active' | 'completed' | 'error' | 'inactive'; + +/** + * `` is a Web Component for displaying a single step in a stepper. + * + * ```html + * Step 1 + * ``` + */ +declare class Step extends DisabledMixin(DirMixin(ElementMixin(ThemableMixin(HTMLElement)))) { + /** + * The URL to navigate to + */ + href: string | null | undefined; + + /** + * The target of the link + */ + target: string | null | undefined; + + /** + * The label text + */ + label: string | null | undefined; + + /** + * The description text + */ + description: string | null | undefined; + + /** + * The state of the step + * @attr {string} state + */ + state: StepState; + + /** + * Whether to exclude the item from client-side routing + * @attr {boolean} router-ignore + */ + routerIgnore: boolean; + + /** + * Whether the step's href matches the current page + */ + readonly current: boolean; + + /** + * Whether the step is completed + */ + completed: boolean; + + /** + * Whether the step has an error + */ + error: boolean; + + /** + * Whether the step is active + */ + active: boolean; +} + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-step': Step; + } +} + +export { Step }; diff --git a/packages/stepper/vaadin-step.js b/packages/stepper/vaadin-step.js new file mode 100644 index 00000000000..1e77aed654c --- /dev/null +++ b/packages/stepper/vaadin-step.js @@ -0,0 +1,6 @@ +/** + * @license + * Copyright (c) 2017 - 2025 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +export { Step } from './src/vaadin-step.js'; diff --git a/packages/stepper/vaadin-stepper.d.ts b/packages/stepper/vaadin-stepper.d.ts new file mode 100644 index 00000000000..381faaa4c42 --- /dev/null +++ b/packages/stepper/vaadin-stepper.d.ts @@ -0,0 +1,56 @@ +/** + * @license + * Copyright (c) 2017 - 2025 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js'; +import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; + +export type StepperOrientation = 'horizontal' | 'vertical'; + +/** + * `` is a Web Component for displaying a step-by-step process. + * + * ```html + * + * Step 1 + * Step 2 + * Step 3 + * + * ``` + */ +declare class Stepper extends ElementMixin(ThemableMixin(HTMLElement)) { + /** + * The orientation of the stepper + * @attr {string} orientation + */ + orientation: StepperOrientation; + + /** + * Sets the state of a specific step + */ + setStepState(state: string, stepIndex: number): void; + + /** + * Marks steps up to the specified index as completed + */ + completeStepsUntil(untilIndex: number): void; + + /** + * Gets the current active step + */ + getActiveStep(): any | null; + + /** + * Resets all steps to inactive state + */ + reset(): void; +} + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-stepper': Stepper; + } +} + +export { Stepper }; diff --git a/packages/stepper/vaadin-stepper.js b/packages/stepper/vaadin-stepper.js new file mode 100644 index 00000000000..37baeaba5a3 --- /dev/null +++ b/packages/stepper/vaadin-stepper.js @@ -0,0 +1,6 @@ +/** + * @license + * Copyright (c) 2017 - 2025 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +export { Stepper } from './src/vaadin-stepper.js';