fix unified search for long field inputs (#166024)

## Summary

Fixes https://github.com/elastic/kibana/issues/166019


![long-field-name-input](be343d1b-e639-4b04-9742-0bcf1e9f032d)


### Checklist

Delete any items that are not applicable to this PR.

- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: nickofthyme <nicholas.partridge@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
Tim Schnell 2023-09-11 02:35:53 -05:00 committed by GitHub
parent 011a9d1e2c
commit c6770af9a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 52 additions and 32 deletions

View file

@ -20,6 +20,7 @@ export interface GenericComboBoxProps<T> {
searchValue: string, searchValue: string,
OPTION_CONTENT_CLASSNAME: string OPTION_CONTENT_CLASSNAME: string
) => React.ReactNode; ) => React.ReactNode;
inputRef?: ((instance: HTMLInputElement | null) => void) | undefined;
[propName: string]: any; [propName: string]: any;
} }

View file

@ -31,12 +31,8 @@ const COMBOBOX_PADDINGS = 10;
const DEFAULT_FONT = '14px Inter'; const DEFAULT_FONT = '14px Inter';
class PhraseValueInputUI extends PhraseSuggestorUI<PhraseValueInputProps> { class PhraseValueInputUI extends PhraseSuggestorUI<PhraseValueInputProps> {
comboBoxRef: React.RefObject<HTMLInputElement>; comboBoxWrapperRef = React.createRef<HTMLDivElement>();
inputRef: HTMLInputElement | null = null;
constructor(props: PhraseValueInputProps) {
super(props);
this.comboBoxRef = React.createRef();
}
public render() { public render() {
return ( return (
@ -69,8 +65,11 @@ class PhraseValueInputUI extends PhraseSuggestorUI<PhraseValueInputProps> {
const valueAsStr = String(value); const valueAsStr = String(value);
const options = value ? uniq([valueAsStr, ...suggestions]) : suggestions; const options = value ? uniq([valueAsStr, ...suggestions]) : suggestions;
return ( return (
<div ref={this.comboBoxRef}> <div ref={this.comboBoxWrapperRef}>
<StringComboBox <StringComboBox
inputRef={(ref) => {
this.inputRef = ref;
}}
isDisabled={this.props.disabled} isDisabled={this.props.disabled}
fullWidth={fullWidth} fullWidth={fullWidth}
compressed={this.props.compressed} compressed={this.props.compressed}
@ -85,7 +84,13 @@ class PhraseValueInputUI extends PhraseSuggestorUI<PhraseValueInputProps> {
options={options} options={options}
getLabel={(option) => option} getLabel={(option) => option}
selectedOptions={value ? [valueAsStr] : []} selectedOptions={value ? [valueAsStr] : []}
onChange={([newValue = '']) => onChange(newValue)} onChange={([newValue = '']) => {
onChange(newValue);
setTimeout(() => {
// Note: requires a tick skip to correctly blur element focus
this.inputRef?.blur();
});
}}
onSearchChange={this.onSearchChange} onSearchChange={this.onSearchChange}
singleSelection={{ asPlainText: true }} singleSelection={{ asPlainText: true }}
onCreateOption={onChange} onCreateOption={onChange}
@ -98,7 +103,7 @@ class PhraseValueInputUI extends PhraseSuggestorUI<PhraseValueInputProps> {
defaultComboboxWidth={DEFAULT_COMBOBOX_WIDTH} defaultComboboxWidth={DEFAULT_COMBOBOX_WIDTH}
defaultFont={DEFAULT_FONT} defaultFont={DEFAULT_FONT}
comboboxPaddings={COMBOBOX_PADDINGS} comboboxPaddings={COMBOBOX_PADDINGS}
comboBoxRef={this.comboBoxRef} comboBoxWrapperRef={this.comboBoxWrapperRef}
label={option.label} label={option.label}
search={searchValue} search={searchValue}
/> />

View file

@ -33,12 +33,7 @@ const COMBOBOX_PADDINGS = 20;
const DEFAULT_FONT = '14px Inter'; const DEFAULT_FONT = '14px Inter';
class PhrasesValuesInputUI extends PhraseSuggestorUI<PhrasesValuesInputProps> { class PhrasesValuesInputUI extends PhraseSuggestorUI<PhrasesValuesInputProps> {
comboBoxRef: React.RefObject<HTMLInputElement>; comboBoxWrapperRef = React.createRef<HTMLDivElement>();
constructor(props: PhrasesValuesInputProps) {
super(props);
this.comboBoxRef = React.createRef();
}
public render() { public render() {
const { suggestions } = this.state; const { suggestions } = this.state;
@ -46,7 +41,7 @@ class PhrasesValuesInputUI extends PhraseSuggestorUI<PhrasesValuesInputProps> {
const options = values ? uniq([...values, ...suggestions]) : suggestions; const options = values ? uniq([...values, ...suggestions]) : suggestions;
return ( return (
<div ref={this.comboBoxRef}> <div ref={this.comboBoxWrapperRef}>
<StringComboBox <StringComboBox
fullWidth={fullWidth} fullWidth={fullWidth}
compressed={compressed} compressed={compressed}
@ -79,7 +74,7 @@ class PhrasesValuesInputUI extends PhraseSuggestorUI<PhrasesValuesInputProps> {
defaultComboboxWidth={DEFAULT_COMBOBOX_WIDTH} defaultComboboxWidth={DEFAULT_COMBOBOX_WIDTH}
defaultFont={DEFAULT_FONT} defaultFont={DEFAULT_FONT}
comboboxPaddings={COMBOBOX_PADDINGS} comboboxPaddings={COMBOBOX_PADDINGS}
comboBoxRef={this.comboBoxRef} comboBoxWrapperRef={this.comboBoxWrapperRef}
label={option.label} label={option.label}
search={searchValue} search={searchValue}
/> />

View file

@ -6,18 +6,16 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import React from 'react'; import React, { ComponentProps } from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { TruncatedLabel } from './truncated_label'; import { TruncatedLabel } from './truncated_label';
describe('truncated_label', () => { describe('truncated_label', () => {
const defaultProps = { const defaultProps: ComponentProps<typeof TruncatedLabel> = {
defaultFont: '14px Inter', defaultFont: '14px Inter',
// jest-canvas-mock mocks measureText as the number of string characters, thats why the width is so low
width: 30,
defaultComboboxWidth: 130, defaultComboboxWidth: 130,
comboboxPaddings: 100, comboboxPaddings: 100,
comboBoxRef: React.createRef<HTMLInputElement>(), comboBoxWrapperRef: React.createRef<HTMLDivElement>(),
search: '', search: '',
label: 'example_field', label: 'example_field',
}; };

View file

@ -15,7 +15,7 @@ import { throttle } from 'lodash';
interface TruncatedLabelProps { interface TruncatedLabelProps {
label: string; label: string;
search: string; search: string;
comboBoxRef: RefObject<HTMLInputElement>; comboBoxWrapperRef: RefObject<HTMLDivElement | null>;
defaultFont: string; defaultFont: string;
defaultComboboxWidth: number; defaultComboboxWidth: number;
comboboxPaddings: number; comboboxPaddings: number;
@ -56,7 +56,7 @@ const truncateLabel = (
export const TruncatedLabel = React.memo(function TruncatedLabel({ export const TruncatedLabel = React.memo(function TruncatedLabel({
label, label,
comboBoxRef, comboBoxWrapperRef,
search, search,
defaultFont, defaultFont,
defaultComboboxWidth, defaultComboboxWidth,
@ -69,15 +69,14 @@ export const TruncatedLabel = React.memo(function TruncatedLabel({
width: defaultComboboxWidth - comboboxPaddings, width: defaultComboboxWidth - comboboxPaddings,
font: defaultFont, font: defaultFont,
}); });
const computeStyles = (_e: UIEvent | undefined, shouldRecomputeAll = false) => { const computeStyles = (_e: UIEvent | undefined, shouldRecomputeAll = false) => {
if (comboBoxRef.current) { if (comboBoxWrapperRef.current) {
const current = { const current = {
...labelProps, ...labelProps,
width: comboBoxRef.current?.clientWidth - comboboxPaddings, width: comboBoxWrapperRef.current.clientWidth - comboboxPaddings,
}; };
if (shouldRecomputeAll) { if (shouldRecomputeAll) {
current.font = window.getComputedStyle(comboBoxRef.current).font; current.font = window.getComputedStyle(comboBoxWrapperRef.current).font;
} }
setLabelProps(current); setLabelProps(current);
} }
@ -88,7 +87,7 @@ export const TruncatedLabel = React.memo(function TruncatedLabel({
}, 50); }, 50);
useEffectOnce(() => { useEffectOnce(() => {
if (comboBoxRef.current) { if (comboBoxWrapperRef.current) {
handleResize(undefined, true); handleResize(undefined, true);
} }

View file

@ -43,7 +43,8 @@ export function FieldInput({ field, dataView, onHandleField }: FieldInputProps)
const { disabled, suggestionsAbstraction } = useContext(FiltersBuilderContextType); const { disabled, suggestionsAbstraction } = useContext(FiltersBuilderContextType);
const fields = dataView ? getFilterableFields(dataView) : []; const fields = dataView ? getFilterableFields(dataView) : [];
const id = useGeneratedHtmlId({ prefix: 'fieldInput' }); const id = useGeneratedHtmlId({ prefix: 'fieldInput' });
const comboBoxRef = useRef<HTMLInputElement>(null); const comboBoxWrapperRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const onFieldChange = useCallback( const onFieldChange = useCallback(
([selectedField]: DataViewField[]) => { ([selectedField]: DataViewField[]) => {
@ -77,12 +78,25 @@ export function FieldInput({ field, dataView, onHandleField }: FieldInputProps)
({ label }) => fields[optionFields.findIndex((optionField) => optionField.label === label)] ({ label }) => fields[optionFields.findIndex((optionField) => optionField.label === label)]
); );
onFieldChange(newValues); onFieldChange(newValues);
setTimeout(() => {
// Note: requires a tick skip to correctly blur element focus
inputRef?.current?.blur();
});
};
const handleFocus: React.FocusEventHandler<HTMLDivElement> = () => {
// Force focus on input due to https://github.com/elastic/eui/issues/7170
inputRef?.current?.focus();
}; };
return ( return (
<div ref={comboBoxRef}> <div ref={comboBoxWrapperRef}>
<EuiComboBox <EuiComboBox
id={id} id={id}
inputRef={(ref) => {
inputRef.current = ref;
}}
options={euiOptions} options={euiOptions}
selectedOptions={selectedEuiOptions} selectedOptions={selectedEuiOptions}
onChange={onComboBoxChange} onChange={onComboBoxChange}
@ -94,6 +108,7 @@ export function FieldInput({ field, dataView, onHandleField }: FieldInputProps)
isClearable={false} isClearable={false}
compressed compressed
fullWidth fullWidth
onFocus={handleFocus}
data-test-subj="filterFieldSuggestionList" data-test-subj="filterFieldSuggestionList"
renderOption={(option, searchValue) => ( renderOption={(option, searchValue) => (
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}> <EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
@ -105,7 +120,7 @@ export function FieldInput({ field, dataView, onHandleField }: FieldInputProps)
defaultComboboxWidth={DEFAULT_COMBOBOX_WIDTH} defaultComboboxWidth={DEFAULT_COMBOBOX_WIDTH}
defaultFont={DEFAULT_FONT} defaultFont={DEFAULT_FONT}
comboboxPaddings={COMBOBOX_PADDINGS} comboboxPaddings={COMBOBOX_PADDINGS}
comboBoxRef={comboBoxRef} comboBoxWrapperRef={comboBoxWrapperRef}
label={option.label} label={option.label}
search={searchValue} search={searchValue}
/> />

View file

@ -22,6 +22,13 @@ export const cursorOrCss = css`
export const fieldAndParamCss = (euiTheme: EuiThemeComputed) => css` export const fieldAndParamCss = (euiTheme: EuiThemeComputed) => css`
min-width: calc(${euiTheme.size.xl} * 5); min-width: calc(${euiTheme.size.xl} * 5);
flex-grow: 1;
.euiFormRow {
max-width: 800px;
}
&:focus-within {
flex-grow: 4;
}
`; `;
export const operationCss = (euiTheme: EuiThemeComputed) => css` export const operationCss = (euiTheme: EuiThemeComputed) => css`