From 74d1daaefd8a3844a5fd33ebe69140aee0e9beb6 Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Wed, 1 Jul 2026 17:32:16 +0300 Subject: [PATCH] fix(a11y): render workspace progress steps as spans instead of buttons WizardNavItem defaults to component="button", which is semantically wrong for non-interactive progress indicators. The steps already had pointer-events: none via CSS, but the button role and element type remained, causing each step to be reported as a button in the DOM. Replace WizardNavItem with plain
  • / elements that apply PatternFly's wizard nav classes directly (pf-v6-c-wizard__nav-link, pf-m-current, pf-m-disabled). Visual output is unchanged. Fixes: https://redhat.atlassian.net/browse/CRW-10924 Assisted-by: Claude Sonnet 4.6 Signed-off-by: Oleksii Orel --- .../workspaceCreationTimeCheck.check.tsx | 18 +-- .../__snapshots__/index.spec.tsx.snap | 65 ++------ .../Wizard/__tests__/index.spec.tsx | 21 +-- .../WorkspaceProgress/Wizard/index.tsx | 56 +++---- .../__snapshots__/index.spec.tsx.snap | 141 ++++-------------- 5 files changed, 88 insertions(+), 213 deletions(-) diff --git a/packages/dashboard-frontend/src/__tests__/workspaceCreationTimeCheck.check.tsx b/packages/dashboard-frontend/src/__tests__/workspaceCreationTimeCheck.check.tsx index e618b32008..1e76283461 100644 --- a/packages/dashboard-frontend/src/__tests__/workspaceCreationTimeCheck.check.tsx +++ b/packages/dashboard-frontend/src/__tests__/workspaceCreationTimeCheck.check.tsx @@ -196,16 +196,16 @@ describe('Workspace creation time', () => { ); // step 1: Initializing - const stepInitializing = screen.getByRole('button', { name: 'Initializing' }); + const stepInitializing = screen.getByText('Initializing').closest('.pf-v6-c-wizard__nav-link'); expect(stepInitializing).toBeInTheDocument(); await waitFor(() => expect(stepInitializing).toHaveAttribute('aria-current', 'step'), { timeout: 5000, }); // step 2: Checking for the limit of running workspaces - const stepCheckingForLimit = screen.getByRole('button', { - name: 'Checking for the limit of running workspaces', - }); + const stepCheckingForLimit = screen + .getByText('Checking for the limit of running workspaces') + .closest('.pf-v6-c-wizard__nav-link'); expect(stepCheckingForLimit).toBeInTheDocument(); await waitFor(() => expect(stepCheckingForLimit).toHaveAttribute('aria-current', 'step'), { timeout: 5000, @@ -215,18 +215,16 @@ describe('Workspace creation time', () => { // skipping because it is never activated, but it's substeps are // step 4: Waiting for workspace to start - const stepStartingWorkspace = screen.getByRole('button', { - name: 'Waiting for workspace to start', - }); + const stepStartingWorkspace = screen + .getByText('Waiting for workspace to start') + .closest('.pf-v6-c-wizard__nav-link'); expect(stepStartingWorkspace).toBeInTheDocument(); await waitFor(() => expect(stepStartingWorkspace).toHaveAttribute('aria-current', 'step'), { timeout: 5000, }); // step 5: Open IDE - const stepOpenIde = screen.getByRole('button', { - name: 'Open IDE', - }); + const stepOpenIde = screen.getByText('Open IDE').closest('.pf-v6-c-wizard__nav-link'); expect(stepOpenIde).toBeInTheDocument(); await waitFor(() => expect(stepOpenIde).toHaveAttribute('aria-current', 'step'), { timeout: 5000, diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/Wizard/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/components/WorkspaceProgress/Wizard/__tests__/__snapshots__/index.spec.tsx.snap index ed13378770..c262d6955e 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/Wizard/__tests__/__snapshots__/index.spec.tsx.snap +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/Wizard/__tests__/__snapshots__/index.spec.tsx.snap @@ -15,62 +15,42 @@ exports[`WorkspaceProgressWizard component snapshot 1`] = `
  • - +
  • - +
  • - +
      - +
    1. - +
    2. - +
  • diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/Wizard/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/components/WorkspaceProgress/Wizard/__tests__/index.spec.tsx index 7c55c0d59f..4255146888 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/Wizard/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/Wizard/__tests__/index.spec.tsx @@ -13,7 +13,7 @@ import React from 'react'; import { Step, StepId } from '@/components/WorkspaceProgress'; -import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; +import getComponentRenderer from '@/services/__mocks__/getComponentRenderer'; import WorkspaceProgressWizard, { WorkspaceProgressWizardStep } from '..'; @@ -79,24 +79,27 @@ describe('WorkspaceProgressWizard', () => { const { reRenderComponent } = renderComponent(activeStepId, steps, ref); - const buttonInitialize = screen.getByRole('button', { name: Step.INITIALIZE }); - const buttonLimitCheck = screen.getByRole('button', { name: Step.LIMIT_CHECK }); + const navLinks = document.querySelectorAll('.pf-v6-c-wizard__nav-link'); + const spanInitialize = navLinks[0]; + const spanLimitCheck = navLinks[1]; - expect(buttonInitialize.className.split(' ')).toEqual(expect.arrayContaining(['pf-m-current'])); - expect(buttonLimitCheck.className.split(' ')).not.toEqual( + expect(spanInitialize.tagName.toLowerCase()).toBe('span'); + expect(spanInitialize.className.split(' ')).toEqual(expect.arrayContaining(['pf-m-current'])); + expect(spanLimitCheck.className.split(' ')).not.toEqual( expect.arrayContaining(['pf-m-current']), ); const nextActiveStepId = Step.LIMIT_CHECK; reRenderComponent(nextActiveStepId, steps, ref); - const nextButtonInitialize = screen.getByRole('button', { name: Step.INITIALIZE }); - const nextButtonLimitCheck = screen.getByRole('button', { name: Step.LIMIT_CHECK }); + const nextNavLinks = document.querySelectorAll('.pf-v6-c-wizard__nav-link'); + const nextSpanInitialize = nextNavLinks[0]; + const nextSpanLimitCheck = nextNavLinks[1]; - expect(nextButtonInitialize.className.split(' ')).not.toEqual( + expect(nextSpanInitialize.className.split(' ')).not.toEqual( expect.arrayContaining(['pf-m-current']), ); - expect(nextButtonLimitCheck.className.split(' ')).toEqual( + expect(nextSpanLimitCheck.className.split(' ')).toEqual( expect.arrayContaining(['pf-m-current']), ); }); diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/Wizard/index.tsx b/packages/dashboard-frontend/src/components/WorkspaceProgress/Wizard/index.tsx index 35ea3ad2a1..f6f83bbccd 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/Wizard/index.tsx +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/Wizard/index.tsx @@ -10,7 +10,7 @@ * Red Hat, Inc. - initial API and implementation */ -import { WizardNav, WizardNavItem } from '@patternfly/react-core'; +import { WizardNav } from '@patternfly/react-core'; import wizardStyles from '@patternfly/react-styles/css/components/Wizard/wizard'; import React from 'react'; @@ -34,11 +34,6 @@ export type Props = { }; class WorkspaceProgressWizard extends React.Component { - private handleGoToStepById() { - console.warn('Not implemented: handleGoToStepById'); - return false; - } - public goToNext(): void { const flattenedSteps = this.flattenSteps(this.props.steps); // find the index of the next after the current active step @@ -91,53 +86,62 @@ class WorkspaceProgressWizard extends React.Component { return flattenedSteps.findIndex(step => step.id === stepId) + 1; } + private navLinkClassName(isCurrent: boolean, isDisabled: boolean): string { + return [ + wizardStyles.wizardNavLink, + isCurrent ? wizardStyles.modifiers.current : '', + isDisabled ? wizardStyles.modifiers.disabled : '', + ] + .filter(Boolean) + .join(' '); + } + private buildWizardNav( activeStepId: StepId, steps: WorkspaceProgressWizardStep[], ): React.ReactElement { const stepsWithDefaults = this.setDefaultValues(steps); - const flattenedSteps = this.flattenSteps(stepsWithDefaults); return ( {stepsWithDefaults.map(step => { const { canJumpTo, name, steps = [], id } = step; - const flattenedStepNumber = this.getFlattenedStepsNumber(flattenedSteps, id); const hasChildren = steps.length !== 0; const allChildrenFinished = steps.every(subStep => subStep.isFinishedStep); const showChildren = hasChildren && allChildrenFinished === false; const hasActiveChild = steps.some(subStep => subStep.id === activeStepId); + const isCurrent = activeStepId === id || hasActiveChild; return ( - this.handleGoToStepById()} - > +
  • + + {name} + {showChildren && ( {steps.map(subStep => { const { canJumpTo, name, id } = subStep; - const flattenedStepNumber = this.getFlattenedStepsNumber(flattenedSteps, id); + const isSubCurrent = activeStepId === id; + return ( - this.handleGoToStepById()} - /> +
  • + + {name} + +
  • ); })}
    )} - + ); })} diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/components/WorkspaceProgress/__tests__/__snapshots__/index.spec.tsx.snap index c0074aba3c..84e5ceb638 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/__tests__/__snapshots__/index.spec.tsx.snap +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/__tests__/__snapshots__/index.spec.tsx.snap @@ -15,15 +15,9 @@ exports[`LoaderProgress workspace creation flow snapshot 1`] = `
  • - +
  • - +
  • - +
      - +
    1. - +
    2. - +
    3. - +
  • - +
  • - +
  • @@ -586,15 +524,9 @@ exports[`LoaderProgress workspace starting flow snapshot 1`] = `
  • - +
  • - +
  • - +
  • - +