@rhds/elements
Version:
Red Hat Design System Elements
274 lines • 16.2 kB
JavaScript
var _RhProgressStepper_instances, _RhProgressStepper_maxWidth, _RhProgressStepper_contentString, _RhProgressStepper_resizeTimeoutId, _RhProgressStepper_ro, _RhProgressStepper_onChange, _RhProgressStepper_updateState;
import { __classPrivateFieldGet, __classPrivateFieldSet, __decorate } from "tslib";
import { LitElement, html, isServer } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { property } from 'lit/decorators/property.js';
import { state } from 'lit/decorators/state.js';
import { classMap } from 'lit/directives/class-map.js';
import { provide } from '@lit/context';
import { observes } from '@patternfly/pfe-core/decorators/observes.js';
import { themable } from '@rhds/elements/lib/themable.js';
import { Breakpoint2xl, BreakpointLg, BreakpointMd, BreakpointSm, BreakpointXl, BreakpointXs, } from '@rhds/tokens/media.js';
import '@rhds/elements/rh-icon/rh-icon.js';
import { compactContext, currentStepContext } from './context.js';
import { RhProgressStep, RhProgressStepChangeEvent } from './rh-progress-step.js';
export * from './rh-progress-step.js';
import { css } from "lit";
const styles = css `*,:after,:before{box-sizing:border-box}:host{--_step-line-color:var(--rh-color-border-strong);--_current-step-color:var(--_rh-color-status-disabled,#6a6e73);--_current-step-label-color:var(--rh-color-text-primary);--_step-line-angle:-180deg;--_step-line-position-block:0 100%;--_step-line-position-inline:calc(-100% - var(--_step-icon-width)*-1) 100%;--_step-line-size-inline:calc(100% - var(--_step-icon-width));--_step-line-size-block:100%;--_step-template-areas:"icon" "label" "description";--_step-template-columns:auto;--_step-template-rows:calc(var(--rh-length-xl, 24px) + 4px) auto 1fr;--_step-justify-self:baseline;--_step-compact-vertical-padding:0;--_step-compact-horizontal-padding:var(
--rh-length-2xl,32px
);--_step-column-gap:var(
--rh-length-lg,16px
);--_step-row-gap:var(
--rh-length-xs,4px
);--_step-compact-grid-row:2;--_step-description-margin:inherit;--_step-align-items:center;--_step-icon-width:calc(var(--rh-size-icon-02, 24px) + 4px);container-type:inline-size;display:block}#container{display:flex;flex-direction:column;gap:var(--rh-length-lg,16px)}#step-list{padding:0;display:grid;place-content:baseline normal;grid-auto-flow:column;grid-auto-columns:var(--_stepper-grid-auto-column,1fr)}.visually-hidden{block-size:1px;border:0;clip:rect(0,0,0,0);inline-size:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;color:var(--_current-step-label-color);font-weight:var(
--_current-step-weight,var(--rh-font-weight-body-text-medium,500)
)}::slotted(rh-progress-step:first-of-type){--_step-line-style:none}::slotted(:not([state],[icon])),::slotted([state=inactive]){--_step-line-color:var(--rh-color-border-subtle)!important}.vertical{--_step-line-angle:90deg;--_step-line-position-block:calc(-100% - var(--_step-icon-width)*-1) 100%;--_step-line-position-inline:0 100%;--_step-line-size-inline:100%;--_step-line-size-block:calc(100% - var(--_step-icon-width));--_step-template-areas:"icon label" "line description";--_step-template-columns:var(--_step-icon-width) auto;--_step-template-rows:auto;--_step-justify-self:start;--_step-icon-justify-self:center;--_step-description-grid-column:2;--_step-compact-vertical-padding:var(--rh-length-2xl,32px);--_step-column-gap:var(--rh-length-lg,16px);--_step-row-gap:var(--rh-length-xs,4px);--_step-align-items:baseline}.vertical #step-list{grid-auto-flow:row}.vertical ::slotted(rh-progress-step){--_step-compact-horizontal-padding:0}.compact{grid-template-rows:auto;--_step-template-areas:"icon";--_step-grid-row:var(--_step-compact-grid-row);--_step-template-rows:auto;--_step-compact-vertical-padding:0;--_step-compact-horizontal-padding:var(--rh-length-lg,16px);--_step-template-columns:var(--_step-icon-width);--_step-row-gap:0;--_step-column-gap:0;--_stepper-grid-auto-column:calc(var(--_step-icon-width) + var(--_step-compact-horizontal-padding))}.compact.vertical{--_step-template-areas:"icon" "line";--_step-compact-horizontal-padding:0;--_step-compact-vertical-padding:var(
--rh-length-lg,16px
)}.compact ::slotted(rh-progress-step){--_step-compact-horizontal-padding:var(
--rh-length-lg,16px
)}.compact #current-step{block-size:auto;border:none;clip:auto;inline-size:auto;margin:0;overflow:auto;padding:0;position:relative;white-space:normal;grid-column:1/-1;grid-row:1}.inactive{--_current-step-color:var(--_rh-color-status-disabled,#6a6e73)}.active{--_current-step-color:var(--rh-color-status-note)}.complete{--_current-step-color:var(--rh-color-status-success)}.warn{--_current-step-color:light-dark(var(--rh-color-yellow-50,#b98412),var(--rh-color-yellow-30,#ffcc17));--_current-step-label-color:light-dark(var(--rh-color-yellow-60,#96640f),var(--rh-color-yellow-20,#ffe072))!important}.fail{--_current-step-color:var(--rh-color-status-danger);--_current-step-label-color:var(--rh-color-status-danger)!important}`;
/**
* Breakpoint mappings for responsive behavior.
* Used to determine when to switch to vertical orientation
* based on container width rather than viewport width.
*/
const BREAKPOINTS = new Map(Object.entries({
'2xs': '320px',
'xs': BreakpointXs,
'sm': BreakpointSm,
'md': BreakpointMd,
'lg': BreakpointLg,
'xl': BreakpointXl,
'2xl': Breakpoint2xl,
}));
/**
* A progress stepper conveys the steps necessary to complete a process or task, and the status of
* each step. Steps have titles and descriptions; and each step can be in one of a number of possible states:
* - inactive (yet to be performed)
* - active (currently being performed)
* - warn (succeeded, but with warnings)
* - fail (failed to occur)
* Or a custom state, set using the `icon` attribute.
*
* ## Usage guidelines
* - Use 3-5 steps maximum to reduce cognitive load
* - Designed to complement standard previous/next navigation. Avoid using as the only navigation.
* - When process is completed, users cannot go back and must start over
*
* ## Accessibility
* - Communicates list structure and step states to screen readers
* - Supports keyboard navigation for linked step titles
* - Maintains logical focus order (left to right, top to bottom)
* - Provides aria-current for the active step
*
* @summary Communicate how many steps are required to complete a process
*
* @alias Progress stepper
*/
let RhProgressStepper = class RhProgressStepper extends LitElement {
constructor() {
super(...arguments);
_RhProgressStepper_instances.add(this);
/**
* Makes the element `vertical` at various container query based breakpoints.
* Breakpoints available 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
*
* Use when horizontal space becomes limited. The element automatically
* changes to vertical orientation at screen sizes of <768px.
*/
this.verticalAt = undefined;
/**
* Sets the orientation of the progress stepper.
* - `horizontal` - Steps are displayed in a horizontal row (default)
* - `vertical` - Steps are displayed in a vertical column
*
* ## Responsive behavior
* - >992px: Padding between steps is set to --rh-space-5xl
* - ≤992px: Padding reduces to --rh-space-2xl
* - <768px: Orientation automatically changes to vertical
*
* Use vertical orientation when horizontal space is limited or when
* you need to display more detailed step information.
*/
this.orientation = 'horizontal';
/**
* Makes element display as `compact`.
*
* ## Usage guidelines
* - Use when there is limited space and less visual prominence is needed
* - Maintain the compact size as designed - do not stretch spacing between steps
* - Switch to default size or different orientation instead of stretching compact
* - Always include step titles even in compact mode for accessibility
*/
this.compact = false;
/**
* Defines the current step, so it can be marked as such with ARIA,
* and so its label can be displayed in compact layouts.
*
* ## Accessibility
* This property ensures only one step is marked with aria-current="step"
* as required by ARIA specification. Screen readers announce this step
* as the current location in the process.
*/
this.currentStep = null;
/**
* Set when ResizeObserver detects width is less than the breakpoint (default: `--rh-breakpoint-sm`)
* When true, the stepper switches to vertical orientation automatically.
*/
this.mobile = true;
/**
* Set to match current step's `state`
*/
this.currentState = '';
_RhProgressStepper_maxWidth.set(this, 768);
/**
* Normalized string content of the current step
* Extracts text content from the current step's title and description
* for screen reader accessibility and visual display.
*/
_RhProgressStepper_contentString.set(this, '');
_RhProgressStepper_resizeTimeoutId.set(this, void 0);
/**
* ResizeObserver for responsive behavior.
* This callback is debounced with a simple timeout to prevent excessive updates.
*
* In the future, we should consider StyleObserver:
* @see https://www.bram.us/2025/02/24/solved-by-styleobserver-element-matchcontainer/
* @see https://github.com/LeaVerou/style-observer/
*/
_RhProgressStepper_ro.set(this, new ResizeObserver(entries => {
if (this.compact || this.orientation === 'vertical') {
return;
}
if (__classPrivateFieldGet(this, _RhProgressStepper_resizeTimeoutId, "f")) {
clearTimeout(__classPrivateFieldGet(this, _RhProgressStepper_resizeTimeoutId, "f"));
}
__classPrivateFieldSet(this, _RhProgressStepper_resizeTimeoutId, window.setTimeout(() => {
const [{ contentBoxSize: [{ inlineSize } = {}] = [] } = {}] = entries;
if (inlineSize != null) {
this.mobile = inlineSize < __classPrivateFieldGet(this, _RhProgressStepper_maxWidth, "f");
}
}, 100), "f");
}));
}
/**
* Initializes responsive behavior on first update.
* Sets mobile state based on element width,
* ensuring the stepper displays correctly on initial load.
*/
firstUpdated() {
// ensure we update initially on client hydration
const isHydrated = isServer && !this.hasUpdated;
if (!isHydrated) {
this.mobile = this.offsetWidth < __classPrivateFieldGet(this, _RhProgressStepper_maxWidth, "f");
}
}
connectedCallback() {
super.connectedCallback();
// Set ARIA role="list" to communicate list structure to screen reader
this.role = 'list';
if (!isServer) {
__classPrivateFieldGet(this, _RhProgressStepper_ro, "f")?.observe(this);
__classPrivateFieldGet(this, _RhProgressStepper_instances, "m", _RhProgressStepper_updateState).call(this);
}
this.addEventListener('change', __classPrivateFieldGet(this, _RhProgressStepper_instances, "m", _RhProgressStepper_updateState));
}
render() {
const compact = this.compact ?? false;
const vertical = this.orientation === 'vertical' || this.mobile;
const currentState = this.currentState || '';
return html `
<div id="container" class="${classMap({ compact, vertical, [currentState]: true })}">
<!-- "Current step" label for screen readers and compact display -->
<!-- Visually hidden except in compact mode -->
<strong id="current-step"
class="visually-hidden"
?hidden="${!compact}">${__classPrivateFieldGet(this, _RhProgressStepper_contentString, "f")}</strong>
<!-- Use this slot for \`<rh-progress-step>\` items
Each step should include title and optional description -->
<slot id="step-list" ="${__classPrivateFieldGet(this, _RhProgressStepper_instances, "m", _RhProgressStepper_onChange)}"></slot>
</div>
`;
}
/**
* Handles changes to the verticalAt property.
* Updates the breakpoint threshold for responsive vertical orientation switching.
*/
verticalAtChanged() {
const breakpoint = BREAKPOINTS.get(this.verticalAt);
if (breakpoint) {
const int = parseInt(breakpoint.replace('px', ''));
if (!Number.isNaN(breakpoint)) {
__classPrivateFieldSet(this, _RhProgressStepper_maxWidth, int, "f");
}
}
}
};
_RhProgressStepper_maxWidth = new WeakMap();
_RhProgressStepper_contentString = new WeakMap();
_RhProgressStepper_resizeTimeoutId = new WeakMap();
_RhProgressStepper_ro = new WeakMap();
_RhProgressStepper_instances = new WeakSet();
_RhProgressStepper_onChange = function _RhProgressStepper_onChange(event) {
if (event instanceof RhProgressStepChangeEvent) {
__classPrivateFieldGet(this, _RhProgressStepper_instances, "m", _RhProgressStepper_updateState).call(this);
}
};
_RhProgressStepper_updateState = function _RhProgressStepper_updateState() {
// Identifies all steps with `[state=active]`, `fail` or `warn`
// `[state=complete]` is not a stateful step, since `complete` is always a past step
const statefulSteps = this.querySelectorAll(/* css */ `
rh-progress-step:is([state="active"], [state="fail"], [state="warn"], [icon]),
rh-progress-step:has(> [slot=icon])
`);
// always, only take the last item in the list, in order to prevent having more
// than one aria-current step, which is not approved of in the aria spec
// see https://w3c.github.io/aria/#aria-current
const activeStep = Array.from(statefulSteps).at(-1);
this.currentStep = activeStep instanceof RhProgressStep ? activeStep : null;
if (this.currentStep) {
this.currentState = this.currentStep.state || '';
__classPrivateFieldSet(this, _RhProgressStepper_contentString, '', "f");
// Use childNodes instead of children to access both Element and Text nodes
// This ensures we capture all text content for accessibility
for (const node of this.currentStep.childNodes) {
if (node instanceof Element && !node.hasAttribute('slot')) {
__classPrivateFieldSet(this, _RhProgressStepper_contentString, __classPrivateFieldGet(this, _RhProgressStepper_contentString, "f") + node.textContent?.trim(), "f");
}
else if (node instanceof Text) {
__classPrivateFieldSet(this, _RhProgressStepper_contentString, __classPrivateFieldGet(this, _RhProgressStepper_contentString, "f") + node.data.trim(), "f");
}
}
;
}
};
RhProgressStepper.styles = [styles];
__decorate([
property({ reflect: true, attribute: 'vertical-at' })
], RhProgressStepper.prototype, "verticalAt", void 0);
__decorate([
property({ reflect: true })
], RhProgressStepper.prototype, "orientation", void 0);
__decorate([
provide({ context: compactContext }),
property({ reflect: true, type: Boolean })
], RhProgressStepper.prototype, "compact", void 0);
__decorate([
provide({ context: currentStepContext }),
state()
], RhProgressStepper.prototype, "currentStep", void 0);
__decorate([
state()
], RhProgressStepper.prototype, "mobile", void 0);
__decorate([
state()
], RhProgressStepper.prototype, "currentState", void 0);
__decorate([
observes('verticalAt')
], RhProgressStepper.prototype, "verticalAtChanged", null);
RhProgressStepper = __decorate([
customElement('rh-progress-stepper'),
themable
], RhProgressStepper);
export { RhProgressStepper };
//# sourceMappingURL=rh-progress-stepper.js.map