Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
12dfe46
Severity text pattern first pass
rajsite Feb 5, 2026
6d65680
Update step base classes
rajsite Feb 6, 2026
8f9a2fd
Change files
rajsite Feb 6, 2026
34a3a43
WIP
rajsite Feb 7, 2026
9e7f34c
WIP
rajsite Feb 9, 2026
3272082
template layout
rajsite Feb 10, 2026
9da273f
WIP interaction states
rajsite Feb 12, 2026
7e588e1
fix test
rajsite Feb 12, 2026
8021eba
severity text placement
rajsite Feb 12, 2026
f535c4d
🧈
rajsite Feb 12, 2026
f8d76da
Clarify CSS Layer style guidelines
rajsite Feb 12, 2026
4cedb1b
Merge branch 'main' into step-components
rajsite Feb 12, 2026
3b09934
Merge branch 'main' into step-components
rajsite Feb 12, 2026
71a10c0
Merge branch 'main' into step-components
rajsite Feb 16, 2026
53e71f6
Finalize default interaction styles
rajsite Feb 16, 2026
32a5147
prop names
rajsite Feb 16, 2026
1f1b04c
Merge branch 'main' into step-components
rajsite Feb 17, 2026
9a0663b
success severity
rajsite Feb 17, 2026
11613c9
StepInternals added and Severity pattern clean-up
rajsite Feb 18, 2026
2013cf6
Fix test
rajsite Feb 18, 2026
adec05c
actually fix test πŸ˜…
rajsite Feb 18, 2026
d15dbee
Add severity error
rajsite Feb 18, 2026
dc3ca33
warning severity
rajsite Feb 18, 2026
3f05847
Add selected state
rajsite Feb 18, 2026
71dcc49
revert disable jsamine retry
rajsite Feb 19, 2026
9a5f319
revert button layers change for now
rajsite Feb 19, 2026
fa6ef00
Merge remote-tracking branch 'origin/main' into step-components
rajsite Feb 19, 2026
c968296
interaction state tests
rajsite Feb 19, 2026
e459f37
Add disabled state
rajsite Feb 19, 2026
7cbabfd
update test
rajsite Feb 19, 2026
ba0a4f0
Fix anchor story
rajsite Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "step and anchor-step",
"packageName": "@ni/nimble-components",
"email": "1588923+rajsite@users.noreply.github.com",
"dependentChangeType": "patch"
}
29 changes: 19 additions & 10 deletions packages/nimble-components/docs/css-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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*/
```

Expand All @@ -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 {}
Expand All @@ -153,6 +159,9 @@ For example:
:host([disabled]) {
border: gray;
}
:host([disabled][custom-state]) {
border: darkviolet;
}
}
@layer top {}
```
Expand Down
62 changes: 58 additions & 4 deletions packages/nimble-components/src/anchor-step/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { DesignSystem, FoundationElement } from '@ni/fast-foundation';
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';
import { StepInternals } from '../patterns/step/models/step-internals';
import { AnchorStepSeverity } from './types';

declare global {
interface HTMLElementTagNameMap {
Expand All @@ -11,12 +17,60 @@ declare global {
/**
* A nimble-styled anchor step for a stepper
*/
export class AnchorStep extends FoundationElement {}
export class AnchorStep extends mixinSeverityPattern(AnchorBase) implements StepPattern {
/**
* @public
* @remarks
* HTML Attribute: disabled
*/
@attr()
public severity: AnchorStepSeverity = AnchorStepSeverity.default;

const nimbleAnchorStep = AnchorStep.compose({
/**
* @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;

/**
* @internal
*/
public readonly stepInternals = new StepInternals();
}

const nimbleAnchorStep = AnchorStep.compose<AnchorOptions>({
baseName: 'anchor-step',
template,
styles
styles,
shadowOptions: {
delegatesFocus: true
}
});

DesignSystem.getOrCreate().withPrefix('nimble').register(nimbleAnchorStep());
Expand Down
10 changes: 8 additions & 2 deletions packages/nimble-components/src/anchor-step/styles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
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('flex')}
${stepStyles}
${'' /* Anchor specific styles */}
@layer base {
.control {
text-decoration: none;
}
}
`;
68 changes: 65 additions & 3 deletions packages/nimble-components/src/anchor-step/template.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,68 @@
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 '.';
import { severityTextTemplate } from '../patterns/severity/template';

export const template = html<AnchorStep>`
<template>anchor step</template>
export const template: FoundationElementTemplate<
ViewTemplate<AnchorStep>,
AnchorOptions
> = (context, definition) => html<AnchorStep>`
<template slot="step">
<div class="
container
${x => (x.stepInternals.orientation === 'vertical' ? 'vertical' : '')}
${x => (x.stepInternals.last ? 'last' : '')}
">
<a
class="control"
part="control"
download="${x => x.download}"
href=${x => (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')}
>
<div class="icon">
<slot ${slotted('defaultSlottedContent')}></slot>
</div>
<div class="content">
<div class="title-wrapper">
${startSlotTemplate(context, definition)}
<div class="title"><slot name="title"></slot></div>
${endSlotTemplate(context, definition)}
<div class="line"></div>
</div>
<div class="subtitle">
<slot name="subtitle"></slot>
</div>
</div>
</a>
${severityTextTemplate}
</div>
</template>
`;
4 changes: 4 additions & 0 deletions packages/nimble-components/src/anchor-step/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Severity } from '../patterns/severity/types';

export const AnchorStepSeverity = Severity;
export type AnchorStepSeverity = (typeof AnchorStepSeverity)[keyof typeof AnchorStepSeverity];
2 changes: 1 addition & 1 deletion packages/nimble-components/src/anchor/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')}
Expand Down
2 changes: 1 addition & 1 deletion packages/nimble-components/src/breadcrumb-item/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')}
Expand Down
42 changes: 42 additions & 0 deletions packages/nimble-components/src/patterns/severity/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { cssPartial } from '@ni/fast-element';
import {
failColor,
errorTextFontLineHeight,
warningColor,
errorTextFont,
buttonLabelFontColor
} from '../../theme-provider/design-tokens';

// 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;
}

.severity-text {
display: block;
font: ${errorTextFont};
color: ${buttonLabelFontColor};
width: 100%;
position: absolute;
${'' /* 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;
white-space: nowrap;
}

:host([severity="error"]) .severity-text {
color: ${failColor};
}

:host([severity="warning"]) .severity-text {
color: ${warningColor};
}

:host([severity]) .severity-text:empty {
display: none;
}
`;
14 changes: 14 additions & 0 deletions packages/nimble-components/src/patterns/severity/template.ts
Original file line number Diff line number Diff line change
@@ -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<SeverityPattern>`
<div
class="severity-text"
${overflow('severityHasOverflow')}
title="${x => (x.severityHasOverflow && x.severityText ? x.severityText : undefined)}"
aria-live="polite"
>
${x => x.severityText}
</div>
`;
Original file line number Diff line number Diff line change
@@ -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');
}
}
Loading
Loading