From 530b26608eee62a5820344cd8b1f5475f58e2017 Mon Sep 17 00:00:00 2001 From: Melanie Sumner Date: Thu, 16 May 2024 11:39:13 -0500 Subject: [PATCH] Address a11y issues in browser-based console UI (#26872) --- changelog/26872.txt | 3 ++ ui/app/components/console/command-input.js | 5 ++- ui/app/components/sidebar/frame.hbs | 9 +++- .../styles/components/console-ui-panel.scss | 8 ++-- .../components/console/command-input.hbs | 9 ++-- .../templates/components/console/ui-panel.hbs | 42 +++++++++++-------- .../addon/components/namespace-reminder.hbs | 4 +- ui/tests/acceptance/console-test.js | 10 ++--- .../pki/pki-engine-workflow-test.js | 1 + ui/tests/acceptance/redirect-to-test.js | 7 +++- .../settings/auth/configure/section-test.js | 4 +- .../settings/mount-secret-backend-test.js | 16 ++++--- ui/tests/acceptance/wrapped-token-test.js | 7 +++- ui/tests/helpers/commands.js | 9 ++-- .../components/console/ui-panel-test.js | 16 +++---- .../components/sidebar/frame-test.js | 4 +- ui/tests/pages/components/console/ui-panel.js | 9 +++- 17 files changed, 104 insertions(+), 59 deletions(-) create mode 100644 changelog/26872.txt diff --git a/changelog/26872.txt b/changelog/26872.txt new file mode 100644 index 0000000000..3309a23106 --- /dev/null +++ b/changelog/26872.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Resolved accessibility issues with Web REPL. Associated label and help text with input, added a conditional to show the console/ui-panel only when toggled open, added keyboard focus trap. +``` \ No newline at end of file diff --git a/ui/app/components/console/command-input.js b/ui/app/components/console/command-input.js index 5cbb860198..bdeb404fa4 100644 --- a/ui/app/components/console/command-input.js +++ b/ui/app/components/console/command-input.js @@ -17,16 +17,17 @@ export default Component.extend({ actions: { handleKeyUp(event) { const keyCode = event.keyCode; + const val = event.target.value; switch (keyCode) { case keys.ENTER: - this.onExecuteCommand(event.target.value); + this.onExecuteCommand(val); break; case keys.UP: case keys.DOWN: this.onShiftCommand(keyCode); break; default: - this.onValueUpdate(event.target.value); + this.onValueUpdate(val); } }, fullscreen() { diff --git a/ui/app/components/sidebar/frame.hbs b/ui/app/components/sidebar/frame.hbs index 2139385f3a..7cd0c84b2f 100644 --- a/ui/app/components/sidebar/frame.hbs +++ b/ui/app/components/sidebar/frame.hbs @@ -49,7 +49,14 @@ {{yield}}
- + {{#if this.console.isOpen}} + + {{/if}}
\ No newline at end of file diff --git a/ui/app/styles/components/console-ui-panel.scss b/ui/app/styles/components/console-ui-panel.scss index ed4b420ef5..52203b342f 100644 --- a/ui/app/styles/components/console-ui-panel.scss +++ b/ui/app/styles/components/console-ui-panel.scss @@ -14,7 +14,9 @@ $console-close-height: 35px; overflow: auto; position: fixed; bottom: 0; - transition: min-height $speed $easing, transform $speed ease-in; + transition: + min-height $speed $easing, + transform $speed ease-in; will-change: transform, min-height; -webkit-overflow-scrolling: touch; z-index: 199; @@ -118,11 +120,11 @@ $console-close-height: 35px; .panel-open .console-ui-panel { box-shadow: $box-shadow-highest; - min-height: 400px; + min-height: 425px; } .main--console-open { - padding-bottom: 400px; + padding-bottom: 425px; } .panel-open .console-ui-panel.fullscreen { diff --git a/ui/app/templates/components/console/command-input.hbs b/ui/app/templates/components/console/command-input.hbs index d7c6394bff..57c604de2c 100644 --- a/ui/app/templates/components/console/command-input.hbs +++ b/ui/app/templates/components/console/command-input.hbs @@ -10,19 +10,20 @@ {{/if}} diff --git a/ui/app/templates/components/console/ui-panel.hbs b/ui/app/templates/components/console/ui-panel.hbs index c90f8aa026..13d9ccc39a 100644 --- a/ui/app/templates/components/console/ui-panel.hbs +++ b/ui/app/templates/components/console/ui-panel.hbs @@ -3,17 +3,19 @@ SPDX-License-Identifier: BUSL-1.1 ~}} -
- -
+
+
+ +
+

The Vault Web REPL provides an easy way to execute common Vault CLI commands, such as write, read, delete, and list. It @@ -22,14 +24,18 @@ HashiCorp Developer site.

Examples:

-

→ Write secrets to kv v1: write <mount>/my-secret foo=bar

-

→ List kv v1 secret keys: list <mount>/

-

→ Read a kv v1 secret: read <mount>/my-secret

-

→ Mount a kv v2 secret engine: write sys/mounts/<mount> type=kv - options=version=2

-

→ Read a kv v2 secret: kv-get <mount>/secret-path

-

→ Read a kv v2 secret's metadata: kv-get <mount>/secret-path - -metadata

+

+ Write secrets to kv v1: write <mount>/my-secret foo=bar

+

+ List kv v1 secret keys: list <mount>/

+

+ Read a kv v1 secret: read <mount>/my-secret

+

+ Mount a kv v2 secret engine: write sys/mounts/<mount> type=kv options=version=2

+

+ Read a kv v2 secret: kv-get <mount>/secret-path

+

+ Read a kv v2 secret's metadata: kv-get <mount>/secret-path-metadata

+

{{yield (hash namespace=this.namespace)}}

{{else}} -

+

This {{@noun}} will be diff --git a/ui/tests/acceptance/console-test.js b/ui/tests/acceptance/console-test.js index 527605da81..0b532ff6bc 100644 --- a/ui/tests/acceptance/console-test.js +++ b/ui/tests/acceptance/console-test.js @@ -25,8 +25,6 @@ module('Acceptance | console', function (hooks) { assert.expect(6); await enginesPage.visit(); await settled(); - await consoleComponent.toggle(); - await settled(); const ids = [uuidv4(), uuidv4(), uuidv4()]; for (const id of ids) { const inputString = `write sys/mounts/console-route-${id} type=kv`; @@ -59,7 +57,7 @@ module('Acceptance | console', function (hooks) { test('fullscreen command expands the cli panel', async function (assert) { await consoleComponent.toggle(); await settled(); - await consoleComponent.runCommands('fullscreen'); + await consoleComponent.runCommands('fullscreen', false); await settled(); const consoleEle = document.querySelector('[data-test-component="console/ui-panel"]'); // wait for the CSS transition to finish @@ -74,7 +72,7 @@ module('Acceptance | console', function (hooks) { test('array output is correctly formatted', async function (assert) { await consoleComponent.toggle(); await settled(); - await consoleComponent.runCommands('read -field=policies /auth/token/lookup-self'); + await consoleComponent.runCommands('read -field=policies /auth/token/lookup-self', false); await settled(); const consoleOut = document.querySelector('.console-ui-output>pre'); // wait for the CSS transition to finish @@ -86,7 +84,7 @@ module('Acceptance | console', function (hooks) { test('number output is correctly formatted', async function (assert) { await consoleComponent.toggle(); await settled(); - await consoleComponent.runCommands('read -field=creation_time /auth/token/lookup-self'); + await consoleComponent.runCommands('read -field=creation_time /auth/token/lookup-self', false); await settled(); const consoleOut = document.querySelector('.console-ui-output>pre'); // wait for the CSS transition to finish @@ -97,7 +95,7 @@ module('Acceptance | console', function (hooks) { test('boolean output is correctly formatted', async function (assert) { await consoleComponent.toggle(); await settled(); - await consoleComponent.runCommands('read -field=orphan /auth/token/lookup-self'); + await consoleComponent.runCommands('read -field=orphan /auth/token/lookup-self', false); await settled(); const consoleOut = document.querySelector('.console-ui-output>pre'); // have to wrap in a later so that we can wait for the CSS transition to finish diff --git a/ui/tests/acceptance/pki/pki-engine-workflow-test.js b/ui/tests/acceptance/pki/pki-engine-workflow-test.js index 6852ce1858..c9df281b8f 100644 --- a/ui/tests/acceptance/pki/pki-engine-workflow-test.js +++ b/ui/tests/acceptance/pki/pki-engine-workflow-test.js @@ -111,6 +111,7 @@ module('Acceptance | pki workflow', function (hooks) { allow_subdomains=true \ max_ttl="720h"`, ]); + await runCmd([`write ${this.mountPath}/root/generate/internal common_name="Hashicorp Test"`]); const pki_admin_policy = adminPolicy(this.mountPath, 'roles'); const pki_reader_policy = readerPolicy(this.mountPath, 'roles'); diff --git a/ui/tests/acceptance/redirect-to-test.js b/ui/tests/acceptance/redirect-to-test.js index dc788d1dba..aee810e359 100644 --- a/ui/tests/acceptance/redirect-to-test.js +++ b/ui/tests/acceptance/redirect-to-test.js @@ -25,7 +25,12 @@ const visit = async (url) => { const consoleComponent = create(consoleClass); const wrappedAuth = async () => { - await consoleComponent.runCommands(`write -field=token auth/token/create policies=default -wrap-ttl=5m`); + await consoleComponent.toggle(); + await settled(); + await consoleComponent.runCommands( + `write -field=token auth/token/create policies=default -wrap-ttl=5m`, + false + ); await settled(); // because of flaky test, trying to capture the token using a dom selector instead of the page object const token = document.querySelector('[data-test-component="console/log-text"] pre').textContent; diff --git a/ui/tests/acceptance/settings/auth/configure/section-test.js b/ui/tests/acceptance/settings/auth/configure/section-test.js index 318333a896..b6533273c0 100644 --- a/ui/tests/acceptance/settings/auth/configure/section-test.js +++ b/ui/tests/acceptance/settings/auth/configure/section-test.js @@ -7,7 +7,7 @@ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { create } from 'ember-cli-page-object'; -import { fillIn } from '@ember/test-helpers'; +import { fillIn, settled } from '@ember/test-helpers'; import { v4 as uuidv4 } from 'uuid'; import enablePage from 'vault/tests/pages/settings/auth/enable'; @@ -59,6 +59,8 @@ module('Acceptance | settings/auth/configure/section', function (hooks) { for (const type of ['aws', 'azure', 'gcp', 'github', 'kubernetes']) { test(`it shows tabs for auth method: ${type}`, async function (assert) { const path = `${type}-showtab-${this.uid}`; + await cli.toggle(); + await settled(); await cli.consoleInput(`write sys/auth/${path} type=${type}`); await cli.enter(); await indexPage.visit({ path }); diff --git a/ui/tests/acceptance/settings/mount-secret-backend-test.js b/ui/tests/acceptance/settings/mount-secret-backend-test.js index b437a2e8da..50b80fac21 100644 --- a/ui/tests/acceptance/settings/mount-secret-backend-test.js +++ b/ui/tests/acceptance/settings/mount-secret-backend-test.js @@ -153,12 +153,16 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) { capabilities = ["read"] } `; - await consoleComponent.runCommands([ - // delete any previous mount with same name - `delete sys/mounts/${enginePath}`, - `write sys/policies/acl/kv-v2-degrade policy=${btoa(V2_POLICY)}`, - 'write -field=client_token auth/token/create policies=kv-v2-degrade', - ]); + await consoleComponent.toggle(); + await consoleComponent.runCommands( + [ + // delete any previous mount with same name + `delete sys/mounts/${enginePath}`, + `write sys/policies/acl/kv-v2-degrade policy=${btoa(V2_POLICY)}`, + 'write -field=client_token auth/token/create policies=kv-v2-degrade', + ], + false + ); await settled(); const userToken = consoleComponent.lastLogOutput; await logout.visit(); diff --git a/ui/tests/acceptance/wrapped-token-test.js b/ui/tests/acceptance/wrapped-token-test.js index 150741b0ad..876281a0d8 100644 --- a/ui/tests/acceptance/wrapped-token-test.js +++ b/ui/tests/acceptance/wrapped-token-test.js @@ -14,7 +14,12 @@ import consoleClass from 'vault/tests/pages/components/console/ui-panel'; const consoleComponent = create(consoleClass); const wrappedAuth = async () => { - await consoleComponent.runCommands(`write -field=token auth/token/create policies=default -wrap-ttl=3m`); + await consoleComponent.toggle(); + await settled(); + await consoleComponent.runCommands( + `write -field=token auth/token/create policies=default -wrap-ttl=3m`, + false + ); await settled(); return consoleComponent.lastLogOutput; }; diff --git a/ui/tests/helpers/commands.js b/ui/tests/helpers/commands.js index 3cf3a19d18..4f3da8e84f 100644 --- a/ui/tests/helpers/commands.js +++ b/ui/tests/helpers/commands.js @@ -1,10 +1,11 @@ -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; -import { create } from 'ember-cli-page-object'; /** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: BUSL-1.1 */ +import consoleClass from 'vault/tests/pages/components/console/ui-panel'; +import { create } from 'ember-cli-page-object'; + /** * Helper functions to run common commands in the consoleComponent during tests. * Please note that a user must be logged in during the test context for the commands to run. @@ -45,8 +46,10 @@ export async function runCmd(commands, throwErrors = true) { if (!Array.isArray(commands)) { commands = [commands]; } - await cc.runCommands(commands); + await cc.toggle(); + await cc.runCommands(commands, false); const lastOutput = cc.lastLogOutput; + await cc.toggle(); if (throwErrors && lastOutput.includes('Error')) { throw new Error(`Error occurred while running commands: "${commands.join('; ')}" - ${lastOutput}`); } diff --git a/ui/tests/integration/components/console/ui-panel-test.js b/ui/tests/integration/components/console/ui-panel-test.js index 9275d43e9d..25c0a22e99 100644 --- a/ui/tests/integration/components/console/ui-panel-test.js +++ b/ui/tests/integration/components/console/ui-panel-test.js @@ -23,20 +23,21 @@ module('Integration | Component | console/ui panel', function (hooks) { test('it clears console input on enter', async function (assert) { await render(hbs`{{console/ui-panel}}`); - - await component.runCommands('list this/thing/here'); + await component.runCommands('list this/thing/here', false); await settled(); assert.strictEqual(component.consoleInputValue, '', 'empties input field on enter'); }); test('it clears the log when using clear command', async function (assert) { await render(hbs`{{console/ui-panel}}`); - - await component.runCommands(['list this/thing/here', 'list this/other/thing', 'read another/thing']); + await component.runCommands( + ['list this/thing/here', 'list this/other/thing', 'read another/thing'], + false + ); await settled(); assert.notEqual(component.logOutput, '', 'there is output in the log'); - await component.runCommands('clear'); + await component.runCommands('clear', false); await settled(); await component.up(); await settled(); @@ -51,7 +52,7 @@ module('Integration | Component | console/ui panel', function (hooks) { test('it adds command to history on enter', async function (assert) { await render(hbs`{{console/ui-panel}}`); - await component.runCommands('list this/thing/here'); + await component.runCommands('list this/thing/here', false); await settled(); await component.up(); await settled(); @@ -67,8 +68,7 @@ module('Integration | Component | console/ui panel', function (hooks) { test('it cycles through history with more than one command', async function (assert) { await render(hbs`{{console/ui-panel}}`); - - await component.runCommands(['list this/thing/here', 'read that/thing/there', 'qwerty']); + await component.runCommands(['list this/thing/here', 'read that/thing/there', 'qwerty'], false); await settled(); await component.up(); await settled(); diff --git a/ui/tests/integration/components/sidebar/frame-test.js b/ui/tests/integration/components/sidebar/frame-test.js index 3bae4cc3fd..f597b13389 100644 --- a/ui/tests/integration/components/sidebar/frame-test.js +++ b/ui/tests/integration/components/sidebar/frame-test.js @@ -35,7 +35,7 @@ module('Integration | Component | sidebar-frame', function (hooks) { assert.dom('[data-test-sidebar-nav]').doesNotExist('Sidebar is hidden'); }); - test('it should render link status, console ui panel and yield block for app content', async function (assert) { + test('it should render link status, console ui panel container and yield block for app content', async function (assert) { const currentCluster = this.owner.lookup('service:currentCluster'); currentCluster.setCluster({ hcpLinkStatus: 'connected' }); const version = this.owner.lookup('service:version'); @@ -50,7 +50,7 @@ module('Integration | Component | sidebar-frame', function (hooks) { `); assert.dom('[data-test-link-status]').exists('Link status component renders'); - assert.dom('[data-test-component="console/ui-panel"]').exists('Console UI panel renders'); + assert.dom('[data-test-console-panel]').exists('Console UI panel container renders'); assert.dom('.page-container').exists('Block yields for app content'); }); diff --git a/ui/tests/pages/components/console/ui-panel.js b/ui/tests/pages/components/console/ui-panel.js index 3bb6503c31..6556a5e77f 100644 --- a/ui/tests/pages/components/console/ui-panel.js +++ b/ui/tests/pages/components/console/ui-panel.js @@ -11,6 +11,7 @@ import keys from 'core/utils/key-codes'; export default { toggle: clickable('[data-test-console-toggle]'), + dismissConsole: clickable(['data-test-dismiss-console-button']), consoleInput: fillable('[data-test-component="console/command-input"] input'), consoleInputValue: value('[data-test-component="console/command-input"] input'), logOutput: text('[data-test-component="console/output-log"]'), @@ -54,12 +55,18 @@ export default { eventProperties: { keyCode: keys.ENTER }, }), hasInput: isPresent('[data-test-component="console/command-input"] input'), - runCommands: async function (commands) { + runCommands: async function (commands, shouldToggle = true) { const toExecute = Array.isArray(commands) ? commands : [commands]; + if (shouldToggle) { + await this.toggle(); // toggle the console open + } for (const command of toExecute) { await this.consoleInput(command); await this.enter(); await settled(); } + if (shouldToggle) { + await this.toggle(); // toggle it closed + } }, };