Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Address a11y issues in browser-based console UI #26872

Merged
merged 18 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/26872.txt
Original file line number Diff line number Diff line change
@@ -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.
```
5 changes: 3 additions & 2 deletions ui/app/components/console/command-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@ export default Component.extend({
actions: {
handleKeyUp(event) {
const keyCode = event.keyCode;
const val = event.target.value;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this to make it more consistent with the rest of the UI code so the implementation matches.

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() {
Expand Down
9 changes: 8 additions & 1 deletion ui/app/components/sidebar/frame.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,14 @@
<LinkStatus @status={{this.currentCluster.cluster.hcpLinkStatus}} />
{{yield}}
<div data-test-console-panel class={{if this.console.isOpen "panel-open"}}>
<Console::UiPanel @isFullscreen={{this.consoleFullscreen}} />
{{#if this.console.isOpen}}
<Console::UiPanel
@isFullscreen={{this.consoleFullscreen}}
{{focus-trap
focusTrapOptions=(hash initialFocus="#console-input" clickOutsideDeactivates=true onDeactivate=this.closeConsole)
}}
/>
{{/if}}
</div>
</Frame.Main>
</Hds::AppFrame>
8 changes: 5 additions & 3 deletions ui/app/styles/components/console-ui-panel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -118,11 +120,11 @@ $console-close-height: 35px;

.panel-open .console-ui-panel {
box-shadow: $box-shadow-highest;
min-height: 400px;
min-height: 425px;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think eventually we'll want to re-visit this to make it a little more flexible, but right now it needed to be increased to account for the close button.

The close button was positioned in the DOM outside of the container it was then positioned inside of, which was causing the element to overlap. We don't want it to overlap other content.

}

.main--console-open {
padding-bottom: 400px;
padding-bottom: 425px;
}

.panel-open .console-ui-panel.fullscreen {
Expand Down
9 changes: 5 additions & 4 deletions ui/app/templates/components/console/command-input.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,20 @@
<Chevron />
{{/if}}
<input
aria-label="command input"
aria-label="web R.E.P.L."
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding it as "R.E.P.L." instead of just REPL helps screen readers to read it out correctly.

aria-describedby="namespace-reminder"
onkeyup={{action "handleKeyUp"}}
value={{this.value}}
autocomplete="off"
spellcheck="false"
id="console-input"
/>
<Hds::Button
class="hds-side-nav__icon-button"
{{on "click" (action "fullscreen")}}
{{hds-tooltip (if this.isFullscreen "minimize" "maximize")}}
data-test-tool-tip-trigger
data-test-dismiss-console-button
@icon={{if this.isFullscreen "minimize" "maximize"}}
@text={{if this.isFullscreen "Minimize" "Maximize"}}
@text={{if this.isFullscreen "Minimize window" "Maximize window"}}
@isIconOnly={{true}}
/>
</div>
Expand Down
42 changes: 24 additions & 18 deletions ui/app/templates/components/console/ui-panel.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@
SPDX-License-Identifier: BUSL-1.1
~}}

<div class="console-close-button">
<Hds::Button
class="hds-side-nav__icon-button"
{{on "click" (action "closeConsole")}}
data-test-console-panel-close
@text="Close console"
@icon="x"
@isIconOnly={{true}}
/>
</div>
<div class="console-ui-panel-content">
<div class="content has-bottom-margin-s">
<div class="console-close-button">
<Hds::Button
class="hds-side-nav__icon-button"
{{on "click" (action "closeConsole")}}
data-test-console-panel-close
@text="Close console"
@icon="x"
@isIconOnly={{true}}
/>
</div>
</div>
<div class="content has-bottom-margin-l">
<p class="console-ui-panel-intro is-font-mono has-bottom-margin-s">
The Vault Web REPL provides an easy way to execute common Vault CLI commands, such as write, read, delete, and list. It
Expand All @@ -22,14 +24,18 @@
<Hds::Link::Inline @href={{doc-link "/vault/docs/command/web"}}>HashiCorp Developer site</Hds::Link::Inline>.
</p>
<p class="console-ui-panel-intro is-font-mono has-bottom-margin-s">Examples:</p>
<p class="console-ui-panel-intro is-font-mono">→ Write secrets to kv v1: write &lt;mount&gt;/my-secret foo=bar</p>
<p class="console-ui-panel-intro is-font-mono">→ List kv v1 secret keys: list &lt;mount&gt;/</p>
<p class="console-ui-panel-intro is-font-mono">→ Read a kv v1 secret: read &lt;mount&gt;/my-secret</p>
<p class="console-ui-panel-intro is-font-mono">→ Mount a kv v2 secret engine: write sys/mounts/&lt;mount&gt; type=kv
options=version=2</p>
<p class="console-ui-panel-intro is-font-mono">→ Read a kv v2 secret: kv-get &lt;mount&gt;/secret-path</p>
<p class="console-ui-panel-intro is-font-mono">→ Read a kv v2 secret's metadata: kv-get &lt;mount&gt;/secret-path
-metadata</p>
<p class="console-ui-panel-intro is-font-mono">
hashishaw marked this conversation as resolved.
Show resolved Hide resolved
<span aria-hidden="true">→ </span>Write secrets to kv v1: write &lt;mount&gt;/my-secret foo=bar</p>
<p class="console-ui-panel-intro is-font-mono">
<span aria-hidden="true">→ </span>List kv v1 secret keys: list &lt;mount&gt;/</p>
<p class="console-ui-panel-intro is-font-mono">
<span aria-hidden="true">→ </span>Read a kv v1 secret: read &lt;mount&gt;/my-secret</p>
<p class="console-ui-panel-intro is-font-mono">
<span aria-hidden="true">→ </span>Mount a kv v2 secret engine: write sys/mounts/&lt;mount&gt; type=kv options=version=2</p>
<p class="console-ui-panel-intro is-font-mono">
<span aria-hidden="true">→ </span>Read a kv v2 secret: kv-get &lt;mount&gt;/secret-path</p>
<p class="console-ui-panel-intro is-font-mono">
<span aria-hidden="true">→ </span>Read a kv v2 secret's metadata: kv-get &lt;mount&gt;/secret-path-metadata</p>
</div>
<Console::OutputLog @outputLog={{this.cliLog}} />
<Console::CommandInput
Expand Down
4 changes: 2 additions & 2 deletions ui/lib/core/addon/components/namespace-reminder.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

{{#if this.showMessage}}
{{#if (has-block)}}
<p class="namespace-reminder">
<p class="namespace-reminder" id="namespace-reminder">
{{yield (hash namespace=this.namespace)}}
</p>
{{else}}
<p class="namespace-reminder">
<p class="namespace-reminder" id="namespace-reminder">
This
{{@noun}}
will be
Expand Down
10 changes: 4 additions & 6 deletions ui/tests/acceptance/console-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions ui/tests/acceptance/pki/pki-engine-workflow-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
7 changes: 6 additions & 1 deletion ui/tests/acceptance/redirect-to-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 });
Expand Down
16 changes: 10 additions & 6 deletions ui/tests/acceptance/settings/mount-secret-backend-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
7 changes: 6 additions & 1 deletion ui/tests/acceptance/wrapped-token-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
9 changes: 6 additions & 3 deletions ui/tests/helpers/commands.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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}`);
}
Expand Down
16 changes: 8 additions & 8 deletions ui/tests/integration/components/console/ui-panel-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions ui/tests/integration/components/sidebar/frame-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
});

Expand Down
Loading
Loading