diff --git a/.pnp.cjs b/.pnp.cjs index 43455a2bb1..1f44b5529a 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -38,6 +38,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { [null, {\ "packageLocation": "./",\ "packageDependencies": [\ + ["@axe-core/react", "npm:4.4.5"],\ ["@databiosphere/bard-client", "npm:0.1.0"],\ ["@fortawesome/fontawesome-svg-core", "npm:1.2.36"],\ ["@fortawesome/free-regular-svg-icons", "npm:5.15.4"],\ @@ -160,6 +161,40 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["@axe-core/puppeteer", [\ + ["npm:4.4.5", {\ + "packageLocation": "./.yarn/cache/@axe-core-puppeteer-npm-4.4.5-35e9d98813-7b7f3cf736.zip/node_modules/@axe-core/puppeteer/",\ + "packageDependencies": [\ + ["@axe-core/puppeteer", "npm:4.4.5"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:dbab8a24bb1e83f64d01070d471958b56ec3d11fd3ae5a8c71ee6e018bca5308c73bd59656625959ac80a6ed20fc47a8e80415eaa84ea83f83b375ceeaa8e710#npm:4.4.5", {\ + "packageLocation": "./.yarn/__virtual__/@axe-core-puppeteer-virtual-54df1a5096/0/cache/@axe-core-puppeteer-npm-4.4.5-35e9d98813-7b7f3cf736.zip/node_modules/@axe-core/puppeteer/",\ + "packageDependencies": [\ + ["@axe-core/puppeteer", "virtual:dbab8a24bb1e83f64d01070d471958b56ec3d11fd3ae5a8c71ee6e018bca5308c73bd59656625959ac80a6ed20fc47a8e80415eaa84ea83f83b375ceeaa8e710#npm:4.4.5"],\ + ["@types/puppeteer", null],\ + ["axe-core", "npm:4.4.3"],\ + ["puppeteer", "npm:13.5.1"]\ + ],\ + "packagePeers": [\ + "@types/puppeteer",\ + "puppeteer"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@axe-core/react", [\ + ["npm:4.4.5", {\ + "packageLocation": "./.yarn/cache/@axe-core-react-npm-4.4.5-f096f4dbc4-8a10b84c85.zip/node_modules/@axe-core/react/",\ + "packageDependencies": [\ + ["@axe-core/react", "npm:4.4.5"],\ + ["axe-core", "npm:4.4.3"],\ + ["requestidlecallback", "npm:0.3.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@babel/code-frame", [\ ["npm:7.16.7", {\ "packageLocation": "./.yarn/cache/@babel-code-frame-npm-7.16.7-093eb9e124-db2f7faa31.zip/node_modules/@babel/code-frame/",\ @@ -6016,6 +6051,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["axe-core", "npm:4.4.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:4.4.3", {\ + "packageLocation": "./.yarn/cache/axe-core-npm-4.4.3-6a07ed8cf6-c3ea000d9a.zip/node_modules/axe-core/",\ + "packageDependencies": [\ + ["axe-core", "npm:4.4.3"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["axios", [\ @@ -17547,6 +17589,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["requestidlecallback", [\ + ["npm:0.3.0", {\ + "packageLocation": "./.yarn/cache/requestidlecallback-npm-0.3.0-040e372761-2405aef711.zip/node_modules/requestidlecallback/",\ + "packageDependencies": [\ + ["requestidlecallback", "npm:0.3.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["require-directory", [\ ["npm:2.1.1", {\ "packageLocation": "./.yarn/cache/require-directory-npm-2.1.1-8608aee50b-fb47e70bf0.zip/node_modules/require-directory/",\ @@ -19092,6 +19143,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageLocation": "./integration-tests/",\ "packageDependencies": [\ ["terra-integration-tests", "workspace:integration-tests"],\ + ["@axe-core/puppeteer", "virtual:dbab8a24bb1e83f64d01070d471958b56ec3d11fd3ae5a8c71ee6e018bca5308c73bd59656625959ac80a6ed20fc47a8e80415eaa84ea83f83b375ceeaa8e710#npm:4.4.5"],\ ["@google-cloud/secret-manager", "npm:4.0.0"],\ ["date-fns", "npm:2.24.0"],\ ["google-auth-library", "npm:7.14.1"],\ @@ -19115,6 +19167,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageLocation": "./",\ "packageDependencies": [\ ["terra-ui", "workspace:."],\ + ["@axe-core/react", "npm:4.4.5"],\ ["@databiosphere/bard-client", "npm:0.1.0"],\ ["@fortawesome/fontawesome-svg-core", "npm:1.2.36"],\ ["@fortawesome/free-regular-svg-icons", "npm:5.15.4"],\ diff --git a/.yarn/cache/@axe-core-puppeteer-npm-4.4.5-35e9d98813-7b7f3cf736.zip b/.yarn/cache/@axe-core-puppeteer-npm-4.4.5-35e9d98813-7b7f3cf736.zip new file mode 100644 index 0000000000..8daa5e932c Binary files /dev/null and b/.yarn/cache/@axe-core-puppeteer-npm-4.4.5-35e9d98813-7b7f3cf736.zip differ diff --git a/.yarn/cache/@axe-core-react-npm-4.4.5-f096f4dbc4-8a10b84c85.zip b/.yarn/cache/@axe-core-react-npm-4.4.5-f096f4dbc4-8a10b84c85.zip new file mode 100644 index 0000000000..802c151bcb Binary files /dev/null and b/.yarn/cache/@axe-core-react-npm-4.4.5-f096f4dbc4-8a10b84c85.zip differ diff --git a/.yarn/cache/axe-core-npm-4.4.3-6a07ed8cf6-c3ea000d9a.zip b/.yarn/cache/axe-core-npm-4.4.3-6a07ed8cf6-c3ea000d9a.zip new file mode 100644 index 0000000000..e3325f9398 Binary files /dev/null and b/.yarn/cache/axe-core-npm-4.4.3-6a07ed8cf6-c3ea000d9a.zip differ diff --git a/.yarn/cache/requestidlecallback-npm-0.3.0-040e372761-2405aef711.zip b/.yarn/cache/requestidlecallback-npm-0.3.0-040e372761-2405aef711.zip new file mode 100644 index 0000000000..ca4f5729e0 Binary files /dev/null and b/.yarn/cache/requestidlecallback-npm-0.3.0-040e372761-2405aef711.zip differ diff --git a/integration-tests/package.json b/integration-tests/package.json index c1ece7a94e..33a17d300e 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -17,6 +17,7 @@ "puppeteer-cluster": "^0.23.0" }, "dependencies": { + "@axe-core/puppeteer": "^4.4.5", "@google-cloud/secret-manager": "^4.0.0", "date-fns": "^2.24.0", "google-auth-library": "^7.9.2", diff --git a/integration-tests/tests/billing-projects.js b/integration-tests/tests/billing-projects.js index fedec99735..545ecaa74d 100644 --- a/integration-tests/tests/billing-projects.js +++ b/integration-tests/tests/billing-projects.js @@ -1,6 +1,8 @@ // This test is owned by the Workspaces Team. const _ = require('lodash/fp') -const { assertTextNotFound, click, clickable, findText, gotoPage, select, signIntoTerra, waitForNoSpinners } = require('../utils/integration-utils') +const { + assertTextNotFound, click, clickable, findText, gotoPage, select, signIntoTerra, waitForNoSpinners, verifyAccessibility +} = require('../utils/integration-utils') const { userEmail } = require('../utils/integration-config') const { registerTest } = require('../utils/jest-utils') const { withUserToken } = require('../utils/terra-sa-utils') @@ -265,6 +267,9 @@ const testBillingSpendReportFn = withUserToken(async ({ page, testUrl, token }) await billingPage.assertText('Top 10 Spending Workspaces') await billingPage.assertChartValue(10, 'Extra Inexpensive Workspace', 'Compute', '$0.01') + // Check accessibility of spend report page. + await verifyAccessibility(page) + // Select a billing project that is not owned by the user await billingPage.visit() await billingPage.selectProject(notOwnedBillingProjectName) @@ -277,6 +282,9 @@ const testBillingSpendReportFn = withUserToken(async ({ page, testUrl, token }) await billingPage.selectProject(azureBillingProjectName, AZURE) await billingPage.assertTextNotFound('Spend report') await billingPage.assertTextNotFound('View billing account') + + // Check accessibility of initial view. + await verifyAccessibility(page) }) registerTest({ @@ -303,6 +311,9 @@ const testBillingWorkspacesFn = withUserToken(async ({ page, testUrl, token }) = await billingPage.showWorkspaceDetails(`${ownedBillingProjectName}_ws`) await billingPage.assertText(`Google Project${ownedBillingProjectName}_project`) + // Check accessibility of workspaces view (GCP). + await verifyAccessibility(page) + // Select a billing project that is not owned by the user and verify workspace tab is visible await billingPage.visit() await billingPage.selectProject(notOwnedBillingProjectName) @@ -314,6 +325,9 @@ const testBillingWorkspacesFn = withUserToken(async ({ page, testUrl, token }) = await verifyWorkspaceControls() await billingPage.showWorkspaceDetails(`${azureBillingProjectName}_ws`) await billingPage.assertText(`Resource Group ID${azureBillingProjectName}_mrg`) + + // Check accessibility of workspaces view (Azure). + await verifyAccessibility(page) }) registerTest({ @@ -339,6 +353,9 @@ const testBillingMembersFn = withUserToken(async ({ page, testUrl, token }) => { await billingPage.assertText('testuser1@example.com') await billingPage.assertText('testuser3@example.com') + // Check accessibility of users view (as owner). + await verifyAccessibility(page) + // Select a billing project that is not owned by the user await billingPage.visit() await billingPage.selectProject(notOwnedBillingProjectName) @@ -353,6 +370,9 @@ const testBillingMembersFn = withUserToken(async ({ page, testUrl, token }) => { // The test user has the User role, so they should see members with the Owner role, but not with the User role await billingPage.assertText('testuser1@example.com') await billingPage.assertTextNotFound('testuser3@example.com') + + // Check accessibility of users view (as non-owner). + await verifyAccessibility(page) }) registerTest({ diff --git a/integration-tests/tests/workspace-dashboard.js b/integration-tests/tests/workspace-dashboard.js index 439e4bf00f..36ac21d4c9 100644 --- a/integration-tests/tests/workspace-dashboard.js +++ b/integration-tests/tests/workspace-dashboard.js @@ -2,7 +2,7 @@ const _ = require('lodash/fp') const { viewWorkspaceDashboard, withWorkspace } = require('../utils/integration-helpers') const { - assertNavChildNotFound, assertTextNotFound, click, clickable, findElement, findText, gotoPage, navChild, noSpinnersAfter + assertNavChildNotFound, assertTextNotFound, click, clickable, findElement, findText, gotoPage, navChild, noSpinnersAfter, verifyAccessibility } = require('../utils/integration-utils') const { registerTest } = require('../utils/jest-utils') const { withUserToken } = require('../utils/terra-sa-utils') @@ -103,6 +103,9 @@ const testGoogleWorkspace = _.flow( // Verify expected tabs are present. await dashboard.assertTabs(['data', 'analyses', 'workflows', 'job history'], true) + + // Check accessibility. + await verifyAccessibility(page) }) registerTest({ @@ -248,8 +251,11 @@ const testAzureWorkspace = withUserToken(async ({ page, token, testUrl }) => { // Verify tabs that currently depend on Google project ID are not present. await dashboard.assertTabs(['data', 'notebooks', 'workflows', 'job history'], false) - // Verify Analyses tab is present (config override is set) + // Verify Analyses tab is present. await dashboard.assertTabs(['analyses'], true) + + // Check accessibility. + await verifyAccessibility(page) }) registerTest({ diff --git a/integration-tests/utils/integration-utils.js b/integration-tests/utils/integration-utils.js index 3c150db367..76e8cc299c 100644 --- a/integration-tests/utils/integration-utils.js +++ b/integration-tests/utils/integration-utils.js @@ -1,3 +1,4 @@ +const { AxePuppeteer } = require('@axe-core/puppeteer') const _ = require('lodash/fp') const { mkdirSync, writeFileSync } = require('fs') const { resolve } = require('path') @@ -438,6 +439,13 @@ const gotoPage = async (page, url) => { await waitForNoSpinners(page) } +const verifyAccessibility = async page => { + const results = await new AxePuppeteer(page).withTags(['wcag2a', 'wcag2aa']).analyze() + if (results.violations.length > 0) { + throw new Error(`Accessibility issues found:\n${JSON.stringify(results.violations, null, 2)}`) + } +} + module.exports = { assertNavChildNotFound, assertTextNotFound, @@ -478,5 +486,6 @@ module.exports = { maybeSaveScreenshot, gotoPage, savePageContent, - findButtonInDialogByAriaLabel + findButtonInDialogByAriaLabel, + verifyAccessibility } diff --git a/package.json b/package.json index 6de9e4de11..108483710b 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "postinstall": "husky install" }, "devDependencies": { + "@axe-core/react": "^4.4.5", "@testing-library/dom": "^8.17.1", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^12.1.5", diff --git a/src/appLoader.js b/src/appLoader.js index 23ef4a0ce5..c93e9266d5 100644 --- a/src/appLoader.js +++ b/src/appLoader.js @@ -1,6 +1,7 @@ import 'src/style.css' import _ from 'lodash/fp' +import React from 'react' import ReactDOM from 'react-dom' import { h } from 'react-hyperscript-helpers' import RModal from 'react-modal' @@ -23,4 +24,19 @@ initializeClientId().then(() => { initializeAuth() initializeTCell() startPollingServiceAlerts() + + if (process.env.NODE_ENV === 'development') { + const axe = require('@axe-core/react') + + const config = { + tags: ['wcag2a', 'wcag2aa'], + rules: [ + { + id: 'color-contrast', + excludeHidden: true + } + ] + } + axe(React, ReactDOM, 1000, config) + } }) diff --git a/src/components/ContextBar.test.js b/src/components/ContextBar.test.js index 7cda02e4ad..b750a093ee 100644 --- a/src/components/ContextBar.test.js +++ b/src/components/ContextBar.test.js @@ -331,12 +331,12 @@ const contextBarProps = { describe('ContextBar - buttons', () => { it('will render default icons', () => { // Act - const { getByText, getByLabelText } = render(h(ContextBar, contextBarProps)) + const { getByText, getByLabelText, getByTestId } = render(h(ContextBar, contextBarProps)) // Assert expect(getByText('Rate:')) expect(getByLabelText('Environment Configuration')) - expect(getByLabelText('Terminal button')).toHaveAttribute('disabled') + expect(getByTestId('terminal-button-id')).toHaveAttribute('disabled') }) it('will render Jupyter button with an enabled Terminal Button', () => { @@ -348,13 +348,13 @@ describe('ContextBar - buttons', () => { } // Act - const { getByText, getByLabelText } = render(h(ContextBar, jupyterContextBarProps)) + const { getByText, getByLabelText, getByTestId } = render(h(ContextBar, jupyterContextBarProps)) //Assert expect(getByText('Rate:')) expect(getByLabelText('Environment Configuration')) expect(getByLabelText(new RegExp(/Jupyter Environment/i))) - expect(getByLabelText('Terminal button')).toBeEnabled() + expect(getByTestId('terminal-button-id')).toBeEnabled() expect(getByText(Utils.formatUSD(RUNTIME_COST + PERSISTENT_DISK_COST))) expect(getByText(/Running \$.*\/hr/)) }) @@ -368,13 +368,13 @@ describe('ContextBar - buttons', () => { } // Act - const { getByText, getByLabelText } = render(h(ContextBar, jupyterContextBarProps)) + const { getByText, getByLabelText, getByTestId } = render(h(ContextBar, jupyterContextBarProps)) //Assert expect(getByText('Rate:')) expect(getByLabelText('Environment Configuration')) expect(getByLabelText(new RegExp(/Jupyter Environment/i))) - expect(getByLabelText('Terminal button')).toBeEnabled() + expect(getByTestId('terminal-button-id')).toBeEnabled() expect(getByText(Utils.formatUSD(RUNTIME_COST + PERSISTENT_DISK_COST))) expect(getByText(/Creating \$.*\/hr/)) }) @@ -390,7 +390,7 @@ describe('ContextBar - buttons', () => { } // Act - const { getByText, getByLabelText } = render(h(ContextBar, rstudioGalaxyContextBarProps)) + const { getByText, getByLabelText, getByTestId } = render(h(ContextBar, rstudioGalaxyContextBarProps)) //Assert expect(getByText('Rate:')) @@ -398,7 +398,7 @@ describe('ContextBar - buttons', () => { expect(getByLabelText('Environment Configuration')) expect(getByLabelText(new RegExp(/RStudio Environment/i))) expect(getByLabelText(new RegExp(/Galaxy Environment/i))) - expect(getByLabelText('Terminal button')).toHaveAttribute('disabled') + expect(getByTestId('terminal-button-id')).toHaveAttribute('disabled') expect(getByText(/Running \$.*\/hr/)) expect(getByText(/Creating \$.*\/hr/)) expect(getByText(/Disk \$.*\/hr/)) @@ -413,13 +413,13 @@ describe('ContextBar - buttons', () => { } // Act - const { getByText, getByLabelText } = render(h(ContextBar, rstudioGalaxyContextBarProps)) + const { getByText, getByLabelText, getByTestId } = render(h(ContextBar, rstudioGalaxyContextBarProps)) //Assert expect(getByText('Rate:')) expect(getByText('$0.00')) expect(getByLabelText('Environment Configuration')) - expect(getByLabelText('Terminal button')).toHaveAttribute('disabled') + expect(getByTestId('terminal-button-id')).toHaveAttribute('disabled') expect(getByLabelText(new RegExp(/Cromwell Environment/i))) }) @@ -431,14 +431,14 @@ describe('ContextBar - buttons', () => { } // Act - const { getByText, getByLabelText } = render(h(ContextBar, jupyterContextBarProps)) + const { getByText, getByLabelText, getByTestId } = render(h(ContextBar, jupyterContextBarProps)) //Assert expect(getByText('Rate:')) expect(getByText(Utils.formatUSD(RUNTIME_COST))) expect(getByLabelText('Environment Configuration')) expect(getByLabelText(new RegExp(/Azure Environment/i))) - expect(getByLabelText('Terminal button')).toHaveAttribute('disabled') + expect(getByTestId('terminal-button-id')).toHaveAttribute('disabled') }) it('will render button with error status', () => { @@ -551,8 +551,8 @@ describe('ContextBar - actions', () => { } // Act - const { getByLabelText } = render(h(ContextBar, jupyterContextBarProps)) - fireEvent.click(getByLabelText('Terminal button')) + const { getByTestId } = render(h(ContextBar, jupyterContextBarProps)) + fireEvent.click(getByTestId('terminal-button-id')) // Assert expect(Ajax().Runtimes.runtime).toBeCalledWith(jupyter.googleProject, jupyter.runtimeName) @@ -579,8 +579,8 @@ describe('ContextBar - actions', () => { } // Act - const { getByLabelText } = render(h(ContextBar, jupyterContextBarProps)) - fireEvent.click(getByLabelText('Terminal button')) + const { getByTestId } = render(h(ContextBar, jupyterContextBarProps)) + fireEvent.click(getByTestId('terminal-button-id')) // Assert expect(mockRuntimesStartFn).not.toHaveBeenCalled() diff --git a/src/components/PopupTrigger.js b/src/components/PopupTrigger.js index 3d273ee1c7..be8f361b30 100644 --- a/src/components/PopupTrigger.js +++ b/src/components/PopupTrigger.js @@ -144,6 +144,7 @@ export const MenuTrigger = ({ children, content, popupProps = {}, ...props }) => content: h(VerticalNavigation, [content]), popupProps: { role: 'menu', + 'aria-modal': undefined, 'aria-orientation': 'vertical', ...popupProps }, diff --git a/src/pages/billing/Project.js b/src/pages/billing/Project.js index a197ae995c..558a3aafa8 100644 --- a/src/pages/billing/Project.js +++ b/src/pages/billing/Project.js @@ -48,17 +48,17 @@ const getBillingAccountIcon = status => { } const WorkspaceCardHeaders = memoWithName('WorkspaceCardHeaders', ({ needsStatusColumn, sort, onSort }) => { - return div({ style: { display: 'flex', justifyContent: 'space-between', marginTop: '1.5rem', padding: '0 1rem', marginBottom: '0.5rem' } }, [ + return div({ role: 'row', style: { display: 'flex', justifyContent: 'space-between', marginTop: '1.5rem', padding: '0 1rem', marginBottom: '0.5rem' } }, [ needsStatusColumn && div({ style: { width: billingAccountIconSize } }, [ div({ className: 'sr-only' }, ['Status']) ]), - div({ 'aria-sort': ariaSort(sort, 'name'), style: { flex: 1, paddingLeft: needsStatusColumn ? '1rem' : '2rem' } }, [ + div({ role: 'columnheader', 'aria-sort': ariaSort(sort, 'name'), style: { flex: 1, paddingLeft: needsStatusColumn ? '1rem' : '2rem' } }, [ h(HeaderRenderer, { sort, onSort, name: 'name' }) ]), - div({ 'aria-sort': ariaSort(sort, 'createdBy'), style: { flex: 1 } }, [ + div({ role: 'columnheader', 'aria-sort': ariaSort(sort, 'createdBy'), style: { flex: 1 } }, [ h(HeaderRenderer, { sort, onSort, name: 'createdBy' }) ]), - div({ 'aria-sort': ariaSort(sort, 'lastModified'), style: { flex: `0 0 ${workspaceLastModifiedWidth}px` } }, [ + div({ role: 'columnheader', 'aria-sort': ariaSort(sort, 'lastModified'), style: { flex: `0 0 ${workspaceLastModifiedWidth}px` } }, [ h(HeaderRenderer, { sort, onSort, name: 'lastModified' }) ]), div({ style: { flex: `0 0 ${workspaceExpandIconSize}px` } }, [ @@ -95,11 +95,11 @@ const WorkspaceCard = memoWithName('WorkspaceCard', ({ workspace, billingProject expandedInfoContainer: { display: 'flex', flexDirection: 'column', width: '100%' } } - return div({ role: 'listitem', style: { ...Style.cardList.longCardShadowless, padding: 0, flexDirection: 'column' } }, [ + return div({ role: 'row', style: { ...Style.cardList.longCardShadowless, padding: 0, flexDirection: 'column' } }, [ h(IdContainer, [id => h(Fragment, [ div({ style: workspaceCardStyles.row }, [ billingAccountStatus && getBillingAccountIcon(billingAccountStatus), - div({ style: { ...workspaceCardStyles.field, display: 'flex', alignItems: 'center', paddingLeft: billingAccountStatus ? '1rem' : '2rem' } }, [ + div({ role: 'rowheader', style: { ...workspaceCardStyles.field, display: 'flex', alignItems: 'center', paddingLeft: billingAccountStatus ? '1rem' : '2rem' } }, [ h(Link, { style: Style.noWrapEllipsis, href: Nav.getLink('workspace-dashboard', { namespace, name }), @@ -111,8 +111,8 @@ const WorkspaceCard = memoWithName('WorkspaceCard', ({ workspace, billingProject } }, [name]) ]), - div({ style: workspaceCardStyles.field }, [createdBy]), - div({ style: { height: '1rem', flex: `0 0 ${workspaceLastModifiedWidth}px` } }, [ + div({ role: 'cell', style: workspaceCardStyles.field }, [createdBy]), + div({ role: 'cell', style: { height: '1rem', flex: `0 0 ${workspaceLastModifiedWidth}px` } }, [ Utils.makeStandardDate(lastModified) ]), div({ style: { flex: `0 0 ${workspaceExpandIconSize}px` } }, [ @@ -564,26 +564,28 @@ const ProjectDetail = ({ authorizeAndLoadAccounts, billingAccounts, billingProje const tabToTable = { workspaces: h(Fragment, [ - h(WorkspaceCardHeaders, { - needsStatusColumn: billingAccountsOutOfDate, - sort: workspaceSort, - onSort: setWorkspaceSort - }), - div({ role: 'list', 'aria-label': `workspaces in billing project ${billingProject.projectName}`, style: { flexGrow: 1, width: '100%' } }, [ - _.flow( - _.orderBy([workspaceSort.field], [workspaceSort.direction]), - _.map(workspace => { - const isExpanded = expandedWorkspaceName === workspace.name - return h(WorkspaceCard, { - workspace: { ...workspace, billingAccountDisplayName: billingAccounts[workspace.billingAccount]?.displayName }, - billingProject, - billingAccountStatus: billingAccountsOutOfDate && getBillingAccountStatus(workspace), - key: workspace.workspaceId, - isExpanded, - onExpand: () => setExpandedWorkspaceName(isExpanded ? undefined : workspace.name) + div({ role: 'table', 'aria-label': `workspaces in billing project ${billingProject.projectName}` }, [ + h(WorkspaceCardHeaders, { + needsStatusColumn: billingAccountsOutOfDate, + sort: workspaceSort, + onSort: setWorkspaceSort + }), + div({}, [ + _.flow( + _.orderBy([workspaceSort.field], [workspaceSort.direction]), + _.map(workspace => { + const isExpanded = expandedWorkspaceName === workspace.name + return h(WorkspaceCard, { + workspace: { ...workspace, billingAccountDisplayName: billingAccounts[workspace.billingAccount]?.displayName }, + billingProject, + billingAccountStatus: billingAccountsOutOfDate && getBillingAccountStatus(workspace), + key: workspace.workspaceId, + isExpanded, + onExpand: () => setExpandedWorkspaceName(isExpanded ? undefined : workspace.name) + }) }) - }) - )(workspacesInProject) + )(workspacesInProject) + ]) ]) ]), members: h(Fragment, [ diff --git a/src/pages/workspaces/workspace/analysis/ContextBar.js b/src/pages/workspaces/workspace/analysis/ContextBar.js index 42706d0e87..c0fd74793f 100644 --- a/src/pages/workspaces/workspace/analysis/ContextBar.js +++ b/src/pages/workspaces/workspace/analysis/ContextBar.js @@ -195,9 +195,9 @@ export const ContextBar = ({ getEnvironmentStatusIcons() ]), h(Clickable, { - 'aria-label': 'Terminal button', style: { borderTop: `1px solid ${colors.accent()}`, paddingLeft: '1rem', alignItems: 'center', ...contextBarStyles.contextBarButton, color: !isTerminalEnabled ? colors.dark(0.7) : contextBarStyles.contextBarButton.color }, hover: contextBarStyles.hover, + 'data-testid': 'terminal-button-id', tooltipSide: 'left', disabled: !isTerminalEnabled, href: terminalLaunchLink, @@ -212,7 +212,7 @@ export const ContextBar = ({ tooltipDelay: 100, useTooltipAsLabel: false, ...Utils.newTabLinkProps - }, [icon('terminal', { size: 40 })]) + }, [icon('terminal', { size: 40 }), span({ className: 'sr-only' }, ['Terminal button'])]) ]) ]) ]) diff --git a/yarn.lock b/yarn.lock index f5d571c051..af7656fe27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27,6 +27,27 @@ __metadata: languageName: node linkType: hard +"@axe-core/puppeteer@npm:^4.4.5": + version: 4.4.5 + resolution: "@axe-core/puppeteer@npm:4.4.5" + dependencies: + axe-core: ^4.4.3 + peerDependencies: + puppeteer: ">=1.10.0 <= 16" + checksum: 7b7f3cf736abcaec442711563904cb768b61ac605e947769103ae7ce1a1be0ed0ff1cf3cad515dc9c88573fc115a06c3b400fdc8ba7c67e647bc3cede3cdabbb + languageName: node + linkType: hard + +"@axe-core/react@npm:^4.4.5": + version: 4.4.5 + resolution: "@axe-core/react@npm:4.4.5" + dependencies: + axe-core: ^4.4.1 + requestidlecallback: ^0.3.0 + checksum: 8a10b84c8587d119d463674a122bcc960ffb9611f7ecebfc41b85c23cf8f42648a358795394ec97254d3a5412dff9f998b9a8139efddce1719a7743f588b558d + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.8.3": version: 7.16.7 resolution: "@babel/code-frame@npm:7.16.7" @@ -3979,6 +4000,13 @@ __metadata: languageName: node linkType: hard +"axe-core@npm:^4.4.1, axe-core@npm:^4.4.3": + version: 4.4.3 + resolution: "axe-core@npm:4.4.3" + checksum: c3ea000d9ace3ba0bc747c8feafc24b0de62a0f7d93021d0f77b19c73fca15341843510f6170da563d51535d6cfb7a46c5fc0ea36170549dbb44b170208450a2 + languageName: node + linkType: hard + "axios@npm:^0.25.0": version: 0.25.0 resolution: "axios@npm:0.25.0" @@ -12807,6 +12835,13 @@ __metadata: languageName: node linkType: hard +"requestidlecallback@npm:^0.3.0": + version: 0.3.0 + resolution: "requestidlecallback@npm:0.3.0" + checksum: 2405aef711b516e326ff18849b24ad2c0e623d2b60397bdc7919fa40d8575fce0a16a563a53f94ec89d255325a99e5deee952e6024584a5179cbbabb4469f0e8 + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -14212,6 +14247,7 @@ resolve@^2.0.0-next.3: version: 0.0.0-use.local resolution: "terra-integration-tests@workspace:integration-tests" dependencies: + "@axe-core/puppeteer": ^4.4.5 "@google-cloud/secret-manager": ^4.0.0 date-fns: ^2.24.0 google-auth-library: ^7.9.2 @@ -14233,6 +14269,7 @@ resolve@^2.0.0-next.3: version: 0.0.0-use.local resolution: "terra-ui@workspace:." dependencies: + "@axe-core/react": ^4.4.5 "@databiosphere/bard-client": ^0.1.0 "@fortawesome/fontawesome-svg-core": ^1.2.36 "@fortawesome/free-regular-svg-icons": ^5.15.4