Improve timelion accessibility (#13531)
* 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
This commit is contained in:
parent
d1e26e28eb
commit
904bda9b27
|
@ -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;
|
||||
|
@ -169,6 +170,10 @@ timelion-interval {
|
|||
|
||||
.cell-action {
|
||||
opacity: 0;
|
||||
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,6 +193,7 @@ timelion-interval {
|
|||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.ngLegendValue:focus,
|
||||
.ngLegendValue:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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(*)"
|
||||
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@ export class FunctionSuggestions {
|
|||
}
|
||||
|
||||
reset() {
|
||||
this.index = 0;
|
||||
this.index = -1;
|
||||
this.list = [];
|
||||
this.isVisible = false;
|
||||
}
|
||||
|
@ -14,9 +14,13 @@ export class FunctionSuggestions {
|
|||
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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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' }"
|
||||
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>';
|
||||
}
|
||||
|
|
|
@ -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()">
|
||||
|
@ -24,7 +24,7 @@
|
|||
</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
|
||||
|
@ -32,7 +32,7 @@
|
|||
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)">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue