Skip to content

Commit

Permalink
[Security Solutions] fix timeline tabs + layout (elastic#86581) (elas…
Browse files Browse the repository at this point in the history
…tic#86596)

* fix timeline tabs + fix screenreader

* review

* fix jest tests
  • Loading branch information
XavierM authored Dec 20, 2020
1 parent cf9dbf0 commit 5249807
Show file tree
Hide file tree
Showing 32 changed files with 409 additions and 156 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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 { mount } from 'enzyme';
import React from 'react';

import {
ariaIndexToArrayIndex,
arrayIndexToAriaIndex,
getNotesContainerClassName,
getRowRendererClassName,
isArrowRight,
} from './helpers';

describe('helpers', () => {
describe('ariaIndexToArrayIndex', () => {
test('it returns the expected array index', () => {
expect(ariaIndexToArrayIndex(1)).toEqual(0);
});
});

describe('arrayIndexToAriaIndex', () => {
test('it returns the expected aria index', () => {
expect(arrayIndexToAriaIndex(0)).toEqual(1);
});
});

describe('isArrowRight', () => {
test('it returns true if the right arrow key was pressed', () => {
let result = false;
const onKeyDown = (keyboardEvent: React.KeyboardEvent) => {
result = isArrowRight(keyboardEvent);
};

const wrapper = mount(<div onKeyDown={onKeyDown} />);
wrapper.find('div').simulate('keydown', { key: 'ArrowRight' });
wrapper.update();

expect(result).toBe(true);
});

test('it returns false if another key was pressed', () => {
let result = false;
const onKeyDown = (keyboardEvent: React.KeyboardEvent) => {
result = isArrowRight(keyboardEvent);
};

const wrapper = mount(<div onKeyDown={onKeyDown} />);
wrapper.find('div').simulate('keydown', { key: 'Enter' });
wrapper.update();

expect(result).toBe(false);
});
});

describe('getRowRendererClassName', () => {
test('it returns the expected class name', () => {
expect(getRowRendererClassName(2)).toBe('row-renderer-2');
});
});

describe('getNotesContainerClassName', () => {
test('it returns the expected class name', () => {
expect(getNotesContainerClassName(2)).toBe('notes-container-2');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
*/

import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '../drag_and_drop/helpers';
import {
NOTES_CONTAINER_CLASS_NAME,
NOTE_CONTENT_CLASS_NAME,
ROW_RENDERER_CLASS_NAME,
} from '../../../timelines/components/timeline/body/helpers';
import { HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME } from '../with_hover_actions';

/**
Expand Down Expand Up @@ -63,6 +68,9 @@ export const isArrowDownOrArrowUp = (event: React.KeyboardEvent): boolean =>
export const isArrowKey = (event: React.KeyboardEvent): boolean =>
isArrowRightOrArrowLeft(event) || isArrowDownOrArrowUp(event);

/** Returns `true` if the right arrow key was pressed */
export const isArrowRight = (event: React.KeyboardEvent): boolean => event.key === 'ArrowRight';

/** Returns `true` if the escape key was pressed */
export const isEscape = (event: React.KeyboardEvent): boolean => event.key === 'Escape';

Expand Down Expand Up @@ -284,6 +292,12 @@ export type OnColumnFocused = ({
newFocusedColumnAriaColindex: number | null;
}) => void;

export const getRowRendererClassName = (ariaRowindex: number) =>
`${ROW_RENDERER_CLASS_NAME}-${ariaRowindex}`;

export const getNotesContainerClassName = (ariaRowindex: number) =>
`${NOTES_CONTAINER_CLASS_NAME}-${ariaRowindex}`;

/**
* This function implements arrow key support for the `onKeyDownFocusHandler`.
*
Expand Down Expand Up @@ -312,6 +326,28 @@ export const onArrowKeyDown = ({
onColumnFocused?: OnColumnFocused;
rowindexAttribute: string;
}) => {
if (isArrowDown(event) && event.shiftKey) {
const firstRowRendererDraggable = containerElement?.querySelector<HTMLDivElement>(
`.${getRowRendererClassName(focusedAriaRowindex)} .${DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME}`
);

if (firstRowRendererDraggable) {
firstRowRendererDraggable.focus();
return;
}
}

if (isArrowRight(event) && event.shiftKey) {
const firstNoteContent = containerElement?.querySelector<HTMLDivElement>(
`.${getNotesContainerClassName(focusedAriaRowindex)} .${NOTE_CONTENT_CLASS_NAME}`
);

if (firstNoteContent) {
firstNoteContent.focus();
return;
}
}

const ariaColindex = isArrowRightOrArrowLeft(event)
? getNewAriaColindex({
focusedAriaColindex,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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 { mount } from 'enzyme';
import React from 'react';

import { TooltipWithKeyboardShortcut } from '.';

const props = {
content: <div>{'To pay respect'}</div>,
shortcut: 'F',
showShortcut: true,
};

describe('TooltipWithKeyboardShortcut', () => {
test('it renders the provided content', () => {
const wrapper = mount(<TooltipWithKeyboardShortcut {...props} />);

expect(wrapper.find('[data-test-subj="content"]').text()).toBe('To pay respect');
});

test('it renders the additionalScreenReaderOnlyContext', () => {
const wrapper = mount(
<TooltipWithKeyboardShortcut {...props} additionalScreenReaderOnlyContext={'field.name'} />
);

expect(wrapper.find('[data-test-subj="additionalScreenReaderOnlyContext"]').text()).toBe(
'field.name'
);
});

test('it renders the expected shortcut', () => {
const wrapper = mount(<TooltipWithKeyboardShortcut {...props} />);

expect(wrapper.find('[data-test-subj="shortcut"]').first().text()).toBe('Press\u00a0F');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { EuiScreenReaderOnly, EuiText } from '@elastic/eui';
import { EuiText, EuiScreenReaderOnly } from '@elastic/eui';
import React from 'react';

import * as i18n from './translations';
Expand All @@ -23,14 +23,14 @@ const TooltipWithKeyboardShortcutComponent = ({
showShortcut,
}: Props) => (
<>
<div>{content}</div>
<div data-test-subj="content">{content}</div>
{additionalScreenReaderOnlyContext !== '' && (
<EuiScreenReaderOnly>
<EuiScreenReaderOnly data-test-subj="additionalScreenReaderOnlyContext">
<p>{additionalScreenReaderOnlyContext}</p>
</EuiScreenReaderOnly>
)}
{showShortcut && (
<EuiText color="subdued" size="s" textAlign="center">
<EuiText color="subdued" data-test-subj="shortcut" size="s" textAlign="center">
<span>{i18n.PRESS}</span>
{'\u00a0'}
<span className="euiBadge euiBadge--hollow">{shortcut}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import '../../mock/match_media';
import { useKibana } from '../../lib/kibana';
import { TestProviders } from '../../mock';
import { FilterManager } from '../../../../../../../src/plugins/data/public';
import { useAddToTimeline } from '../../hooks/use_add_to_timeline';
import { useSourcererScope } from '../../containers/sourcerer';
import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content';
import {
Expand All @@ -41,8 +40,14 @@ jest.mock('uuid', () => {
v4: jest.fn(() => 'uuid.v4()'),
};
});

jest.mock('../../hooks/use_add_to_timeline');
const mockStartDragToTimeline = jest.fn();
jest.mock('../../hooks/use_add_to_timeline', () => {
const original = jest.requireActual('../../hooks/use_add_to_timeline');
return {
...original,
useAddToTimeline: () => ({ startDragToTimeline: mockStartDragToTimeline }),
};
});
const mockAddFilters = jest.fn();
const mockGetTimelineFilterManager = jest.fn().mockReturnValue({
addFilters: mockAddFilters,
Expand Down Expand Up @@ -78,8 +83,7 @@ const defaultProps = {

describe('DraggableWrapperHoverContent', () => {
beforeAll(() => {
// our mock implementation of the useAddToTimeline hook returns a mock startDragToTimeline function:
(useAddToTimeline as jest.Mock).mockReturnValue({ startDragToTimeline: jest.fn() });
mockStartDragToTimeline.mockReset();
(useSourcererScope as jest.Mock).mockReturnValue({
browserFields: mockBrowserFields,
selectedPatterns: [],
Expand Down Expand Up @@ -376,7 +380,7 @@ describe('DraggableWrapperHoverContent', () => {
});
});

test('when clicked, it invokes the `startDragToTimeline` function returned by the `useAddToTimeline` hook', () => {
test('when clicked, it invokes the `startDragToTimeline` function returned by the `useAddToTimeline` hook', async () => {
const wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent
Expand All @@ -389,25 +393,17 @@ describe('DraggableWrapperHoverContent', () => {
</TestProviders>
);

// The following "startDragToTimeline" function returned by our mock
// useAddToTimeline hook is called when the user clicks the
// Add to timeline investigation action:
const { startDragToTimeline } = useAddToTimeline({
draggableId,
fieldName: aggregatableStringField,
});

wrapper.find('[data-test-subj="add-to-timeline"]').first().simulate('click');
wrapper.update();

waitFor(() => {
expect(startDragToTimeline).toHaveBeenCalled();
await waitFor(() => {
wrapper.update();
expect(mockStartDragToTimeline).toHaveBeenCalled();
});
});
});

describe('Top N', () => {
test(`it renders the 'Show top field' button when showTopN is false and an aggregatable string field is provided`, async () => {
test(`it renders the 'Show top field' button when showTopN is false and an aggregatable string field is provided`, () => {
const aggregatableStringField = 'cloud.account.id';
const wrapper = mount(
<TestProviders>
Expand All @@ -425,7 +421,7 @@ describe('DraggableWrapperHoverContent', () => {
expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(true);
});

test(`it renders the 'Show top field' button when showTopN is false and a allowlisted signal field is provided`, async () => {
test(`it renders the 'Show top field' button when showTopN is false and a allowlisted signal field is provided`, () => {
const allowlistedField = 'signal.rule.name';
const wrapper = mount(
<TestProviders>
Expand All @@ -443,7 +439,7 @@ describe('DraggableWrapperHoverContent', () => {
expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(true);
});

test(`it does NOT render the 'Show top field' button when showTopN is false and a field not known to BrowserFields is provided`, async () => {
test(`it does NOT render the 'Show top field' button when showTopN is false and a field not known to BrowserFields is provided`, () => {
const notKnownToBrowserFields = 'unknown.field';
const wrapper = mount(
<TestProviders>
Expand All @@ -461,7 +457,7 @@ describe('DraggableWrapperHoverContent', () => {
expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(false);
});

test(`it should invokes goGetTimelineId when user is over the 'Show top field' button`, () => {
test(`it should invokes goGetTimelineId when user is over the 'Show top field' button`, async () => {
const allowlistedField = 'signal.rule.name';
const wrapper = mount(
<TestProviders>
Expand All @@ -476,12 +472,12 @@ describe('DraggableWrapperHoverContent', () => {
);
const button = wrapper.find(`[data-test-subj="show-top-field"]`).first();
button.simulate('mouseenter');
waitFor(() => {
await waitFor(() => {
expect(goGetTimelineId).toHaveBeenCalledWith(true);
});
});

test(`invokes the toggleTopN function when the 'Show top field' button is clicked`, async () => {
test(`invokes the toggleTopN function when the 'Show top field' button is clicked`, () => {
const allowlistedField = 'signal.rule.name';
const wrapper = mount(
<TestProviders>
Expand All @@ -502,7 +498,7 @@ describe('DraggableWrapperHoverContent', () => {
expect(toggleTopN).toBeCalled();
});

test(`it does NOT render the Top N histogram when when showTopN is false`, async () => {
test(`it does NOT render the Top N histogram when when showTopN is false`, () => {
const allowlistedField = 'signal.rule.name';
const wrapper = mount(
<TestProviders>
Expand All @@ -522,7 +518,7 @@ describe('DraggableWrapperHoverContent', () => {
);
});

test(`it does NOT render the 'Show top field' button when showTopN is true`, async () => {
test(`it does NOT render the 'Show top field' button when showTopN is true`, () => {
const allowlistedField = 'signal.rule.name';
const wrapper = mount(
<TestProviders>
Expand All @@ -541,7 +537,7 @@ describe('DraggableWrapperHoverContent', () => {
expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(false);
});

test(`it renders the Top N histogram when when showTopN is true`, async () => {
test(`it renders the Top N histogram when when showTopN is true`, () => {
const allowlistedField = 'signal.rule.name';
const wrapper = mount(
<TestProviders>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ const DraggableWrapperHoverContentComponent: React.FC<Props> = ({
color="text"
data-test-subj="add-to-timeline"
iconType="timeline"
onClick={handleStartDragToTimeline}
/>
</EuiToolTip>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import { BrowserFields, DocValueFields } from '../../containers/source';
import { useTimelineEvents } from '../../../timelines/containers';
import { timelineActions } from '../../../timelines/store/timeline';
import { useKibana } from '../../lib/kibana';
import { ColumnHeaderOptions, KqlMode } from '../../../timelines/store/timeline/model';
import {
ColumnHeaderOptions,
KqlMode,
TimelineTabs,
} from '../../../timelines/store/timeline/model';
import { HeaderSection } from '../header_section';
import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers';
import { Sort } from '../../../timelines/components/timeline/body/sort';
Expand Down Expand Up @@ -334,6 +338,7 @@ const EventsViewerComponent: React.FC<Props> = ({
onRuleChange={onRuleChange}
refetch={refetch}
sort={sort}
tabType={TimelineTabs.query}
totalPages={calculateTotalPages({
itemsCount: totalCountMinusDeleted,
itemsPerPage,
Expand Down
Loading

0 comments on commit 5249807

Please sign in to comment.