Skip to content

Commit 3af3f80

Browse files
[Security Solution] Fix Timeline filter EuiSuperSelect styling
1 parent 409776f commit 3af3f80

2 files changed

Lines changed: 277 additions & 3 deletions

File tree

x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx

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

7-
import { EuiFlexGroup, EuiFlexItem, EuiSuperSelect, EuiToolTip } from '@elastic/eui';
7+
import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
88
import React, { useCallback } from 'react';
99
import styled, { createGlobalStyle } from 'styled-components';
1010

@@ -15,6 +15,7 @@ import { DispatchUpdateReduxTime } from '../../../../common/components/super_dat
1515
import { DataProvider } from '../data_providers/data_provider';
1616
import { QueryBarTimeline } from '../query_bar';
1717

18+
import { EuiSuperSelect } from './super_select';
1819
import { options } from './helpers';
1920
import * as i18n from './translations';
2021

@@ -28,8 +29,8 @@ const SearchOrFilterGlobalStyle = createGlobalStyle`
2829
width: 350px !important;
2930
}
3031
31-
.${searchOrFilterPopoverClassName}__popoverPanel {
32-
width: ${searchOrFilterPopoverWidth};
32+
.${searchOrFilterPopoverClassName}.euiPopover__panel {
33+
width: ${searchOrFilterPopoverWidth} !important;
3334
3435
.euiSuperSelect__listbox {
3536
width: ${searchOrFilterPopoverWidth} !important;
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
/*
8+
Duplicated EuiSuperSelect, because due to the recent changes there is no way to pass panelClassName
9+
prop to EuiInputPopover, which doesn't allow us to properly style the EuiInputPopover panel
10+
(we want the panel to be wider than the input)
11+
*/
12+
13+
import {
14+
EuiSuperSelectProps,
15+
EuiScreenReaderOnly,
16+
EuiSuperSelectControl,
17+
EuiInputPopover,
18+
EuiContextMenuItem,
19+
keys,
20+
EuiI18n,
21+
} from '@elastic/eui';
22+
import React, { Component } from 'react';
23+
import classNames from 'classnames';
24+
25+
enum ShiftDirection {
26+
BACK = 'back',
27+
FORWARD = 'forward',
28+
}
29+
30+
export class EuiSuperSelect<T extends string> extends Component<EuiSuperSelectProps<T>> {
31+
static defaultProps = {
32+
compressed: false,
33+
fullWidth: false,
34+
hasDividers: false,
35+
isInvalid: false,
36+
isLoading: false,
37+
};
38+
39+
private itemNodes: Array<HTMLButtonElement | null> = [];
40+
private _isMounted: boolean = false;
41+
42+
state = {
43+
isPopoverOpen: this.props.isOpen || false,
44+
};
45+
46+
componentDidMount() {
47+
this._isMounted = true;
48+
if (this.props.isOpen) {
49+
this.openPopover();
50+
}
51+
}
52+
53+
componentWillUnmount() {
54+
this._isMounted = false;
55+
}
56+
57+
setItemNode = (node: HTMLButtonElement | null, index: number) => {
58+
this.itemNodes[index] = node;
59+
};
60+
61+
openPopover = () => {
62+
this.setState({
63+
isPopoverOpen: true,
64+
});
65+
66+
const focusSelected = () => {
67+
const indexOfSelected = this.props.options.reduce<number | null>((acc, option, index) => {
68+
if (acc != null) return acc;
69+
if (option == null) return null;
70+
return option.value === this.props.valueOfSelected ? index : null;
71+
}, null);
72+
73+
requestAnimationFrame(() => {
74+
if (!this._isMounted) {
75+
return;
76+
}
77+
78+
if (this.props.valueOfSelected != null) {
79+
if (indexOfSelected != null) {
80+
this.focusItemAt(indexOfSelected);
81+
} else {
82+
focusSelected();
83+
}
84+
}
85+
});
86+
};
87+
88+
requestAnimationFrame(focusSelected);
89+
};
90+
91+
closePopover = () => {
92+
this.setState({
93+
isPopoverOpen: false,
94+
});
95+
};
96+
97+
itemClicked = (value: T) => {
98+
this.setState({
99+
isPopoverOpen: false,
100+
});
101+
if (this.props.onChange) {
102+
this.props.onChange(value);
103+
}
104+
};
105+
106+
onSelectKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
107+
if (event.key === keys.ARROW_UP || event.key === keys.ARROW_DOWN) {
108+
event.preventDefault();
109+
event.stopPropagation();
110+
this.openPopover();
111+
}
112+
};
113+
114+
onItemKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
115+
switch (event.key) {
116+
case keys.ESCAPE:
117+
// close the popover and prevent ancestors from handling
118+
event.preventDefault();
119+
event.stopPropagation();
120+
this.closePopover();
121+
break;
122+
123+
case keys.TAB:
124+
// no-op
125+
event.preventDefault();
126+
event.stopPropagation();
127+
break;
128+
129+
case keys.ARROW_UP:
130+
event.preventDefault();
131+
event.stopPropagation();
132+
this.shiftFocus(ShiftDirection.BACK);
133+
break;
134+
135+
case keys.ARROW_DOWN:
136+
event.preventDefault();
137+
event.stopPropagation();
138+
this.shiftFocus(ShiftDirection.FORWARD);
139+
break;
140+
}
141+
};
142+
143+
focusItemAt(index: number) {
144+
const targetElement = this.itemNodes[index];
145+
if (targetElement != null) {
146+
targetElement.focus();
147+
}
148+
}
149+
150+
shiftFocus(direction: ShiftDirection) {
151+
const currentIndex = this.itemNodes.indexOf(document.activeElement as HTMLButtonElement);
152+
let targetElementIndex: number;
153+
154+
if (currentIndex === -1) {
155+
// somehow the select options has lost focus
156+
targetElementIndex = 0;
157+
} else {
158+
if (direction === ShiftDirection.BACK) {
159+
targetElementIndex = currentIndex === 0 ? this.itemNodes.length - 1 : currentIndex - 1;
160+
} else {
161+
targetElementIndex = currentIndex === this.itemNodes.length - 1 ? 0 : currentIndex + 1;
162+
}
163+
}
164+
165+
this.focusItemAt(targetElementIndex);
166+
}
167+
168+
render() {
169+
const {
170+
className,
171+
options,
172+
valueOfSelected,
173+
onChange,
174+
isOpen,
175+
isInvalid,
176+
hasDividers,
177+
itemClassName,
178+
itemLayoutAlign,
179+
fullWidth,
180+
popoverClassName,
181+
compressed,
182+
...rest
183+
} = this.props;
184+
185+
const popoverClasses = classNames('euiSuperSelect', popoverClassName);
186+
187+
const buttonClasses = classNames(
188+
{
189+
// eslint-disable-next-line @typescript-eslint/naming-convention
190+
'euiSuperSelect--isOpen__button': this.state.isPopoverOpen,
191+
},
192+
className
193+
);
194+
195+
const itemClasses = classNames(
196+
'euiSuperSelect__item',
197+
{
198+
// eslint-disable-next-line @typescript-eslint/naming-convention
199+
'euiSuperSelect__item--hasDividers': hasDividers,
200+
},
201+
itemClassName
202+
);
203+
204+
const button = (
205+
<EuiSuperSelectControl
206+
options={options}
207+
value={valueOfSelected}
208+
onClick={this.state.isPopoverOpen ? this.closePopover : this.openPopover}
209+
onKeyDown={this.onSelectKeyDown}
210+
className={buttonClasses}
211+
fullWidth={fullWidth}
212+
isInvalid={isInvalid}
213+
compressed={compressed}
214+
{...rest}
215+
/>
216+
);
217+
218+
const items = options.map((option, index) => {
219+
const { value, dropdownDisplay, inputDisplay, ...optionRest } = option;
220+
221+
return (
222+
<EuiContextMenuItem
223+
key={index}
224+
className={itemClasses}
225+
icon={valueOfSelected === value ? 'check' : 'empty'}
226+
onClick={() => this.itemClicked(value)}
227+
onKeyDown={this.onItemKeyDown}
228+
layoutAlign={itemLayoutAlign}
229+
buttonRef={(node) => this.setItemNode(node, index)}
230+
role="option"
231+
id={value}
232+
aria-selected={valueOfSelected === value}
233+
{...optionRest}
234+
>
235+
{dropdownDisplay || inputDisplay}
236+
</EuiContextMenuItem>
237+
);
238+
});
239+
240+
return (
241+
<EuiInputPopover
242+
className={popoverClasses}
243+
input={button}
244+
isOpen={isOpen || this.state.isPopoverOpen}
245+
closePopover={this.closePopover}
246+
panelPaddingSize="none"
247+
panelClassName={popoverClasses}
248+
fullWidth={fullWidth}
249+
repositionOnScroll
250+
anchorPosition="downCenter"
251+
>
252+
<EuiScreenReaderOnly>
253+
<p role="alert">
254+
<EuiI18n
255+
token="euiSuperSelect.screenReaderAnnouncement"
256+
default="You are in a form selector of {optionsCount} items and must select a single option.
257+
Use the up and down keys to navigate or escape to close."
258+
values={{ optionsCount: options.length }}
259+
/>
260+
</p>
261+
</EuiScreenReaderOnly>
262+
<div
263+
className="euiSuperSelect__listbox"
264+
role="listbox"
265+
aria-activedescendant={valueOfSelected}
266+
tabIndex={0}
267+
>
268+
{items}
269+
</div>
270+
</EuiInputPopover>
271+
);
272+
}
273+
}

0 commit comments

Comments
 (0)