diff --git a/packages/core/src/components/workflow-step/workflow-step.scss b/packages/core/src/components/workflow-step/workflow-step.scss index 572b37cea08..5ef0d71d94f 100644 --- a/packages/core/src/components/workflow-step/workflow-step.scss +++ b/packages/core/src/components/workflow-step/workflow-step.scss @@ -166,8 +166,8 @@ } &:focus-visible { - outline: 1px solid var(--focus--border-color); - border-radius: 0; + outline: 1px solid var(--theme-color-focus-bdr); + outline-offset: var(--theme-btn--focus--outline-offset); } &.selected { diff --git a/packages/core/src/components/workflow-step/workflow-step.tsx b/packages/core/src/components/workflow-step/workflow-step.tsx index fccd7f0e7a5..6150c67a260 100644 --- a/packages/core/src/components/workflow-step/workflow-step.tsx +++ b/packages/core/src/components/workflow-step/workflow-step.tsx @@ -79,7 +79,6 @@ export class WorkflowStep { @Event() selectedChanged!: EventEmitter; private customIconSlot: boolean = false; - @Watch('selected') selectedHandler() { this.setWorkflowStepStyles(); @@ -139,7 +138,12 @@ export class WorkflowStep { this.selectedChanged.emit(this.hostElement); } } - + onKeyDown(event: KeyboardEvent) { + if (event.key === ' ' || event.key === 'Enter') { + event.preventDefault(); + this.onStepClick(); + } + } getIconAriaLabel() { switch (this.iconName) { case iconCircle: @@ -181,14 +185,15 @@ export class WorkflowStep { > ) : null; - return ( - this.onStepClick()} - > +
this.onStepClick()} + onKeyDown={(e) => this.onKeyDown(e)} class={{ step: true, selected: this.selected, diff --git a/packages/core/src/components/workflow-steps/test/workflow-steps.ct.ts b/packages/core/src/components/workflow-steps/test/workflow-steps.ct.ts index 316d8a6f68e..e63552bc00c 100644 --- a/packages/core/src/components/workflow-steps/test/workflow-steps.ct.ts +++ b/packages/core/src/components/workflow-steps/test/workflow-steps.ct.ts @@ -29,6 +29,51 @@ regressionTest('renders', async ({ mount, page }) => { await expect(step).toBeVisible(); }); +regressionTest('accessibility', async ({ mount, makeAxeBuilder }) => { + await mount(` + + Step 1 + Step 2 + Step 3 + + `); + + const results = await makeAxeBuilder().analyze(); + expect(results.violations).toEqual([]); +}); +regressionTest( + 'should have correct aria attributes for accessibility', + async ({ mount, page }) => { + await mount(` + + Step 1 + Step 2 + Step 3 + + `); + + const workflowSteps = page.locator('ix-workflow-steps'); + const steps = page.locator('ix-workflow-step'); + + await expect(workflowSteps).toHaveClass(/hydrated/); + + await workflowSteps.evaluate( + (el: HTMLIxWorkflowStepsElement) => (el.selectedIndex = 2) + ); + + const step1Div = steps.nth(0).locator('.step'); + const step2Div = steps.nth(1).locator('.step'); + const step3Div = steps.nth(2).locator('.step'); + + await expect(step1Div).toHaveAttribute('role', 'button'); + await expect(step1Div).toHaveAttribute('tabindex', '0'); + + await expect(step2Div).toHaveAttribute('aria-disabled', 'true'); + await expect(step2Div).toHaveAttribute('tabindex', '-1'); + + await expect(step3Div).toHaveAttribute('aria-current', 'step'); + } +); regressionTest('should be clickable', async ({ mount, page }) => { await mount(` @@ -73,6 +118,28 @@ regressionTest('should prevent click navigation', async ({ mount, page }) => { await expect(firstStepDiv).toHaveClass(/selected/); await expect(lastStepDiv).not.toHaveClass(/selected/); }); +regressionTest( + 'supports keyboard selection via Enter and Space', + async ({ mount, page }) => { + await mount(` + + Step 1 + Step 2 + + `); + + const first = page.locator('ix-workflow-step').nth(0).locator('.step'); + const second = page.locator('ix-workflow-step').nth(1).locator('.step'); + + await first.focus(); + await first.press('Enter'); + await expect(first).toHaveClass(/selected/); + + await second.focus(); + await second.press('Space'); + await expect(second).toHaveClass(/selected/); + } +); regressionTest( 'should have the correct visuals after toggling state from open to error and back again to open',