[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
This commit is contained in:
Andrew Goldstein 2021-01-12 01:02:53 -07:00 committed by GitHub
parent 5dca937c01
commit 46083c0973
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 575 additions and 180 deletions

View file

@ -16,6 +16,7 @@ import {
FIELDS_BROWSER_SELECTED_CATEGORY_COUNT, FIELDS_BROWSER_SELECTED_CATEGORY_COUNT,
FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT, FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT,
} from '../screens/fields_browser'; } from '../screens/fields_browser';
import { TIMELINE_FIELDS_BUTTON } from '../screens/timeline';
import { cleanKibana } from '../tasks/common'; import { cleanKibana } from '../tasks/common';
import { import {
@ -182,5 +183,19 @@ describe('Fields Browser', () => {
cy.get(FIELDS_BROWSER_HEADER_HOST_GEO_CONTINENT_NAME_HEADER).should('not.exist'); cy.get(FIELDS_BROWSER_HEADER_HOST_GEO_CONTINENT_NAME_HEADER).should('not.exist');
}); });
it('restores focus to the Customize Columns button when `Reset Fields` is clicked', () => {
openTimelineFieldsBrowser();
resetFields();
cy.get(TIMELINE_FIELDS_BUTTON).should('have.focus');
});
it('restores focus to the Customize Columns button when Esc is pressed', () => {
openTimelineFieldsBrowser();
cy.get('body').type('{esc}');
cy.get(TIMELINE_FIELDS_BUTTON).should('have.focus');
});
}); });
}); });

View file

@ -8,6 +8,7 @@ import {
TIMELINE_DATA_PROVIDERS, TIMELINE_DATA_PROVIDERS,
TIMELINE_DATA_PROVIDERS_EMPTY, TIMELINE_DATA_PROVIDERS_EMPTY,
TIMELINE_DROPPED_DATA_PROVIDERS, TIMELINE_DROPPED_DATA_PROVIDERS,
TIMELINE_DATA_PROVIDERS_ACTION_MENU,
} from '../screens/timeline'; } from '../screens/timeline';
import { HOSTS_NAMES_DRAGGABLE } from '../screens/hosts/all_hosts'; import { HOSTS_NAMES_DRAGGABLE } from '../screens/hosts/all_hosts';
@ -53,6 +54,17 @@ describe('timeline data providers', () => {
}); });
}); });
it('displays the data provider action menu when Enter is pressed', () => {
dragAndDropFirstHostToTimeline();
openTimelineUsingToggle();
cy.get(TIMELINE_DATA_PROVIDERS_ACTION_MENU).should('not.exist');
cy.get(TIMELINE_DROPPED_DATA_PROVIDERS).first().focus();
cy.get(TIMELINE_DROPPED_DATA_PROVIDERS).first().parent().type('{enter}');
cy.get(TIMELINE_DATA_PROVIDERS_ACTION_MENU).should('exist');
});
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', () => { 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', () => {
dragFirstHostToTimeline(); dragFirstHostToTimeline();

View file

@ -4,12 +4,22 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { TIMELINE_FLYOUT_HEADER, TIMELINE_DATA_PROVIDERS } from '../screens/timeline'; import { TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON } from '../screens/security_main';
import {
CREATE_NEW_TIMELINE,
TIMELINE_DATA_PROVIDERS,
TIMELINE_FLYOUT_HEADER,
TIMELINE_SETTINGS_ICON,
} from '../screens/timeline';
import { cleanKibana } from '../tasks/common'; import { cleanKibana } from '../tasks/common';
import { dragFirstHostToTimeline, waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts'; import { dragFirstHostToTimeline, waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts';
import { loginAndWaitForPage } from '../tasks/login'; import { loginAndWaitForPage } from '../tasks/login';
import { openTimelineUsingToggle, closeTimelineUsingToggle } from '../tasks/security_main'; import {
closeTimelineUsingCloseButton,
closeTimelineUsingToggle,
openTimelineUsingToggle,
} from '../tasks/security_main';
import { HOSTS_URL } from '../urls/navigation'; import { HOSTS_URL } from '../urls/navigation';
@ -26,6 +36,34 @@ describe('timeline flyout button', () => {
closeTimelineUsingToggle(); closeTimelineUsingToggle();
}); });
it('re-focuses the toggle button when timeline is closed by clicking the active timeline toggle button', () => {
openTimelineUsingToggle();
closeTimelineUsingToggle();
cy.get(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).should('have.focus');
});
it('re-focuses the toggle button when timeline is closed by clicking the [X] close button', () => {
openTimelineUsingToggle();
closeTimelineUsingCloseButton();
cy.get(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).should('have.focus');
});
it('re-focuses the toggle button when timeline is closed by pressing the Esc key', () => {
openTimelineUsingToggle();
cy.get('body').type('{esc}');
cy.get(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).should('have.focus');
});
it('the `(+)` button popover menu owns focus', () => {
cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').click({ force: true });
cy.get(CREATE_NEW_TIMELINE).should('have.focus');
cy.get('body').type('{esc}');
cy.get(CREATE_NEW_TIMELINE).should('not.be.visible');
});
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', () => { 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', () => {
dragFirstHostToTimeline(); dragFirstHostToTimeline();

View file

@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
export const CLOSE_TIMELINE_BUTTON = '[data-test-subj="close-timeline"]';
export const MAIN_PAGE = '[data-test-subj="kibanaChrome"]'; export const MAIN_PAGE = '[data-test-subj="kibanaChrome"]';
export const TIMELINE_TOGGLE_BUTTON = '[data-test-subj="flyoutOverlay"]'; export const TIMELINE_TOGGLE_BUTTON = '[data-test-subj="flyoutOverlay"]';

View file

@ -111,6 +111,8 @@ export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinne
export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]';
export const TIMELINE_DATA_PROVIDERS_ACTION_MENU = '[data-test-subj="providerActions"]';
export const TIMELINE_DATA_PROVIDERS_EMPTY = export const TIMELINE_DATA_PROVIDERS_EMPTY =
'[data-test-subj="dataProviders"] [data-test-subj="empty"]'; '[data-test-subj="dataProviders"] [data-test-subj="empty"]';

View file

@ -5,6 +5,7 @@
*/ */
import { import {
CLOSE_TIMELINE_BUTTON,
MAIN_PAGE, MAIN_PAGE,
TIMELINE_TOGGLE_BUTTON, TIMELINE_TOGGLE_BUTTON,
TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON, TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON,
@ -18,6 +19,10 @@ export const closeTimelineUsingToggle = () => {
cy.get(TIMELINE_TOGGLE_BUTTON).filter(':visible').click(); cy.get(TIMELINE_TOGGLE_BUTTON).filter(':visible').click();
}; };
export const closeTimelineUsingCloseButton = () => {
cy.get(CLOSE_TIMELINE_BUTTON).filter(':visible').click();
};
export const openTimelineIfClosed = () => export const openTimelineIfClosed = () =>
cy.get(MAIN_PAGE).then(($page) => { cy.get(MAIN_PAGE).then(($page) => {
if ($page.find(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).length === 1) { if ($page.find(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).length === 1) {

View file

@ -220,7 +220,7 @@ const AlertsUtilityBarComponent: React.FC<AlertsUtilityBarProps> = ({
disabled={areEventsLoading} disabled={areEventsLoading}
iconType="arrowDown" iconType="arrowDown"
iconSide="right" iconSide="right"
ownFocus={false} ownFocus={true}
popoverContent={UtilityBarAdditionalFiltersContent} popoverContent={UtilityBarAdditionalFiltersContent}
> >
{i18n.ADDITIONAL_FILTERS_ACTIONS} {i18n.ADDITIONAL_FILTERS_ACTIONS}

View file

@ -6,7 +6,7 @@
import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import { EuiSpacer, EuiWindowEvent } from '@elastic/eui';
import { noop } from 'lodash/fp'; import { noop } from 'lodash/fp';
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
@ -14,6 +14,7 @@ import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/h
import { SecurityPageName } from '../../../app/types'; import { SecurityPageName } from '../../../app/types';
import { TimelineId } from '../../../../common/types/timeline'; import { TimelineId } from '../../../../common/types/timeline';
import { useGlobalTime } from '../../../common/containers/use_global_time'; import { useGlobalTime } from '../../../common/containers/use_global_time';
import { isTab } from '../../../common/components/accessibility/helpers';
import { UpdateDateRange } from '../../../common/components/charts/common'; import { UpdateDateRange } from '../../../common/components/charts/common';
import { FiltersGlobal } from '../../../common/components/filters_global'; import { FiltersGlobal } from '../../../common/components/filters_global';
import { getRulesUrl } from '../../../common/components/link_to/redirect_to_detection_engine'; import { getRulesUrl } from '../../../common/components/link_to/redirect_to_detection_engine';
@ -39,7 +40,12 @@ import { LinkButton } from '../../../common/components/links';
import { useFormatUrl } from '../../../common/components/link_to'; import { useFormatUrl } from '../../../common/components/link_to';
import { useGlobalFullScreen } from '../../../common/containers/use_full_screen'; import { useGlobalFullScreen } from '../../../common/containers/use_full_screen';
import { Display } from '../../../hosts/pages/display'; import { Display } from '../../../hosts/pages/display';
import { showGlobalFilters } from '../../../timelines/components/timeline/helpers'; import {
focusUtilityBarAction,
onTimelineTabKeyPressed,
resetKeyboardFocus,
showGlobalFilters,
} from '../../../timelines/components/timeline/helpers';
import { timelineSelectors } from '../../../timelines/store/timeline'; import { timelineSelectors } from '../../../timelines/store/timeline';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import { buildShowBuildingBlockFilter } from '../../components/alerts_table/default_config'; import { buildShowBuildingBlockFilter } from '../../components/alerts_table/default_config';
@ -48,6 +54,7 @@ import { SourcererScopeName } from '../../../common/store/sourcerer/model';
const DetectionEnginePageComponent = () => { const DetectionEnginePageComponent = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const containerElement = useRef<HTMLDivElement | null>(null);
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const graphEventId = useShallowEqualSelector( const graphEventId = useShallowEqualSelector(
(state) => (getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults).graphEventId (state) => (getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults).graphEventId
@ -128,6 +135,28 @@ const DetectionEnginePageComponent = () => {
const { indicesExist, indexPattern } = useSourcererScope(SourcererScopeName.detections); const { indicesExist, indexPattern } = useSourcererScope(SourcererScopeName.detections);
const onSkipFocusBeforeEventsTable = useCallback(() => {
focusUtilityBarAction(containerElement.current);
}, [containerElement]);
const onSkipFocusAfterEventsTable = useCallback(() => {
resetKeyboardFocus();
}, []);
const onKeyDown = useCallback(
(keyboardEvent: React.KeyboardEvent) => {
if (isTab(keyboardEvent)) {
onTimelineTabKeyPressed({
containerElement: containerElement.current,
keyboardEvent,
onSkipFocusBeforeEventsTable,
onSkipFocusAfterEventsTable,
});
}
},
[containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable]
);
if (isUserAuthenticated != null && !isUserAuthenticated && !loading) { if (isUserAuthenticated != null && !isUserAuthenticated && !loading) {
return ( return (
<WrapperPage> <WrapperPage>
@ -154,7 +183,7 @@ const DetectionEnginePageComponent = () => {
{hasEncryptionKey != null && !hasEncryptionKey && <NoApiIntegrationKeyCallOut />} {hasEncryptionKey != null && !hasEncryptionKey && <NoApiIntegrationKeyCallOut />}
<ReadOnlyAlertsCallOut /> <ReadOnlyAlertsCallOut />
{indicesExist ? ( {indicesExist ? (
<> <div onKeyDown={onKeyDown} ref={containerElement}>
<EuiWindowEvent event="resize" handler={noop} /> <EuiWindowEvent event="resize" handler={noop} />
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}> <FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
<SiemSearchBar id="global" indexPattern={indexPattern} /> <SiemSearchBar id="global" indexPattern={indexPattern} />
@ -211,7 +240,7 @@ const DetectionEnginePageComponent = () => {
to={to} to={to}
/> />
</WrapperPage> </WrapperPage>
</> </div>
) : ( ) : (
<WrapperPage> <WrapperPage>
<DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} /> <DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} />

View file

@ -19,7 +19,7 @@ import {
} from '@elastic/eui'; } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
import { noop } from 'lodash/fp'; import { noop } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useParams, useHistory } from 'react-router-dom'; import { useParams, useHistory } from 'react-router-dom';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
@ -81,7 +81,12 @@ import { useGlobalFullScreen } from '../../../../../common/containers/use_full_s
import { Display } from '../../../../../hosts/pages/display'; import { Display } from '../../../../../hosts/pages/display';
import { ExceptionListTypeEnum, ExceptionListIdentifiers } from '../../../../../shared_imports'; import { ExceptionListTypeEnum, ExceptionListIdentifiers } from '../../../../../shared_imports';
import { useRuleAsync } from '../../../../containers/detection_engine/rules/use_rule_async'; import { useRuleAsync } from '../../../../containers/detection_engine/rules/use_rule_async';
import { showGlobalFilters } from '../../../../../timelines/components/timeline/helpers'; import {
focusUtilityBarAction,
onTimelineTabKeyPressed,
resetKeyboardFocus,
showGlobalFilters,
} from '../../../../../timelines/components/timeline/helpers';
import { timelineSelectors } from '../../../../../timelines/store/timeline'; import { timelineSelectors } from '../../../../../timelines/store/timeline';
import { timelineDefaults } from '../../../../../timelines/store/timeline/defaults'; import { timelineDefaults } from '../../../../../timelines/store/timeline/defaults';
import { useSourcererScope } from '../../../../../common/containers/sourcerer'; import { useSourcererScope } from '../../../../../common/containers/sourcerer';
@ -95,6 +100,7 @@ import {
import * as detectionI18n from '../../translations'; import * as detectionI18n from '../../translations';
import * as ruleI18n from '../translations'; import * as ruleI18n from '../translations';
import * as i18n from './translations'; import * as i18n from './translations';
import { isTab } from '../../../../../common/components/accessibility/helpers';
enum RuleDetailTabs { enum RuleDetailTabs {
alerts = 'alerts', alerts = 'alerts',
@ -127,6 +133,7 @@ const getRuleDetailsTabs = (rule: Rule | null) => {
const RuleDetailsPageComponent = () => { const RuleDetailsPageComponent = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const containerElement = useRef<HTMLDivElement | null>(null);
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const graphEventId = useShallowEqualSelector( const graphEventId = useShallowEqualSelector(
(state) => (state) =>
@ -408,6 +415,28 @@ const RuleDetailsPageComponent = () => {
} }
}, [rule]); }, [rule]);
const onSkipFocusBeforeEventsTable = useCallback(() => {
focusUtilityBarAction(containerElement.current);
}, [containerElement]);
const onSkipFocusAfterEventsTable = useCallback(() => {
resetKeyboardFocus();
}, []);
const onKeyDown = useCallback(
(keyboardEvent: React.KeyboardEvent) => {
if (isTab(keyboardEvent)) {
onTimelineTabKeyPressed({
containerElement: containerElement.current,
keyboardEvent,
onSkipFocusBeforeEventsTable,
onSkipFocusAfterEventsTable,
});
}
},
[containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable]
);
if ( if (
redirectToDetections( redirectToDetections(
isSignalIndexExists, isSignalIndexExists,
@ -430,7 +459,7 @@ const RuleDetailsPageComponent = () => {
<ReadOnlyAlertsCallOut /> <ReadOnlyAlertsCallOut />
<ReadOnlyRulesCallOut /> <ReadOnlyRulesCallOut />
{indicesExist ? ( {indicesExist ? (
<> <div onKeyDown={onKeyDown} ref={containerElement}>
<EuiWindowEvent event="resize" handler={noop} /> <EuiWindowEvent event="resize" handler={noop} />
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}> <FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
<SiemSearchBar id="global" indexPattern={indexPattern} /> <SiemSearchBar id="global" indexPattern={indexPattern} />
@ -588,7 +617,7 @@ const RuleDetailsPageComponent = () => {
)} )}
{ruleDetailTab === RuleDetailTabs.failures && <FailureHistory id={rule?.id} />} {ruleDetailTab === RuleDetailTabs.failures && <FailureHistory id={rule?.id} />}
</WrapperPage> </WrapperPage>
</> </div>
) : ( ) : (
<WrapperPage> <WrapperPage>
<DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} /> <DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} />

View file

@ -6,7 +6,7 @@
import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import { EuiSpacer, EuiWindowEvent } from '@elastic/eui';
import { noop } from 'lodash/fp'; import { noop } from 'lodash/fp';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
@ -40,7 +40,12 @@ import * as i18n from './translations';
import { filterHostData } from './navigation'; import { filterHostData } from './navigation';
import { hostsModel } from '../store'; import { hostsModel } from '../store';
import { HostsTableType } from '../store/model'; import { HostsTableType } from '../store/model';
import { showGlobalFilters } from '../../timelines/components/timeline/helpers'; import { isTab } from '../../common/components/accessibility/helpers';
import {
onTimelineTabKeyPressed,
resetKeyboardFocus,
showGlobalFilters,
} from '../../timelines/components/timeline/helpers';
import { timelineSelectors } from '../../timelines/store/timeline'; import { timelineSelectors } from '../../timelines/store/timeline';
import { timelineDefaults } from '../../timelines/store/timeline/defaults'; import { timelineDefaults } from '../../timelines/store/timeline/defaults';
import { useSourcererScope } from '../../common/containers/sourcerer'; import { useSourcererScope } from '../../common/containers/sourcerer';
@ -48,6 +53,7 @@ import { useDeepEqualSelector, useShallowEqualSelector } from '../../common/hook
const HostsComponent = () => { const HostsComponent = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const containerElement = useRef<HTMLDivElement | null>(null);
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const graphEventId = useShallowEqualSelector( const graphEventId = useShallowEqualSelector(
(state) => (state) =>
@ -114,10 +120,34 @@ const HostsComponent = () => {
[indexPattern, query, tabsFilters, uiSettings] [indexPattern, query, tabsFilters, uiSettings]
); );
const onSkipFocusBeforeEventsTable = useCallback(() => {
containerElement.current
?.querySelector<HTMLButtonElement>('.inspectButtonComponent:last-of-type')
?.focus();
}, [containerElement]);
const onSkipFocusAfterEventsTable = useCallback(() => {
resetKeyboardFocus();
}, []);
const onKeyDown = useCallback(
(keyboardEvent: React.KeyboardEvent) => {
if (isTab(keyboardEvent)) {
onTimelineTabKeyPressed({
containerElement: containerElement.current,
keyboardEvent,
onSkipFocusBeforeEventsTable,
onSkipFocusAfterEventsTable,
});
}
},
[containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable]
);
return ( return (
<> <>
{indicesExist ? ( {indicesExist ? (
<> <div onKeyDown={onKeyDown} ref={containerElement}>
<EuiWindowEvent event="resize" handler={noop} /> <EuiWindowEvent event="resize" handler={noop} />
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}> <FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
<SiemSearchBar indexPattern={indexPattern} id="global" /> <SiemSearchBar indexPattern={indexPattern} id="global" />
@ -167,7 +197,7 @@ const HostsComponent = () => {
type={hostsModel.HostsType.page} type={hostsModel.HostsType.page}
/> />
</WrapperPage> </WrapperPage>
</> </div>
) : ( ) : (
<WrapperPage> <WrapperPage>
<HeaderPage border title={i18n.PAGE_TITLE} /> <HeaderPage border title={i18n.PAGE_TITLE} />

View file

@ -6,7 +6,7 @@
import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import { EuiSpacer, EuiWindowEvent } from '@elastic/eui';
import { noop } from 'lodash/fp'; import { noop } from 'lodash/fp';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
@ -38,8 +38,13 @@ import { OverviewEmpty } from '../../overview/components/overview_empty';
import * as i18n from './translations'; import * as i18n from './translations';
import { NetworkComponentProps } from './types'; import { NetworkComponentProps } from './types';
import { NetworkRouteType } from './navigation/types'; import { NetworkRouteType } from './navigation/types';
import { showGlobalFilters } from '../../timelines/components/timeline/helpers'; import {
onTimelineTabKeyPressed,
resetKeyboardFocus,
showGlobalFilters,
} from '../../timelines/components/timeline/helpers';
import { timelineSelectors } from '../../timelines/store/timeline'; import { timelineSelectors } from '../../timelines/store/timeline';
import { isTab } from '../../common/components/accessibility/helpers';
import { TimelineId } from '../../../common/types/timeline'; import { TimelineId } from '../../../common/types/timeline';
import { timelineDefaults } from '../../timelines/store/timeline/defaults'; import { timelineDefaults } from '../../timelines/store/timeline/defaults';
import { useSourcererScope } from '../../common/containers/sourcerer'; import { useSourcererScope } from '../../common/containers/sourcerer';
@ -48,6 +53,7 @@ import { useDeepEqualSelector, useShallowEqualSelector } from '../../common/hook
const NetworkComponent = React.memo<NetworkComponentProps>( const NetworkComponent = React.memo<NetworkComponentProps>(
({ networkPagePath, hasMlUserPermissions, capabilitiesFetched }) => { ({ networkPagePath, hasMlUserPermissions, capabilitiesFetched }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const containerElement = useRef<HTMLDivElement | null>(null);
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const graphEventId = useShallowEqualSelector( const graphEventId = useShallowEqualSelector(
(state) => (state) =>
@ -91,6 +97,31 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
); );
const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope();
const onSkipFocusBeforeEventsTable = useCallback(() => {
containerElement.current
?.querySelector<HTMLButtonElement>('.inspectButtonComponent:last-of-type')
?.focus();
}, [containerElement]);
const onSkipFocusAfterEventsTable = useCallback(() => {
resetKeyboardFocus();
}, []);
const onKeyDown = useCallback(
(keyboardEvent: React.KeyboardEvent) => {
if (isTab(keyboardEvent)) {
onTimelineTabKeyPressed({
containerElement: containerElement.current,
keyboardEvent,
onSkipFocusBeforeEventsTable,
onSkipFocusAfterEventsTable,
});
}
},
[containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable]
);
const filterQuery = convertToBuildEsQuery({ const filterQuery = convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings), config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
indexPattern, indexPattern,
@ -107,7 +138,7 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
return ( return (
<> <>
{indicesExist ? ( {indicesExist ? (
<> <div onKeyDown={onKeyDown} ref={containerElement}>
<EuiWindowEvent event="resize" handler={noop} /> <EuiWindowEvent event="resize" handler={noop} />
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}> <FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
<SiemSearchBar indexPattern={indexPattern} id="global" /> <SiemSearchBar indexPattern={indexPattern} id="global" />
@ -176,7 +207,7 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
<NetworkRoutesLoading /> <NetworkRoutesLoading />
)} )}
</WrapperPage> </WrapperPage>
</> </div>
) : ( ) : (
<WrapperPage> <WrapperPage>
<HeaderPage border title={i18n.PAGE_TITLE} /> <HeaderPage border title={i18n.PAGE_TITLE} />

View file

@ -239,7 +239,7 @@ const FieldsBrowserComponent: React.FC<Props> = ({
data-test-subj="header" data-test-subj="header"
filteredBrowserFields={filteredBrowserFields} filteredBrowserFields={filteredBrowserFields}
isSearching={isSearching} isSearching={isSearching}
onOutsideClick={onOutsideClick} onOutsideClick={closeAndRestoreFocus}
onSearchInputChange={onInputChange} onSearchInputChange={onInputChange}
onUpdateColumns={onUpdateColumns} onUpdateColumns={onUpdateColumns}
searchInput={searchInput} searchInput={searchInput}

View file

@ -55,6 +55,7 @@ const AddTimelineButtonComponent: React.FC<AddTimelineButtonComponentProps> = ({
id="timelineSettingsPopover" id="timelineSettingsPopover"
isOpen={showActions} isOpen={showActions}
closePopover={onClosePopover} closePopover={onClosePopover}
ownFocus
repositionOnScroll repositionOnScroll
> >
<EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="none"> <EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="none">

View file

@ -13,11 +13,10 @@ import { IS_DRAGGING_CLASS_NAME } from '../../../../common/components/drag_and_d
import { DataProvider } from '../../timeline/data_providers/data_provider'; import { DataProvider } from '../../timeline/data_providers/data_provider';
import { flattenIntoAndGroups } from '../../timeline/data_providers/helpers'; import { flattenIntoAndGroups } from '../../timeline/data_providers/helpers';
import { DataProviders } from '../../timeline/data_providers'; import { DataProviders } from '../../timeline/data_providers';
import { FLYOUT_BUTTON_BAR_CLASS_NAME, FLYOUT_BUTTON_CLASS_NAME } from '../../timeline/helpers';
import { FlyoutHeaderPanel } from '../header'; import { FlyoutHeaderPanel } from '../header';
import { TimelineTabs } from '../../../../../common/types/timeline'; import { TimelineTabs } from '../../../../../common/types/timeline';
export const FLYOUT_BUTTON_CLASS_NAME = 'timeline-flyout-button';
export const getBadgeCount = (dataProviders: DataProvider[]): number => export const getBadgeCount = (dataProviders: DataProvider[]): number =>
flattenIntoAndGroups(dataProviders).reduce((total, group) => total + group.length, 0); flattenIntoAndGroups(dataProviders).reduce((total, group) => total + group.length, 0);
@ -76,7 +75,11 @@ interface FlyoutBottomBarProps {
export const FlyoutBottomBar = React.memo<FlyoutBottomBarProps>( export const FlyoutBottomBar = React.memo<FlyoutBottomBarProps>(
({ activeTab, showDataproviders, timelineId }) => { ({ activeTab, showDataproviders, timelineId }) => {
return ( return (
<Container $isGlobal={showDataproviders} data-test-subj="flyoutBottomBar"> <Container
className={FLYOUT_BUTTON_BAR_CLASS_NAME}
$isGlobal={showDataproviders}
data-test-subj="flyoutBottomBar"
>
{showDataproviders && <FlyoutHeaderPanel timelineId={timelineId} />} {showDataproviders && <FlyoutHeaderPanel timelineId={timelineId} />}
{(showDataproviders || (!showDataproviders && activeTab !== TimelineTabs.query)) && ( {(showDataproviders || (!showDataproviders && activeTab !== TimelineTabs.query)) && (
<DataProvidersPanel paddingSize="none"> <DataProvidersPanel paddingSize="none">

View file

@ -13,6 +13,10 @@ import { FormattedRelative } from '@kbn/i18n/react';
import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline';
import { TimelineEventsCountBadge } from '../../../../common/hooks/use_timeline_events_count'; import { TimelineEventsCountBadge } from '../../../../common/hooks/use_timeline_events_count';
import {
ACTIVE_TIMELINE_BUTTON_CLASS_NAME,
focusActiveTimelineButton,
} from '../../timeline/helpers';
import { UNTITLED_TIMELINE, UNTITLED_TEMPLATE } from '../../timeline/properties/translations'; import { UNTITLED_TIMELINE, UNTITLED_TEMPLATE } from '../../timeline/properties/translations';
import { timelineActions } from '../../../store/timeline'; import { timelineActions } from '../../../store/timeline';
import * as i18n from './translations'; import * as i18n from './translations';
@ -50,11 +54,10 @@ const ActiveTimelinesComponent: React.FC<ActiveTimelinesProps> = ({
isOpen, isOpen,
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const handleToggleOpen = useCallback(() => {
const handleToggleOpen = useCallback( dispatch(timelineActions.showTimeline({ id: timelineId, show: !isOpen }));
() => dispatch(timelineActions.showTimeline({ id: timelineId, show: !isOpen })), focusActiveTimelineButton();
[dispatch, isOpen, timelineId] }, [dispatch, isOpen, timelineId]);
);
const title = !isEmpty(timelineTitle) const title = !isEmpty(timelineTitle)
? timelineTitle ? timelineTitle
@ -83,6 +86,7 @@ const ActiveTimelinesComponent: React.FC<ActiveTimelinesProps> = ({
<ButtonWrapper grow={false}> <ButtonWrapper grow={false}>
<StyledEuiButtonEmpty <StyledEuiButtonEmpty
aria-label={i18n.TIMELINE_TOGGLE_BUTTON_ARIA_LABEL({ isOpen, title })} aria-label={i18n.TIMELINE_TOGGLE_BUTTON_ARIA_LABEL({ isOpen, title })}
className={ACTIVE_TIMELINE_BUTTON_CLASS_NAME}
flush="both" flush="both"
data-test-subj="flyoutOverlay" data-test-subj="flyoutOverlay"
size="s" size="s"

View file

@ -33,6 +33,7 @@ import { ActiveTimelines } from './active_timelines';
import * as i18n from './translations'; import * as i18n from './translations';
import * as commonI18n from '../../timeline/properties/translations'; import * as commonI18n from '../../timeline/properties/translations';
import { getTimelineStatusByIdSelector } from './selectors'; import { getTimelineStatusByIdSelector } from './selectors';
import { focusActiveTimelineButton } from '../../timeline/helpers';
// to hide side borders // to hide side borders
const StyledPanel = styled(EuiPanel)` const StyledPanel = styled(EuiPanel)`
@ -79,10 +80,10 @@ const FlyoutHeaderPanelComponent: React.FC<FlyoutHeaderPanelProps> = ({ timeline
[dataProviders, kqlQuery] [dataProviders, kqlQuery]
); );
const handleClose = useCallback( const handleClose = useCallback(() => {
() => dispatch(timelineActions.showTimeline({ id: timelineId, show: false })), dispatch(timelineActions.showTimeline({ id: timelineId, show: false }));
[dispatch, timelineId] focusActiveTimelineButton();
); }, [dispatch, timelineId]);
return ( return (
<StyledPanel <StyledPanel

View file

@ -12,6 +12,7 @@ import { useDispatch } from 'react-redux';
import { StatefulTimeline } from '../../timeline'; import { StatefulTimeline } from '../../timeline';
import * as i18n from './translations'; import * as i18n from './translations';
import { timelineActions } from '../../../store/timeline'; import { timelineActions } from '../../../store/timeline';
import { focusActiveTimelineButton } from '../../timeline/helpers';
interface FlyoutPaneComponentProps { interface FlyoutPaneComponentProps {
timelineId: string; timelineId: string;
@ -28,10 +29,10 @@ const EuiFlyoutContainer = styled.div`
const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({ timelineId }) => { const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({ timelineId }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const handleClose = useCallback( const handleClose = useCallback(() => {
() => dispatch(timelineActions.showTimeline({ id: timelineId, show: false })), dispatch(timelineActions.showTimeline({ id: timelineId, show: false }));
[dispatch, timelineId] focusActiveTimelineButton();
); }, [dispatch, timelineId]);
return ( return (
<EuiFlyoutContainer data-test-subj="flyout-pane"> <EuiFlyoutContainer data-test-subj="flyout-pane">

View file

@ -17,18 +17,17 @@ import {
EuiButton, EuiButton,
EuiFlexGroup, EuiFlexGroup,
EuiFlexItem, EuiFlexItem,
EuiInMemoryTable,
} from '@elastic/eui'; } from '@elastic/eui';
import React, { useState, useCallback, useMemo, useRef } from 'react'; import React, { useState, useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import { State } from '../../../common/store'; import { State } from '../../../common/store';
import { RowRendererId } from '../../../../common/types/timeline';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../store/timeline/actions'; import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../store/timeline/actions';
import { timelineSelectors } from '../../store/timeline'; import { timelineSelectors } from '../../store/timeline';
import { timelineDefaults } from '../../store/timeline/defaults'; import { timelineDefaults } from '../../store/timeline/defaults';
import { renderers } from './catalog';
import { RowRenderersBrowser } from './row_renderers_browser'; import { RowRenderersBrowser } from './row_renderers_browser';
import * as i18n from './translations'; import * as i18n from './translations';
@ -81,7 +80,6 @@ interface StatefulRowRenderersBrowserProps {
const StatefulRowRenderersBrowserComponent: React.FC<StatefulRowRenderersBrowserProps> = ({ const StatefulRowRenderersBrowserComponent: React.FC<StatefulRowRenderersBrowserProps> = ({
timelineId, timelineId,
}) => { }) => {
const tableRef = useRef<EuiInMemoryTable<{}>>();
const dispatch = useDispatch(); const dispatch = useDispatch();
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const excludedRowRendererIds = useDeepEqualSelector( const excludedRowRendererIds = useDeepEqualSelector(
@ -105,12 +103,12 @@ const StatefulRowRenderersBrowserComponent: React.FC<StatefulRowRenderersBrowser
const hideFieldBrowser = useCallback(() => setShow(false), []); const hideFieldBrowser = useCallback(() => setShow(false), []);
const handleDisableAll = useCallback(() => { const handleDisableAll = useCallback(() => {
tableRef?.current?.setSelection([]); setExcludedRowRendererIds(Object.values(RowRendererId));
}, [tableRef]); }, [setExcludedRowRendererIds]);
const handleEnableAll = useCallback(() => { const handleEnableAll = useCallback(() => {
tableRef?.current?.setSelection(renderers); setExcludedRowRendererIds([]);
}, [tableRef]); }, [setExcludedRowRendererIds]);
return ( return (
<> <>
@ -168,7 +166,6 @@ const StatefulRowRenderersBrowserComponent: React.FC<StatefulRowRenderersBrowser
<StyledEuiModalBody> <StyledEuiModalBody>
<RowRenderersBrowser <RowRenderersBrowser
ref={tableRef}
excludedRowRendererIds={excludedRowRendererIds} excludedRowRendererIds={excludedRowRendererIds}
setExcludedRowRendererIds={setExcludedRowRendererIds} setExcludedRowRendererIds={setExcludedRowRendererIds}
/> />

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { EuiFlexItem, EuiInMemoryTable } from '@elastic/eui'; import { EuiCheckbox, EuiFlexItem, EuiInMemoryTable } from '@elastic/eui';
import React, { useMemo, useCallback } from 'react'; import React, { useMemo, useCallback } from 'react';
import { xor, xorBy } from 'lodash/fp'; import { xor } from 'lodash/fp';
import styled from 'styled-components'; import styled from 'styled-components';
import { RowRendererId } from '../../../../common/types/timeline'; import { RowRendererId } from '../../../../common/types/timeline';
@ -76,101 +76,89 @@ const StyledNameButton = styled.button`
text-align: left; text-align: left;
`; `;
const RowRenderersBrowserComponent = React.forwardRef( const RowRenderersBrowserComponent = ({
({ excludedRowRendererIds = [], setExcludedRowRendererIds }: RowRenderersBrowserProps, ref) => { excludedRowRendererIds = [],
const notExcludedRowRenderers = useMemo(() => { setExcludedRowRendererIds,
if (excludedRowRendererIds.length === Object.keys(RowRendererId).length) return []; }: RowRenderersBrowserProps) => {
const handleNameClick = useCallback(
(item: RowRendererOption) => () => {
const newSelection = xor([item.id], excludedRowRendererIds);
return renderers.filter((renderer) => !excludedRowRendererIds.includes(renderer.id)); setExcludedRowRendererIds(newSelection);
}, [excludedRowRendererIds]); },
[excludedRowRendererIds, setExcludedRowRendererIds]
);
const handleNameClick = useCallback( const nameColumnRenderCallback = useCallback(
(item: RowRendererOption) => () => { (value, item) => (
const newSelection = xor([item], notExcludedRowRenderers); <StyledNameButton className="kbn-resetFocusState" onClick={handleNameClick(item)}>
// @ts-expect-error {value}
ref?.current?.setSelection(newSelection); </StyledNameButton>
}, ),
[notExcludedRowRenderers, ref] [handleNameClick]
); );
const nameColumnRenderCallback = useCallback( const idColumnRenderCallback = useCallback(
(value, item) => ( (_, item) => (
<StyledNameButton className="kbn-resetFocusState" onClick={handleNameClick(item)}> <EuiCheckbox
{value} id={item.id}
</StyledNameButton> onChange={handleNameClick(item)}
), checked={!excludedRowRendererIds.includes(item.id)}
[handleNameClick]
);
const columns = useMemo(
() => [
{
field: 'name',
name: 'Name',
sortable: true,
width: '10%',
render: nameColumnRenderCallback,
},
{
field: 'description',
name: 'Description',
width: '25%',
render: (description: React.ReactNode) => description,
},
{
field: 'example',
name: 'Example',
width: '65%',
render: ExampleWrapperComponent,
},
{
field: 'searchableDescription',
name: 'Searchable Description',
sortable: false,
width: '0px',
render: renderSearchableDescriptionNoop,
},
],
[nameColumnRenderCallback]
);
const handleSelectable = useCallback(() => true, []);
const handleSelectionChange = useCallback(
(selection: RowRendererOption[]) => {
if (!selection || !selection.length)
return setExcludedRowRendererIds(Object.values(RowRendererId));
const excludedRowRenderers = xorBy('id', renderers, selection);
setExcludedRowRendererIds(excludedRowRenderers.map((rowRenderer) => rowRenderer.id));
},
[setExcludedRowRendererIds]
);
const selectionValue = useMemo(
() => ({
selectable: handleSelectable,
onSelectionChange: handleSelectionChange,
initialSelected: notExcludedRowRenderers,
}),
[handleSelectable, handleSelectionChange, notExcludedRowRenderers]
);
return (
<StyledEuiInMemoryTable
ref={ref}
items={renderers}
itemId="id"
columns={columns}
search={search}
sorting={initialSorting}
isSelectable={true}
selection={selectionValue}
/> />
); ),
} [excludedRowRendererIds, handleNameClick]
); );
const columns = useMemo(
() => [
{
field: 'id',
name: '',
sortable: false,
width: '32px',
render: idColumnRenderCallback,
},
{
field: 'name',
name: 'Name',
sortable: true,
width: '10%',
render: nameColumnRenderCallback,
},
{
field: 'description',
name: 'Description',
width: '25%',
render: (description: React.ReactNode) => description,
},
{
field: 'example',
name: 'Example',
width: '65%',
render: ExampleWrapperComponent,
},
{
field: 'searchableDescription',
name: 'Searchable Description',
sortable: false,
width: '0px',
render: renderSearchableDescriptionNoop,
},
],
[idColumnRenderCallback, nameColumnRenderCallback]
);
return (
<StyledEuiInMemoryTable
items={renderers}
itemId="id"
columns={columns}
search={search}
sorting={initialSorting}
isSelectable={true}
/>
);
};
RowRenderersBrowserComponent.displayName = 'RowRenderersBrowserComponent'; RowRenderersBrowserComponent.displayName = 'RowRenderersBrowserComponent';

View file

@ -474,6 +474,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
}, },
] ]
} }
tabType="query"
timelineId="test" timelineId="test"
/> />
`; `;

View file

@ -4,19 +4,23 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover } from '@elastic/eui';
import React, { useCallback, useMemo, useRef, useState } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react';
import { Draggable } from 'react-beautiful-dnd'; import { Draggable } from 'react-beautiful-dnd';
import { Resizable, ResizeCallback } from 're-resizable'; import { Resizable, ResizeCallback } from 're-resizable';
import deepEqual from 'fast-deep-equal'; import deepEqual from 'fast-deep-equal';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { useDraggableKeyboardWrapper } from '../../../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook'; import { useDraggableKeyboardWrapper } from '../../../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook';
import { import {
DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME, DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME,
getDraggableFieldId, getDraggableFieldId,
} from '../../../../../common/components/drag_and_drop/helpers'; } from '../../../../../common/components/drag_and_drop/helpers';
import { TimelineTabs } from '../../../../../../common/types/timeline';
import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
import { OnFilterChange } from '../../events'; import { OnFilterChange } from '../../events';
import { Direction } from '../../../../../graphql/types';
import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers'; import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers';
import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles'; import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles';
import { Sort } from '../sort'; import { Sort } from '../sort';
@ -24,6 +28,25 @@ import { Sort } from '../sort';
import { Header } from './header'; import { Header } from './header';
import { timelineActions } from '../../../../store/timeline'; import { timelineActions } from '../../../../store/timeline';
import * as i18n from './translations';
const ContextMenu = styled(EuiContextMenu)`
width: 115px;
& .euiContextMenuItem {
font-size: 12px;
padding: 4px 8px;
width: 115px;
}
`;
const PopoverContainer = styled.div<{ $width: number }>`
& .euiPopover__anchor {
padding-right: 8px;
width: ${({ $width }) => $width}px;
}
`;
const RESIZABLE_ENABLE = { right: true }; const RESIZABLE_ENABLE = { right: true };
interface ColumneHeaderProps { interface ColumneHeaderProps {
@ -32,6 +55,7 @@ interface ColumneHeaderProps {
isDragging: boolean; isDragging: boolean;
onFilterChange?: OnFilterChange; onFilterChange?: OnFilterChange;
sort: Sort[]; sort: Sort[];
tabType: TimelineTabs;
timelineId: string; timelineId: string;
} }
@ -42,10 +66,11 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
isDragging, isDragging,
onFilterChange, onFilterChange,
sort, sort,
tabType,
}) => { }) => {
const keyboardHandlerRef = useRef<HTMLDivElement | null>(null); const keyboardHandlerRef = useRef<HTMLDivElement | null>(null);
const [, setClosePopOverTrigger] = useState(false); const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState<boolean>(false);
const [, setHoverActionsOwnFocus] = useState<boolean>(false); const restoreFocus = useCallback(() => keyboardHandlerRef.current?.focus(), []);
const dispatch = useDispatch(); const dispatch = useDispatch();
const resizableSize = useMemo( const resizableSize = useMemo(
@ -84,10 +109,93 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
const draggableId = useMemo( const draggableId = useMemo(
() => () =>
getDraggableFieldId({ getDraggableFieldId({
contextId: `timeline-column-headers-${timelineId}`, contextId: `timeline-column-headers-${tabType}-${timelineId}`,
fieldId: header.id, fieldId: header.id,
}), }),
[timelineId, header.id] [tabType, timelineId, header.id]
);
const onColumnSort = useCallback(
(sortDirection: Direction) => {
const columnId = header.id;
const headerIndex = sort.findIndex((col) => col.columnId === columnId);
const newSort =
headerIndex === -1
? [
...sort,
{
columnId,
columnType: `${header.type}`,
sortDirection,
},
]
: [
...sort.slice(0, headerIndex),
{
columnId,
columnType: `${header.type}`,
sortDirection,
},
...sort.slice(headerIndex + 1),
];
dispatch(
timelineActions.updateSort({
id: timelineId,
sort: newSort,
})
);
},
[dispatch, header, sort, timelineId]
);
const handleClosePopOverTrigger = useCallback(() => {
setHoverActionsOwnFocus(false);
restoreFocus();
}, [restoreFocus]);
const panels: EuiContextMenuPanelDescriptor[] = useMemo(
() => [
{
id: 0,
items: [
{
icon: <EuiIcon type="eyeClosed" size="s" />,
name: i18n.HIDE_COLUMN,
onClick: () => {
dispatch(timelineActions.removeColumn({ id: timelineId, columnId: header.id }));
handleClosePopOverTrigger();
},
},
{
disabled: !header.aggregatable,
icon: <EuiIcon type="sortUp" size="s" />,
name: i18n.SORT_AZ,
onClick: () => {
onColumnSort(Direction.asc);
handleClosePopOverTrigger();
},
},
{
disabled: !header.aggregatable,
icon: <EuiIcon type="sortDown" size="s" />,
name: i18n.SORT_ZA,
onClick: () => {
onColumnSort(Direction.desc);
handleClosePopOverTrigger();
},
},
],
},
],
[dispatch, handleClosePopOverTrigger, header.aggregatable, header.id, onColumnSort, timelineId]
);
const headerButton = useMemo(
() => (
<Header timelineId={timelineId} header={header} onFilterChange={onFilterChange} sort={sort} />
),
[header, onFilterChange, sort, timelineId]
); );
const DraggableContent = useCallback( const DraggableContent = useCallback(
@ -99,26 +207,28 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
ref={dragProvided.innerRef} ref={dragProvided.innerRef}
> >
<EventsThContent> <EventsThContent>
<Header <PopoverContainer $width={header.width}>
timelineId={timelineId} <EuiPopover
header={header} anchorPosition="downLeft"
onFilterChange={onFilterChange} button={headerButton}
sort={sort} closePopover={handleClosePopOverTrigger}
/> isOpen={hoverActionsOwnFocus}
ownFocus
panelPaddingSize="none"
>
<ContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
</PopoverContainer>
</EventsThContent> </EventsThContent>
</EventsTh> </EventsTh>
), ),
[header, onFilterChange, sort, timelineId] [handleClosePopOverTrigger, headerButton, header.width, hoverActionsOwnFocus, panels]
); );
const onFocus = useCallback(() => { const onFocus = useCallback(() => {
keyboardHandlerRef.current?.focus(); keyboardHandlerRef.current?.focus();
}, []); }, []);
const handleClosePopOverTrigger = useCallback(() => {
setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger);
}, []);
const openPopover = useCallback(() => { const openPopover = useCallback(() => {
setHoverActionsOwnFocus(true); setHoverActionsOwnFocus(true);
}, []); }, []);
@ -131,6 +241,15 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
openPopover, openPopover,
}); });
const keyDownHandler = useCallback(
(keyboardEvent: React.KeyboardEvent) => {
if (!hoverActionsOwnFocus) {
onKeyDown(keyboardEvent);
}
},
[hoverActionsOwnFocus, onKeyDown]
);
return ( return (
<Resizable <Resizable
enable={RESIZABLE_ENABLE} enable={RESIZABLE_ENABLE}
@ -147,7 +266,7 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
data-test-subj="draggableWrapperKeyboardHandler" data-test-subj="draggableWrapperKeyboardHandler"
onClick={onFocus} onClick={onFocus}
onBlur={onBlur} onBlur={onBlur}
onKeyDown={onKeyDown} onKeyDown={keyDownHandler}
ref={keyboardHandlerRef} ref={keyboardHandlerRef}
role="columnheader" role="columnheader"
tabIndex={0} tabIndex={0}
@ -171,6 +290,7 @@ export const ColumnHeader = React.memo(
ColumnHeaderComponent, ColumnHeaderComponent,
(prevProps, nextProps) => (prevProps, nextProps) =>
prevProps.draggableIndex === nextProps.draggableIndex && prevProps.draggableIndex === nextProps.draggableIndex &&
prevProps.tabType === nextProps.tabType &&
prevProps.timelineId === nextProps.timelineId && prevProps.timelineId === nextProps.timelineId &&
prevProps.isDragging === nextProps.isDragging && prevProps.isDragging === nextProps.isDragging &&
prevProps.onFilterChange === nextProps.onFilterChange && prevProps.onFilterChange === nextProps.onFilterChange &&

View file

@ -19,6 +19,7 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended
import { ColumnHeadersComponent } from '.'; import { ColumnHeadersComponent } from '.';
import { cloneDeep } from 'lodash/fp'; import { cloneDeep } from 'lodash/fp';
import { timelineActions } from '../../../../store/timeline'; import { timelineActions } from '../../../../store/timeline';
import { TimelineTabs } from '../../../../../../common/types/timeline';
const mockDispatch = jest.fn(); const mockDispatch = jest.fn();
jest.mock('react-redux', () => { jest.mock('react-redux', () => {
@ -55,6 +56,7 @@ describe('ColumnHeaders', () => {
showEventsSelect={false} showEventsSelect={false}
showSelectAllCheckbox={false} showSelectAllCheckbox={false}
sort={sort} sort={sort}
tabType={TimelineTabs.query}
timelineId={timelineId} timelineId={timelineId}
/> />
</TestProviders> </TestProviders>
@ -74,6 +76,7 @@ describe('ColumnHeaders', () => {
showEventsSelect={false} showEventsSelect={false}
showSelectAllCheckbox={false} showSelectAllCheckbox={false}
sort={sort} sort={sort}
tabType={TimelineTabs.query}
timelineId={timelineId} timelineId={timelineId}
/> />
</TestProviders> </TestProviders>
@ -94,6 +97,7 @@ describe('ColumnHeaders', () => {
showEventsSelect={false} showEventsSelect={false}
showSelectAllCheckbox={false} showSelectAllCheckbox={false}
sort={sort} sort={sort}
tabType={TimelineTabs.query}
timelineId={timelineId} timelineId={timelineId}
/> />
</TestProviders> </TestProviders>
@ -152,6 +156,7 @@ describe('ColumnHeaders', () => {
showEventsSelect={false} showEventsSelect={false}
showSelectAllCheckbox={false} showSelectAllCheckbox={false}
sort={mockSort} sort={mockSort}
tabType={TimelineTabs.query}
timelineId={timelineId} timelineId={timelineId}
/> />
</TestProviders> </TestProviders>
@ -193,6 +198,7 @@ describe('ColumnHeaders', () => {
showEventsSelect={false} showEventsSelect={false}
showSelectAllCheckbox={false} showSelectAllCheckbox={false}
sort={mockSort} sort={mockSort}
tabType={TimelineTabs.query}
timelineId={timelineId} timelineId={timelineId}
/> />
</TestProviders> </TestProviders>
@ -229,6 +235,7 @@ describe('ColumnHeaders', () => {
showEventsSelect={false} showEventsSelect={false}
showSelectAllCheckbox={false} showSelectAllCheckbox={false}
sort={mockSort} sort={mockSort}
tabType={TimelineTabs.query}
timelineId={timelineId} timelineId={timelineId}
/> />
</TestProviders> </TestProviders>

View file

@ -31,7 +31,7 @@ import {
useGlobalFullScreen, useGlobalFullScreen,
useTimelineFullScreen, useTimelineFullScreen,
} from '../../../../../common/containers/use_full_screen'; } from '../../../../../common/containers/use_full_screen';
import { TimelineId } from '../../../../../../common/types/timeline'; import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
import { OnSelectAll } from '../../events'; import { OnSelectAll } from '../../events';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
import { StatefulFieldsBrowser } from '../../../fields_browser'; import { StatefulFieldsBrowser } from '../../../fields_browser';
@ -76,6 +76,7 @@ interface Props {
showEventsSelect: boolean; showEventsSelect: boolean;
showSelectAllCheckbox: boolean; showSelectAllCheckbox: boolean;
sort: Sort[]; sort: Sort[];
tabType: TimelineTabs;
timelineId: string; timelineId: string;
} }
@ -122,6 +123,7 @@ export const ColumnHeadersComponent = ({
showEventsSelect, showEventsSelect,
showSelectAllCheckbox, showSelectAllCheckbox,
sort, sort,
tabType,
timelineId, timelineId,
}: Props) => { }: Props) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -186,9 +188,10 @@ export const ColumnHeadersComponent = ({
header={header} header={header}
isDragging={draggingIndex === draggableIndex} isDragging={draggingIndex === draggableIndex}
sort={sort} sort={sort}
tabType={tabType}
/> />
)), )),
[columnHeaders, timelineId, draggingIndex, sort] [columnHeaders, timelineId, draggingIndex, sort, tabType]
); );
const fullScreen = useMemo( const fullScreen = useMemo(
@ -335,7 +338,7 @@ export const ColumnHeadersComponent = ({
<Droppable <Droppable
direction={'horizontal'} direction={'horizontal'}
droppableId={`${droppableTimelineColumnsPrefix}${timelineId}`} droppableId={`${droppableTimelineColumnsPrefix}-${tabType}.${timelineId}`}
isDropDisabled={false} isDropDisabled={false}
type={DRAG_TYPE_FIELD} type={DRAG_TYPE_FIELD}
renderClone={renderClone} renderClone={renderClone}

View file

@ -22,10 +22,22 @@ export const FULL_SCREEN = i18n.translate('xpack.securitySolution.timeline.fullS
defaultMessage: 'Full screen', defaultMessage: 'Full screen',
}); });
export const HIDE_COLUMN = i18n.translate('xpack.securitySolution.timeline.hideColumnLabel', {
defaultMessage: 'Hide column',
});
export const SORT_AZ = i18n.translate('xpack.securitySolution.timeline.sortAZLabel', {
defaultMessage: 'Sort A-Z',
});
export const SORT_FIELDS = i18n.translate('xpack.securitySolution.timeline.sortFieldsButton', { export const SORT_FIELDS = i18n.translate('xpack.securitySolution.timeline.sortFieldsButton', {
defaultMessage: 'Sort fields', defaultMessage: 'Sort fields',
}); });
export const SORT_ZA = i18n.translate('xpack.securitySolution.timeline.sortZALabel', {
defaultMessage: 'Sort Z-A',
});
export const TYPE = i18n.translate('xpack.securitySolution.timeline.typeTooltip', { export const TYPE = i18n.translate('xpack.securitySolution.timeline.typeTooltip', {
defaultMessage: 'Type', defaultMessage: 'Type',
}); });

View file

@ -195,6 +195,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
showEventsSelect={false} showEventsSelect={false}
showSelectAllCheckbox={showCheckboxes} showSelectAllCheckbox={showCheckboxes}
sort={sort} sort={sort}
tabType={tabType}
timelineId={id} timelineId={id}
/> />

View file

@ -6,9 +6,11 @@ exports[`Provider rendering renders correctly against snapshot 1`] = `
field="name" field="name"
isEnabled={true} isEnabled={true}
isExcluded={false} isExcluded={false}
isPopoverOpen={false}
kqlQuery="" kqlQuery=""
operator=":" operator=":"
providerId="id-Provider 1" providerId="id-Provider 1"
setIsPopoverOpen={[Function]}
toggleEnabledProvider={[Function]} toggleEnabledProvider={[Function]}
toggleExcludedProvider={[Function]} toggleExcludedProvider={[Function]}
toggleTypeProvider={[Function]} toggleTypeProvider={[Function]}

View file

@ -5,7 +5,7 @@
*/ */
import { noop } from 'lodash/fp'; import { noop } from 'lodash/fp';
import React from 'react'; import React, { useState } from 'react';
import { DataProvider, DataProviderType, IS_OPERATOR } from './data_provider'; import { DataProvider, DataProviderType, IS_OPERATOR } from './data_provider';
import { ProviderItemBadge } from './provider_item_badge'; import { ProviderItemBadge } from './provider_item_badge';
@ -14,21 +14,27 @@ interface OwnProps {
dataProvider: DataProvider; dataProvider: DataProvider;
} }
export const Provider = React.memo<OwnProps>(({ dataProvider }) => ( export const Provider = React.memo<OwnProps>(({ dataProvider }) => {
<ProviderItemBadge const [isPopoverOpen, setIsPopoverOpen] = useState(false);
deleteProvider={noop}
field={dataProvider.queryMatch.displayField || dataProvider.queryMatch.field} return (
kqlQuery={dataProvider.kqlQuery} <ProviderItemBadge
isEnabled={dataProvider.enabled} deleteProvider={noop}
isExcluded={dataProvider.excluded} field={dataProvider.queryMatch.displayField || dataProvider.queryMatch.field}
providerId={dataProvider.id} kqlQuery={dataProvider.kqlQuery}
toggleExcludedProvider={noop} isEnabled={dataProvider.enabled}
toggleEnabledProvider={noop} isExcluded={dataProvider.excluded}
toggleTypeProvider={noop} providerId={dataProvider.id}
val={dataProvider.queryMatch.displayValue || dataProvider.queryMatch.value} isPopoverOpen={isPopoverOpen}
operator={dataProvider.queryMatch.operator || IS_OPERATOR} setIsPopoverOpen={setIsPopoverOpen}
type={dataProvider.type || DataProviderType.default} toggleExcludedProvider={noop}
/> toggleEnabledProvider={noop}
)); toggleTypeProvider={noop}
val={dataProvider.queryMatch.displayValue || dataProvider.queryMatch.value}
operator={dataProvider.queryMatch.operator || IS_OPERATOR}
type={dataProvider.type || DataProviderType.default}
/>
);
});
Provider.displayName = 'Provider'; Provider.displayName = 'Provider';

View file

@ -231,6 +231,7 @@ export class ProviderItemActions extends React.PureComponent<OwnProps> {
button={button} button={button}
anchorPosition="downCenter" anchorPosition="downCenter"
panelPaddingSize="none" panelPaddingSize="none"
ownFocus={true}
> >
<div style={{ userSelect: 'none' }}> <div style={{ userSelect: 'none' }}>
<EuiContextMenu initialPanelId={0} panels={panelTree} data-test-subj="providerActions" /> <EuiContextMenu initialPanelId={0} panels={panelTree} data-test-subj="providerActions" />

View file

@ -28,10 +28,12 @@ interface ProviderItemBadgeProps {
kqlQuery: string; kqlQuery: string;
isEnabled: boolean; isEnabled: boolean;
isExcluded: boolean; isExcluded: boolean;
isPopoverOpen: boolean;
onDataProviderEdited?: OnDataProviderEdited; onDataProviderEdited?: OnDataProviderEdited;
operator: QueryOperator; operator: QueryOperator;
providerId: string; providerId: string;
register?: DataProvidersAnd; register?: DataProvidersAnd;
setIsPopoverOpen: (isPopoverOpen: boolean) => void;
timelineId?: string; timelineId?: string;
toggleEnabledProvider: () => void; toggleEnabledProvider: () => void;
toggleExcludedProvider: () => void; toggleExcludedProvider: () => void;
@ -50,10 +52,12 @@ export const ProviderItemBadge = React.memo<ProviderItemBadgeProps>(
kqlQuery, kqlQuery,
isEnabled, isEnabled,
isExcluded, isExcluded,
isPopoverOpen,
onDataProviderEdited, onDataProviderEdited,
operator, operator,
providerId, providerId,
register, register,
setIsPopoverOpen,
timelineId, timelineId,
toggleEnabledProvider, toggleEnabledProvider,
toggleExcludedProvider, toggleExcludedProvider,
@ -75,16 +79,15 @@ export const ProviderItemBadge = React.memo<ProviderItemBadgeProps>(
getManageTimelineById, getManageTimelineById,
timelineId, timelineId,
]); ]);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const togglePopover = useCallback(() => { const togglePopover = useCallback(() => {
setIsPopoverOpen(!isPopoverOpen); setIsPopoverOpen(!isPopoverOpen);
}, [isPopoverOpen]); }, [isPopoverOpen, setIsPopoverOpen]);
const closePopover = useCallback(() => { const closePopover = useCallback(() => {
setIsPopoverOpen(false); setIsPopoverOpen(false);
wrapperRef?.current?.focus(); wrapperRef?.current?.focus();
}, [wrapperRef]); }, [wrapperRef, setIsPopoverOpen]);
const onToggleEnabledProvider = useCallback(() => { const onToggleEnabledProvider = useCallback(() => {
toggleEnabledProvider(); toggleEnabledProvider();

View file

@ -155,7 +155,7 @@ interface DataProvidersGroupItem extends Omit<Props, 'dataProviders'> {
export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>( export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
({ browserFields, group, groupIndex, dataProvider, index, timelineId }) => { ({ browserFields, group, groupIndex, dataProvider, index, timelineId }) => {
const keyboardHandlerRef = useRef<HTMLDivElement | null>(null); const keyboardHandlerRef = useRef<HTMLDivElement | null>(null);
const [, setHoverActionsOwnFocus] = useState<boolean>(false); const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [, setClosePopOverTrigger] = useState(false); const [, setClosePopOverTrigger] = useState(false);
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -240,7 +240,7 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
}, []); }, []);
const openPopover = useCallback(() => { const openPopover = useCallback(() => {
setHoverActionsOwnFocus(true); setIsPopoverOpen(true);
}, []); }, []);
const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({ const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({
@ -251,6 +251,15 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
openPopover, openPopover,
}); });
const keyDownHandler = useCallback(
(keyboardEvent: React.KeyboardEvent<Element>) => {
if (keyboardHandlerRef.current === document.activeElement) {
onKeyDown(keyboardEvent);
}
},
[onKeyDown]
);
const DraggableContent = useCallback( const DraggableContent = useCallback(
(provided, snapshot) => ( (provided, snapshot) => (
<div <div
@ -275,6 +284,7 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
kqlQuery={index > 0 ? dataProvider.kqlQuery : group[0].kqlQuery} kqlQuery={index > 0 ? dataProvider.kqlQuery : group[0].kqlQuery}
isEnabled={index > 0 ? dataProvider.enabled : group[0].enabled} isEnabled={index > 0 ? dataProvider.enabled : group[0].enabled}
isExcluded={index > 0 ? dataProvider.excluded : group[0].excluded} isExcluded={index > 0 ? dataProvider.excluded : group[0].excluded}
isPopoverOpen={isPopoverOpen}
onDataProviderEdited={handleDataProviderEdited} onDataProviderEdited={handleDataProviderEdited}
operator={ operator={
index > 0 index > 0
@ -284,6 +294,7 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
register={dataProvider} register={dataProvider}
providerId={index > 0 ? group[0].id : dataProvider.id} providerId={index > 0 ? group[0].id : dataProvider.id}
timelineId={timelineId} timelineId={timelineId}
setIsPopoverOpen={setIsPopoverOpen}
toggleEnabledProvider={handleToggleEnabledProvider} toggleEnabledProvider={handleToggleEnabledProvider}
toggleExcludedProvider={handleToggleExcludedProvider} toggleExcludedProvider={handleToggleExcludedProvider}
toggleTypeProvider={handleToggleTypeProvider} toggleTypeProvider={handleToggleTypeProvider}
@ -315,7 +326,9 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
handleToggleExcludedProvider, handleToggleExcludedProvider,
handleToggleTypeProvider, handleToggleTypeProvider,
index, index,
isPopoverOpen,
keyboardHandlerRef, keyboardHandlerRef,
setIsPopoverOpen,
timelineId, timelineId,
] ]
); );
@ -326,7 +339,7 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
data-test-subj="draggableWrapperKeyboardHandler" data-test-subj="draggableWrapperKeyboardHandler"
onClick={onFocus} onClick={onFocus}
onBlur={onBlur} onBlur={onBlur}
onKeyDown={onKeyDown} onKeyDown={keyDownHandler}
ref={keyboardHandlerRef} ref={keyboardHandlerRef}
role="button" role="button"
tabIndex={0} tabIndex={0}

View file

@ -242,3 +242,40 @@ export const onTimelineTabKeyPressed = ({
}); });
} }
}; };
export const ACTIVE_TIMELINE_BUTTON_CLASS_NAME = 'active-timeline-button';
export const FLYOUT_BUTTON_BAR_CLASS_NAME = 'timeline-flyout-button-bar';
export const FLYOUT_BUTTON_CLASS_NAME = 'timeline-flyout-button';
/**
* This function focuses the active timeline button on the next tick. Focus
* is updated on the next tick because this function is typically
* invoked in `onClick` handlers that also dispatch Redux actions (that
* in-turn update focus states).
*/
export const focusActiveTimelineButton = () => {
setTimeout(() => {
document
.querySelector<HTMLButtonElement>(
`div.${FLYOUT_BUTTON_BAR_CLASS_NAME} .${ACTIVE_TIMELINE_BUTTON_CLASS_NAME}`
)
?.focus();
}, 0);
};
/**
* Focuses the utility bar action contained by the provided `containerElement`
* when a valid container is provided
*/
export const focusUtilityBarAction = (containerElement: HTMLElement | null) => {
containerElement
?.querySelector<HTMLButtonElement>('div.siemUtilityBar__action:last-of-type button')
?.focus();
};
/**
* Resets keyboard focus on the page
*/
export const resetKeyboardFocus = () => {
document.querySelector<HTMLAnchorElement>('header.headerGlobalNav a.euiHeaderLogo')?.focus();
};

View file

@ -432,6 +432,7 @@ export const EventsHeadingHandle = styled.div.attrs(({ className = '' }) => ({
*/ */
export const EventsLoading = styled(EuiLoadingSpinner)` export const EventsLoading = styled(EuiLoadingSpinner)`
margin: 0 2px;
vertical-align: middle; vertical-align: middle;
`; `;