Create keyboard mode for ui-ace editor (#13339)

* Add kbn-ui-ace-keyboard-mode directive

* Implemented PR feedback

* Fix broken tests
This commit is contained in:
Tim Roes 2017-08-09 21:24:04 +02:00 committed by GitHub
parent 12142da71b
commit e66c1d2ac4
7 changed files with 167 additions and 0 deletions

View file

@ -130,6 +130,7 @@
<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"

View file

@ -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';

View file

@ -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');
});
});
});

View file

@ -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);
}
}));

View file

@ -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;
}
}

View file

@ -1,6 +1,7 @@
<div
json-input
require-keys="true"
kbn-ui-ace-keyboard-mode
ui-ace="{
mode: 'json',
onLoad: aceLoaded

View file

@ -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 () {