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

[ui] General keyboard navigation in the UI #12831

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5e4d53e
Initialized keyboard service
philrenaud Apr 28, 2022
8572f93
Neat but funky: dynamic subnav traversal
philrenaud Apr 29, 2022
4dfe021
👻
philrenaud Apr 29, 2022
b6faeb3
generalized traverseSubnav method
philrenaud Apr 29, 2022
0905d16
Shift as special modifier key
philrenaud Apr 29, 2022
f001cc0
Nice little demo panel
philrenaud Apr 29, 2022
3d4c637
Keyboard shortcuts keycard
philrenaud Apr 29, 2022
a3c495f
Some animation styles on keyboard shortcuts
philrenaud Apr 29, 2022
3fd3d40
Handle situations where a link is deeply nested from its parent menu …
philrenaud Apr 29, 2022
d3121cf
Keyboard service cleanup
philrenaud Apr 29, 2022
3ade9b5
helper-based initializer and teardown for new contextual commands
philrenaud May 6, 2022
84a2f33
Keyboard shortcuts modal component added and demo-ghost removed
philrenaud May 6, 2022
30957db
Removed j and k from subnav traversal
philrenaud May 6, 2022
c0c45ce
Register and unregister methods for subnav plus new subnavs for volum…
philrenaud May 9, 2022
2ba2e70
register main nav method
philrenaud May 9, 2022
65b2307
Generalizing the register nav method
philrenaud May 9, 2022
7d034cb
12762 table keynav (#12975)
philrenaud May 17, 2022
0a51649
Go to Optimize keyboard link and storage key changed to g r
philrenaud Jun 24, 2022
c6191d4
parameterized jobs keyboard nav
philrenaud Jun 24, 2022
0444b74
Dynamic numeric keynav for multiple tables (#13482)
philrenaud Jul 13, 2022
68e5999
Variables keyboard nav added
philrenaud Jul 13, 2022
357b3d7
Variable traversal functionality added to shortcuts
philrenaud Jul 13, 2022
9fb422e
Get tests passing in Keynav: remove math helpers and a few other defe…
philrenaud Jul 14, 2022
2579036
[ui] Keyboard nav: "u" key to go up a level (#13754)
philrenaud Jul 15, 2022
3983019
Replace ArrowLeft etc. with an ascii arrow (#13776)
philrenaud Jul 18, 2022
354a149
Keyboard Nav: let users rebind their shortcuts (#13781)
philrenaud Jul 19, 2022
711b56c
willDestroy hook to prevent tests from doubling/tripling up addEventL…
philrenaud Jul 21, 2022
964637a
remove unused tests
philrenaud Jul 22, 2022
2ee33d0
Keyboard Navigation acceptance tests (#13893)
philrenaud Jul 22, 2022
6b6ba94
Weird issue where linktos with query props specifically from the task…
philrenaud Aug 2, 2022
e3d6a26
Adds the concept of exclusivity to a keycommand, removing peers that …
philrenaud Aug 3, 2022
cd45721
Lintfix
philrenaud Aug 3, 2022
7284497
Changelog and PR feedback
philrenaud Aug 3, 2022
c23b7a6
Changelog and PR feedback
philrenaud Aug 3, 2022
36cc240
Fix to rebinding in firefox by blurring the now-disabled button on re…
philrenaud Aug 9, 2022
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/12831.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: add general keyboard navigation to the Nomad UI
```
8 changes: 7 additions & 1 deletion ui/.template-lintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ module.exports = {
'no-action': 'off',
'no-invalid-interactive': 'off',
'no-inline-styles': 'off',
'no-curly-component-invocation': { allow: ['format-volume-name'] },
'no-curly-component-invocation': {
allow: ['format-volume-name', 'keyboard-commands'],
},
'no-implicit-this': { allow: ['keyboard-commands'] },
},
ignore: [
'app/components/breadcrumbs/*', // using {{(modifier)}} syntax
],
};
1 change: 1 addition & 0 deletions ui/app/components/allocation-subnav.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import classic from 'ember-classic-decorator';
@tagName('')
export default class AllocationSubnav extends Component {
@service router;
@service keyboard;

@equal('router.currentRouteName', 'allocations.allocation.fs')
fsIsActive;
Expand Down
7 changes: 7 additions & 0 deletions ui/app/components/app-breadcrumbs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Component from '@glimmer/component';

export default class AppBreadcrumbsComponent extends Component {
isOneCrumbUp(iter = 0, totalNum = 0) {
return iter === totalNum - 2;
}
}
14 changes: 11 additions & 3 deletions ui/app/components/breadcrumbs/default.hbs
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
{{! template-lint-disable no-unknown-arguments-for-builtin-components }}
<li data-test-breadcrumb-default>
<li data-test-breadcrumb-default
{{(modifier
this.maybeKeyboardShortcut
label="Go up a level"
pattern=(array "u")
menuLevel=true
action=(action this.traverseUpALevel @crumb.args)
exclusive=true
)}}
>
<LinkTo
@params={{@crumb.args}}
data-test-breadcrumb={{@crumb.args.firstObject}}
>
data-test-breadcrumb={{@crumb.args.firstObject}}>
{{#if @crumb.title}}
<dl>
<dt>
Expand Down
18 changes: 18 additions & 0 deletions ui/app/components/breadcrumbs/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
import KeyboardShortcutModifier from 'nomad-ui/modifiers/keyboard-shortcut';
import { inject as service } from '@ember/service';

export default class BreadcrumbsTemplate extends Component {
@service router;

@action
traverseUpALevel(args) {
const [path, ...rest] = args;
this.router.transitionTo(path, ...rest);
}

get maybeKeyboardShortcut() {
return this.args.isOneCrumbUp() ? KeyboardShortcutModifier : null;
philrenaud marked this conversation as resolved.
Show resolved Hide resolved
}
}
11 changes: 10 additions & 1 deletion ui/app/components/breadcrumbs/job.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,16 @@
</LinkTo>
</li>
{{/if}}
<li>
<li
{{(modifier
this.maybeKeyboardShortcut
label="Go up a level"
pattern=(array "u")
menuLevel=true
action=(action this.traverseUpALevel (array "jobs.job" this.job.idWithNamespace))
exclusive=true
)}}
>
<LinkTo
@route="jobs.job.index"
@model={{this.job}}
Expand Down
4 changes: 2 additions & 2 deletions ui/app/components/breadcrumbs/job.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { assert } from '@ember/debug';
import { action } from '@ember/object';
import Component from '@glimmer/component';
import BreadcrumbsTemplate from './default';

export default class BreadcrumbsJob extends Component {
export default class BreadcrumbsJob extends BreadcrumbsTemplate {
get job() {
return this.args.crumb.job;
}
Expand Down
5 changes: 4 additions & 1 deletion ui/app/components/client-subnav.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import { inject as service } from '@ember/service';

@tagName('')
export default class ClientSubnav extends Component {}
export default class ClientSubnav extends Component {
@service keyboard;
}
3 changes: 3 additions & 0 deletions ui/app/components/evaluation-sidebar/detail.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{{#let this.currentEvalDetail as |evaluation|}}
{{#if this.isSideBarOpen}}
{{keyboard-commands this.keyCommands}}
{{/if}}
<Portal @target="eval-detail-portal">
<div
data-test-eval-detail
Expand Down
8 changes: 8 additions & 0 deletions ui/app/components/evaluation-sidebar/detail.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,12 @@ export default class Detail extends Component {
closeSidebar() {
return this.statechart.send('MODAL_CLOSE');
}

keyCommands = [
{
label: 'Close Evaluations Sidebar',
pattern: ['Escape'],
action: () => this.closeSidebar(),
},
];
}
6 changes: 6 additions & 0 deletions ui/app/components/gutter-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import classic from 'ember-classic-decorator';
export default class GutterMenu extends Component {
@service system;
@service router;
@service keyboard;

@computed('system.namespaces.@each.name')
get sortedNamespaces() {
Expand Down Expand Up @@ -37,6 +38,11 @@ export default class GutterMenu extends Component {

onHamburgerClick() {}

// Seemingly redundant, but serves to ensure the action is passed to the keyboard service correctly
transitionTo(destination) {
return this.router.transitionTo(destination);
}

gotoJobsForNamespace(namespace) {
if (!namespace || !namespace.get('id')) return;

Expand Down
1 change: 1 addition & 0 deletions ui/app/components/job-subnav.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Component from '@glimmer/component';

export default class JobSubnav extends Component {
@service can;
@service keyboard;

get shouldRenderClientsTab() {
const { job } = this.args;
Expand Down
70 changes: 70 additions & 0 deletions ui/app/components/keyboard-shortcuts-modal.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{{#if this.keyboard.shortcutsVisible}}
{{keyboard-commands (array this.escapeCommand)}}
<div class="keyboard-shortcuts"
{{on-click-outside
(toggle "keyboard.shortcutsVisible" this)
}}
>
<header>
<button
{{autofocus}}
class="button is-borderless dismiss"
type="button"
{{on "click" (toggle "keyboard.shortcutsVisible" this)}}
aria-label="Dismiss"
>
{{x-icon "cancel"}}
</button>
<h2>Keyboard Shortcuts</h2>
<p>Click a key pattern to re-bind it to a shortcut of your choosing.</p>
</header>
<ul class="commands-list">
{{#each this.commands as |command|}}
<li data-test-command-label={{command.label}}>
<strong>{{command.label}}</strong>
<span class="keys">
{{#if command.recording}}
<span class="recording">Recording; ESC to cancel.</span>
{{else}}
{{#if command.custom}}
<button type="button" class="reset-to-default" {{on "click" (action this.keyboard.resetCommandToDefault command)}}>reset to default</button>
{{/if}}
{{/if}}

<button data-test-rebinder disabled={{or (not command.rebindable) command.recording}} type="button" {{on "click" (action this.keyboard.rebindCommand command)}}>
{{#each command.pattern as |key|}}
<span>{{clean-keycommand key}}</span>
{{/each}}
</button>
</span>
</li>
{{/each}}
</ul>
<footer>
<strong>Keyboard shortcuts {{#if this.keyboard.enabled}}enabled{{else}}disabled{{/if}}</strong>
<Toggle
data-test-enable-shortcuts-toggle
@isActive={{this.keyboard.enabled}}
@onToggle={{this.toggleListener}}
title="{{if this.keyboard.enabled "enable" "disable"}} keyboard shortcuts"
/>
</footer>
</div>
{{/if}}

{{#if (and this.keyboard.enabled this.keyboard.displayHints)}}
{{#each this.hints as |hint|}}
<span
{{did-insert (fn this.tetherToElement hint.element hint)}}
{{will-destroy (fn this.untetherFromElement hint)}}
data-test-keyboard-hint
data-shortcut={{hint.pattern}}
class="{{if hint.menuLevel "menu-level"}}"
aria-hidden="true"
>
{{#each hint.pattern as |key|}}
<span>{{key}}</span>
{{/each}}
</span>
{{/each}}
{{/if}}
70 changes: 70 additions & 0 deletions ui/app/components/keyboard-shortcuts-modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
import { action } from '@ember/object';
import Tether from 'tether';

export default class KeyboardShortcutsModalComponent extends Component {
@service keyboard;
@service config;

escapeCommand = {
label: 'Hide Keyboard Shortcuts',
pattern: ['Escape'],
action: () => {
this.keyboard.shortcutsVisible = false;
},
};

/**
* commands: filter keyCommands to those that have an action and a label,
* to distinguish between those that are just visual hints of existing commands
*/
@computed('keyboard.keyCommands.[]')
get commands() {
return this.keyboard.keyCommands.reduce((memo, c) => {
if (c.label && c.action && !memo.find((m) => m.label === c.label)) {
memo.push(c);
}
return memo;
}, []);
}

/**
* hints: filter keyCommands to those that have an element property,
* and then compute a position on screen to place the hint.
*/
@computed('keyboard.{keyCommands.length,displayHints}')
get hints() {
if (this.keyboard.displayHints) {
return this.keyboard.keyCommands.filter((c) => c.element);
} else {
return [];
}
}

@action
tetherToElement(element, hint, self) {
if (!this.config.isTest) {
let binder = new Tether({
element: self,
target: element,
attachment: 'top left',
targetAttachment: 'top left',
targetModifier: 'visible',
});
hint.binder = binder;
}
}

@action
untetherFromElement(hint) {
if (!this.config.isTest) {
hint.binder.destroy();
}
}

@action toggleListener() {
this.keyboard.enabled = !this.keyboard.enabled;
}
}
6 changes: 6 additions & 0 deletions ui/app/components/plugin-subnav.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';

export default class PluginSubnavComponent extends Component {
@service keyboard;
}
9 changes: 9 additions & 0 deletions ui/app/components/safe-link-to.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { LinkComponent } from '@ember/legacy-built-in-components';
import classic from 'ember-classic-decorator';

// Necessary for programmatic routing away pages with <LinkTo>s that contain @query properties.
// (There's an issue with query param calculations in the new component that uses the router service)
// https://github.com/emberjs/ember.js/issues/20051

@classic
export default class SafeLinkToComponent extends LinkComponent {}
6 changes: 5 additions & 1 deletion ui/app/components/server-agent-row.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@ export default class ServerAgentRow extends Component {
return currentURL.replace(/%40/g, '@') === targetURL.replace(/%40/g, '@');
}

click() {
goToAgent() {
const transition = () =>
this.router.transitionTo('servers.server', this.agent);
lazyClick([transition, event]);
}

click() {
this.goToAgent();
}
}
5 changes: 4 additions & 1 deletion ui/app/components/server-subnav.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import { inject as service } from '@ember/service';

@tagName('')
export default class ServerSubnav extends Component {}
export default class ServerSubnav extends Component {
@service keyboard;
}
6 changes: 6 additions & 0 deletions ui/app/components/storage-subnav.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';

export default class StorageSubnavComponent extends Component {
@service keyboard;
}
1 change: 1 addition & 0 deletions ui/app/components/task-subnav.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import classic from 'ember-classic-decorator';
@tagName('')
export default class TaskSubnav extends Component {
@service router;
@service keyboard;

@equal('router.currentRouteName', 'allocations.allocation.task.fs')
fsIsActive;
Expand Down
14 changes: 12 additions & 2 deletions ui/app/components/variable-paths.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
<tbody>
{{#each this.folders as |folder|}}
<tr data-test-folder-row {{on "click" (fn this.handleFolderClick folder.data.absolutePath)}}>
<td colspan="3">
<td colspan="3"
{{keyboard-shortcut
enumerated=true
action=(fn this.handleFolderClick folder.data.absolutePath)
}}
>
<span>
<FlightIcon @name="folder" />
<LinkTo @route="variables.path" @model={{folder.data.absolutePath}} @query={{hash namespace="*"}}>
Expand All @@ -26,7 +31,12 @@

{{#each this.files as |file|}}
<tr data-test-file-row {{on "click" (fn this.handleFileClick file.absoluteFilePath)}}>
<td>
<td
{{keyboard-shortcut
enumerated=true
action=(fn this.handleFileClick file.absoluteFilePath)
}}
>
<FlightIcon @name="file-text" />
<LinkTo
@route="variables.variable"
Expand Down
Loading