Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Add readonly behavior to step and anchor step",
"packageName": "@ni/nimble-components",
"email": "1588923+rajsite@users.noreply.github.com",
"dependentChangeType": "patch"
}
10 changes: 10 additions & 0 deletions packages/nimble-components/src/anchor-step/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ export class AnchorStep extends mixinSeverityPattern(AnchorBase) implements Step
* @internal
*/
public readonly stepInternals = new StepInternals();

/**
* @internal
*/
public onClick(e: Event): void {
if (this.disabled || this.readOnly) {
e.preventDefault();
e.stopImmediatePropagation();
}
}
}

const nimbleAnchorStep = AnchorStep.compose<AnchorOptions>({
Expand Down
1 change: 1 addition & 0 deletions packages/nimble-components/src/anchor-step/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ AnchorOptions
aria-owns="${x => x.ariaOwns}"
aria-relevant="${x => x.ariaRelevant}"
aria-roledescription="${x => x.ariaRoledescription}"
@click="${(x, c) => x.onClick(c.event)}"
${ref('control')}
>
<div class="icon-background"></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { parameterizeSpec } from '@ni/jasmine-parameterized';
import { AnchorStep, anchorStepTag } from '..';
import { waitForUpdatesAsync } from '../../testing/async-helpers';
import { fixture, type Fixture } from '../../utilities/tests/fixture';
import { AnchorStepPageObject } from '../testing/anchor-step.pageobject';

async function setup(): Promise<Fixture<AnchorStep>> {
return await fixture<AnchorStep>(
Expand All @@ -14,9 +15,11 @@ describe('AnchorStep', () => {
let element: AnchorStep;
let connect: () => Promise<void>;
let disconnect: () => Promise<void>;
let pageObject: AnchorStepPageObject;

beforeEach(async () => {
({ element, connect, disconnect } = await setup());
pageObject = new AnchorStepPageObject(element);
});

afterEach(async () => {
Expand Down Expand Up @@ -110,4 +113,35 @@ describe('AnchorStep', () => {

expect(element.control!.hasAttribute('tabindex')).toBeFalse();
});

describe('click event', () => {
it('should fire when clicked', async () => {
const stepClicked = jasmine.createSpy();
element.addEventListener('click', stepClicked);
await connect();

pageObject.click();
expect(stepClicked.calls.count()).toEqual(1);
});

it('should not fire when disabled', async () => {
const stepClicked = jasmine.createSpy();
element.addEventListener('click', stepClicked);
element.disabled = true;
await connect();

pageObject.click();
expect(stepClicked.calls.count()).toEqual(0);
});

it('should not fire when readonly', async () => {
const stepClicked = jasmine.createSpy();
element.addEventListener('click', stepClicked);
element.readOnly = true;
await connect();

pageObject.click();
expect(stepClicked.calls.count()).toEqual(0);
});
});
});
56 changes: 40 additions & 16 deletions packages/nimble-components/src/patterns/step/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export const styles = css`
:host([selected]) .control {
--ni-private-step-icon-color: ${borderHoverColor};
--ni-private-step-icon-border-color: ${borderHoverColor};
--ni-private-step-icon-border-width: 2px;
--ni-private-step-icon-background-color: rgb(from ${borderHoverColor} r g b / 30%);
--ni-private-step-icon-background-size: var(--ni-private-step-icon-background-none-size);
--ni-private-step-line-color: ${borderHoverColor};
Expand Down Expand Up @@ -223,10 +224,6 @@ export const styles = css`
box-shadow ${smallDelay} ease-in-out;
}

:host([selected]) .icon {
--ni-private-step-icon-border-width: 2px;
}

.icon::before {
content: '';
position: absolute;
Expand All @@ -236,7 +233,8 @@ export const styles = css`
outline-color: var(--ni-private-step-icon-outline-inset-color);
outline-style: solid;
outline-width: 0px;
outline-offset: 0px;
${'' /* outline-offset should always be <=-1 to always render inset */}
outline-offset: -1px;
border: none;
border-radius: 100%;
color: transparent;
Expand Down Expand Up @@ -314,9 +312,9 @@ export const styles = css`
min-width: ${standardPadding};
height: 1px;
min-height: 1px;
transform: scale(1, 1);
background: var(--ni-private-step-line-color);
background-clip: content-box;
transform: scale(1, 1);
transition:
background-color ${smallDelay} ease-in-out,
transform ${smallDelay} ease-in-out;
Expand All @@ -330,8 +328,8 @@ export const styles = css`
width: 1px;
min-width: 1px;
height: 100%;
padding-top: ${smallPadding};
min-height: ${standardPadding};
padding-top: ${smallPadding};
}

.subtitle {
Expand Down Expand Up @@ -360,6 +358,7 @@ export const styles = css`
@layer hover {
.control:hover {
--ni-private-step-icon-border-color: ${borderHoverColor};
--ni-private-step-icon-border-width: 2px;
--ni-private-step-icon-background-size: var(--ni-private-step-icon-background-inset-size);
--ni-private-step-line-color: ${borderHoverColor};
}
Expand Down Expand Up @@ -390,8 +389,12 @@ export const styles = css`
--ni-private-step-line-color: ${borderHoverColor};
}

.control:hover .icon {
--ni-private-step-icon-border-width: 2px;
:host([readonly]) .control:hover {
--ni-private-step-icon-color: revert-layer;
--ni-private-step-icon-border-color: revert-layer;
--ni-private-step-icon-border-width: revert-layer;
--ni-private-step-icon-background-size: revert-layer;
--ni-private-step-line-color: revert-layer;
}

.control:hover .line {
Expand All @@ -401,11 +404,16 @@ export const styles = css`
.container.vertical .control:hover .line {
transform: scale(2, 1);
}

:host([readonly]) .container .control:hover .line {
transform: revert-layer;
}
}

@layer focusVisible {
.control${focusVisible} {
--ni-private-step-icon-border-color: ${borderHoverColor};
--ni-private-step-icon-border-width: 2px;
--ni-private-step-icon-outline-inset-color: ${borderHoverColor};
--ni-private-step-icon-background-size: var(--ni-private-step-icon-background-inset-size);
--ni-private-step-line-color: ${borderHoverColor};
Expand Down Expand Up @@ -441,10 +449,6 @@ export const styles = css`
--ni-private-step-line-color: ${borderHoverColor};
}

.control${focusVisible} .icon {
--ni-private-step-icon-border-width: 2px;
}

.control${focusVisible} .icon::before {
outline-width: ${borderWidth};
${'' /* -1px control to outline edge -2px focus border -1px inset gap */}
Expand All @@ -463,6 +467,7 @@ export const styles = css`
@layer active {
.control:active {
--ni-private-step-icon-border-color: ${borderHoverColor};
--ni-private-step-icon-border-width: 2px;
--ni-private-step-icon-background-color: ${fillSelectedColor};
--ni-private-step-icon-background-size: var(--ni-private-step-icon-background-full-size);
--ni-private-step-line-color: ${borderHoverColor};
Expand Down Expand Up @@ -494,26 +499,45 @@ export const styles = css`
--ni-private-step-line-color: ${borderHoverColor};
}

.control:active .icon {
--ni-private-step-icon-border-width: 2px;
:host([readonly]) .control:active {
--ni-private-step-icon-color: revert-layer;
--ni-private-step-icon-border-color: revert-layer;
--ni-private-step-icon-border-width: revert-layer;
--ni-private-step-icon-background-color: revert-layer;
--ni-private-step-icon-background-size: revert-layer;
--ni-private-step-line-color: revert-layer;
}

.control:active .icon::before {
outline-width: 0px;
outline-offset: 0px;
outline-offset: -1px;
}

:host([readonly]) .control:active .icon::before {
outline-width: revert-layer;
outline-offset: revert-layer;
}

.control:active .line {
transform: scale(1, 1);
}

:host([readonly]) .control:active .line {
transform: revert-layer;
}
}

@layer disabled {
:host([readonly]) .control {
cursor: default;
}

:host([disabled]) .control {
cursor: default;
color: ${buttonLabelDisabledFontColor};
--ni-private-step-icon-color: rgb(from ${buttonLabelFontColor} r g b / 30%);
--ni-private-step-icon-border-color: transparent;
--ni-private-step-icon-border-width: 1px;
--ni-private-step-icon-background-color: rgba(${borderRgbPartialColor}, 0.1);
--ni-private-step-icon-background-size: var(--ni-private-step-icon-background-full-size);
--ni-private-step-icon-outline-inset-color: transparent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,8 @@ export abstract class StepBasePageObject<T extends StepPattern = StepPattern> {
}
return label;
}

public click(): void {
this.element.control!.click();
}
}
6 changes: 6 additions & 0 deletions packages/nimble-components/src/patterns/step/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,10 @@ export interface StepPattern extends SeverityPattern, HTMLElement {
* @internal
*/
stepInternals: StepInternals;

/**
* Primary control for interactions
* @internal
*/
control?: HTMLElement;
}
10 changes: 10 additions & 0 deletions packages/nimble-components/src/step/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ export class Step extends mixinSeverityPattern(FoundationButton) implements Step
* @internal
*/
public readonly stepInternals = new StepInternals();

/**
* @internal
*/
public onClick(e: Event): void {
if (this.disabled || this.readOnly) {
e.preventDefault();
e.stopImmediatePropagation();
}
}
}

const nimbleStep = Step.compose<ButtonOptions>({
Expand Down
1 change: 1 addition & 0 deletions packages/nimble-components/src/step/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ ButtonOptions
aria-pressed="${x => x.ariaPressed}"
aria-relevant="${x => x.ariaRelevant}"
aria-roledescription="${x => x.ariaRoledescription}"
@click="${(x, c) => x.onClick(c.event)}"
${ref('control')}
>
<div class="icon-background"></div>
Expand Down
61 changes: 50 additions & 11 deletions packages/nimble-components/src/step/tests/step.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,51 @@ import { html } from '@ni/fast-element';
import { Step, stepTag } from '..';
import { fixture, type Fixture } from '../../utilities/tests/fixture';
import { waitForUpdatesAsync } from '../../testing/async-helpers';
import { StepPageObject } from '../testing/step.pageobject';

async function setup(): Promise<Fixture<Step>> {
return await fixture<Step>(
html`<${stepTag}></${stepTag}>`
);
}

describe('Step', () => {
async function setup(): Promise<Fixture<Step>> {
return await fixture<Step>(html`<${stepTag}></${stepTag}>`);
}
let element: Step;
let connect: () => Promise<void>;
let disconnect: () => Promise<void>;
let pageObject: StepPageObject;

beforeEach(async () => {
({ element, connect, disconnect } = await setup());
pageObject = new StepPageObject(element);
});

afterEach(async () => {
await disconnect();
});

it('can construct an element instance', () => {
expect(document.createElement(stepTag)).toBeInstanceOf(Step);
});

it('should default tabIndex on the internal button to 0', async () => {
const { element, connect, disconnect } = await setup();
await connect();

const innerStep = element.shadowRoot!.querySelector('button')!;
expect(innerStep.getAttribute('tabindex')).toBeNull();
expect(innerStep.tabIndex).toEqual(0);

await disconnect();
});

it('should set the `tabindex` attribute on the internal button when provided', async () => {
const { element, connect, disconnect } = await setup();
element.setAttribute('tabindex', '-1');
await connect();

const innerStep = element.shadowRoot!.querySelector('button')!;
expect(innerStep.getAttribute('tabindex')).toEqual('-1');
expect(innerStep.tabIndex).toEqual(-1);

await disconnect();
});

it('should clear the `tabindex` attribute on the internal button when cleared from the host', async () => {
const { element, connect, disconnect } = await setup();
element.setAttribute('tabindex', '-1');
await connect();

Expand All @@ -46,7 +56,36 @@ describe('Step', () => {
const innerStep = element.shadowRoot!.querySelector('button')!;
expect(innerStep.getAttribute('tabindex')).toBeNull();
expect(innerStep.tabIndex).toEqual(0);
});

await disconnect();
describe('click event', () => {
it('should fire when clicked', async () => {
const stepClicked = jasmine.createSpy();
element.addEventListener('click', stepClicked);
await connect();

pageObject.click();
expect(stepClicked.calls.count()).toEqual(1);
});

it('should not fire when disabled', async () => {
const stepClicked = jasmine.createSpy();
element.addEventListener('click', stepClicked);
element.disabled = true;
await connect();

pageObject.click();
expect(stepClicked.calls.count()).toEqual(0);
});

it('should not fire when readonly', async () => {
const stepClicked = jasmine.createSpy();
element.addEventListener('click', stepClicked);
element.readOnly = true;
await connect();

pageObject.click();
expect(stepClicked.calls.count()).toEqual(0);
});
});
});
Loading
Loading