Skip to content

Commit

Permalink
Create keyboard mode for ui-ace editor (#13339)
Browse files Browse the repository at this point in the history
* Add kbn-ui-ace-keyboard-mode directive

* Implemented PR feedback

* Fix broken tests
  • Loading branch information
timroes authored and Tim Roes committed Aug 9, 2017
1 parent d82f555 commit 493fad2
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 0 deletions.
Expand Up @@ -130,6 +130,7 @@ <h1 class="kuiTitle">

<div
ng-if="field.type === 'json' || field.type === 'array'"
kbn-ui-ace-keyboard-mode
ui-ace="{ onLoad: aceLoaded, mode: 'json' }"
id="{{field.name}}"
ng-model="field.value"
Expand Down
Expand Up @@ -5,6 +5,7 @@ import { savedObjectManagementRegistry } from 'plugins/kibana/management/saved_o
import objectViewHTML from 'plugins/kibana/management/sections/objects/_view.html';
import uiRoutes from 'ui/routes';
import { uiModules } from 'ui/modules';
import 'ui/accessibility/kbn_ui_ace_keyboard_mode';
import { castEsToKbnFieldTypeName } from '../../../../../../utils';
import { SavedObjectsClientProvider } from 'ui/saved_objects';

Expand Down
57 changes: 57 additions & 0 deletions src/ui/public/accessibility/__tests__/kbn_ui_ace_keyboard_mode.js
@@ -0,0 +1,57 @@
import angular from 'angular';
import sinon from 'sinon';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import '../kbn_ui_ace_keyboard_mode';
import {
ENTER_KEY,
ESC_KEY_CODE,
} from 'ui_framework/services';

describe('kbnUiAceKeyboardMode directive', () => {
let element;

beforeEach(ngMock.module('kibana'));

beforeEach(ngMock.inject(($compile, $rootScope) => {
element = $compile(`<div ui-ace kbn-ui-ace-keyboard-mode></div>`)($rootScope.$new());
}));

it('should add the hint element', () => {
expect(element.find('.uiAceKeyboardHint').length).to.be(1);
});

describe('hint element', () => {
it('should be tabable', () => {
expect(element.find('.uiAceKeyboardHint').attr('tabindex')).to.be('0');
});

it('should move focus to textbox and be inactive if pressed enter on it', () => {
const textarea = element.find('textarea');
sinon.spy(textarea[0], 'focus');
const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap
ev.keyCode = ENTER_KEY;
element.find('.uiAceKeyboardHint').trigger(ev);
expect(textarea[0].focus.called).to.be(true);
expect(element.find('.uiAceKeyboardHint').hasClass('uiAceKeyboardHint-isInactive')).to.be(true);
});

it('should be shown again, when pressing Escape in ace editor', () => {
const textarea = element.find('textarea');
const hint = element.find('.uiAceKeyboardHint');
sinon.spy(hint[0], 'focus');
const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap
ev.keyCode = ESC_KEY_CODE;
textarea.trigger(ev);
expect(hint[0].focus.called).to.be(true);
expect(hint.hasClass('uiAceKeyboardHint-isInactive')).to.be(false);
});
});

describe('ui-ace textarea', () => {
it('should not be tabable anymore', () => {
expect(element.find('textarea').attr('tabindex')).to.be('-1');
});
});

});
80 changes: 80 additions & 0 deletions src/ui/public/accessibility/kbn_ui_ace_keyboard_mode.js
@@ -0,0 +1,80 @@
/**
* The `kbn-ui-ace-keyboard-mode` directive should be used on any element, that
* `ui-ace` is used on. It will prevent the keyboard trap, that ui-ace usually
* has, i.e. tabbing into the box won't give you any possibilities to leave
* it via keyboard again, since tab inside the textbox works like a tab character.
*
* This directive won't change anything, if the user uses the mouse. But if she
* tabs to the ace editor, an overlay will be shown, that you have to press Enter
* to enter editing mode, and that it can be left by pressing Escape again.
*
* That way the ui-ace editor won't trap keyboard focus, and won't cause that
* accessibility issue anymore.
*/

import angular from 'angular';
import { uiModules } from 'ui/modules';
import './kbn_ui_ace_keyboard_mode.less';
import { ESC_KEY_CODE, ENTER_KEY } from 'ui_framework/services';

let aceKeyboardModeId = 0;

uiModules.get('kibana').directive('kbnUiAceKeyboardMode', () => ({
restrict: 'A',
link(scope, element) {
const uniqueId = `uiAceKeyboardHint-${scope.$id}-${aceKeyboardModeId++}`;

const hint = angular.element(
`<div
class="uiAceKeyboardHint"
id="${uniqueId}"
tabindex="0"
role="application"
>
<p class="kuiText kuiVerticalRhythmSmall">
Press Enter to start editing.
</p>
<p class="kuiText kuiVerticalRhythmSmall">
When you&rsquo;re done, press Escape to stop editing.
</p>
</div>
`);

const uiAceTextbox = element.find('textarea');

function startEditing() {
// We are not using ng-class in the element, so that we won't need to $compile it
hint.addClass('uiAceKeyboardHint-isInactive');
uiAceTextbox.focus();
}

function enableOverlay() {
hint.removeClass('uiAceKeyboardHint-isInactive');
}

hint.keydown((ev) => {
if (ev.keyCode === ENTER_KEY) {
ev.preventDefault();
startEditing();
}
});

uiAceTextbox.blur(() => {
enableOverlay();
});

uiAceTextbox.keydown((ev) => {
if (ev.keyCode === ESC_KEY_CODE) {
ev.preventDefault();
ev.stopPropagation();
enableOverlay();
hint.focus();
}
});

hint.click(startEditing);
// Prevent tabbing into the ACE textarea, we now handle all focusing for it
uiAceTextbox.attr('tabindex', '-1');
element.prepend(hint);
}
}));
26 changes: 26 additions & 0 deletions src/ui/public/accessibility/kbn_ui_ace_keyboard_mode.less
@@ -0,0 +1,26 @@
@import (reference) "~ui/styles/variables";

.uiAceKeyboardHint {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
background: rgba(255, 255, 255, 0.7);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
opacity: 0;

&:focus {
opacity: 1;
border: 2px solid @globalColorBlue;
z-index: 1000;
}

&.uiAceKeyboardHint-isInactive {
display: none;
}
}
1 change: 1 addition & 0 deletions src/ui/public/filter_editor/filter_query_dsl_editor.html
@@ -1,6 +1,7 @@
<div
json-input
require-keys="true"
kbn-ui-ace-keyboard-mode
ui-ace="{
mode: 'json',
onLoad: aceLoaded
Expand Down
1 change: 1 addition & 0 deletions src/ui/public/filter_editor/filter_query_dsl_editor.js
Expand Up @@ -2,6 +2,7 @@ import 'ace';
import _ from 'lodash';
import { uiModules } from 'ui/modules';
import template from './filter_query_dsl_editor.html';
import 'ui/accessibility/kbn_ui_ace_keyboard_mode';

const module = uiModules.get('kibana');
module.directive('filterQueryDslEditor', function () {
Expand Down

0 comments on commit 493fad2

Please sign in to comment.