[Graph] Fix various a11y issues (#54097)

This commit is contained in:
Joe Reuter 2020-01-13 10:26:57 +01:00 committed by GitHub
parent aeebedfa4d
commit 204155b4e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 113 additions and 40 deletions

View file

@ -9,6 +9,7 @@ exports[`after fetch hideWriteControls 1`] = `
entityName="dashboard"
entityNamePlural="dashboards"
findItems={[Function]}
headingId="dashboardListingHeading"
initialFilter=""
listingLimit={1}
noItemsFragment={
@ -16,13 +17,15 @@ exports[`after fetch hideWriteControls 1`] = `
<EuiEmptyPrompt
iconType="visualizeApp"
title={
<h2>
<h1
id="dashboardListingHeading"
>
<FormattedMessage
defaultMessage="Looks like you don't have any dashboards."
id="kbn.dashboard.listing.noItemsMessage"
values={Object {}}
/>
</h2>
</h1>
}
/>
</div>
@ -63,6 +66,7 @@ exports[`after fetch initialFilter 1`] = `
entityName="dashboard"
entityNamePlural="dashboards"
findItems={[Function]}
headingId="dashboardListingHeading"
initialFilter="my dashboard"
listingLimit={1000}
noItemsFragment={
@ -114,13 +118,15 @@ exports[`after fetch initialFilter 1`] = `
}
iconType="dashboardApp"
title={
<h2>
<h1
id="dashboardListingHeading"
>
<FormattedMessage
defaultMessage="Create your first dashboard"
id="kbn.dashboard.listing.createNewDashboard.title"
values={Object {}}
/>
</h2>
</h1>
}
/>
</div>
@ -161,6 +167,7 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
entityName="dashboard"
entityNamePlural="dashboards"
findItems={[Function]}
headingId="dashboardListingHeading"
initialFilter=""
listingLimit={1}
noItemsFragment={
@ -212,13 +219,15 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
}
iconType="dashboardApp"
title={
<h2>
<h1
id="dashboardListingHeading"
>
<FormattedMessage
defaultMessage="Create your first dashboard"
id="kbn.dashboard.listing.createNewDashboard.title"
values={Object {}}
/>
</h2>
</h1>
}
/>
</div>
@ -259,6 +268,7 @@ exports[`after fetch renders table rows 1`] = `
entityName="dashboard"
entityNamePlural="dashboards"
findItems={[Function]}
headingId="dashboardListingHeading"
initialFilter=""
listingLimit={1000}
noItemsFragment={
@ -310,13 +320,15 @@ exports[`after fetch renders table rows 1`] = `
}
iconType="dashboardApp"
title={
<h2>
<h1
id="dashboardListingHeading"
>
<FormattedMessage
defaultMessage="Create your first dashboard"
id="kbn.dashboard.listing.createNewDashboard.title"
values={Object {}}
/>
</h2>
</h1>
}
/>
</div>
@ -357,6 +369,7 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
entityName="dashboard"
entityNamePlural="dashboards"
findItems={[Function]}
headingId="dashboardListingHeading"
initialFilter=""
listingLimit={1}
noItemsFragment={
@ -408,13 +421,15 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
}
iconType="dashboardApp"
title={
<h2>
<h1
id="dashboardListingHeading"
>
<FormattedMessage
defaultMessage="Create your first dashboard"
id="kbn.dashboard.listing.createNewDashboard.title"
values={Object {}}
/>
</h2>
</h1>
}
/>
</div>
@ -455,6 +470,7 @@ exports[`renders empty page in before initial fetch to avoid flickering 1`] = `
entityName="dashboard"
entityNamePlural="dashboards"
findItems={[Function]}
headingId="dashboardListingHeading"
initialFilter=""
listingLimit={1000}
noItemsFragment={
@ -506,13 +522,15 @@ exports[`renders empty page in before initial fetch to avoid flickering 1`] = `
}
iconType="dashboardApp"
title={
<h2>
<h1
id="dashboardListingHeading"
>
<FormattedMessage
defaultMessage="Create your first dashboard"
id="kbn.dashboard.listing.createNewDashboard.title"
values={Object {}}
/>
</h2>
</h1>
}
/>
</div>

View file

@ -42,6 +42,7 @@ export class DashboardListing extends React.Component {
return (
<I18nProvider>
<TableListView
headingId="dashboardListingHeading"
createItem={this.props.hideWriteControls ? null : this.props.createItem}
findItems={this.props.findItems}
deleteItems={this.props.hideWriteControls ? null : this.props.deleteItems}
@ -73,12 +74,12 @@ export class DashboardListing extends React.Component {
<EuiEmptyPrompt
iconType="visualizeApp"
title={
<h2>
<h1 id="dashboardListingHeading">
<FormattedMessage
id="kbn.dashboard.listing.noItemsMessage"
defaultMessage="Looks like you don't have any dashboards."
/>
</h2>
</h1>
}
/>
</div>
@ -90,12 +91,12 @@ export class DashboardListing extends React.Component {
<EuiEmptyPrompt
iconType="dashboardApp"
title={
<h2>
<h1 id="dashboardListingHeading">
<FormattedMessage
id="kbn.dashboard.listing.createNewDashboard.title"
defaultMessage="Create your first dashboard"
/>
</h2>
</h1>
}
body={
<Fragment>

View file

@ -36,6 +36,7 @@ class VisualizeListingTable extends Component {
const { visualizeCapabilities, uiSettings, toastNotifications } = getServices();
return (
<TableListView
headingId="visualizeListingHeading"
// we allow users to create visualizations even if they can't save them
// for data exploration purposes
createItem={this.props.createItem}
@ -113,12 +114,12 @@ class VisualizeListingTable extends Component {
<EuiEmptyPrompt
iconType="visualizeApp"
title={
<h2>
<h1 id="visualizeListingHeading">
<FormattedMessage
id="kbn.visualize.listing.noItemsMessage"
defaultMessage="Looks like you don't have any visualizations."
/>
</h2>
</h1>
}
/>
</div>
@ -130,12 +131,12 @@ class VisualizeListingTable extends Component {
<EuiEmptyPrompt
iconType="visualizeApp"
title={
<h2>
<h1 id="visualizeListingHeading">
<FormattedMessage
id="kbn.visualize.listing.createNew.title"
defaultMessage="Create your first visualization"
/>
</h2>
</h1>
}
body={
<Fragment>

View file

@ -67,6 +67,11 @@ export interface TableListViewProps {
tableListTitle: string;
toastNotifications: ToastsStart;
uiSettings: IUiSettingsClient;
/**
* Id of the heading element describing the table. This id will be used as `aria-labelledby` of the wrapper element.
* If the table is not empty, this component renders its own h1 element using the same id.
*/
headingId?: string;
}
export interface TableListViewState {
@ -463,7 +468,7 @@ class TableListView extends React.Component<TableListViewProps, TableListViewSta
<EuiFlexGroup justifyContent="spaceBetween" alignItems="flexEnd" data-test-subj="top-nav">
<EuiFlexItem grow={false}>
<EuiTitle size="l">
<h1>{this.props.tableListTitle}</h1>
<h1 id={this.props.headingId}>{this.props.tableListTitle}</h1>
</EuiTitle>
</EuiFlexItem>
@ -498,7 +503,11 @@ class TableListView extends React.Component<TableListViewProps, TableListViewSta
className="itemListing__page"
restrictWidth
>
<EuiPageBody>{this.renderPageContent()}</EuiPageBody>
<EuiPageBody
aria-labelledby={this.state.hasInitialFetchReturned ? this.props.headingId : undefined}
>
{this.renderPageContent()}
</EuiPageBody>
</EuiPage>
);
}

View file

@ -13,6 +13,7 @@
.help-block {
font-size: $euiFontSizeXS;
color: $euiTextColor;
}
}

View file

@ -1,4 +1,4 @@
<div id="graphBasic" ng-controller="graphuiPlugin">
<main id="graphBasic" ng-controller="graphuiPlugin" aria-labelledby="graphHeading">
<!-- Local nav. -->
<kbn-top-nav name="workspacesTopNav" config="topNavMenu">
</kbn-top-nav>
@ -81,6 +81,7 @@
<button
class="kuiButton kuiButton--basic kuiButton--small"
tooltip="{{ ::'xpack.graph.sidebar.topMenu.undoButtonTooltip' | i18n: { defaultMessage: 'Undo' } }}"
aria-label="{{ ::'xpack.graph.sidebar.topMenu.undoButtonTooltip' | i18n: { defaultMessage: 'Undo' } }}"
type="button"
ng-click="workspace.undo()"
ng-disabled="workspace === null||workspace.undoLog.length <1"
@ -91,6 +92,7 @@
<button
class="kuiButton kuiButton--basic kuiButton--small"
tooltip="{{ ::'xpack.graph.sidebar.topMenu.redoButtonTooltip' | i18n: { defaultMessage: 'Redo' } }}"
aria-label="{{ ::'xpack.graph.sidebar.topMenu.redoButtonTooltip' | i18n: { defaultMessage: 'Redo' } }}"
type="button"
ng-disabled="workspace === null ||workspace.redoLog.length === 0"
ng-click="workspace.redo()"
@ -100,48 +102,56 @@
<button class="kuiButton kuiButton--basic kuiButton--small" ng-disabled="workspace === null ||liveResponseFields.length === 0||workspace.nodes.length === 0"
tooltip="{{ ::'xpack.graph.sidebar.topMenu.expandSelectionButtonTooltip' | i18n: { defaultMessage: 'Expand selection' } }}"
aria-label="{{ ::'xpack.graph.sidebar.topMenu.expandSelectionButtonTooltip' | i18n: { defaultMessage: 'Expand selection' } }}"
ng-click="setDetail(null);workspace.expandSelecteds({toFields:liveResponseFields});">
<span class="kuiIcon fa-plus"></span>
</button>
<button class="kuiButton kuiButton--basic kuiButton--small" ng-disabled="workspace === null ||workspace.nodes.length === 0"
tooltip="{{ ::'xpack.graph.sidebar.topMenu.addLinksButtonTooltip' | i18n: { defaultMessage: 'Add links between existing terms' } }}"
aria-label="{{ ::'xpack.graph.sidebar.topMenu.addLinksButtonTooltip' | i18n: { defaultMessage: 'Add links between existing terms' } }}"
ng-click="workspace.fillInGraph();">
<span class="kuiIcon fa-link"></span>
</button>
<button class="kuiButton kuiButton--basic kuiButton--small" ng-disabled="workspace === null ||workspace.nodes.length === 0"
tooltip="{{ ::'xpack.graph.sidebar.topMenu.removeVerticesButtonTooltip' | i18n: { defaultMessage: 'Remove vertices from workspace' } }}"
aria-label="{{ ::'xpack.graph.sidebar.topMenu.removeVerticesButtonTooltip' | i18n: { defaultMessage: 'Remove vertices from workspace' } }}"
ng-click="setDetail(null);workspace.deleteSelection();" data-test-subj="graphRemoveSelection">
<span class="kuiIcon fa-trash"></span>
</button>
<button class="kuiButton kuiButton--basic kuiButton--small" ng-disabled="workspace === null ||workspace.selectedNodes.length === 0"
tooltip="{{ ::'xpack.graph.sidebar.topMenu.blacklistButtonTooltip' | i18n: { defaultMessage: 'Blacklist selection from return to workspace' } }}"
aria-label="{{ ::'xpack.graph.sidebar.topMenu.blacklistButtonTooltip' | i18n: { defaultMessage: 'Blacklist selection from return to workspace' } }}"
ng-click="workspace.blacklistSelection();">
<span class="kuiIcon fa-ban"></span>
</button>
<button class="kuiButton kuiButton--basic kuiButton--small" ng-disabled="workspace === null ||workspace.selectedNodes.length === 0"
tooltip="{{ ::'xpack.graph.sidebar.topMenu.customStyleButtonTooltip' | i18n: { defaultMessage: 'Custom style selected vertices' } }}"
aria-label="{{ ::'xpack.graph.sidebar.topMenu.customStyleButtonTooltip' | i18n: { defaultMessage: 'Custom style selected vertices' } }}"
ng-click="setDetail({showStyle:true})">
<span class="kuiIcon fa-paint-brush"></span>
</button>
<button class="kuiButton kuiButton--basic kuiButton--small" ng-disabled="workspace === null||workspace.nodes.length === 0"
tooltip="{{ ::'xpack.graph.sidebar.topMenu.drillDownButtonTooltip' | i18n: { defaultMessage: 'Drill down' } }}"
aria-label="{{ ::'xpack.graph.sidebar.topMenu.drillDownButtonTooltip' | i18n: { defaultMessage: 'Drill down' } }}"
ng-click="setDetail({showDrillDowns:true})">
<span class="kuiIcon fa-info"></span>
</button>
<button class="kuiButton kuiButton--basic kuiButton--small" ng-disabled="workspace.nodes.length === 0" ng-if="workspace.nodes.length === 0||workspace.force === null"
tooltip="{{ ::'xpack.graph.sidebar.topMenu.runLayoutButtonTooltip' | i18n: { defaultMessage: 'Run layout' } }}"
aria-label="{{ ::'xpack.graph.sidebar.topMenu.runLayoutButtonTooltip' | i18n: { defaultMessage: 'Run layout' } }}"
ng-click="workspace.runLayout()" data-test-subj="graphResumeLayout">
<span class="kuiIcon fa-play"></span>
</button>
<button class="kuiButton kuiButton--basic kuiButton--small" ng-if="workspace.force !== null&&workspace.nodes.length>0"
tooltip="{{ ::'xpack.graph.sidebar.topMenu.pauseLayoutButtonTooltip' | i18n: { defaultMessage: 'Pause layout' } }}"
aria-label="{{ ::'xpack.graph.sidebar.topMenu.pauseLayoutButtonTooltip' | i18n: { defaultMessage: 'Pause layout' } }}"
ng-click="workspace.stopLayout()" data-test-subj="graphPauseLayout">
<span class="kuiIcon fa-pause"></span>
</button>
@ -386,4 +396,4 @@
</div>
<!--end svg container-->
</div>
</main>

View file

@ -16,6 +16,7 @@ import { FieldManager } from './field_manager';
import { SearchBarProps, SearchBar } from './search_bar';
import { GraphStore } from '../state_management';
import { GuidancePanel } from './guidance_panel';
import { GraphTitle } from './graph_title';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
@ -52,6 +53,7 @@ export function GraphApp(props: GraphAppProps) {
>
<Provider store={reduxStore}>
<>
{props.isInitialized && <GraphTitle />}
<div className="gphGraph__bar">
<SearchBar {...searchBarProps} />
<EuiSpacer size="s" />

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { connect } from 'react-redux';
import { EuiScreenReaderOnly } from '@elastic/eui';
import React from 'react';
import { GraphState, metaDataSelector } from '../state_management';
interface GraphTitleProps {
title: string;
}
/**
* Component showing the title of the current workspace as a heading visible for screen readers
*/
export const GraphTitle = connect<GraphTitleProps, {}, {}, GraphState>((state: GraphState) => ({
title: metaDataSelector(state).title,
}))(({ title }: GraphTitleProps) => (
<EuiScreenReaderOnly>
<h1 id="graphHeading">{title}</h1>
</EuiScreenReaderOnly>
));

View file

@ -15,16 +15,10 @@
position: relative;
padding-left: $euiSizeXL;
margin-bottom: $euiSizeL;
button {
// make buttons wrap lines like regular text
display: contents;
}
}
.gphGuidancePanel__item--disabled {
color: $euiColorDarkShade;
pointer-events: none;
button {
color: $euiColorDarkShade !important;

View file

@ -13,6 +13,7 @@ import {
EuiText,
EuiLink,
EuiCallOut,
EuiScreenReaderOnly,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import classNames from 'classnames';
@ -53,6 +54,7 @@ function ListItem({
'gphGuidancePanel__item--disabled': state === 'disabled',
})}
aria-disabled={state === 'disabled'}
aria-current={state === 'active' ? 'step' : undefined}
>
{state !== 'disabled' && (
<span
@ -96,7 +98,7 @@ function GuidancePanelComponent(props: GuidancePanelProps) {
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>
<h1>
<h1 id="graphHeading">
{i18n.translate('xpack.graph.guidancePanel.title', {
defaultMessage: 'Three steps to your graph',
})}
@ -104,7 +106,7 @@ function GuidancePanelComponent(props: GuidancePanelProps) {
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ul className="gphGuidancePanel__list">
<ol className="gphGuidancePanel__list" aria-labelledby="graphHeading">
<ListItem state={hasDatasource ? 'done' : 'active'}>
<EuiLink onClick={onOpenDatasourcePicker}>
{i18n.translate(
@ -116,7 +118,7 @@ function GuidancePanelComponent(props: GuidancePanelProps) {
</EuiLink>
</ListItem>
<ListItem state={hasFields ? 'done' : hasDatasource ? 'active' : 'disabled'}>
<EuiLink onClick={onOpenFieldPicker}>
<EuiLink onClick={onOpenFieldPicker} disabled={!hasFields && !hasDatasource}>
{i18n.translate('xpack.graph.guidancePanel.fieldsItem.fieldsButtonLabel', {
defaultMessage: 'Add fields.',
})}
@ -128,7 +130,7 @@ function GuidancePanelComponent(props: GuidancePanelProps) {
defaultMessage="Enter a query in the search bar to start exploring. Don't know where to start? {topTerms}."
values={{
topTerms: (
<EuiLink onClick={onFillWorkspace}>
<EuiLink onClick={onFillWorkspace} disabled={!hasFields}>
{i18n.translate('xpack.graph.guidancePanel.nodesItem.topTermsButtonLabel', {
defaultMessage: 'Graph the top terms',
})}
@ -137,7 +139,7 @@ function GuidancePanelComponent(props: GuidancePanelProps) {
}}
/>
</ListItem>
</ul>
</ol>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
@ -157,7 +159,15 @@ function GuidancePanelComponent(props: GuidancePanelProps) {
title={i18n.translate('xpack.graph.noDataSourceNotificationMessageTitle', {
defaultMessage: 'No data source',
})}
heading="h1"
>
<EuiScreenReaderOnly>
<p id="graphHeading">
{i18n.translate('xpack.graph.noDataSourceNotificationMessageTitle', {
defaultMessage: 'No data source',
})}
</p>
</EuiScreenReaderOnly>
<p>
<FormattedMessage
id="xpack.graph.noDataSourceNotificationMessageText"

View file

@ -30,6 +30,7 @@ export function Listing(props: ListingProps) {
return (
<I18nProvider>
<TableListView
headingId="graphListingHeading"
createItem={props.capabilities.save ? props.createItem : undefined}
findItems={props.findItems}
deleteItems={props.capabilities.delete ? props.deleteItems : undefined}
@ -67,14 +68,14 @@ function getNoItemsMessage(
return (
<div>
<EuiEmptyPrompt
iconType="visualizeApp"
iconType="graphApp"
title={
<h2>
<h1 id="graphListingHeading">
<FormattedMessage
id="xpack.graph.listing.noItemsMessage"
defaultMessage="Looks like you don't have any graphs."
/>
</h2>
</h1>
}
/>
</div>
@ -88,12 +89,12 @@ function getNoItemsMessage(
<EuiEmptyPrompt
iconType="graphApp"
title={
<h2>
<h1 id="graphListingHeading">
<FormattedMessage
id="xpack.graph.listing.createNewGraph.title"
defaultMessage="Create your first graph"
/>
</h2>
</h1>
}
body={
<Fragment>