From 12dfe466ebb7cb8bcdf8523fded4c8d63d33b4ad Mon Sep 17 00:00:00 2001 From: rajsite <1588923+rajsite@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:06:21 -0600 Subject: [PATCH 01/26] Severity text pattern first pass --- .../src/patterns/severity/styles.ts | 29 +++++ .../src/patterns/severity/template.ts | 14 +++ .../testing/severity-pattern.pageobject.ts | 41 +++++++ .../severity/tests/severity-pattern.spec.ts | 100 ++++++++++++++++++ .../src/patterns/severity/types.ts | 56 ++++++++++ 5 files changed, 240 insertions(+) create mode 100644 packages/nimble-components/src/patterns/severity/styles.ts create mode 100644 packages/nimble-components/src/patterns/severity/template.ts create mode 100644 packages/nimble-components/src/patterns/severity/testing/severity-pattern.pageobject.ts create mode 100644 packages/nimble-components/src/patterns/severity/tests/severity-pattern.spec.ts create mode 100644 packages/nimble-components/src/patterns/severity/types.ts diff --git a/packages/nimble-components/src/patterns/severity/styles.ts b/packages/nimble-components/src/patterns/severity/styles.ts new file mode 100644 index 0000000000..559b0388f7 --- /dev/null +++ b/packages/nimble-components/src/patterns/severity/styles.ts @@ -0,0 +1,29 @@ +import { css } from '@ni/fast-element'; +import { + failColor, + errorTextFont, + errorTextFontLineHeight +} from '../../theme-provider/design-tokens'; + +export const styles = css` + .severity-text { + display: none; + } + + :host([severity]) .severity-text { + display: block; + font: ${errorTextFont}; + color: ${failColor}; + width: 100%; + position: absolute; + bottom: calc(-1 * (${errorTextFontLineHeight} + 2px)); + left: 0px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + :host([severity]) .severity-text:empty { + display: none; + } +`; diff --git a/packages/nimble-components/src/patterns/severity/template.ts b/packages/nimble-components/src/patterns/severity/template.ts new file mode 100644 index 0000000000..38553ed07d --- /dev/null +++ b/packages/nimble-components/src/patterns/severity/template.ts @@ -0,0 +1,14 @@ +import { html } from '@ni/fast-element'; +import type { SeverityPattern } from './types'; +import { overflow } from '../../utilities/directive/overflow'; + +export const severityTextTemplate = html` +
+ ${x => x.severityText} +
+`; diff --git a/packages/nimble-components/src/patterns/severity/testing/severity-pattern.pageobject.ts b/packages/nimble-components/src/patterns/severity/testing/severity-pattern.pageobject.ts new file mode 100644 index 0000000000..7c6f539807 --- /dev/null +++ b/packages/nimble-components/src/patterns/severity/testing/severity-pattern.pageobject.ts @@ -0,0 +1,41 @@ +import type { FoundationElement } from '@ni/fast-foundation'; +import { processUpdates } from '../../../testing/async-helpers'; + +/** + * A page object for the elements that use the error pattern mixin. + */ +export class SeverityPatternPageObject { + public constructor(private readonly element: FoundationElement) {} + + public getDisplayedSeverityText(): string { + const severityTextDiv = this.getSeverityTextElement(); + if ( + !severityTextDiv + || getComputedStyle(severityTextDiv).display === 'none' + ) { + return ''; + } + return severityTextDiv.textContent?.trim() ?? ''; + } + + public getSeverityTextTitle(): string { + const severityTextDiv = this.getSeverityTextElement(); + if (!severityTextDiv) { + throw new Error('Severity text element not found'); + } + return severityTextDiv.title; + } + + public dispatchEventToSeverityText(event: MouseEvent): void { + const severityTextDiv = this.getSeverityTextElement(); + if (!severityTextDiv) { + throw new Error('Severity text element not found'); + } + severityTextDiv.dispatchEvent(event); + processUpdates(); + } + + private getSeverityTextElement(): HTMLDivElement | null { + return this.element.shadowRoot!.querySelector('.severity-text'); + } +} diff --git a/packages/nimble-components/src/patterns/severity/tests/severity-pattern.spec.ts b/packages/nimble-components/src/patterns/severity/tests/severity-pattern.spec.ts new file mode 100644 index 0000000000..8257dbd05a --- /dev/null +++ b/packages/nimble-components/src/patterns/severity/tests/severity-pattern.spec.ts @@ -0,0 +1,100 @@ +import { customElement } from '@ni/fast-element'; +import { FoundationElement } from '@ni/fast-foundation'; +import { + type Fixture, + fixture, + uniqueElementName +} from '../../../utilities/tests/fixture'; +import { mixinSeverityPattern, Severity } from '../types'; +import { styles } from '../styles'; +import { severityTextTemplate } from '../template'; +import { processUpdates } from '../../../testing/async-helpers'; +import { SeverityPatternPageObject } from '../testing/severity-pattern.pageobject'; + +const elementName = uniqueElementName(); +@customElement({ + name: elementName, + template: severityTextTemplate, + styles +}) +class TestSeverityPattern extends mixinSeverityPattern(FoundationElement) {} + +async function setup(): Promise> { + return await fixture(elementName); +} + +describe('SeverityPatternMixin', () => { + let element: TestSeverityPattern; + let connect: () => Promise; + let disconnect: () => Promise; + let pageObject: SeverityPatternPageObject; + + function setSeverity(severityText: string | undefined, severity: Severity): void { + element.severityText = severityText; + element.severity = severity; + processUpdates(); + } + + beforeEach(async () => { + ({ element, connect, disconnect } = await setup()); + await connect(); + pageObject = new SeverityPatternPageObject(element); + }); + + afterEach(async () => { + await disconnect(); + }); + + it('defaults severity to Severity.default to false', () => { + expect(element.severity).toBe(Severity.default); + }); + + it('defaults severityText to undefined', () => { + expect(element.severityText).toBeUndefined(); + }); + + it('shows severity text when severity error', () => { + const severityText = 'Something is wrong!'; + setSeverity(severityText, Severity.error); + expect(pageObject.getDisplayedSeverityText()).toBe(severityText); + }); + + it('does not show severity text when severity default', () => { + const severityText = 'Something is wrong!'; + setSeverity(severityText, Severity.default); + expect(pageObject.getDisplayedSeverityText()).toBe(''); + }); + + describe('overflow behavior', () => { + beforeEach(() => { + element.style.display = 'block'; + element.style.width = '100px'; + }); + + it('sets title when error text is ellipsized', () => { + const severityText = 'a very long error message because something terrible happened and we need to show the user what it is'; + setSeverity(severityText, Severity.error); + + pageObject.dispatchEventToSeverityText(new MouseEvent('mouseover')); + expect(pageObject.getSeverityTextTitle()).toBe(severityText); + }); + + it('does not set title when error text is fully visible', () => { + setSeverity('abc', Severity.error); + + pageObject.dispatchEventToSeverityText(new MouseEvent('mouseover')); + expect(pageObject.getSeverityTextTitle()).toBe(''); + }); + + it('removes title on mouseout of error text', () => { + const errorText = 'a very long error message because something terrible happened and we need to show the user what it is'; + setSeverity(errorText, Severity.error); + + pageObject.dispatchEventToSeverityText(new MouseEvent('mouseover')); + expect(pageObject.getSeverityTextTitle()).toBe(errorText); + + pageObject.dispatchEventToSeverityText(new MouseEvent('mouseout')); + expect(pageObject.getSeverityTextTitle()).toBe(''); + }); + }); +}); diff --git a/packages/nimble-components/src/patterns/severity/types.ts b/packages/nimble-components/src/patterns/severity/types.ts new file mode 100644 index 0000000000..f8510880b0 --- /dev/null +++ b/packages/nimble-components/src/patterns/severity/types.ts @@ -0,0 +1,56 @@ +import { attr, FASTElement, observable } from '@ni/fast-element'; + +export const Severity = { + default: undefined, + error: 'error', + warning: 'warning', + success: 'success', + information: 'information' +} as const; +export type Severity = (typeof Severity)[keyof typeof Severity]; + +export interface SeverityPattern { + severityText?: string; + severity: Severity; + severityHasOverflow: boolean; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type FASTElementConstructor = abstract new (...args: any[]) => FASTElement; + +// As the returned class is internal to the function, we can't write a signature that uses is directly, so rely on inference +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type +export function mixinSeverityPattern( + base: TBase +) { + /** + * Mixin to provide severity text related properties to an element + */ + abstract class SeverityPatternElement extends base implements SeverityPattern { + /** + * The severity state of the element + */ + public severity: Severity; + + /** + * The severity text that will be displayed when a component is not in the default severity state + */ + public severityText?: string; + + /* @internal + * Indicates if the severity text has overflowed its container. The value should not be + * set directly. Instead, it is used with the `overflow` directive. + */ + public severityHasOverflow = false; + } + attr()( + SeverityPatternElement.prototype, + 'severity' + ); + attr({ attribute: 'severity-text' })( + SeverityPatternElement.prototype, + 'severityText' + ); + observable(SeverityPatternElement.prototype, 'severityHasOverflow'); + return SeverityPatternElement; +} From 6d65680637247bb9703f458c42d77b3ee1755f43 Mon Sep 17 00:00:00 2001 From: rajsite <1588923+rajsite@users.noreply.github.com> Date: Thu, 5 Feb 2026 18:35:37 -0600 Subject: [PATCH 02/26] Update step base classes --- packages/nimble-components/src/anchor-step/index.ts | 13 +++++++++---- .../nimble-components/src/anchor-step/styles.ts | 2 +- packages/nimble-components/src/step/index.ts | 10 +++++++--- packages/nimble-components/src/step/styles.ts | 2 +- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/nimble-components/src/anchor-step/index.ts b/packages/nimble-components/src/anchor-step/index.ts index d7319415dc..22be4e126b 100644 --- a/packages/nimble-components/src/anchor-step/index.ts +++ b/packages/nimble-components/src/anchor-step/index.ts @@ -1,6 +1,8 @@ -import { DesignSystem, FoundationElement } from '@ni/fast-foundation'; +import { DesignSystem, type AnchorOptions } from '@ni/fast-foundation'; import { styles } from './styles'; import { template } from './template'; +import { AnchorBase } from '../anchor-base'; +import { mixinSeverityPattern } from '../patterns/severity/types'; declare global { interface HTMLElementTagNameMap { @@ -11,12 +13,15 @@ declare global { /** * A nimble-styled anchor step for a stepper */ -export class AnchorStep extends FoundationElement {} +export class AnchorStep extends mixinSeverityPattern(AnchorBase) {} -const nimbleAnchorStep = AnchorStep.compose({ +const nimbleAnchorStep = AnchorStep.compose({ baseName: 'anchor-step', template, - styles + styles, + shadowOptions: { + delegatesFocus: true + } }); DesignSystem.getOrCreate().withPrefix('nimble').register(nimbleAnchorStep()); diff --git a/packages/nimble-components/src/anchor-step/styles.ts b/packages/nimble-components/src/anchor-step/styles.ts index 5d190a3e55..a09b765818 100644 --- a/packages/nimble-components/src/anchor-step/styles.ts +++ b/packages/nimble-components/src/anchor-step/styles.ts @@ -2,5 +2,5 @@ import { css } from '@ni/fast-element'; import { display } from '../utilities/style/display'; export const styles = css` - ${display('flex')} + ${display('inline-flex')} `; diff --git a/packages/nimble-components/src/step/index.ts b/packages/nimble-components/src/step/index.ts index 425ff12c6c..9fe20dbd67 100644 --- a/packages/nimble-components/src/step/index.ts +++ b/packages/nimble-components/src/step/index.ts @@ -1,4 +1,8 @@ -import { DesignSystem, FoundationElement } from '@ni/fast-foundation'; +import { + Button as FoundationButton, + type ButtonOptions, + DesignSystem +} from '@ni/fast-foundation'; import { styles } from './styles'; import { template } from './template'; @@ -11,9 +15,9 @@ declare global { /** * A nimble-styled step for a stepper */ -export class Step extends FoundationElement {} +export class Step extends FoundationButton {} -const nimbleStep = Step.compose({ +const nimbleStep = Step.compose({ baseName: 'step', template, styles diff --git a/packages/nimble-components/src/step/styles.ts b/packages/nimble-components/src/step/styles.ts index 5d190a3e55..a09b765818 100644 --- a/packages/nimble-components/src/step/styles.ts +++ b/packages/nimble-components/src/step/styles.ts @@ -2,5 +2,5 @@ import { css } from '@ni/fast-element'; import { display } from '../utilities/style/display'; export const styles = css` - ${display('flex')} + ${display('inline-flex')} `; From 8f9a2fd9f3d26a099e83cbaef10ef821ec58c1ba Mon Sep 17 00:00:00 2001 From: rajsite <1588923+rajsite@users.noreply.github.com> Date: Thu, 5 Feb 2026 18:36:36 -0600 Subject: [PATCH 03/26] Change files --- ...le-components-0f9f3301-9f8e-4a37-9bd5-bcfd51aa944e.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@ni-nimble-components-0f9f3301-9f8e-4a37-9bd5-bcfd51aa944e.json diff --git a/change/@ni-nimble-components-0f9f3301-9f8e-4a37-9bd5-bcfd51aa944e.json b/change/@ni-nimble-components-0f9f3301-9f8e-4a37-9bd5-bcfd51aa944e.json new file mode 100644 index 0000000000..6bd74a8e63 --- /dev/null +++ b/change/@ni-nimble-components-0f9f3301-9f8e-4a37-9bd5-bcfd51aa944e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "step and anchor-step", + "packageName": "@ni/nimble-components", + "email": "1588923+rajsite@users.noreply.github.com", + "dependentChangeType": "patch" +} From 34a3a43b2d4b88ca00c7cc9046ff015924234932 Mon Sep 17 00:00:00 2001 From: rajsite <1588923+rajsite@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:08:59 -0600 Subject: [PATCH 04/26] WIP --- .../src/anchor-step/index.ts | 36 +++- .../src/anchor-step/styles.ts | 4 +- .../src/anchor-step/template.ts | 49 ++++- .../src/patterns/step/styles.ts | 190 ++++++++++++++++++ .../src/patterns/step/types.ts | 23 +++ packages/nimble-components/src/step/index.ts | 29 ++- packages/nimble-components/src/step/styles.ts | 4 +- .../nimble-components/src/step/template.ts | 53 ++++- .../src/nimble/stepper/stepper.stories.ts | 5 +- 9 files changed, 379 insertions(+), 14 deletions(-) create mode 100644 packages/nimble-components/src/patterns/step/styles.ts create mode 100644 packages/nimble-components/src/patterns/step/types.ts diff --git a/packages/nimble-components/src/anchor-step/index.ts b/packages/nimble-components/src/anchor-step/index.ts index 22be4e126b..d55dfe8077 100644 --- a/packages/nimble-components/src/anchor-step/index.ts +++ b/packages/nimble-components/src/anchor-step/index.ts @@ -1,8 +1,10 @@ import { DesignSystem, type AnchorOptions } from '@ni/fast-foundation'; +import { attr, nullableNumberConverter } from '@ni/fast-element'; import { styles } from './styles'; import { template } from './template'; import { AnchorBase } from '../anchor-base'; import { mixinSeverityPattern } from '../patterns/severity/types'; +import type { StepPattern } from '../patterns/step/types'; declare global { interface HTMLElementTagNameMap { @@ -13,7 +15,39 @@ declare global { /** * A nimble-styled anchor step for a stepper */ -export class AnchorStep extends mixinSeverityPattern(AnchorBase) {} +export class AnchorStep extends mixinSeverityPattern(AnchorBase) implements StepPattern { + /** + * @public + * @remarks + * HTML Attribute: disabled + */ + @attr({ mode: 'boolean' }) + public disabled = false; + + /** + * @public + * @remarks + * HTML Attribute: readonly + */ + @attr({ attribute: 'readonly', mode: 'boolean' }) + public readOnly = false; + + /** + * @public + * @remarks + * HTML Attribute: selected + */ + @attr({ mode: 'boolean' }) + public selected = false; + + /** + * @public + * @remarks + * HTML Attribute: tabindex + */ + @attr({ attribute: 'tabindex', converter: nullableNumberConverter }) + public override tabIndex!: number; +} const nimbleAnchorStep = AnchorStep.compose({ baseName: 'anchor-step', diff --git a/packages/nimble-components/src/anchor-step/styles.ts b/packages/nimble-components/src/anchor-step/styles.ts index a09b765818..85ecc68c7e 100644 --- a/packages/nimble-components/src/anchor-step/styles.ts +++ b/packages/nimble-components/src/anchor-step/styles.ts @@ -1,6 +1,6 @@ import { css } from '@ni/fast-element'; -import { display } from '../utilities/style/display'; +import { styles as stepStyles } from '../patterns/step/styles'; export const styles = css` - ${display('inline-flex')} + ${stepStyles} `; diff --git a/packages/nimble-components/src/anchor-step/template.ts b/packages/nimble-components/src/anchor-step/template.ts index 55a4994405..f97cd4525c 100644 --- a/packages/nimble-components/src/anchor-step/template.ts +++ b/packages/nimble-components/src/anchor-step/template.ts @@ -1,6 +1,49 @@ -import { html } from '@ni/fast-element'; +import { html, ref, slotted, ViewTemplate } from '@ni/fast-element'; +import { type FoundationElementTemplate, type AnchorOptions, startSlotTemplate, endSlotTemplate } from '@ni/fast-foundation'; import type { AnchorStep } from '.'; -export const template = html` - +export const template: FoundationElementTemplate< +ViewTemplate, +AnchorOptions +> = (context, definition) => html` + (x.disabled ? null : x.href)} + hreflang="${x => x.hreflang}" + ping="${x => x.ping}" + referrerpolicy="${x => x.referrerpolicy}" + rel="${x => x.rel}" + target="${x => x.target}" + type="${x => x.type}" + tabindex="${x => x.tabIndex}" + aria-atomic="${x => x.ariaAtomic}" + aria-busy="${x => x.ariaBusy}" + aria-controls="${x => x.ariaControls}" + aria-current="${x => x.ariaCurrent}" + aria-describedby="${x => x.ariaDescribedby}" + aria-details="${x => x.ariaDetails}" + aria-disabled="${x => x.ariaDisabled}" + aria-errormessage="${x => x.ariaErrormessage}" + aria-expanded="${x => x.ariaExpanded}" + aria-flowto="${x => x.ariaFlowto}" + aria-haspopup="${x => x.ariaHaspopup}" + aria-hidden="${x => x.ariaHidden}" + aria-invalid="${x => x.ariaInvalid}" + aria-keyshortcuts="${x => x.ariaKeyshortcuts}" + aria-label="${x => x.ariaLabel}" + aria-labelledby="${x => x.ariaLabelledby}" + aria-live="${x => x.ariaLive}" + aria-owns="${x => x.ariaOwns}" + aria-relevant="${x => x.ariaRelevant}" + aria-roledescription="${x => x.ariaRoledescription}" + ${ref('control')} + > + ${startSlotTemplate(context, definition)} + + + + ${endSlotTemplate(context, definition)} + `; diff --git a/packages/nimble-components/src/patterns/step/styles.ts b/packages/nimble-components/src/patterns/step/styles.ts new file mode 100644 index 0000000000..57184f7715 --- /dev/null +++ b/packages/nimble-components/src/patterns/step/styles.ts @@ -0,0 +1,190 @@ +import { css } from '@ni/fast-element'; +import { display } from '../../utilities/style/display'; +import { focusVisible } from '../../utilities/style/focus'; +import { + borderHoverColor, + borderWidth, + buttonLabelFont, + buttonLabelFontColor, + buttonLabelDisabledFontColor, + controlHeight, + fillSelectedColor, + iconColor, + smallDelay, + standardPadding, +} from '../../theme-provider/design-tokens'; + +import { accessiblyHidden } from '../../utilities/style/accessibly-hidden'; + +export const styles = css` + @layer base, checked, hover, focusVisible, active, disabled, top; + + @layer base { + ${display('inline-flex')} + + :host { + background-color: transparent; + height: ${controlHeight}; + color: ${buttonLabelFontColor}; + font: ${buttonLabelFont}; + cursor: pointer; + white-space: nowrap; + outline: none; + border: 1px solid red; + ${ + /* + Not sure why but this is needed to get buttons with icons and buttons + without icons to align on the same line when the button is inline-flex + See: https://github.com/ni/nimble/issues/503 + */ '' + } + vertical-align: middle; + } + + .control { + background-color: transparent; + height: 100%; + width: 100%; + border: ${borderWidth} solid transparent; + color: inherit; + border-radius: inherit; + fill: inherit; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + cursor: inherit; + font: inherit; + outline: none; + margin: 0; + padding: 0 ${standardPadding}; + position: relative; + transition: + box-shadow ${smallDelay} ease-in-out, + border-color ${smallDelay} ease-in-out, + background-size ${smallDelay} ease-in-out; + background-size: 100% 100%; + background-repeat: no-repeat; + background-position: center; + } + + :host([content-hidden]) .control { + aspect-ratio: 1 / 1; + padding: 0px; + } + + .control::before { + content: ''; + position: absolute; + width: 100%; + height: 100%; + pointer-events: none; + outline: 0px solid transparent; + color: transparent; + background-clip: border-box; + transition: outline ${smallDelay} ease-in-out; + } + + .content { + display: contents; + } + + [part='start'] { + display: contents; + ${iconColor.cssCustomProperty}: ${buttonLabelFontColor}; + } + + slot[name='start']::slotted(*) { + flex-shrink: 0; + } + + :host([content-hidden]) .content { + ${accessiblyHidden} + } + + [part='end'] { + display: contents; + ${iconColor.cssCustomProperty}: ${buttonLabelFontColor}; + } + + slot[name='end']::slotted(*) { + flex-shrink: 0; + } + } + + @layer hover { + .control:hover { + border-color: ${borderHoverColor}; + box-shadow: 0px 0px 0px ${borderWidth} ${borderHoverColor} inset; + } + } + + @layer focusVisible { + .control${focusVisible} { + border-color: ${borderHoverColor}; + box-shadow: 0px 0px 0px ${borderWidth} ${borderHoverColor} inset; + } + + .control${focusVisible}::before { + outline: ${borderWidth} solid ${borderHoverColor}; + outline-offset: -3px; + } + } + + @layer active { + .control:active { + box-shadow: none; + color: ${buttonLabelFontColor}; + border-color: ${borderHoverColor}; + background-image: linear-gradient( + ${fillSelectedColor}, + ${fillSelectedColor} + ); + background-size: calc(100% - 2px) calc(100% - 2px); + } + + .control:active::before { + outline: none; + } + + .control:active [part='start'], + .control:active [part='end'] { + ${iconColor.cssCustomProperty}: ${buttonLabelFontColor}; + } + } + + @layer disabled { + :host([disabled]) { + cursor: default; + } + + :host([disabled]) .control { + color: ${buttonLabelDisabledFontColor}; + box-shadow: none; + background-image: none; + background-size: 100% 100%; + } + + :host([disabled]) .control::before { + box-shadow: none; + } + + :host([disabled]) slot[name='start']::slotted(*), + :host([disabled]) slot[name='end']::slotted(*) { + opacity: 0.3; + ${iconColor.cssCustomProperty}: ${buttonLabelFontColor}; + } + } + + @layer top { + @media (prefers-reduced-motion) { + .control { + transition-duration: 0s; + } + } + + :host([content-hidden]) [part='end'] { + display: none; + } + } +`; diff --git a/packages/nimble-components/src/patterns/step/types.ts b/packages/nimble-components/src/patterns/step/types.ts new file mode 100644 index 0000000000..55467747d3 --- /dev/null +++ b/packages/nimble-components/src/patterns/step/types.ts @@ -0,0 +1,23 @@ +import type { Severity } from '../severity/types'; + +export interface StepPattern { + /** + * Whether or not the step is disabled. + */ + disabled: boolean; + + /** + * Whether or not the step is readOnly. + */ + readOnly: boolean; + + /** + * Whether or not the step is selected. + */ + selected: boolean; + + /** + * The severity state of the step. + */ + severity: Severity; +} diff --git a/packages/nimble-components/src/step/index.ts b/packages/nimble-components/src/step/index.ts index 9fe20dbd67..39aaddc28f 100644 --- a/packages/nimble-components/src/step/index.ts +++ b/packages/nimble-components/src/step/index.ts @@ -3,8 +3,11 @@ import { type ButtonOptions, DesignSystem } from '@ni/fast-foundation'; +import { attr, nullableNumberConverter } from '@ni/fast-element'; import { styles } from './styles'; import { template } from './template'; +import type { StepPattern } from '../patterns/step/types'; +import { mixinSeverityPattern } from '../patterns/severity/types'; declare global { interface HTMLElementTagNameMap { @@ -15,7 +18,31 @@ declare global { /** * A nimble-styled step for a stepper */ -export class Step extends FoundationButton {} +export class Step extends mixinSeverityPattern(FoundationButton) implements StepPattern { + /** + * @public + * @remarks + * HTML Attribute: readonly + */ + @attr({ attribute: 'readonly', mode: 'boolean' }) + public readOnly = false; + + /** + * @public + * @remarks + * HTML Attribute: selected + */ + @attr({ mode: 'boolean' }) + public selected = false; + + /** + * @public + * @remarks + * HTML Attribute: tabindex + */ + @attr({ attribute: 'tabindex', converter: nullableNumberConverter }) + public override tabIndex!: number; +} const nimbleStep = Step.compose({ baseName: 'step', diff --git a/packages/nimble-components/src/step/styles.ts b/packages/nimble-components/src/step/styles.ts index a09b765818..85ecc68c7e 100644 --- a/packages/nimble-components/src/step/styles.ts +++ b/packages/nimble-components/src/step/styles.ts @@ -1,6 +1,6 @@ import { css } from '@ni/fast-element'; -import { display } from '../utilities/style/display'; +import { styles as stepStyles } from '../patterns/step/styles'; export const styles = css` - ${display('inline-flex')} + ${stepStyles} `; diff --git a/packages/nimble-components/src/step/template.ts b/packages/nimble-components/src/step/template.ts index 0619d62721..27644c5c4c 100644 --- a/packages/nimble-components/src/step/template.ts +++ b/packages/nimble-components/src/step/template.ts @@ -1,6 +1,53 @@ -import { html } from '@ni/fast-element'; +import { html, ref, slotted, ViewTemplate } from '@ni/fast-element'; +import { endSlotTemplate, startSlotTemplate, type ButtonOptions, type FoundationElementTemplate } from '@ni/fast-foundation'; import type { Step } from '.'; -export const template = html` - +export const template: FoundationElementTemplate< +ViewTemplate, +ButtonOptions +> = (context, definition) => html` + `; diff --git a/packages/storybook/src/nimble/stepper/stepper.stories.ts b/packages/storybook/src/nimble/stepper/stepper.stories.ts index 88edbd1539..8e9577ca69 100644 --- a/packages/storybook/src/nimble/stepper/stepper.stories.ts +++ b/packages/storybook/src/nimble/stepper/stepper.stories.ts @@ -25,8 +25,9 @@ const metadata: Meta = { statusLink: 'https://github.com/ni/nimble/issues/624' })} <${stepperTag}> - <${anchorStepTag}> - <${stepTag}> + <${anchorStepTag}>anchorstep + - + <${stepTag}>step `), argTypes: { From 9e7f34c3ce8bb20c65019fb1cbe937a8edbcce9e Mon Sep 17 00:00:00 2001 From: rajsite <1588923+rajsite@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:55:12 -0600 Subject: [PATCH 05/26] WIP --- .../src/anchor-step/styles.ts | 6 + .../src/anchor-step/template.ts | 93 +++++----- .../src/patterns/step/styles.ts | 163 +++++------------- packages/nimble-components/src/step/styles.ts | 7 + .../nimble-components/src/step/template.ts | 101 ++++++----- .../src/nimble/stepper/stepper.stories.ts | 13 +- 6 files changed, 172 insertions(+), 211 deletions(-) diff --git a/packages/nimble-components/src/anchor-step/styles.ts b/packages/nimble-components/src/anchor-step/styles.ts index 85ecc68c7e..3f34605fec 100644 --- a/packages/nimble-components/src/anchor-step/styles.ts +++ b/packages/nimble-components/src/anchor-step/styles.ts @@ -3,4 +3,10 @@ import { styles as stepStyles } from '../patterns/step/styles'; export const styles = css` ${stepStyles} + ${'' /* Anchor specific styles */} + @layer base { + .control { + text-decoration: none; + } + } `; diff --git a/packages/nimble-components/src/anchor-step/template.ts b/packages/nimble-components/src/anchor-step/template.ts index f97cd4525c..88b276b9a6 100644 --- a/packages/nimble-components/src/anchor-step/template.ts +++ b/packages/nimble-components/src/anchor-step/template.ts @@ -1,49 +1,62 @@ import { html, ref, slotted, ViewTemplate } from '@ni/fast-element'; import { type FoundationElementTemplate, type AnchorOptions, startSlotTemplate, endSlotTemplate } from '@ni/fast-foundation'; import type { AnchorStep } from '.'; +import { severityTextTemplate } from '../patterns/severity/template'; export const template: FoundationElementTemplate< ViewTemplate, AnchorOptions > = (context, definition) => html` - (x.disabled ? null : x.href)} - hreflang="${x => x.hreflang}" - ping="${x => x.ping}" - referrerpolicy="${x => x.referrerpolicy}" - rel="${x => x.rel}" - target="${x => x.target}" - type="${x => x.type}" - tabindex="${x => x.tabIndex}" - aria-atomic="${x => x.ariaAtomic}" - aria-busy="${x => x.ariaBusy}" - aria-controls="${x => x.ariaControls}" - aria-current="${x => x.ariaCurrent}" - aria-describedby="${x => x.ariaDescribedby}" - aria-details="${x => x.ariaDetails}" - aria-disabled="${x => x.ariaDisabled}" - aria-errormessage="${x => x.ariaErrormessage}" - aria-expanded="${x => x.ariaExpanded}" - aria-flowto="${x => x.ariaFlowto}" - aria-haspopup="${x => x.ariaHaspopup}" - aria-hidden="${x => x.ariaHidden}" - aria-invalid="${x => x.ariaInvalid}" - aria-keyshortcuts="${x => x.ariaKeyshortcuts}" - aria-label="${x => x.ariaLabel}" - aria-labelledby="${x => x.ariaLabelledby}" - aria-live="${x => x.ariaLive}" - aria-owns="${x => x.ariaOwns}" - aria-relevant="${x => x.ariaRelevant}" - aria-roledescription="${x => x.ariaRoledescription}" - ${ref('control')} - > - ${startSlotTemplate(context, definition)} - - - - ${endSlotTemplate(context, definition)} - + `; diff --git a/packages/nimble-components/src/patterns/step/styles.ts b/packages/nimble-components/src/patterns/step/styles.ts index 57184f7715..8df8da4fe1 100644 --- a/packages/nimble-components/src/patterns/step/styles.ts +++ b/packages/nimble-components/src/patterns/step/styles.ts @@ -1,20 +1,14 @@ import { css } from '@ni/fast-element'; import { display } from '../../utilities/style/display'; -import { focusVisible } from '../../utilities/style/focus'; import { - borderHoverColor, - borderWidth, buttonLabelFont, buttonLabelFontColor, - buttonLabelDisabledFontColor, - controlHeight, - fillSelectedColor, - iconColor, - smallDelay, - standardPadding, + smallPadding, + bodyFont, + errorTextFont, } from '../../theme-provider/design-tokens'; -import { accessiblyHidden } from '../../utilities/style/accessibly-hidden'; +// import { accessiblyHidden } from '../../utilities/style/accessibly-hidden'; export const styles = css` @layer base, checked, hover, focusVisible, active, disabled, top; @@ -23,157 +17,82 @@ export const styles = css` ${display('inline-flex')} :host { - background-color: transparent; - height: ${controlHeight}; + height: 46px; color: ${buttonLabelFontColor}; font: ${buttonLabelFont}; - cursor: pointer; white-space: nowrap; outline: none; border: 1px solid red; - ${ - /* - Not sure why but this is needed to get buttons with icons and buttons - without icons to align on the same line when the button is inline-flex - See: https://github.com/ni/nimble/issues/503 - */ '' - } - vertical-align: middle; } - .control { - background-color: transparent; + .control { + display: inline-flex; + align-items: start; + justify-content: flex-start; height: 100%; width: 100%; - border: ${borderWidth} solid transparent; - color: inherit; - border-radius: inherit; - fill: inherit; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 4px; - cursor: inherit; font: inherit; + color: inherit; + background-color: transparent; + gap: ${smallPadding}; + cursor: pointer; outline: none; margin: 0; - padding: 0 ${standardPadding}; + padding: 0 ${smallPadding} 0 0; position: relative; - transition: - box-shadow ${smallDelay} ease-in-out, - border-color ${smallDelay} ease-in-out, - background-size ${smallDelay} ease-in-out; - background-size: 100% 100%; - background-repeat: no-repeat; - background-position: center; } - :host([content-hidden]) .control { - aspect-ratio: 1 / 1; - padding: 0px; - } - - .control::before { - content: ''; - position: absolute; - width: 100%; - height: 100%; - pointer-events: none; - outline: 0px solid transparent; - color: transparent; - background-clip: border-box; - transition: outline ${smallDelay} ease-in-out; + .icon { + display: inline-flex; + align-items: center; + justify-content: center; + height: 32px; + width: 32px; + border: 1px solid purple; } .content { - display: contents; - } - - [part='start'] { - display: contents; - ${iconColor.cssCustomProperty}: ${buttonLabelFontColor}; + display: inline-flex; + flex-direction: column; + gap: ${smallPadding}; + padding-top: ${smallPadding}; } - slot[name='start']::slotted(*) { - flex-shrink: 0; + .title { + display: inline-flex; + font: ${bodyFont}; } - :host([content-hidden]) .content { - ${accessiblyHidden} + [part='start'] { + display: none; } [part='end'] { - display: contents; - ${iconColor.cssCustomProperty}: ${buttonLabelFontColor}; + display: none; + } + + .line { + display: block; + height: 5px; + border: 1px solid green; } - slot[name='end']::slotted(*) { - flex-shrink: 0; + .subtitle { + font: ${errorTextFont}; } } @layer hover { - .control:hover { - border-color: ${borderHoverColor}; - box-shadow: 0px 0px 0px ${borderWidth} ${borderHoverColor} inset; - } } @layer focusVisible { - .control${focusVisible} { - border-color: ${borderHoverColor}; - box-shadow: 0px 0px 0px ${borderWidth} ${borderHoverColor} inset; - } - - .control${focusVisible}::before { - outline: ${borderWidth} solid ${borderHoverColor}; - outline-offset: -3px; - } } @layer active { - .control:active { - box-shadow: none; - color: ${buttonLabelFontColor}; - border-color: ${borderHoverColor}; - background-image: linear-gradient( - ${fillSelectedColor}, - ${fillSelectedColor} - ); - background-size: calc(100% - 2px) calc(100% - 2px); - } - - .control:active::before { - outline: none; - } - - .control:active [part='start'], - .control:active [part='end'] { - ${iconColor.cssCustomProperty}: ${buttonLabelFontColor}; - } } @layer disabled { - :host([disabled]) { - cursor: default; - } - - :host([disabled]) .control { - color: ${buttonLabelDisabledFontColor}; - box-shadow: none; - background-image: none; - background-size: 100% 100%; - } - :host([disabled]) .control::before { - box-shadow: none; - } - - :host([disabled]) slot[name='start']::slotted(*), - :host([disabled]) slot[name='end']::slotted(*) { - opacity: 0.3; - ${iconColor.cssCustomProperty}: ${buttonLabelFontColor}; - } } @layer top { @@ -182,9 +101,5 @@ export const styles = css` transition-duration: 0s; } } - - :host([content-hidden]) [part='end'] { - display: none; - } } `; diff --git a/packages/nimble-components/src/step/styles.ts b/packages/nimble-components/src/step/styles.ts index 85ecc68c7e..5303f92582 100644 --- a/packages/nimble-components/src/step/styles.ts +++ b/packages/nimble-components/src/step/styles.ts @@ -3,4 +3,11 @@ import { styles as stepStyles } from '../patterns/step/styles'; export const styles = css` ${stepStyles} + ${'' /* Button specific styles */} + @layer base { + .control { + border: none; + text-align: start; + } + } `; diff --git a/packages/nimble-components/src/step/template.ts b/packages/nimble-components/src/step/template.ts index 27644c5c4c..a8bffefdfd 100644 --- a/packages/nimble-components/src/step/template.ts +++ b/packages/nimble-components/src/step/template.ts @@ -1,53 +1,66 @@ import { html, ref, slotted, ViewTemplate } from '@ni/fast-element'; import { endSlotTemplate, startSlotTemplate, type ButtonOptions, type FoundationElementTemplate } from '@ni/fast-foundation'; import type { Step } from '.'; +import { severityTextTemplate } from '../patterns/severity/template'; export const template: FoundationElementTemplate< ViewTemplate, ButtonOptions > = (context, definition) => html` - +
+ + ${severityTextTemplate} +
`; diff --git a/packages/storybook/src/nimble/stepper/stepper.stories.ts b/packages/storybook/src/nimble/stepper/stepper.stories.ts index 8e9577ca69..02e9dc6a7f 100644 --- a/packages/storybook/src/nimble/stepper/stepper.stories.ts +++ b/packages/storybook/src/nimble/stepper/stepper.stories.ts @@ -25,10 +25,17 @@ const metadata: Meta = { statusLink: 'https://github.com/ni/nimble/issues/624' })} <${stepperTag}> - <${anchorStepTag}>anchorstep + <${anchorStepTag} href="#" severity-text="hello" style="width:250px"> + 😀 +
Title
+
Subtitle
+ - - <${stepTag}>step - + <${stepTag} severity-text="hello" style="width:250px"> + 😀 +
Title
+
Subtitle
+ `), argTypes: { stepType: { From 32720828219dd745af5d7ab36e2f49dc376d3132 Mon Sep 17 00:00:00 2001 From: rajsite <1588923+rajsite@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:54:28 -0600 Subject: [PATCH 06/26] template layout --- .../src/anchor-step/template.ts | 4 +-- .../src/patterns/step/styles.ts | 33 +++++++++++++++---- .../nimble-components/src/step/template.ts | 4 +-- .../nimble-components/src/stepper/styles.ts | 6 +++- .../src/nimble/stepper/stepper.stories.ts | 5 ++- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/packages/nimble-components/src/anchor-step/template.ts b/packages/nimble-components/src/anchor-step/template.ts index 88b276b9a6..ffb8ce4e1b 100644 --- a/packages/nimble-components/src/anchor-step/template.ts +++ b/packages/nimble-components/src/anchor-step/template.ts @@ -46,9 +46,9 @@ AnchorOptions
-
+
${startSlotTemplate(context, definition)} - +
${endSlotTemplate(context, definition)}
diff --git a/packages/nimble-components/src/patterns/step/styles.ts b/packages/nimble-components/src/patterns/step/styles.ts index 8df8da4fe1..f46c5730a5 100644 --- a/packages/nimble-components/src/patterns/step/styles.ts +++ b/packages/nimble-components/src/patterns/step/styles.ts @@ -6,7 +6,9 @@ import { smallPadding, bodyFont, errorTextFont, + controlSlimHeight, } from '../../theme-provider/design-tokens'; +import {styles as severityStyles} from '../severity/styles'; // import { accessiblyHidden } from '../../utilities/style/accessibly-hidden'; @@ -15,7 +17,7 @@ export const styles = css` @layer base { ${display('inline-flex')} - + ${severityStyles} :host { height: 46px; color: ${buttonLabelFontColor}; @@ -25,6 +27,13 @@ export const styles = css` border: 1px solid red; } + .container { + display: inline-flex; + width: 100%; + height: 100%; + position: relative; + } + .control { display: inline-flex; align-items: start; @@ -49,17 +58,23 @@ export const styles = css` height: 32px; width: 32px; border: 1px solid purple; + flex: none; } .content { display: inline-flex; + width: 100%; flex-direction: column; - gap: ${smallPadding}; padding-top: ${smallPadding}; } - .title { + .title-wrapper { display: inline-flex; + height: ${controlSlimHeight}; + flex-direction: row; + align-items: center; + justify-content: start; + gap: ${smallPadding}; font: ${bodyFont}; } @@ -67,14 +82,20 @@ export const styles = css` display: none; } + .title { + display: inline-block; + flex: none; + } + [part='end'] { display: none; } .line { - display: block; - height: 5px; - border: 1px solid green; + display: inline-block; + flex: 1; + height: 1px; + background: green; } .subtitle { diff --git a/packages/nimble-components/src/step/template.ts b/packages/nimble-components/src/step/template.ts index a8bffefdfd..b607305ae5 100644 --- a/packages/nimble-components/src/step/template.ts +++ b/packages/nimble-components/src/step/template.ts @@ -50,9 +50,9 @@ ButtonOptions
-
+
${startSlotTemplate(context, definition)} - +
${endSlotTemplate(context, definition)}
diff --git a/packages/nimble-components/src/stepper/styles.ts b/packages/nimble-components/src/stepper/styles.ts index 5d190a3e55..98d23ca7ae 100644 --- a/packages/nimble-components/src/stepper/styles.ts +++ b/packages/nimble-components/src/stepper/styles.ts @@ -2,5 +2,9 @@ import { css } from '@ni/fast-element'; import { display } from '../utilities/style/display'; export const styles = css` - ${display('flex')} + ${display('inline-flex')} + + :host { + gap: 0px; + } `; diff --git a/packages/storybook/src/nimble/stepper/stepper.stories.ts b/packages/storybook/src/nimble/stepper/stepper.stories.ts index 02e9dc6a7f..fcf1aad5a2 100644 --- a/packages/storybook/src/nimble/stepper/stepper.stories.ts +++ b/packages/storybook/src/nimble/stepper/stepper.stories.ts @@ -25,13 +25,12 @@ const metadata: Meta = { statusLink: 'https://github.com/ni/nimble/issues/624' })} <${stepperTag}> - <${anchorStepTag} href="#" severity-text="hello" style="width:250px"> + <${anchorStepTag} severity="error" href="#" severity-text="Error Description" style="width:150px"> 😀
Title
Subtitle
- - - <${stepTag} severity-text="hello" style="width:250px"> + <${stepTag} severity="error" severity-text="Error Description" style="width:150px"> 😀
Title
Subtitle
From 9da273f910afae215ec0ba4c4fac4f7cd80b9bb1 Mon Sep 17 00:00:00 2001 From: rajsite <1588923+rajsite@users.noreply.github.com> Date: Wed, 11 Feb 2026 18:59:53 -0600 Subject: [PATCH 07/26] WIP interaction states --- .../src/patterns/severity/styles.ts | 6 +- .../src/patterns/step/styles.ts | 56 +++++++++++++++++-- packages/nimble-components/src/step/styles.ts | 1 + .../nimble-components/src/stepper/styles.ts | 1 + .../nimble-components/src/stepper/template.ts | 4 +- .../utilities/models/device-pixel-ratio.ts | 22 ++++++++ .../src/nimble/stepper/stepper.stories.ts | 6 +- 7 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 packages/nimble-components/src/utilities/models/device-pixel-ratio.ts diff --git a/packages/nimble-components/src/patterns/severity/styles.ts b/packages/nimble-components/src/patterns/severity/styles.ts index 559b0388f7..b156d909c6 100644 --- a/packages/nimble-components/src/patterns/severity/styles.ts +++ b/packages/nimble-components/src/patterns/severity/styles.ts @@ -1,11 +1,13 @@ -import { css } from '@ni/fast-element'; +import { cssPartial } from '@ni/fast-element'; import { failColor, errorTextFont, errorTextFontLineHeight } from '../../theme-provider/design-tokens'; -export const styles = css` +// These styles end up inside a @layer block so must use the +// cssPartial tag instead of the css tag +export const styles = cssPartial` .severity-text { display: none; } diff --git a/packages/nimble-components/src/patterns/step/styles.ts b/packages/nimble-components/src/patterns/step/styles.ts index f46c5730a5..7408c3632b 100644 --- a/packages/nimble-components/src/patterns/step/styles.ts +++ b/packages/nimble-components/src/patterns/step/styles.ts @@ -7,8 +7,13 @@ import { bodyFont, errorTextFont, controlSlimHeight, + borderRgbPartialColor, + borderHoverColor, + borderWidth, + smallDelay, } from '../../theme-provider/design-tokens'; -import {styles as severityStyles} from '../severity/styles'; +import { styles as severityStyles } from '../severity/styles'; +import { focusVisible } from '../../utilities/style/focus'; // import { accessiblyHidden } from '../../utilities/style/accessibly-hidden'; @@ -31,7 +36,6 @@ export const styles = css` display: inline-flex; width: 100%; height: 100%; - position: relative; } .control { @@ -40,7 +44,6 @@ export const styles = css` justify-content: flex-start; height: 100%; width: 100%; - font: inherit; color: inherit; background-color: transparent; gap: ${smallPadding}; @@ -48,17 +51,45 @@ export const styles = css` outline: none; margin: 0; padding: 0 ${smallPadding} 0 0; - position: relative; } .icon { display: inline-flex; align-items: center; justify-content: center; + flex: none; height: 32px; width: 32px; - border: 1px solid purple; - flex: none; + border: 2px solid transparent; + border-radius: 100%; + background-image: radial-gradient( + closest-side, + rgba(${borderRgbPartialColor}, 0.1) calc(100% - 1px/var(--ni-private-device-resolution)), + transparent 100% + ); + background-origin: border-box; + background-size: 100% 100%; + background-repeat: no-repeat; + background-position: center center; + position: relative; + transition: + box-shadow ${smallDelay} ease-in-out, + border-color ${smallDelay} ease-in-out, + background-size ${smallDelay} ease-in-out; + } + + .icon::before { + content: ''; + position: absolute; + width: 100%; + height: 100%; + pointer-events: none; + outline: 0px solid transparent; + border: 1px solid transparent; + border-radius: 100%; + color: transparent; + background-clip: border-box; + transition: outline ${smallDelay} ease-in-out; } .content { @@ -104,9 +135,22 @@ export const styles = css` } @layer hover { + .control:hover .icon { + border-color: ${borderHoverColor}; + background-size: calc(100% - 6px) calc(100% - 6px); + } } @layer focusVisible { + .control${focusVisible} .icon { + border-color: ${borderHoverColor}; + background-size: calc(100% - 6px) calc(100% - 6px); + } + + .control${focusVisible} .icon::before { + outline: ${borderWidth} solid ${borderHoverColor}; + outline-offset: -2px; + } } @layer active { diff --git a/packages/nimble-components/src/step/styles.ts b/packages/nimble-components/src/step/styles.ts index 5303f92582..cfaff82a26 100644 --- a/packages/nimble-components/src/step/styles.ts +++ b/packages/nimble-components/src/step/styles.ts @@ -6,6 +6,7 @@ export const styles = css` ${'' /* Button specific styles */} @layer base { .control { + font: inherit; border: none; text-align: start; } diff --git a/packages/nimble-components/src/stepper/styles.ts b/packages/nimble-components/src/stepper/styles.ts index 98d23ca7ae..7a737d118d 100644 --- a/packages/nimble-components/src/stepper/styles.ts +++ b/packages/nimble-components/src/stepper/styles.ts @@ -5,6 +5,7 @@ export const styles = css` ${display('inline-flex')} :host { + border: 1px solid blue; gap: 0px; } `; diff --git a/packages/nimble-components/src/stepper/template.ts b/packages/nimble-components/src/stepper/template.ts index 18e48e11a4..1ad9dc6bc1 100644 --- a/packages/nimble-components/src/stepper/template.ts +++ b/packages/nimble-components/src/stepper/template.ts @@ -1,6 +1,8 @@ import { html } from '@ni/fast-element'; import type { Stepper } from '.'; +import { devicePixelRatio } from '../utilities/models/device-pixel-ratio'; export const template = html` - + + `; diff --git a/packages/nimble-components/src/utilities/models/device-pixel-ratio.ts b/packages/nimble-components/src/utilities/models/device-pixel-ratio.ts new file mode 100644 index 0000000000..847ac1bea2 --- /dev/null +++ b/packages/nimble-components/src/utilities/models/device-pixel-ratio.ts @@ -0,0 +1,22 @@ +import { observable } from '@ni/fast-element'; + +/** + * Observable class to subscribe to changes in the page's device pixel ratio + * Based on: https://frontendmasters.com/blog/obsessing-over-smooth-radial-gradient-disc-edges/#the-less-code-and-more-flexible-js-solution + */ +class DevicePixelRatio { + @observable + public current!: number; + + public constructor() { + const update = (): void => { + this.current = window.devicePixelRatio; + window + .matchMedia(`(resolution: ${this.current}x)`) + .addEventListener('change', update, { once: true }); + }; + update(); + } +} + +export const devicePixelRatio = new DevicePixelRatio(); diff --git a/packages/storybook/src/nimble/stepper/stepper.stories.ts b/packages/storybook/src/nimble/stepper/stepper.stories.ts index fcf1aad5a2..f48684a95f 100644 --- a/packages/storybook/src/nimble/stepper/stepper.stories.ts +++ b/packages/storybook/src/nimble/stepper/stepper.stories.ts @@ -6,7 +6,7 @@ import { stepTag } from '@ni/nimble-components/dist/esm/step'; import { apiCategory, createUserSelectedThemeStory, - incubatingWarning + // incubatingWarning } from '../../utilities/storybook'; import { ExampleStepType } from './types'; @@ -20,10 +20,10 @@ const metadata: Meta = { actions: {} }, render: createUserSelectedThemeStory(html` - ${incubatingWarning({ + ${'' /* incubatingWarning({ componentName: stepperTag, statusLink: 'https://github.com/ni/nimble/issues/624' - })} + }) */} <${stepperTag}> <${anchorStepTag} severity="error" href="#" severity-text="Error Description" style="width:150px"> 😀 From 7e588e13b6083e60891ea8128295e83b631a174b Mon Sep 17 00:00:00 2001 From: rajsite <1588923+rajsite@users.noreply.github.com> Date: Wed, 11 Feb 2026 19:23:04 -0600 Subject: [PATCH 08/26] fix test --- .../src/patterns/severity/tests/severity-pattern.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nimble-components/src/patterns/severity/tests/severity-pattern.spec.ts b/packages/nimble-components/src/patterns/severity/tests/severity-pattern.spec.ts index 8257dbd05a..a10c98ad58 100644 --- a/packages/nimble-components/src/patterns/severity/tests/severity-pattern.spec.ts +++ b/packages/nimble-components/src/patterns/severity/tests/severity-pattern.spec.ts @@ -1,4 +1,4 @@ -import { customElement } from '@ni/fast-element'; +import { css, customElement } from '@ni/fast-element'; import { FoundationElement } from '@ni/fast-foundation'; import { type Fixture, @@ -15,7 +15,7 @@ const elementName = uniqueElementName(); @customElement({ name: elementName, template: severityTextTemplate, - styles + styles: css`${styles}` }) class TestSeverityPattern extends mixinSeverityPattern(FoundationElement) {} From 8021ebadb56afd47e9000bd420e792bad3105818 Mon Sep 17 00:00:00 2001 From: rajsite <1588923+rajsite@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:41:27 -0600 Subject: [PATCH 09/26] severity text placement --- packages/nimble-components/src/patterns/step/styles.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nimble-components/src/patterns/step/styles.ts b/packages/nimble-components/src/patterns/step/styles.ts index 7408c3632b..0502a4f4ab 100644 --- a/packages/nimble-components/src/patterns/step/styles.ts +++ b/packages/nimble-components/src/patterns/step/styles.ts @@ -36,6 +36,7 @@ export const styles = css` display: inline-flex; width: 100%; height: 100%; + position: relative; } .control { From f535c4d2acc6ae1958473ea3179f4c4e4eb58558 Mon Sep 17 00:00:00 2001 From: rajsite <1588923+rajsite@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:01:44 -0600 Subject: [PATCH 10/26] =?UTF-8?q?=F0=9F=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/patterns/severity/types.ts | 1 - .../src/patterns/step/styles.ts | 44 +++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/nimble-components/src/patterns/severity/types.ts b/packages/nimble-components/src/patterns/severity/types.ts index f8510880b0..5df6cf8022 100644 --- a/packages/nimble-components/src/patterns/severity/types.ts +++ b/packages/nimble-components/src/patterns/severity/types.ts @@ -5,7 +5,6 @@ export const Severity = { error: 'error', warning: 'warning', success: 'success', - information: 'information' } as const; export type Severity = (typeof Severity)[keyof typeof Severity]; diff --git a/packages/nimble-components/src/patterns/step/styles.ts b/packages/nimble-components/src/patterns/step/styles.ts index 0502a4f4ab..147bd71c51 100644 --- a/packages/nimble-components/src/patterns/step/styles.ts +++ b/packages/nimble-components/src/patterns/step/styles.ts @@ -11,6 +11,7 @@ import { borderHoverColor, borderWidth, smallDelay, + fillSelectedColor, } from '../../theme-provider/design-tokens'; import { styles as severityStyles } from '../severity/styles'; import { focusVisible } from '../../utilities/style/focus'; @@ -30,8 +31,10 @@ export const styles = css` white-space: nowrap; outline: none; border: 1px solid red; + --ni-private-step-icon-background-color: rgba(${borderRgbPartialColor}, 0.1); } + ${'' /* Container wrapper for severity text to position against */} .container { display: inline-flex; width: 100%; @@ -65,7 +68,10 @@ export const styles = css` border-radius: 100%; background-image: radial-gradient( closest-side, - rgba(${borderRgbPartialColor}, 0.1) calc(100% - 1px/var(--ni-private-device-resolution)), + ${'' /* Workaround to prevent aliasing on radial-gradient boundaries, see: + https://frontendmasters.com/blog/obsessing-over-smooth-radial-gradient-disc-edges + */} + var(--ni-private-step-icon-background-color) calc(100% - 1px/var(--ni-private-device-resolution)), transparent 100% ); background-origin: border-box; @@ -86,11 +92,14 @@ export const styles = css` height: 100%; pointer-events: none; outline: 0px solid transparent; + outline-offset: 0px; border: 1px solid transparent; border-radius: 100%; color: transparent; background-clip: border-box; - transition: outline ${smallDelay} ease-in-out; + transition: + outline-offset ${smallDelay} ease-in-out, + outline ${smallDelay} ease-in-out; } .content { @@ -127,7 +136,11 @@ export const styles = css` display: inline-block; flex: 1; height: 1px; - background: green; + background: rgba(${borderRgbPartialColor}, 0.1); + transform: scale(1, 1); + transition: + background-color ${smallDelay} ease-in-out, + transform ${smallDelay} ease-in-out; } .subtitle { @@ -140,6 +153,11 @@ export const styles = css` border-color: ${borderHoverColor}; background-size: calc(100% - 6px) calc(100% - 6px); } + + .control:hover .line { + background-color: ${borderHoverColor}; + transform: scale(1, 2); + } } @layer focusVisible { @@ -152,9 +170,29 @@ export const styles = css` outline: ${borderWidth} solid ${borderHoverColor}; outline-offset: -2px; } + + .control${focusVisible} .line { + background-color: ${borderHoverColor}; + transform: scale(1, 2); + } } @layer active { + .control:active .icon { + border-color: ${borderHoverColor}; + --ni-private-step-icon-background-color: ${fillSelectedColor}; + background-size: 100% 100%; + } + + .control:active .icon::before { + outline: 0px solid transparent; + outline-offset: 0px; + } + + .control:active .line { + background-color: ${borderHoverColor}; + transform: scale(1, 1); + } } @layer disabled { From f8d76da85f54f7e45bf42cdcdfab007cb723b59b Mon Sep 17 00:00:00 2001 From: rajsite <1588923+rajsite@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:58:39 -0600 Subject: [PATCH 11/26] Clarify CSS Layer style guidelines --- .../nimble-components/docs/css-guidelines.md | 29 ++++++++++++------- .../nimble-components/src/anchor/styles.ts | 2 +- .../src/breadcrumb-item/styles.ts | 2 +- .../src/patterns/button/styles.ts | 2 +- .../src/patterns/step/styles.ts | 2 +- .../src/toggle-button/styles.ts | 4 +-- 6 files changed, 25 insertions(+), 16 deletions(-) diff --git a/packages/nimble-components/docs/css-guidelines.md b/packages/nimble-components/docs/css-guidelines.md index 113cc0e586..b3c51f1613 100644 --- a/packages/nimble-components/docs/css-guidelines.md +++ b/packages/nimble-components/docs/css-guidelines.md @@ -108,9 +108,11 @@ Some takeaways from the example: ## Prefer cascade for state changes -If you find yourself in complex logic with lots of `:not()` selectors it's possible the code should be reorganized to leverage the CSS cascade for overriding states. +Avoid using [`:not()` selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:not) or [selector lists](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors/Selector_structure#selector_list) (`#main, .content {}`) and instead rely on CSS cascade for overriding states. This will likely result in groups of properties being duplicated across multiple selectors and that is desired and expected. + +Each selector should represent a single, clear state of the control and define a minimal set of properties that override the base configuration of the control. Duplicated property values are avoided by using well-named design tokens or private css custom properties. -States should flow from plain control -> hover -> focus -> active -> error -> disabled (which overrides all the others). +States should flow from plain control -> hover -> focus -> active -> disabled (which overrides all the others). For example: @@ -119,8 +121,6 @@ For example: :host(:hover) {} :host(${focusVisible}) {} /* focusVisible is specific to FAST */ :host(:active) {} -:host(:invalid) {} -:host(.custom-state) {} :host([disabled]) {} /* disabled styles override all others in the cascade*/ ``` @@ -131,20 +131,26 @@ Useful reference: [When do the :hover, :focus, and :active pseudo-classes apply? New controls and existing controls undergoing major refactoring should migrate to [CSS Cascade Layers with `@layer`](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer) for organizing styles that override states. Cascade Layers with `@layer` define the order of precedence when multiple cascade layers are present. This helps enforce that property overrides have higher specificity over base styles without needing to modify selectors to increase specificity. For consistency, control styles using CSS Cascade Layers should follow these practices: -- Define the cascade layers: `@layer base, hover, focusVisible, active, disabled, top` - - Avoid changing the order of layers, changing layer names, or creating additional layers. -- Layers in the stylesheet should follow the order by which they are defined above. -- Ensure that all styles are in a layer; no styles for the control should be outside of a layer. -- Styles in layers should continue to follow existing best organization practices, including grouping using document order. +- Define the following cascade layers list exactly: `@layer base, hover, focusVisible, active, disabled, top;` + - The cascade layers list should be the first style defined in a control's top-level `css`. + - Avoid changing the order of layers, changing layer names, or creating additional layers. +- Layers in the stylesheet should follow the order by which they are defined above. +- Ensure that all styles are in a layer; no styles for the control should be outside of a layer. + - This includes leveraging shared styles that are imported. Note: imported styles must be shared as `cssPartial` instead of as a top-level `css` to be used in a layer. + - Custom states, like `appearance`, `checked`, etc. should be configured within one of the existing layers (likely in `base` with interaction overrides in different layers). +- Styles in layers should continue to follow existing best organization practices, including grouping using document order. For example: ```css -@layer base, hover, focusVisible, active, disabled, top +@layer base, hover, focusVisible, active, disabled, top; @layer base { :host { border: green; } + :host([custom-state]) { + border: violet; + } } @layer hover {} @layer focusVisible {} @@ -153,6 +159,9 @@ For example: :host([disabled]) { border: gray; } + :host([disabled][custom-state]) { + border: darkviolet; + } } @layer top {} ``` diff --git a/packages/nimble-components/src/anchor/styles.ts b/packages/nimble-components/src/anchor/styles.ts index d70047a87d..3f27e370cb 100644 --- a/packages/nimble-components/src/anchor/styles.ts +++ b/packages/nimble-components/src/anchor/styles.ts @@ -12,7 +12,7 @@ import { } from '../theme-provider/design-tokens'; export const styles = css` - @layer base, hover, focusVisible, active, disabled; + @layer base, hover, focusVisible, active, disabled, top; @layer base { ${display('inline')} diff --git a/packages/nimble-components/src/breadcrumb-item/styles.ts b/packages/nimble-components/src/breadcrumb-item/styles.ts index 62535b6c2b..ded9aef4e8 100644 --- a/packages/nimble-components/src/breadcrumb-item/styles.ts +++ b/packages/nimble-components/src/breadcrumb-item/styles.ts @@ -14,7 +14,7 @@ import { import { focusVisible } from '../utilities/style/focus'; export const styles = css` - @layer base, hover, focusVisible, active, disabled; + @layer base, hover, focusVisible, active, disabled, top; @layer base { ${display('inline-flex')} diff --git a/packages/nimble-components/src/patterns/button/styles.ts b/packages/nimble-components/src/patterns/button/styles.ts index dadd432d5c..14824df023 100644 --- a/packages/nimble-components/src/patterns/button/styles.ts +++ b/packages/nimble-components/src/patterns/button/styles.ts @@ -26,7 +26,7 @@ import { ButtonAppearance } from './types'; import { accessiblyHidden } from '../../utilities/style/accessibly-hidden'; export const styles = css` - @layer base, checked, hover, focusVisible, active, disabled, top; + @layer base, hover, focusVisible, active, disabled, top; @layer base { ${display('inline-flex')} diff --git a/packages/nimble-components/src/patterns/step/styles.ts b/packages/nimble-components/src/patterns/step/styles.ts index 147bd71c51..2de581c39c 100644 --- a/packages/nimble-components/src/patterns/step/styles.ts +++ b/packages/nimble-components/src/patterns/step/styles.ts @@ -19,7 +19,7 @@ import { focusVisible } from '../../utilities/style/focus'; // import { accessiblyHidden } from '../../utilities/style/accessibly-hidden'; export const styles = css` - @layer base, checked, hover, focusVisible, active, disabled, top; + @layer base, hover, focusVisible, active, disabled, top; @layer base { ${display('inline-flex')} diff --git a/packages/nimble-components/src/toggle-button/styles.ts b/packages/nimble-components/src/toggle-button/styles.ts index ecc652dd6c..c2430ca44b 100644 --- a/packages/nimble-components/src/toggle-button/styles.ts +++ b/packages/nimble-components/src/toggle-button/styles.ts @@ -18,7 +18,7 @@ export const styles = css` ${buttonStyles} ${buttonAppearanceVariantStyles} - @layer checked { + @layer base { .control[aria-pressed='true'] { background-color: transparent; color: ${buttonLabelFontColor}; @@ -48,7 +48,7 @@ export const styles = css` appearanceBehavior( ButtonAppearance.outline, css` - @layer checked { + @layer base { :host([appearance-variant='accent']) .control[aria-pressed='true'] { color: ${buttonAccentOutlineFontColor}; From 53e71f61106e49df2bb8e16de7f9e21e6b17b63f Mon Sep 17 00:00:00 2001 From: rajsite <1588923+rajsite@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:20:10 -0600 Subject: [PATCH 12/26] Finalize default interaction styles --- .../src/patterns/severity/styles.ts | 3 +- .../src/patterns/step/styles.ts | 4 +- .../nimble-components/src/stepper/styles.ts | 2 +- .../anchor-step/anchor-step-matrix.stories.ts | 69 +++++++++++++++++++ .../src/nimble/step/step-matrix.stories.ts | 69 +++++++++++++++++++ 5 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 packages/storybook/src/nimble/anchor-step/anchor-step-matrix.stories.ts create mode 100644 packages/storybook/src/nimble/step/step-matrix.stories.ts diff --git a/packages/nimble-components/src/patterns/severity/styles.ts b/packages/nimble-components/src/patterns/severity/styles.ts index b156d909c6..3015f6bdd0 100644 --- a/packages/nimble-components/src/patterns/severity/styles.ts +++ b/packages/nimble-components/src/patterns/severity/styles.ts @@ -18,7 +18,8 @@ export const styles = cssPartial` color: ${failColor}; width: 100%; position: absolute; - bottom: calc(-1 * (${errorTextFontLineHeight} + 2px)); + ${'' /* The -2px modifier of the bottom position is to intentionally have the severity text slightly overlap the control by 2px */} + bottom: calc(-1 * (${errorTextFontLineHeight} - 2px)); left: 0px; overflow: hidden; text-overflow: ellipsis; diff --git a/packages/nimble-components/src/patterns/step/styles.ts b/packages/nimble-components/src/patterns/step/styles.ts index 2de581c39c..1538b53eb5 100644 --- a/packages/nimble-components/src/patterns/step/styles.ts +++ b/packages/nimble-components/src/patterns/step/styles.ts @@ -16,8 +16,6 @@ import { import { styles as severityStyles } from '../severity/styles'; import { focusVisible } from '../../utilities/style/focus'; -// import { accessiblyHidden } from '../../utilities/style/accessibly-hidden'; - export const styles = css` @layer base, hover, focusVisible, active, disabled, top; @@ -30,7 +28,7 @@ export const styles = css` font: ${buttonLabelFont}; white-space: nowrap; outline: none; - border: 1px solid red; + border: none; --ni-private-step-icon-background-color: rgba(${borderRgbPartialColor}, 0.1); } diff --git a/packages/nimble-components/src/stepper/styles.ts b/packages/nimble-components/src/stepper/styles.ts index 7a737d118d..1f9ec2198c 100644 --- a/packages/nimble-components/src/stepper/styles.ts +++ b/packages/nimble-components/src/stepper/styles.ts @@ -5,7 +5,7 @@ export const styles = css` ${display('inline-flex')} :host { - border: 1px solid blue; + border: none; gap: 0px; } `; diff --git a/packages/storybook/src/nimble/anchor-step/anchor-step-matrix.stories.ts b/packages/storybook/src/nimble/anchor-step/anchor-step-matrix.stories.ts new file mode 100644 index 0000000000..f108d2bc22 --- /dev/null +++ b/packages/storybook/src/nimble/anchor-step/anchor-step-matrix.stories.ts @@ -0,0 +1,69 @@ +import type { StoryFn, Meta } from '@storybook/html-vite'; +import { html, ViewTemplate } from '@ni/fast-element'; +import { anchorStepTag } from '@ni/nimble-components/dist/esm/anchor-step'; +import { stepperTag } from '@ni/nimble-components/dist/esm/stepper'; +import { + createMatrix, + sharedMatrixParameters, + createMatrixThemeStory, + cartesianProduct, + createMatrixInteractionsFromStates +} from '../../utilities/matrix'; +import { createStory } from '../../utilities/storybook'; +import { hiddenWrapper } from '../../utilities/hidden'; +import { textCustomizationWrapper } from '../../utilities/text-customization'; + +const metadata: Meta = { + title: 'Tests/Anchor Step', + parameters: { + ...sharedMatrixParameters() + } +}; + +export default metadata; + +const component = (): ViewTemplate => html` + <${stepperTag}> + <${anchorStepTag} + href="#" + > +
Title
+
Subtitle
+ 😀 + + +`; + +export const themeMatrix: StoryFn = createMatrixThemeStory( + createMatrix(component, [ + ]) +); + +const interactionStatesHover = cartesianProduct([ +] as const); + +const interactionStates = cartesianProduct([ +] as const); + +export const interactionsThemeMatrix: StoryFn = createMatrixThemeStory( + createMatrixInteractionsFromStates(component, { + hover: interactionStatesHover, + hoverActive: interactionStates, + active: interactionStates, + focus: interactionStates + }) +); + +export const hidden: StoryFn = createStory( + hiddenWrapper( + html`<${anchorStepTag} hidden + >Hidden Anchor Button` + ) +); + +export const textCustomized: StoryFn = createMatrixThemeStory( + textCustomizationWrapper( + html`<${anchorStepTag}>Anchor Button` + ) +); diff --git a/packages/storybook/src/nimble/step/step-matrix.stories.ts b/packages/storybook/src/nimble/step/step-matrix.stories.ts new file mode 100644 index 0000000000..2953b94168 --- /dev/null +++ b/packages/storybook/src/nimble/step/step-matrix.stories.ts @@ -0,0 +1,69 @@ +import type { StoryFn, Meta } from '@storybook/html-vite'; +import { html, ViewTemplate } from '@ni/fast-element'; +import { stepTag } from '@ni/nimble-components/dist/esm/step'; +import { stepperTag } from '@ni/nimble-components/dist/esm/stepper'; +import { + createMatrix, + sharedMatrixParameters, + createMatrixThemeStory, + cartesianProduct, + createMatrixInteractionsFromStates +} from '../../utilities/matrix'; +import { createStory } from '../../utilities/storybook'; +import { hiddenWrapper } from '../../utilities/hidden'; +import { textCustomizationWrapper } from '../../utilities/text-customization'; + +const metadata: Meta = { + title: 'Tests/Step', + parameters: { + ...sharedMatrixParameters() + } +}; + +export default metadata; + +const component = (): ViewTemplate => html` + <${stepperTag}> + <${stepTag} + href="#" + > +
Title
+
Subtitle
+ 😀 + + +`; + +export const themeMatrix: StoryFn = createMatrixThemeStory( + createMatrix(component, [ + ]) +); + +const interactionStatesHover = cartesianProduct([ +] as const); + +const interactionStates = cartesianProduct([ +] as const); + +export const interactionsThemeMatrix: StoryFn = createMatrixThemeStory( + createMatrixInteractionsFromStates(component, { + hover: interactionStatesHover, + hoverActive: interactionStates, + active: interactionStates, + focus: interactionStates + }) +); + +export const hidden: StoryFn = createStory( + hiddenWrapper( + html`<${stepTag} hidden + >Hidden Anchor Button` + ) +); + +export const textCustomized: StoryFn = createMatrixThemeStory( + textCustomizationWrapper( + html`<${stepTag}>Anchor Button` + ) +); From 32a514786103ba602d1fdcc3dbd8332d7d2ebfe6 Mon Sep 17 00:00:00 2001 From: rajsite <1588923+rajsite@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:48:53 -0600 Subject: [PATCH 13/26] prop names --- .../nimble-components/src/patterns/step/styles.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/nimble-components/src/patterns/step/styles.ts b/packages/nimble-components/src/patterns/step/styles.ts index 1538b53eb5..27da8c50b0 100644 --- a/packages/nimble-components/src/patterns/step/styles.ts +++ b/packages/nimble-components/src/patterns/step/styles.ts @@ -30,6 +30,9 @@ export const styles = css` outline: none; border: none; --ni-private-step-icon-background-color: rgba(${borderRgbPartialColor}, 0.1); + --ni-private-step-icon-background-size: 100% 100%; + ${'' /* 6px = (2px icon border + 1px inset) * 2 sides */} + --ni-private-step-icon-background-inset-size: calc(100% - 6px) calc(100% - 6px); } ${'' /* Container wrapper for severity text to position against */} @@ -73,7 +76,7 @@ export const styles = css` transparent 100% ); background-origin: border-box; - background-size: 100% 100%; + background-size: var(--ni-private-step-icon-background-size); background-repeat: no-repeat; background-position: center center; position: relative; @@ -149,7 +152,7 @@ export const styles = css` @layer hover { .control:hover .icon { border-color: ${borderHoverColor}; - background-size: calc(100% - 6px) calc(100% - 6px); + background-size: var(--ni-private-step-icon-background-inset-size); } .control:hover .line { @@ -161,7 +164,7 @@ export const styles = css` @layer focusVisible { .control${focusVisible} .icon { border-color: ${borderHoverColor}; - background-size: calc(100% - 6px) calc(100% - 6px); + background-size: var(--ni-private-step-icon-background-inset-size); } .control${focusVisible} .icon::before { @@ -179,7 +182,7 @@ export const styles = css` .control:active .icon { border-color: ${borderHoverColor}; --ni-private-step-icon-background-color: ${fillSelectedColor}; - background-size: 100% 100%; + background-size: var(--ni-private-step-icon-background-size); } .control:active .icon::before { From 9a0663b5864645fa2045ddcfc00b26921059cfc4 Mon Sep 17 00:00:00 2001 From: rajsite <1588923+rajsite@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:37:24 -0600 Subject: [PATCH 14/26] success severity --- .../src/patterns/step/styles.ts | 85 ++++++++++++++----- .../src/nimble/stepper/stepper.stories.ts | 4 +- 2 files changed, 66 insertions(+), 23 deletions(-) diff --git a/packages/nimble-components/src/patterns/step/styles.ts b/packages/nimble-components/src/patterns/step/styles.ts index 27da8c50b0..ae460c8b91 100644 --- a/packages/nimble-components/src/patterns/step/styles.ts +++ b/packages/nimble-components/src/patterns/step/styles.ts @@ -12,6 +12,7 @@ import { borderWidth, smallDelay, fillSelectedColor, + passColor } from '../../theme-provider/design-tokens'; import { styles as severityStyles } from '../severity/styles'; import { focusVisible } from '../../utilities/style/focus'; @@ -29,10 +30,6 @@ export const styles = css` white-space: nowrap; outline: none; border: none; - --ni-private-step-icon-background-color: rgba(${borderRgbPartialColor}, 0.1); - --ni-private-step-icon-background-size: 100% 100%; - ${'' /* 6px = (2px icon border + 1px inset) * 2 sides */} - --ni-private-step-icon-background-inset-size: calc(100% - 6px) calc(100% - 6px); } ${'' /* Container wrapper for severity text to position against */} @@ -56,6 +53,21 @@ export const styles = css` outline: none; margin: 0; padding: 0 ${smallPadding} 0 0; + --ni-private-step-icon-background-default-size: 100% 100%; + ${'' /* 6px = (2px icon border + 1px inset) * 2 sides */} + --ni-private-step-icon-background-inset-size: calc(100% - 6px) calc(100% - 6px); + + --ni-private-step-icon-border-color: transparent; + --ni-private-step-icon-background-color: rgba(${borderRgbPartialColor}, 0.1); + --ni-private-step-icon-outline-inset-color: transparent; + --ni-private-step-line-color: rgba(${borderRgbPartialColor}, 0.1); + } + + :host([severity="success"]) .control { + --ni-private-step-icon-border-color: ${passColor}; + --ni-private-step-icon-background-color: ${passColor}; + --ni-private-step-icon-outline-inset-color: ${passColor}; + --ni-private-step-line-color: rgba(${borderRgbPartialColor}, 0.1); } .icon { @@ -65,7 +77,7 @@ export const styles = css` flex: none; height: 32px; width: 32px; - border: 2px solid transparent; + border: 2px solid var(--ni-private-step-icon-border-color); border-radius: 100%; background-image: radial-gradient( closest-side, @@ -76,12 +88,11 @@ export const styles = css` transparent 100% ); background-origin: border-box; - background-size: var(--ni-private-step-icon-background-size); + background-size: var(--ni-private-step-icon-background-default-size); background-repeat: no-repeat; background-position: center center; position: relative; transition: - box-shadow ${smallDelay} ease-in-out, border-color ${smallDelay} ease-in-out, background-size ${smallDelay} ease-in-out; } @@ -92,15 +103,18 @@ export const styles = css` width: 100%; height: 100%; pointer-events: none; - outline: 0px solid transparent; + outline-color: var(--ni-private-step-icon-outline-inset-color); + outline-style: solid; + outline-width: 0px; outline-offset: 0px; border: 1px solid transparent; border-radius: 100%; color: transparent; background-clip: border-box; transition: - outline-offset ${smallDelay} ease-in-out, - outline ${smallDelay} ease-in-out; + outline-color ${smallDelay} ease-in-out, + outline-width ${smallDelay} ease-in-out, + outline-offset ${smallDelay} ease-in-out; } .content { @@ -137,7 +151,7 @@ export const styles = css` display: inline-block; flex: 1; height: 1px; - background: rgba(${borderRgbPartialColor}, 0.1); + background: var(--ni-private-step-line-color); transform: scale(1, 1); transition: background-color ${smallDelay} ease-in-out, @@ -150,48 +164,77 @@ export const styles = css` } @layer hover { + .control:hover { + --ni-private-step-icon-border-color: ${borderHoverColor}; + --ni-private-step-line-color: ${borderHoverColor}; + } + + :host([severity="success"]) .control:hover { + --ni-private-step-icon-border-color: ${passColor}; + --ni-private-step-line-color: ${passColor}; + } + .control:hover .icon { - border-color: ${borderHoverColor}; background-size: var(--ni-private-step-icon-background-inset-size); } .control:hover .line { - background-color: ${borderHoverColor}; transform: scale(1, 2); } } @layer focusVisible { + .control${focusVisible} { + --ni-private-step-icon-border-color: ${borderHoverColor}; + --ni-private-step-icon-outline-inset-color: ${borderHoverColor}; + --ni-private-step-line-color: ${borderHoverColor}; + } + + :host([severity="success"]) .control${focusVisible} { + --ni-private-step-icon-border-color: ${passColor}; + --ni-private-step-icon-background-color: ${passColor}; + --ni-private-step-icon-outline-inset-color: ${passColor}; + --ni-private-step-line-color: ${passColor}; + } + .control${focusVisible} .icon { - border-color: ${borderHoverColor}; background-size: var(--ni-private-step-icon-background-inset-size); } .control${focusVisible} .icon::before { - outline: ${borderWidth} solid ${borderHoverColor}; + outline-width: ${borderWidth}; outline-offset: -2px; } .control${focusVisible} .line { - background-color: ${borderHoverColor}; transform: scale(1, 2); } } @layer active { - .control:active .icon { - border-color: ${borderHoverColor}; + .control:active { + --ni-private-step-icon-border-color: ${borderHoverColor}; --ni-private-step-icon-background-color: ${fillSelectedColor}; - background-size: var(--ni-private-step-icon-background-size); + --ni-private-step-icon-outline-inset-color: transparent; + --ni-private-step-line-color: ${borderHoverColor}; + } + + :host([severity="success"]) .control:active { + --ni-private-step-icon-border-color: ${passColor}; + --ni-private-step-icon-background-color: rgb(from ${passColor} r g b / 30%); + --ni-private-step-line-color: ${passColor}; + } + + .control:active .icon { + background-size: var(--ni-private-step-icon-background-default-size); } .control:active .icon::before { - outline: 0px solid transparent; + outline-width: 0px; outline-offset: 0px; } .control:active .line { - background-color: ${borderHoverColor}; transform: scale(1, 1); } } diff --git a/packages/storybook/src/nimble/stepper/stepper.stories.ts b/packages/storybook/src/nimble/stepper/stepper.stories.ts index f48684a95f..33c64768b7 100644 --- a/packages/storybook/src/nimble/stepper/stepper.stories.ts +++ b/packages/storybook/src/nimble/stepper/stepper.stories.ts @@ -25,12 +25,12 @@ const metadata: Meta = { statusLink: 'https://github.com/ni/nimble/issues/624' }) */} <${stepperTag}> - <${anchorStepTag} severity="error" href="#" severity-text="Error Description" style="width:150px"> + <${anchorStepTag} severity="success" href="#" severity-text="Error Description" style="width:150px"> 😀
Title
Subtitle
- <${stepTag} severity="error" severity-text="Error Description" style="width:150px"> + <${stepTag} severity="success" severity-text="Error Description" style="width:150px"> 😀
Title
Subtitle
From 11613c947a373ceb279f91917291f16b34ba78a2 Mon Sep 17 00:00:00 2001 From: rajsite <1588923+rajsite@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:39:32 -0600 Subject: [PATCH 15/26] StepInternals added and Severity pattern clean-up --- .../src/anchor-step/index.ts | 15 ++ .../src/anchor-step/template.ts | 106 +++++++------ .../src/anchor-step/types.ts | 4 + .../src/patterns/severity/types.ts | 6 +- .../patterns/step/models/step-internals.ts | 13 ++ .../src/patterns/step/types.ts | 9 +- packages/nimble-components/src/step/index.ts | 15 ++ .../nimble-components/src/step/template.ts | 114 +++++++------- packages/nimble-components/src/step/types.ts | 4 + .../nimble-components/src/stepper/template.ts | 2 +- .../nimble-components/src/stepper/types.ts | 10 ++ .../src/nimble/stepper/stepper.stories.ts | 148 ++++++++++++++++-- 12 files changed, 322 insertions(+), 124 deletions(-) create mode 100644 packages/nimble-components/src/anchor-step/types.ts create mode 100644 packages/nimble-components/src/patterns/step/models/step-internals.ts create mode 100644 packages/nimble-components/src/step/types.ts create mode 100644 packages/nimble-components/src/stepper/types.ts diff --git a/packages/nimble-components/src/anchor-step/index.ts b/packages/nimble-components/src/anchor-step/index.ts index d55dfe8077..b411caa756 100644 --- a/packages/nimble-components/src/anchor-step/index.ts +++ b/packages/nimble-components/src/anchor-step/index.ts @@ -5,6 +5,8 @@ import { template } from './template'; import { AnchorBase } from '../anchor-base'; import { mixinSeverityPattern } from '../patterns/severity/types'; import type { StepPattern } from '../patterns/step/types'; +import { StepInternals } from '../patterns/step/models/step-internals'; +import { AnchorStepSeverity } from './types'; declare global { interface HTMLElementTagNameMap { @@ -16,6 +18,14 @@ declare global { * A nimble-styled anchor step for a stepper */ export class AnchorStep extends mixinSeverityPattern(AnchorBase) implements StepPattern { + /** + * @public + * @remarks + * HTML Attribute: disabled + */ + @attr() + public severity: AnchorStepSeverity = AnchorStepSeverity.default; + /** * @public * @remarks @@ -47,6 +57,11 @@ export class AnchorStep extends mixinSeverityPattern(AnchorBase) implements Step */ @attr({ attribute: 'tabindex', converter: nullableNumberConverter }) public override tabIndex!: number; + + /** + * @internal + */ + public readonly stepInternals = new StepInternals(); } const nimbleAnchorStep = AnchorStep.compose({ diff --git a/packages/nimble-components/src/anchor-step/template.ts b/packages/nimble-components/src/anchor-step/template.ts index ffb8ce4e1b..3ef1b8c899 100644 --- a/packages/nimble-components/src/anchor-step/template.ts +++ b/packages/nimble-components/src/anchor-step/template.ts @@ -7,56 +7,62 @@ export const template: FoundationElementTemplate< ViewTemplate, AnchorOptions > = (context, definition) => html` -
- (x.disabled ? null : x.href)} - hreflang="${x => x.hreflang}" - ping="${x => x.ping}" - referrerpolicy="${x => x.referrerpolicy}" - rel="${x => x.rel}" - target="${x => x.target}" - type="${x => x.type}" - tabindex="${x => x.tabIndex}" - aria-atomic="${x => x.ariaAtomic}" - aria-busy="${x => x.ariaBusy}" - aria-controls="${x => x.ariaControls}" - aria-current="${x => x.ariaCurrent}" - aria-describedby="${x => x.ariaDescribedby}" - aria-details="${x => x.ariaDetails}" - aria-disabled="${x => x.ariaDisabled}" - aria-errormessage="${x => x.ariaErrormessage}" - aria-expanded="${x => x.ariaExpanded}" - aria-flowto="${x => x.ariaFlowto}" - aria-haspopup="${x => x.ariaHaspopup}" - aria-hidden="${x => x.ariaHidden}" - aria-invalid="${x => x.ariaInvalid}" - aria-keyshortcuts="${x => x.ariaKeyshortcuts}" - aria-label="${x => x.ariaLabel}" - aria-labelledby="${x => x.ariaLabelledby}" - aria-live="${x => x.ariaLive}" - aria-owns="${x => x.ariaOwns}" - aria-relevant="${x => x.ariaRelevant}" - aria-roledescription="${x => x.ariaRoledescription}" - ${ref('control')} - > -
- -
-
-
- ${startSlotTemplate(context, definition)} -
- ${endSlotTemplate(context, definition)} -
+ `; diff --git a/packages/nimble-components/src/anchor-step/types.ts b/packages/nimble-components/src/anchor-step/types.ts new file mode 100644 index 0000000000..de7f8749b4 --- /dev/null +++ b/packages/nimble-components/src/anchor-step/types.ts @@ -0,0 +1,4 @@ +import { Severity } from '../patterns/severity/types'; + +export const AnchorStepSeverity = Severity; +export type AnchorStepSeverity = (typeof AnchorStepSeverity)[keyof typeof AnchorStepSeverity]; diff --git a/packages/nimble-components/src/patterns/severity/types.ts b/packages/nimble-components/src/patterns/severity/types.ts index 5df6cf8022..3d67e689ca 100644 --- a/packages/nimble-components/src/patterns/severity/types.ts +++ b/packages/nimble-components/src/patterns/severity/types.ts @@ -29,7 +29,7 @@ export function mixinSeverityPattern( /** * The severity state of the element */ - public severity: Severity; + public abstract severity: Severity; /** * The severity text that will be displayed when a component is not in the default severity state @@ -42,10 +42,6 @@ export function mixinSeverityPattern( */ public severityHasOverflow = false; } - attr()( - SeverityPatternElement.prototype, - 'severity' - ); attr({ attribute: 'severity-text' })( SeverityPatternElement.prototype, 'severityText' diff --git a/packages/nimble-components/src/patterns/step/models/step-internals.ts b/packages/nimble-components/src/patterns/step/models/step-internals.ts new file mode 100644 index 0000000000..0dc5e0e61a --- /dev/null +++ b/packages/nimble-components/src/patterns/step/models/step-internals.ts @@ -0,0 +1,13 @@ +import { observable } from '@ni/fast-element'; +import type { StepperOrientation } from '../../../stepper/types'; + +/** + * Internal properties configurable for a step + */ +export class StepInternals { + @observable + public orientation: StepperOrientation; + + @observable + public last = false; +} diff --git a/packages/nimble-components/src/patterns/step/types.ts b/packages/nimble-components/src/patterns/step/types.ts index 55467747d3..4159ef7053 100644 --- a/packages/nimble-components/src/patterns/step/types.ts +++ b/packages/nimble-components/src/patterns/step/types.ts @@ -1,6 +1,7 @@ -import type { Severity } from '../severity/types'; +import type { SeverityPattern } from '../severity/types'; +import type { StepInternals } from './models/step-internals'; -export interface StepPattern { +export interface StepPattern extends SeverityPattern { /** * Whether or not the step is disabled. */ @@ -17,7 +18,7 @@ export interface StepPattern { selected: boolean; /** - * The severity state of the step. + * @internal Internal step state set by the stepper */ - severity: Severity; + stepInternals: StepInternals; } diff --git a/packages/nimble-components/src/step/index.ts b/packages/nimble-components/src/step/index.ts index 39aaddc28f..7cdc306431 100644 --- a/packages/nimble-components/src/step/index.ts +++ b/packages/nimble-components/src/step/index.ts @@ -8,6 +8,8 @@ import { styles } from './styles'; import { template } from './template'; import type { StepPattern } from '../patterns/step/types'; import { mixinSeverityPattern } from '../patterns/severity/types'; +import { StepInternals } from '../patterns/step/models/step-internals'; +import { StepSeverity } from './types'; declare global { interface HTMLElementTagNameMap { @@ -19,6 +21,14 @@ declare global { * A nimble-styled step for a stepper */ export class Step extends mixinSeverityPattern(FoundationButton) implements StepPattern { + /** + * @public + * @remarks + * HTML Attribute: disabled + */ + @attr() + public severity: StepSeverity = StepSeverity.default; + /** * @public * @remarks @@ -42,6 +52,11 @@ export class Step extends mixinSeverityPattern(FoundationButton) implements Step */ @attr({ attribute: 'tabindex', converter: nullableNumberConverter }) public override tabIndex!: number; + + /** + * @internal + */ + public readonly stepInternals = new StepInternals(); } const nimbleStep = Step.compose({ diff --git a/packages/nimble-components/src/step/template.ts b/packages/nimble-components/src/step/template.ts index b607305ae5..0b9b7be5f6 100644 --- a/packages/nimble-components/src/step/template.ts +++ b/packages/nimble-components/src/step/template.ts @@ -7,60 +7,66 @@ export const template: FoundationElementTemplate< ViewTemplate, ButtonOptions > = (context, definition) => html` -
-