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: 1.3.4 release #14138

Merged
merged 7 commits into from
Aug 17, 2022
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/14138.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;
philrenaud marked this conversation as resolved.
Show resolved Hide resolved
}
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: 13 additions & 1 deletion ui/app/controllers/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,26 @@ import codesForError from '../utils/codes-for-error';
import NoLeaderError from '../utils/no-leader-error';
import OTTExchangeError from '../utils/ott-exchange-error';
import classic from 'ember-classic-decorator';

// eslint-disable-next-line no-unused-vars
import KeyboardService from '../services/keyboard';
@classic
export default class ApplicationController extends Controller {
@service config;
@service system;
@service token;
@service flashMessages;

/**
* @type {KeyboardService}
*/
@service keyboard;

// eslint-disable-next-line ember/classic-decorator-hooks
constructor() {
super(...arguments);
this.keyboard.listenForKeypress();
}

queryParams = [
{
region: 'region',
Expand Down
1 change: 1 addition & 0 deletions ui/app/controllers/csi/volumes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default class IndexController extends Controller.extend(
) {
@service system;
@service userSettings;
@service keyboard;
@controller('csi/volumes') volumesController;

@alias('volumesController.isForbidden')
Expand Down
Loading