Skip to content

Commit 46083c0

Browse files
[Security Solution] Accessibility (a11y) fixes (#87783)
## [Security Solution] Accessibility (a11y) fixes This PR fixes the following accessibility (a11y) issues: - Fixes an issue that prevented tabbing through all elements on pages with embedded Timelines - Fixes an issue where the Timeline data providers popover menu was not displayed when Enter is pressed - Fixes an issue where duplicate draggable IDs caused errors when re-arranging Timeline columns - Fixes an issue where Timeline columns could not be removed or sorted via keyboard - Fixes an issue where focus is not restored to the `Customize Columns` button when the `Reset` button is pressed - Fixes an issue where filtering the `Customize Event Renderers` view via the input cleared selected entries - Fixes an issue where the active timeline button wasn't focused when Timeline is closed - Fixes an issue where the `(+)` Create / Open Timeline button's hover panel didn't own focus
1 parent 5dca937 commit 46083c0

File tree

32 files changed

+575
-180
lines changed

32 files changed

+575
-180
lines changed

x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
FIELDS_BROWSER_SELECTED_CATEGORY_COUNT,
1717
FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT,
1818
} from '../screens/fields_browser';
19+
import { TIMELINE_FIELDS_BUTTON } from '../screens/timeline';
1920
import { cleanKibana } from '../tasks/common';
2021

2122
import {
@@ -182,5 +183,19 @@ describe('Fields Browser', () => {
182183

183184
cy.get(FIELDS_BROWSER_HEADER_HOST_GEO_CONTINENT_NAME_HEADER).should('not.exist');
184185
});
186+
187+
it('restores focus to the Customize Columns button when `Reset Fields` is clicked', () => {
188+
openTimelineFieldsBrowser();
189+
resetFields();
190+
191+
cy.get(TIMELINE_FIELDS_BUTTON).should('have.focus');
192+
});
193+
194+
it('restores focus to the Customize Columns button when Esc is pressed', () => {
195+
openTimelineFieldsBrowser();
196+
cy.get('body').type('{esc}');
197+
198+
cy.get(TIMELINE_FIELDS_BUTTON).should('have.focus');
199+
});
185200
});
186201
});

x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
TIMELINE_DATA_PROVIDERS,
99
TIMELINE_DATA_PROVIDERS_EMPTY,
1010
TIMELINE_DROPPED_DATA_PROVIDERS,
11+
TIMELINE_DATA_PROVIDERS_ACTION_MENU,
1112
} from '../screens/timeline';
1213
import { HOSTS_NAMES_DRAGGABLE } from '../screens/hosts/all_hosts';
1314

@@ -53,6 +54,17 @@ describe('timeline data providers', () => {
5354
});
5455
});
5556

57+
it('displays the data provider action menu when Enter is pressed', () => {
58+
dragAndDropFirstHostToTimeline();
59+
openTimelineUsingToggle();
60+
cy.get(TIMELINE_DATA_PROVIDERS_ACTION_MENU).should('not.exist');
61+
62+
cy.get(TIMELINE_DROPPED_DATA_PROVIDERS).first().focus();
63+
cy.get(TIMELINE_DROPPED_DATA_PROVIDERS).first().parent().type('{enter}');
64+
65+
cy.get(TIMELINE_DATA_PROVIDERS_ACTION_MENU).should('exist');
66+
});
67+
5668
it('sets the background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the data providers', () => {
5769
dragFirstHostToTimeline();
5870

x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,22 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { TIMELINE_FLYOUT_HEADER, TIMELINE_DATA_PROVIDERS } from '../screens/timeline';
7+
import { TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON } from '../screens/security_main';
8+
import {
9+
CREATE_NEW_TIMELINE,
10+
TIMELINE_DATA_PROVIDERS,
11+
TIMELINE_FLYOUT_HEADER,
12+
TIMELINE_SETTINGS_ICON,
13+
} from '../screens/timeline';
814
import { cleanKibana } from '../tasks/common';
915

1016
import { dragFirstHostToTimeline, waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts';
1117
import { loginAndWaitForPage } from '../tasks/login';
12-
import { openTimelineUsingToggle, closeTimelineUsingToggle } from '../tasks/security_main';
18+
import {
19+
closeTimelineUsingCloseButton,
20+
closeTimelineUsingToggle,
21+
openTimelineUsingToggle,
22+
} from '../tasks/security_main';
1323

1424
import { HOSTS_URL } from '../urls/navigation';
1525

@@ -26,6 +36,34 @@ describe('timeline flyout button', () => {
2636
closeTimelineUsingToggle();
2737
});
2838

39+
it('re-focuses the toggle button when timeline is closed by clicking the active timeline toggle button', () => {
40+
openTimelineUsingToggle();
41+
closeTimelineUsingToggle();
42+
43+
cy.get(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).should('have.focus');
44+
});
45+
46+
it('re-focuses the toggle button when timeline is closed by clicking the [X] close button', () => {
47+
openTimelineUsingToggle();
48+
closeTimelineUsingCloseButton();
49+
50+
cy.get(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).should('have.focus');
51+
});
52+
53+
it('re-focuses the toggle button when timeline is closed by pressing the Esc key', () => {
54+
openTimelineUsingToggle();
55+
cy.get('body').type('{esc}');
56+
57+
cy.get(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).should('have.focus');
58+
});
59+
60+
it('the `(+)` button popover menu owns focus', () => {
61+
cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').click({ force: true });
62+
cy.get(CREATE_NEW_TIMELINE).should('have.focus');
63+
cy.get('body').type('{esc}');
64+
cy.get(CREATE_NEW_TIMELINE).should('not.be.visible');
65+
});
66+
2967
it('sets the data providers background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the data providers area', () => {
3068
dragFirstHostToTimeline();
3169

x-pack/plugins/security_solution/cypress/screens/security_main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7+
export const CLOSE_TIMELINE_BUTTON = '[data-test-subj="close-timeline"]';
8+
79
export const MAIN_PAGE = '[data-test-subj="kibanaChrome"]';
810

911
export const TIMELINE_TOGGLE_BUTTON = '[data-test-subj="flyoutOverlay"]';

x-pack/plugins/security_solution/cypress/screens/timeline.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinne
111111

112112
export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]';
113113

114+
export const TIMELINE_DATA_PROVIDERS_ACTION_MENU = '[data-test-subj="providerActions"]';
115+
114116
export const TIMELINE_DATA_PROVIDERS_EMPTY =
115117
'[data-test-subj="dataProviders"] [data-test-subj="empty"]';
116118

x-pack/plugins/security_solution/cypress/tasks/security_main.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import {
8+
CLOSE_TIMELINE_BUTTON,
89
MAIN_PAGE,
910
TIMELINE_TOGGLE_BUTTON,
1011
TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON,
@@ -18,6 +19,10 @@ export const closeTimelineUsingToggle = () => {
1819
cy.get(TIMELINE_TOGGLE_BUTTON).filter(':visible').click();
1920
};
2021

22+
export const closeTimelineUsingCloseButton = () => {
23+
cy.get(CLOSE_TIMELINE_BUTTON).filter(':visible').click();
24+
};
25+
2126
export const openTimelineIfClosed = () =>
2227
cy.get(MAIN_PAGE).then(($page) => {
2328
if ($page.find(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).length === 1) {

x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ const AlertsUtilityBarComponent: React.FC<AlertsUtilityBarProps> = ({
220220
disabled={areEventsLoading}
221221
iconType="arrowDown"
222222
iconSide="right"
223-
ownFocus={false}
223+
ownFocus={true}
224224
popoverContent={UtilityBarAdditionalFiltersContent}
225225
>
226226
{i18n.ADDITIONAL_FILTERS_ACTIONS}

x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66

77
import { EuiSpacer, EuiWindowEvent } from '@elastic/eui';
88
import { noop } from 'lodash/fp';
9-
import React, { useCallback, useMemo, useState } from 'react';
9+
import React, { useCallback, useMemo, useRef, useState } from 'react';
1010
import { useDispatch } from 'react-redux';
1111
import { useHistory } from 'react-router-dom';
1212

1313
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
1414
import { SecurityPageName } from '../../../app/types';
1515
import { TimelineId } from '../../../../common/types/timeline';
1616
import { useGlobalTime } from '../../../common/containers/use_global_time';
17+
import { isTab } from '../../../common/components/accessibility/helpers';
1718
import { UpdateDateRange } from '../../../common/components/charts/common';
1819
import { FiltersGlobal } from '../../../common/components/filters_global';
1920
import { getRulesUrl } from '../../../common/components/link_to/redirect_to_detection_engine';
@@ -39,7 +40,12 @@ import { LinkButton } from '../../../common/components/links';
3940
import { useFormatUrl } from '../../../common/components/link_to';
4041
import { useGlobalFullScreen } from '../../../common/containers/use_full_screen';
4142
import { Display } from '../../../hosts/pages/display';
42-
import { showGlobalFilters } from '../../../timelines/components/timeline/helpers';
43+
import {
44+
focusUtilityBarAction,
45+
onTimelineTabKeyPressed,
46+
resetKeyboardFocus,
47+
showGlobalFilters,
48+
} from '../../../timelines/components/timeline/helpers';
4349
import { timelineSelectors } from '../../../timelines/store/timeline';
4450
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
4551
import { buildShowBuildingBlockFilter } from '../../components/alerts_table/default_config';
@@ -48,6 +54,7 @@ import { SourcererScopeName } from '../../../common/store/sourcerer/model';
4854

4955
const DetectionEnginePageComponent = () => {
5056
const dispatch = useDispatch();
57+
const containerElement = useRef<HTMLDivElement | null>(null);
5158
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
5259
const graphEventId = useShallowEqualSelector(
5360
(state) => (getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults).graphEventId
@@ -128,6 +135,28 @@ const DetectionEnginePageComponent = () => {
128135

129136
const { indicesExist, indexPattern } = useSourcererScope(SourcererScopeName.detections);
130137

138+
const onSkipFocusBeforeEventsTable = useCallback(() => {
139+
focusUtilityBarAction(containerElement.current);
140+
}, [containerElement]);
141+
142+
const onSkipFocusAfterEventsTable = useCallback(() => {
143+
resetKeyboardFocus();
144+
}, []);
145+
146+
const onKeyDown = useCallback(
147+
(keyboardEvent: React.KeyboardEvent) => {
148+
if (isTab(keyboardEvent)) {
149+
onTimelineTabKeyPressed({
150+
containerElement: containerElement.current,
151+
keyboardEvent,
152+
onSkipFocusBeforeEventsTable,
153+
onSkipFocusAfterEventsTable,
154+
});
155+
}
156+
},
157+
[containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable]
158+
);
159+
131160
if (isUserAuthenticated != null && !isUserAuthenticated && !loading) {
132161
return (
133162
<WrapperPage>
@@ -154,7 +183,7 @@ const DetectionEnginePageComponent = () => {
154183
{hasEncryptionKey != null && !hasEncryptionKey && <NoApiIntegrationKeyCallOut />}
155184
<ReadOnlyAlertsCallOut />
156185
{indicesExist ? (
157-
<>
186+
<div onKeyDown={onKeyDown} ref={containerElement}>
158187
<EuiWindowEvent event="resize" handler={noop} />
159188
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
160189
<SiemSearchBar id="global" indexPattern={indexPattern} />
@@ -211,7 +240,7 @@ const DetectionEnginePageComponent = () => {
211240
to={to}
212241
/>
213242
</WrapperPage>
214-
</>
243+
</div>
215244
) : (
216245
<WrapperPage>
217246
<DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} />

x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
} from '@elastic/eui';
2020
import { FormattedMessage } from '@kbn/i18n/react';
2121
import { noop } from 'lodash/fp';
22-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
22+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2323
import { useParams, useHistory } from 'react-router-dom';
2424
import { useDispatch } from 'react-redux';
2525

@@ -81,7 +81,12 @@ import { useGlobalFullScreen } from '../../../../../common/containers/use_full_s
8181
import { Display } from '../../../../../hosts/pages/display';
8282
import { ExceptionListTypeEnum, ExceptionListIdentifiers } from '../../../../../shared_imports';
8383
import { useRuleAsync } from '../../../../containers/detection_engine/rules/use_rule_async';
84-
import { showGlobalFilters } from '../../../../../timelines/components/timeline/helpers';
84+
import {
85+
focusUtilityBarAction,
86+
onTimelineTabKeyPressed,
87+
resetKeyboardFocus,
88+
showGlobalFilters,
89+
} from '../../../../../timelines/components/timeline/helpers';
8590
import { timelineSelectors } from '../../../../../timelines/store/timeline';
8691
import { timelineDefaults } from '../../../../../timelines/store/timeline/defaults';
8792
import { useSourcererScope } from '../../../../../common/containers/sourcerer';
@@ -95,6 +100,7 @@ import {
95100
import * as detectionI18n from '../../translations';
96101
import * as ruleI18n from '../translations';
97102
import * as i18n from './translations';
103+
import { isTab } from '../../../../../common/components/accessibility/helpers';
98104

99105
enum RuleDetailTabs {
100106
alerts = 'alerts',
@@ -127,6 +133,7 @@ const getRuleDetailsTabs = (rule: Rule | null) => {
127133

128134
const RuleDetailsPageComponent = () => {
129135
const dispatch = useDispatch();
136+
const containerElement = useRef<HTMLDivElement | null>(null);
130137
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
131138
const graphEventId = useShallowEqualSelector(
132139
(state) =>
@@ -408,6 +415,28 @@ const RuleDetailsPageComponent = () => {
408415
}
409416
}, [rule]);
410417

418+
const onSkipFocusBeforeEventsTable = useCallback(() => {
419+
focusUtilityBarAction(containerElement.current);
420+
}, [containerElement]);
421+
422+
const onSkipFocusAfterEventsTable = useCallback(() => {
423+
resetKeyboardFocus();
424+
}, []);
425+
426+
const onKeyDown = useCallback(
427+
(keyboardEvent: React.KeyboardEvent) => {
428+
if (isTab(keyboardEvent)) {
429+
onTimelineTabKeyPressed({
430+
containerElement: containerElement.current,
431+
keyboardEvent,
432+
onSkipFocusBeforeEventsTable,
433+
onSkipFocusAfterEventsTable,
434+
});
435+
}
436+
},
437+
[containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable]
438+
);
439+
411440
if (
412441
redirectToDetections(
413442
isSignalIndexExists,
@@ -430,7 +459,7 @@ const RuleDetailsPageComponent = () => {
430459
<ReadOnlyAlertsCallOut />
431460
<ReadOnlyRulesCallOut />
432461
{indicesExist ? (
433-
<>
462+
<div onKeyDown={onKeyDown} ref={containerElement}>
434463
<EuiWindowEvent event="resize" handler={noop} />
435464
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
436465
<SiemSearchBar id="global" indexPattern={indexPattern} />
@@ -588,7 +617,7 @@ const RuleDetailsPageComponent = () => {
588617
)}
589618
{ruleDetailTab === RuleDetailTabs.failures && <FailureHistory id={rule?.id} />}
590619
</WrapperPage>
591-
</>
620+
</div>
592621
) : (
593622
<WrapperPage>
594623
<DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} />

0 commit comments

Comments
 (0)