Skip to content

Commit

Permalink
Improve timelion accessibility (#13531)
Browse files Browse the repository at this point in the history
* Fix broken refresh shortcut for non OS X

On non OS X systems, the e.metaKey is most likely the windows key.
Hotkeys with the windows key are usually used as global (non application
internal) hotkeys. Also the documentation states, that Ctrl + Enter
should also work. With this commit it doesn't matter whether you press
the Meta or the Ctrl key + Enter.

* Make timelion datepicker more accessible

* Add meaningful labels to each interval shortcut, and
* Add a proper label to the custom input text field

* Make save options keyboard accessible, fix #12246, fix #12487

* Make sort button of saved-object-finder accessible, fix #12486

* Label timelion options correctly, fix #12907

* Make timelion legends keyboard accessible, fix #11844

* Make timelion autocompletion properly accessible

Fixes #11533, fixes #11525, fixes #12908

* Make timelion charts and actions accessible, fix #12909

* Add some more inline comments

* Fix HTML styles according to styleguide

* Replace sr-only by kuiScreenReaderOnly

* Make timelion autocompletion work with ARIA 1.1
  • Loading branch information
timroes authored and Tim Roes committed Aug 21, 2017
1 parent d1e26e2 commit 904bda9
Show file tree
Hide file tree
Showing 13 changed files with 130 additions and 26 deletions.
8 changes: 7 additions & 1 deletion src/core_plugins/timelion/public/app.less
Expand Up @@ -152,7 +152,8 @@ timelion-interval {
left:5px;
line-height: 10px;

> div {
> .cell-action,
> .cell-id {
display: inline-block;
font-size: 10px;
text-align: center;
Expand All @@ -169,6 +170,10 @@ timelion-interval {

.cell-action {
opacity: 0;

&:focus {
opacity: 1;
}
}
}

Expand All @@ -188,6 +193,7 @@ timelion-interval {
overflow-x: hidden;
}

.ngLegendValue:focus,
.ngLegendValue:hover {
text-decoration: underline;
}
Expand Down
34 changes: 31 additions & 3 deletions src/core_plugins/timelion/public/directives/cells/cells.html
Expand Up @@ -9,15 +9,43 @@
timelion-grid timelion-grid-rows="state.rows"
ng-click="onSelect($index)"
ng-class="{active: $index === state.selected}"
kbn-accessible-click
aria-label="Timelion chart {{$index + 1}}"
aria-current="{{$index === state.selected}}"
>

<div chart="sheet[$index]" search="onSearch" interval="state.interval"></div>
<div class="cell-actions">
<div class="cell-id"><span>{{$index + 1}}</span></div>

<div class="cell-action" ng-click="removeCell($index)" tooltip="Remove" tooltip-append-to-body="1"><span class="fa fa-remove"></span></div>
<div class="cell-action" tooltip="Drag to reorder" tooltip-append-to-body="1" sv-handle><span class="fa fa-arrows"></span></div>
<div class="cell-action" ng-click="transient.fullscreen = true" tooltip="Full screen" tooltip-append-to-body="1"><span class="fa fa-expand"></span></div>
<button
class="cell-action"
ng-click="removeCell($index)"
tooltip="Remove"
tooltip-append-to-body="1"
aria-label="Remove chart"
>
<span class="fa fa-remove"></span>
</button>
<button
class="cell-action"
tooltip="Drag to reorder"
tooltip-append-to-body="1"
sv-handle
aria-label="Drag to reorder"
tabindex="-1"
>
<span class="fa fa-arrows"></span>
</button>
<button
class="cell-action"
ng-click="transient.fullscreen = true"
tooltip="Full screen"
tooltip-append-to-body="1"
aria-label="Full screen chart"
>
<span class="fa fa-expand"></span>
</button>
</div>
</div>

Expand Down
@@ -1,8 +1,14 @@
<div class="chart-container col-md-12 col-sm-12 col-xs-12" timelion-grid timelion-grid-rows="1">
<div chart="series" search="onSearch" interval="state.interval"></div>
<div class="cell-actions">
<div class="cell-fullscreen" ng-click="transient.fullscreen = false" tooltip="Exit full screen" tooltip-append-to-body="1">
<button
class="cell-fullscreen cell-action"
ng-click="transient.fullscreen = false"
tooltip="Exit full screen"
tooltip-append-to-body="1"
aria-label="Exit full screen"
>
<span class="fa fa-compress"></span>
</div>
</button>
</div>
</div>
@@ -1,7 +1,16 @@
<div class="timelionExpressionInputContainer">
<div
class="timelionExpressionInputContainer"
role="combobox"
aria-expanded="{{functionSuggestions.isVisible}}"
aria-owns="timelionSuggestionList"
aria-haspopup="true"
>
<!-- The `role=textbox` is required by VoiceOver to properly detect the autocompletion.
For some reasons it doesn't work without it (even though the default role of
the element is textbox anyway). -->
<textarea
data-expression-input
type="text"
role="textbox"
rows="{{ rows }}"
class="timelionExpressionInput kuiTextArea fullWidth"
placeholder="Try a query with .es(*)"
Expand All @@ -12,6 +21,11 @@
ng-blur="onBlurInput()"
ng-mousedown="onMouseDownInput()"
ng-mouseup="onMouseUpInput()"
aria-label="Timelion expression"
aria-multiline="false"
aria-autocomplete="list"
aria-controls="timelionSuggestionList"
aria-activedescendant="{{ getActiveSuggestionId() }}"
></textarea>

<timelion-expression-suggestions
Expand Down
Expand Up @@ -166,8 +166,12 @@ app.directive('timelionExpressionInput', function ($document, $http, $interval,
break;

case comboBoxKeyCodes.TAB:
// If there are no suggestions, the user tabs to the next input.
if (scope.functionSuggestions.isEmpty()) {
// If there are no suggestions or none is selected, the user tabs to the next input.
if (scope.functionSuggestions.isEmpty() || scope.functionSuggestions.index < 0) {
// Before letting the tab be handled to focus the next element
// we need to hide the suggestions, otherwise it will focus these
// instead of the time interval select.
scope.functionSuggestions.hide();
return;
}

Expand All @@ -177,7 +181,7 @@ app.directive('timelionExpressionInput', function ($document, $http, $interval,
break;

case comboBoxKeyCodes.ENTER:
if (e.metaKey) {
if (e.metaKey || e.ctrlKey) {
// Re-render the chart when the user hits CMD+ENTER.
e.preventDefault();
scope.updateChart();
Expand Down Expand Up @@ -206,6 +210,13 @@ app.directive('timelionExpressionInput', function ($document, $http, $interval,
insertSuggestionIntoExpression(index);
};

scope.getActiveSuggestionId = () => {
if(scope.functionSuggestions.isVisible && scope.functionSuggestions.index > -1) {
return `timelionSuggestion${scope.functionSuggestions.index}`;
}
return '';
};

init();
}
};
Expand Down
Expand Up @@ -6,17 +6,21 @@ export class FunctionSuggestions {
}

reset() {
this.index = 0;
this.index = -1;
this.list = [];
this.isVisible = false;
}

setList(list) {
this.list = list;

// We may get a shorter list than the one we have now, so we need to make sure our index doesn't
// fall outside of the new list's range.
this.index = Math.max(0, Math.min(this.index, this.list.length - 1));
// Only try to position index inside of list range, when it was already focused
// beforehand (i.e. not -1)
if (this.index > -1) {
// We may get a shorter list than the one we have now, so we need to make sure our index doesn't
// fall outside of the new list's range.
this.index = Math.max(0, Math.min(this.index, this.list.length - 1));
}
}

getCount() {
Expand Down
@@ -1,19 +1,26 @@
<div
id="timelionSuggestionList"
class="suggestions"
role="listbox"
ng-class="{ 'suggestions-isPopover': shouldPopover === 'true' }"
data-suggestions-list
>
<div
class="suggestion"
id="timelionSuggestion{{$index}}"
role="option"
tabindex="0"
data-suggestion-list-item
ng-class="{active: $index === selectedIndex}"
ng-repeat="suggestion in suggestions track by $index | orderBy:'name'"
ng-mousedown="onMouseDown($event)"
ng-click="onClickSuggestion({ suggestionIndex: $index })"
aria-label="{{suggestion.name}}"
aria-describedby="timelionSuggestionDescription{{$index}}"
>
<h4>
<strong>.{{suggestion.name}}()</strong>
<small>
<small id="timelionSuggestionDescription{{$index}}">
{{suggestion.help}}
{{suggestion.chainable ? '(Chainable)' : '(Data Source)'}}
</small>
Expand Down
@@ -1,5 +1,6 @@
<input
input-focus
aria-label="Custom interval"
class="kuiTextInput timelion-interval-custom"
ng-show="interval === 'other'"
ng-class="{ 'timelion-interval-custom-compact': interval === 'other' }"
Expand All @@ -8,7 +9,14 @@
id="timelionInterval"
aria-label="Select interval"
class="kuiSelect timelion-interval-presets"
ng-options="intervalOption for intervalOption in intervalOptions"
ng-class="{ 'timelion-interval-presets-compact': interval === 'other'}"
ng-model="interval"
></select>
>
<option
ng-repeat="intervalOption in intervalOptions"
aria-label="{{::intervalLabels[intervalOption]}}"
value="{{::intervalOption}}"
>
{{::intervalOption}}
</option>
</select>
Expand Up @@ -15,6 +15,17 @@ app.directive('timelionInterval', function ($compile, $timeout) {
template,
link: function ($scope, $elem) {
$scope.intervalOptions = ['auto', '1s', '1m', '1h', '1d', '1w', '1M', '1y', 'other'];
$scope.intervalLabels = {
'auto': 'auto',
'1s': '1 second',
'1m': '1 minute',
'1h': '1 hour',
'1d': '1 day',
'1w': '1 week',
'1M': '1 month',
'1y': '1 year',
'other': 'other'
};

$scope.$watch('model', function (newVal, oldVal) {
// Only run this on initialization
Expand Down
Expand Up @@ -63,7 +63,7 @@ export default function timechartFn(Private, config, $rootScope, timefilter, $co
position: 'nw',
labelBoxBorderColor: 'rgb(255,255,255,0)',
labelFormatter: function (label, series) {
return '<span class="ngLegendValue" ng-click="toggleSeries(' + series._id + ')">' +
return '<span class="ngLegendValue" kbn-accessible-click ng-click="toggleSeries(' + series._id + ')">' +
label +
'<span class="ngLegendValueNumber"></span></span>';
}
Expand Down
8 changes: 4 additions & 4 deletions src/core_plugins/timelion/public/partials/save_sheet.html
@@ -1,12 +1,12 @@
<div class="list-group">
<a class="list-group-item" ng-click="section = 'sheet'">
<button class="list-group-item" ng-click="section = 'sheet'" type="button">
<h4 class="list-group-item-heading">Save entire Timelion sheet</h4>
<p class="list-group-item-text">
You want this option if you mostly use Timelion expressions from within the Timelion app and don't need to
add Timelion charts to Kibana dashboards. You may also want this if you make use of references to other
panels.
</p>
</a>
</button>

<div class="list-group-item" ng-show="section == 'sheet'">
<form role="form" class="container-fluid" ng-submit="opts.saveSheet()">
Expand All @@ -24,15 +24,15 @@ <h4 class="list-group-item-heading">Save entire Timelion sheet</h4>
</form>
</div>

<a class="list-group-item" ng-click="section = 'expression'">
<button class="list-group-item" ng-click="section = 'expression'" type="button">
<h4 class="list-group-item-heading">Save current expression as Kibana dashboard panel</h4>
<p class="list-group-item-text">
Need to add a chart to a Kibana dashboard? We can do that! This option will save your currently selected
expression as a panel that can be added to Kibana dashboards as you would add anything else. Note, if you
use references to other panels you will need to remove the refences by copying the referenced expression
directly into the expression you are saving. Click a chart to select a different expression to save.
</p>
</a>
</button>

<div class="list-group-item" ng-show="section == 'expression'">
<form role="form" class="container-fluid" ng-submit="opts.saveExpression(panelTitle)">
Expand Down
6 changes: 4 additions & 2 deletions src/core_plugins/timelion/public/partials/sheet_options.html
Expand Up @@ -2,16 +2,18 @@
<div class="kuiLocalDropdownTitle">Sheet options</div>
<div>
<div class="form-group col-md-6">
<label>Columns <small>Column count must divide evenly into 12</small></label>
<label for="timelionColCount">Columns <small>Column count must divide evenly into 12</small></label>
<select class="form-control"
id="timelionColCount"
ng-change="opts.search()"
ng-options="column for column in [1, 2, 3, 4, 6, 12]"
ng-model="opts.state.columns">
</select>
</div>
<div class="form-group col-md-6">
<label>Rows <small>This is a target based on the current window height</small></label>
<label for="timelionRowCount">Rows <small>This is a target based on the current window height</small></label>
<select class="form-control"
id="timelionRowCount"
ng-change="opts.search()"
ng-options="row for row in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]"
ng-model="opts.state.rows">
Expand Down
9 changes: 8 additions & 1 deletion src/ui/public/partials/saved_object_finder.html
Expand Up @@ -52,12 +52,19 @@
class="kuiVerticalRhythm"
>
<ul class="li-striped list-group list-group-menu" ng-class="{'select-mode': finder.selector.enabled}">
<li class="list-group-item" ng-click="finder.sortHits(finder.hits)">
<li
class="list-group-item"
ng-click="finder.sortHits(finder.hits)"
kbn-accessible-click
aria-live="assertive"
>
<span class="paginate-heading">
<span class="kuiScreenReaderOnly">Sort by</span>
Name
<span
class="fa"
ng-class="finder.isAscending ? 'fa-caret-up' : 'fa-caret-down'">
<span class="kuiScreenReaderOnly">({{finder.isAscending ? 'ascending' : 'descending'}})</span>
</span>
</span>
</li>
Expand Down

0 comments on commit 904bda9

Please sign in to comment.