From 0c2655577db1906c5732bdcade950a2e4c7cfe08 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 8 Sep 2021 14:51:56 -0500 Subject: [PATCH 01/60] update euiselectable with paddingSize prop, tests --- .../selectable_list.test.tsx.snap | 22 +++++++ .../selectable_list_item.test.tsx.snap | 58 ++++++++++++++++--- .../_selectable_list_item.scss | 7 ++- .../selectable_list/selectable_list.test.tsx | 17 ++++++ .../selectable_list/selectable_list.tsx | 33 ++++++++--- .../selectable_list_item.test.tsx | 14 ++++- .../selectable_list/selectable_list_item.tsx | 15 ++++- 7 files changed, 145 insertions(+), 21 deletions(-) diff --git a/src/components/selectable/selectable_list/__snapshots__/selectable_list.test.tsx.snap b/src/components/selectable/selectable_list/__snapshots__/selectable_list.test.tsx.snap index bf34412568e..82c04d01c3b 100644 --- a/src/components/selectable/selectable_list/__snapshots__/selectable_list.test.tsx.snap +++ b/src/components/selectable/selectable_list/__snapshots__/selectable_list.test.tsx.snap @@ -66,6 +66,28 @@ exports[`EuiSelectableListItem props height is full 1`] = ` `; +exports[`EuiSelectableListItem props paddingSize none is rendered 1`] = ` +
+
+
+`; + +exports[`EuiSelectableListItem props paddingSize s is rendered 1`] = ` +
+
+
+`; + exports[`EuiSelectableListItem props renderOption 1`] = `
@@ -25,7 +25,7 @@ exports[`EuiSelectableListItem is rendered 1`] = ` exports[`EuiSelectableListItem props append 1`] = `
  • + + + + +
  • +`; + +exports[`EuiSelectableListItem props paddingSize none is rendered 1`] = `
  • `; +exports[`EuiSelectableListItem props paddingSize s is rendered 1`] = ` +
  • + + + + +
  • +`; + exports[`EuiSelectableListItem props prepend 1`] = `
  • { expect(component).toMatchSnapshot(); }); + + describe('paddingSize', () => { + PADDING_SIZES.forEach((size) => { + test(`${size} is rendered`, () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + }); + }); }); }); diff --git a/src/components/selectable/selectable_list/selectable_list.tsx b/src/components/selectable/selectable_list/selectable_list.tsx index 6c6d41276d1..888d7e52743 100644 --- a/src/components/selectable/selectable_list/selectable_list.tsx +++ b/src/components/selectable/selectable_list/selectable_list.tsx @@ -61,6 +61,7 @@ export type EuiSelectableOptionsListProps = CommonProps & * The default content when `true` is `↩ to select/deselect/include/exclude` */ onFocusBadge?: EuiSelectableListItemProps['onFocusBadge']; + paddingSize?: EuiSelectableListItemProps['paddingSize']; }; export type EuiSelectableListProps = EuiSelectableOptionsListProps & { @@ -199,6 +200,18 @@ export class EuiSelectableList extends Component> { ...optionRest } = option; + const { + activeOptionIndex, + allowExclusions, + onFocusBadge, + paddingSize, + searchValue, + showIcons, + makeOptionId, + renderOption, + setActiveOptionIndex, + } = this.props; + if (isGroupLabel) { return (
  • extends Component> { return ( { - this.props.setActiveOptionIndex(index); + setActiveOptionIndex(index); }} onClick={() => this.onAddOrRemoveOption(option)} ref={ref ? ref.bind(null, index) : undefined} - isFocused={this.props.activeOptionIndex === index} + isFocused={activeOptionIndex === index} title={searchableLabel || label} checked={checked} disabled={disabled} @@ -235,15 +248,16 @@ export class EuiSelectableList extends Component> { append={append} aria-posinset={index + 1 - labelCount} aria-setsize={data.length - labelCount} - onFocusBadge={this.props.onFocusBadge} - allowExclusions={this.props.allowExclusions} - showIcons={this.props.showIcons} + onFocusBadge={onFocusBadge} + allowExclusions={allowExclusions} + showIcons={showIcons} + paddingSize={paddingSize} {...(optionRest as EuiSelectableListItemProps)} > - {this.props.renderOption ? ( - this.props.renderOption(option, this.props.searchValue) + {renderOption ? ( + renderOption(option, searchValue) ) : ( - {label} + {label} )} ); @@ -266,6 +280,7 @@ export class EuiSelectableList extends Component> { visibleOptions, allowExclusions, bordered, + paddingSize, searchable, onFocusBadge, listId, diff --git a/src/components/selectable/selectable_list/selectable_list_item.test.tsx b/src/components/selectable/selectable_list/selectable_list_item.test.tsx index ff865ff7750..12446c3e36e 100644 --- a/src/components/selectable/selectable_list/selectable_list_item.test.tsx +++ b/src/components/selectable/selectable_list/selectable_list_item.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { render } from 'enzyme'; import { requiredProps } from '../../../test/required_props'; -import { EuiSelectableListItem } from './selectable_list_item'; +import { EuiSelectableListItem, PADDING_SIZES } from './selectable_list_item'; describe('EuiSelectableListItem', () => { test('is rendered', () => { @@ -62,6 +62,18 @@ describe('EuiSelectableListItem', () => { expect(component).toMatchSnapshot(); }); + describe('paddingSize', () => { + PADDING_SIZES.forEach((size) => { + test(`${size} is rendered`, () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + }); + }); + describe('onFocusBadge', () => { test('can be true', () => { const component = render(); diff --git a/src/components/selectable/selectable_list/selectable_list_item.tsx b/src/components/selectable/selectable_list/selectable_list_item.tsx index 9dc2e6f2cae..183886ee333 100644 --- a/src/components/selectable/selectable_list/selectable_list_item.tsx +++ b/src/components/selectable/selectable_list/selectable_list_item.tsx @@ -8,7 +8,7 @@ import classNames from 'classnames'; import React, { Component, LiHTMLAttributes } from 'react'; -import { CommonProps } from '../../common'; +import { CommonProps, keysOf } from '../../common'; import { EuiI18n } from '../../i18n'; import { EuiIcon, IconColor, IconType } from '../../icon'; import { EuiSelectableOptionCheckedType } from '../selectable_option'; @@ -26,6 +26,13 @@ function resolveIconAndColor( : { icon: 'cross', color: 'text' }; } +const paddingSizeToClassNameMap = { + none: null, + s: 'euiSelectableListItem--paddingSmall', +}; +export const PADDING_SIZES = keysOf(paddingSizeToClassNameMap); +export type EuiSelectablePaddingSize = typeof PADDING_SIZES[number]; + export type EuiSelectableListItemProps = LiHTMLAttributes & CommonProps & { children?: React.ReactNode; @@ -51,6 +58,10 @@ export type EuiSelectableListItemProps = LiHTMLAttributes & * The default content when `true` is `↩ to select/deselect/include/exclude` */ onFocusBadge?: boolean | EuiBadgeProps; + /** + * Padding for the list items. + */ + paddingSize?: EuiSelectablePaddingSize; }; // eslint-disable-next-line react/prefer-stateless-function @@ -78,6 +89,7 @@ export class EuiSelectableListItem extends Component< append, allowExclusions, onFocusBadge, + paddingSize = 's', ...rest } = this.props; @@ -86,6 +98,7 @@ export class EuiSelectableListItem extends Component< { 'euiSelectableListItem-isFocused': isFocused, }, + paddingSizeToClassNameMap[paddingSize], className ); From 08afa9b3d566f69920aa2ea6be93f7d44640c9c4 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 8 Sep 2021 15:04:05 -0500 Subject: [PATCH 02/60] euisuggest use euiselectable; more tests --- .../__snapshots__/suggest.test.tsx.snap | 385 ++++++++++++++++++ .../__snapshots__/suggest_input.test.tsx.snap | 298 ++++++++++++++ src/components/suggest/index.ts | 2 +- src/components/suggest/suggest.test.tsx | 101 ++++- src/components/suggest/suggest.tsx | 93 +++-- src/components/suggest/suggest_input.test.tsx | 84 +++- src/components/suggest/suggest_input.tsx | 40 +- src/components/suggest/suggest_item.tsx | 24 +- 8 files changed, 946 insertions(+), 81 deletions(-) diff --git a/src/components/suggest/__snapshots__/suggest.test.tsx.snap b/src/components/suggest/__snapshots__/suggest.test.tsx.snap index 18c66bb4159..ef16653f6d3 100644 --- a/src/components/suggest/__snapshots__/suggest.test.tsx.snap +++ b/src/components/suggest/__snapshots__/suggest.test.tsx.snap @@ -29,3 +29,388 @@ exports[`EuiSuggest is rendered 1`] = `
  • `; + +exports[`EuiSuggest options common 1`] = ` +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +`; + +exports[`EuiSuggest options standard 1`] = ` +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +`; + +exports[`EuiSuggest props append 1`] = ` +
    +
    +
    +
    +
    +
    + +
    + + Appended + +
    +
    +
    +
    +
    +`; + +exports[`EuiSuggest props status status: loading is rendered 1`] = ` +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +`; + +exports[`EuiSuggest props status status: saved is rendered 1`] = ` +
    +
    +
    +
    +
    +
    + +
    + + + +
    +
    +
    +
    +
    +`; + +exports[`EuiSuggest props status status: unchanged is rendered 1`] = ` +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +`; + +exports[`EuiSuggest props status status: unsaved is rendered 1`] = ` +
    +
    +
    +
    +
    +
    + +
    + + + +
    +
    +
    +
    +
    +`; + +exports[`EuiSuggest props tooltipContent tooltipContent for status: loading is rendered 1`] = ` +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +`; + +exports[`EuiSuggest props tooltipContent tooltipContent for status: saved is rendered 1`] = ` +
    +
    +
    +
    +
    +
    + +
    + + + +
    +
    +
    +
    +
    +`; + +exports[`EuiSuggest props tooltipContent tooltipContent for status: unchanged is rendered 1`] = ` +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +`; + +exports[`EuiSuggest props tooltipContent tooltipContent for status: unsaved is rendered 1`] = ` +
    +
    +
    +
    +
    +
    + +
    + + + +
    +
    +
    +
    +
    +`; diff --git a/src/components/suggest/__snapshots__/suggest_input.test.tsx.snap b/src/components/suggest/__snapshots__/suggest_input.test.tsx.snap index d41a307ea1e..78941a202de 100644 --- a/src/components/suggest/__snapshots__/suggest_input.test.tsx.snap +++ b/src/components/suggest/__snapshots__/suggest_input.test.tsx.snap @@ -1,6 +1,304 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EuiSuggestInput is rendered 1`] = ` +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +`; + +exports[`EuiSuggestInput props append 1`] = ` +
    +
    +
    +
    +
    + +
    + + Appended + +
    +
    +
    +
    +`; + +exports[`EuiSuggestInput props status status: loading is rendered 1`] = ` +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +`; + +exports[`EuiSuggestInput props status status: saved is rendered 1`] = ` +
    +
    +
    +
    +
    + +
    + + + +
    +
    +
    +
    +`; + +exports[`EuiSuggestInput props status status: unchanged is rendered 1`] = ` +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +`; + +exports[`EuiSuggestInput props status status: unsaved is rendered 1`] = ` +
    +
    +
    +
    +
    + +
    + + + +
    +
    +
    +
    +`; + +exports[`EuiSuggestInput props tooltipContent tooltipContent for status: loading is rendered 1`] = ` +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +`; + +exports[`EuiSuggestInput props tooltipContent tooltipContent for status: saved is rendered 1`] = ` +
    +
    +
    +
    +
    + +
    + + + +
    +
    +
    +
    +`; + +exports[`EuiSuggestInput props tooltipContent tooltipContent for status: unchanged is rendered 1`] = ` +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +`; + +exports[`EuiSuggestInput props tooltipContent tooltipContent for status: unsaved is rendered 1`] = `
    diff --git a/src/components/suggest/index.ts b/src/components/suggest/index.ts index 568d6938797..ac5f54f22de 100644 --- a/src/components/suggest/index.ts +++ b/src/components/suggest/index.ts @@ -10,4 +10,4 @@ export { EuiSuggestInput, EuiSuggestInputProps } from './suggest_input'; export { EuiSuggestItem, EuiSuggestItemProps } from './suggest_item'; -export { EuiSuggest, EuiSuggestProps } from './suggest'; +export { EuiSuggest, EuiSuggestProps, EuiSuggestionProps } from './suggest'; diff --git a/src/components/suggest/suggest.test.tsx b/src/components/suggest/suggest.test.tsx index 6cc64fa37c1..27b0fd9cbfe 100644 --- a/src/components/suggest/suggest.test.tsx +++ b/src/components/suggest/suggest.test.tsx @@ -7,12 +7,13 @@ */ import React from 'react'; -import { render } from 'enzyme'; +import { render, mount } from 'enzyme'; import { requiredProps } from '../../test/required_props'; -import { EuiSuggest } from './suggest'; +import { EuiSuggest, EuiSuggestionProps } from './suggest'; +import { ALL_STATUS } from './suggest_input'; -const sampleItems = [ +const sampleItems: EuiSuggestionProps[] = [ { type: { iconType: 'kqlField', color: 'tint4' }, label: 'Field sample', @@ -33,4 +34,98 @@ describe('EuiSuggest', () => { expect(component).toMatchSnapshot(); }); + + describe('props', () => { + describe('status', () => { + ALL_STATUS.forEach((status) => { + test(`status: ${status} is rendered`, () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + }); + }); + + test('append', () => { + const component = render( + Appended} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + describe('tooltipContent', () => { + ALL_STATUS.forEach((status) => { + test(`tooltipContent for status: ${status} is rendered`, () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + }); + }); + + test('sendValue', () => { + const handler = jest.fn(); + const component = mount( + + ); + + component.find('input').simulate('change', { value: 'a' }); + expect(handler).toBeCalled(); + }); + }); + + describe('options', () => { + test('standard', () => { + const _sampleItems: EuiSuggestionProps[] = sampleItems.map( + (item, idx) => ({ + ...item, + labelDisplay: idx === 0 ? 'fixed' : 'expand', + descriptionDisplay: idx === 0 ? 'truncate' : 'wrap', + labelWidth: idx === 0 ? '70' : 80, + }) + ); + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('common', () => { + const _sampleItems: EuiSuggestionProps[] = sampleItems.map((item) => ({ + ...item, + 'aria-label': 'sampleItem', + 'data-test-subj': 'sampleItem', + className: 'sampleItem', + id: 'sampleItem', + })); + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + }); }); diff --git a/src/components/suggest/suggest.tsx b/src/components/suggest/suggest.tsx index 3020045acec..ba38698fe7b 100644 --- a/src/components/suggest/suggest.tsx +++ b/src/components/suggest/suggest.tsx @@ -7,63 +7,88 @@ */ import React, { FunctionComponent } from 'react'; +import classNames from 'classnames'; import { CommonProps } from '../common'; -import { EuiSuggestItem, EuiSuggestItemProps } from './suggest_item'; + +import { EuiSelectable, EuiSelectableListItemProps } from '../selectable'; + +import { EuiSuggestItem, _EuiSuggestItemPropsBase } from './suggest_item'; import { EuiSuggestInput, EuiSuggestInputProps } from './suggest_input'; +export interface EuiSuggestionProps + extends CommonProps, + _EuiSuggestItemPropsBase { + onClick?: EuiSelectableListItemProps['onClick']; +} + export type EuiSuggestProps = CommonProps & - EuiSuggestInputProps & { + Omit & { /** * List of suggestions to display using 'suggestItem'. */ - suggestions: EuiSuggestItemProps[]; + suggestions: EuiSuggestionProps[]; /** * Handler for click on a suggestItem. */ - onItemClick?: (item: EuiSuggestItemProps) => void; + onItemClick?: (item: EuiSuggestionProps) => void; onInputChange?: (target: EventTarget) => void; }; -export const EuiSuggest: FunctionComponent = ( - props: EuiSuggestProps -) => { - const { - onItemClick, - onInputChange, - status, - append, - tooltipContent, - suggestions, - ...rest - } = props; - +export const EuiSuggest: FunctionComponent = ({ + onItemClick, + onInputChange, + status = 'unchanged', + append, + tooltipContent, + suggestions, + ...rest +}) => { const onChange = (e: React.FormEvent) => { onInputChange ? onInputChange(e.target) : null; }; - const suggestionList = suggestions.map((item: EuiSuggestItemProps, index) => { - const props = { ...item }; + const suggestionList = suggestions.map((item: EuiSuggestionProps) => { + const { className, ...props } = item; if (onItemClick) { props.onClick = () => onItemClick(item); } - return ; + return { + ...props, + className: classNames(className, 'euiSuggestItemOption'), + }; }); - const suggestInput = ( - - ); - - return
    {suggestInput}
    ; -}; + const renderOption = (option: EuiSuggestionProps) => { + // `onClick` handled by EuiSelectable + const { onClick, ...props } = option; + return ; + }; -EuiSuggestInput.defaultProps = { - status: 'unchanged', + return ( +
    + + singleSelection={true} + options={suggestionList} + listProps={{ + bordered: true, + showIcons: false, + onFocusBadge: false, + paddingSize: 'none', + }} + renderOption={renderOption} + > + {(list) => list} + + } + {...rest} + /> +
    + ); }; diff --git a/src/components/suggest/suggest_input.test.tsx b/src/components/suggest/suggest_input.test.tsx index 9a9544ae73b..d0309ae2f6c 100644 --- a/src/components/suggest/suggest_input.test.tsx +++ b/src/components/suggest/suggest_input.test.tsx @@ -7,34 +7,78 @@ */ import React from 'react'; -import { render } from 'enzyme'; +import { render, mount } from 'enzyme'; import { requiredProps } from '../../test/required_props'; -import { EuiSuggestInput } from './suggest_input'; - -const sampleItems = [ - { - type: { iconType: 'kqlField', color: 'tint4' }, - label: 'Field sample', - description: 'Description', - }, - { - type: { iconType: 'kqlValue', color: 'tint0' }, - label: 'Value sample', - description: 'Description', - }, -]; +import { EuiSuggestInput, ALL_STATUS } from './suggest_input'; describe('EuiSuggestInput', () => { test('is rendered', () => { const component = render( - + } {...requiredProps} /> ); expect(component).toMatchSnapshot(); }); + + describe('props', () => { + describe('status', () => { + ALL_STATUS.forEach((status) => { + test(`status: ${status} is rendered`, () => { + const component = render( + } + status={status} + /> + ); + + expect(component).toMatchSnapshot(); + }); + }); + }); + + test('append', () => { + const component = render( + } + append={Appended} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + describe('tooltipContent', () => { + ALL_STATUS.forEach((status) => { + test(`tooltipContent for status: ${status} is rendered`, () => { + const component = render( + } + status={status} + tooltipContent={status} + /> + ); + + expect(component).toMatchSnapshot(); + }); + }); + }); + + test('sendValue', () => { + const handler = jest.fn(); + const component = mount( + } + sendValue={handler} + /> + ); + + component.find('input').simulate('change', { value: 'a' }); + expect(handler).toBeCalled(); + }); + }); }); diff --git a/src/components/suggest/suggest_input.tsx b/src/components/suggest/suggest_input.tsx index 8ad6804bed8..62e724fa81a 100644 --- a/src/components/suggest/suggest_input.tsx +++ b/src/components/suggest/suggest_input.tsx @@ -14,7 +14,10 @@ import { EuiFieldText, EuiFieldTextProps } from '../form'; import { EuiToolTip } from '../tool_tip'; import { EuiIcon } from '../icon'; import { EuiInputPopover } from '../popover'; -import { EuiSuggestItemProps } from './suggest_item'; + +export const ALL_STATUS = ['unsaved', 'saved', 'unchanged', 'loading'] as const; +type StatusTuple = typeof ALL_STATUS; +export type EuiSuggestStatus = StatusTuple[number]; export type EuiSuggestInputProps = CommonProps & EuiFieldTextProps & { @@ -23,7 +26,7 @@ export type EuiSuggestInputProps = CommonProps & /** * Status of the current query 'unsaved', 'saved', 'unchanged' or 'loading'. */ - status?: 'unsaved' | 'saved' | 'unchanged' | 'loading'; + status?: EuiSuggestStatus; /** * Element to be appended to the input bar. @@ -31,11 +34,14 @@ export type EuiSuggestInputProps = CommonProps & append?: JSX.Element; /** - * List of suggestions to display using 'suggestItem'. + * List element to show when open. */ - suggestions: JSX.Element[] | EuiSuggestItemProps[]; + suggestions: JSX.Element; - sendValue?: Function; + /** + * Callback function called when the input changes. + */ + sendValue?: (value: string) => void; }; interface Status { @@ -69,22 +75,18 @@ const statusMap: StatusMap = { loading: {}, }; -export const EuiSuggestInput: FunctionComponent = ( - props -) => { +export const EuiSuggestInput: FunctionComponent = ({ + className, + status = 'unchanged', + append, + tooltipContent, + suggestions, + sendValue, + ...rest +}) => { const [value, setValue] = useState(''); const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const { - className, - status = 'unchanged', - append, - tooltipContent, - suggestions, - sendValue, - ...rest - } = props; - const onFieldChange = (e: any) => { setValue(e.target.value); setIsPopoverOpen(e.target.value !== '' ? true : false); @@ -141,7 +143,7 @@ export const EuiSuggestInput: FunctionComponent = ( 0 && isPopoverOpen} + isOpen={isPopoverOpen} panelPaddingSize="none" fullWidth closePopover={closePopover} diff --git a/src/components/suggest/suggest_item.tsx b/src/components/suggest/suggest_item.tsx index f4de74aef99..c3eebf3938b 100644 --- a/src/components/suggest/suggest_item.tsx +++ b/src/components/suggest/suggest_item.tsx @@ -21,7 +21,7 @@ interface Type { color: string | keyof typeof colorToClassNameMap; } -interface EuiSuggestItemPropsBase { +export interface _EuiSuggestItemPropsBase { /** * Takes 'iconType' for EuiIcon and 'color'. 'color' can be tint1 through tint9. */ @@ -59,11 +59,11 @@ type PropsForButton = Omit< ButtonHTMLAttributes, 'onClick' | 'type' > & { - onClick: MouseEventHandler | undefined; + onClick?: MouseEventHandler; }; export type EuiSuggestItemProps = CommonProps & - EuiSuggestItemPropsBase & + _EuiSuggestItemPropsBase & ExclusiveUnion; interface ColorToClassMap { @@ -81,7 +81,23 @@ interface ColorToClassMap { [key: string]: string; } -type LabelWidthSize = '20' | '30' | '40' | '50' | '60' | '70' | '80' | '90'; +type LabelWidthSize = + | '20' + | '30' + | '40' + | '50' + | '60' + | '70' + | '80' + | '90' + | 20 + | 30 + | 40 + | 50 + | 60 + | 70 + | 80 + | 90; const colorToClassNameMap: ColorToClassMap = { tint0: 'euiSuggestItem__type--tint0', From 37d8c62ed7c993e4c286f55a905b78ab73d08b65 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 8 Sep 2021 15:09:50 -0500 Subject: [PATCH 03/60] CL --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8411022ecc..185ad3b47c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [`master`](https://github.com/elastic/eui/tree/master) -No public interface changes since `37.6.1`. +- Added `paddingSize` prop to `EuiSelectableList` ([#5157](https://github.com/elastic/eui/pull/5157)) +- Refactored `EuiSuggest` to use `EuiSelectable` ([#5157](https://github.com/elastic/eui/pull/5157)) ## [`37.6.1`](https://github.com/elastic/eui/tree/v37.6.1) From ec00be61b51bf33cc4665220a5894ec450bcc86f Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 8 Sep 2021 15:11:21 -0500 Subject: [PATCH 04/60] handle empty list --- src/components/suggest/suggest.tsx | 28 +++++++++++++----------- src/components/suggest/suggest_input.tsx | 4 ++-- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/components/suggest/suggest.tsx b/src/components/suggest/suggest.tsx index ba38698fe7b..d160d8c5148 100644 --- a/src/components/suggest/suggest.tsx +++ b/src/components/suggest/suggest.tsx @@ -73,19 +73,21 @@ export const EuiSuggest: FunctionComponent = ({ tooltipContent={tooltipContent} append={append} suggestions={ - - singleSelection={true} - options={suggestionList} - listProps={{ - bordered: true, - showIcons: false, - onFocusBadge: false, - paddingSize: 'none', - }} - renderOption={renderOption} - > - {(list) => list} - + suggestionList.length > 0 ? ( + + singleSelection={true} + options={suggestionList} + listProps={{ + bordered: true, + showIcons: false, + onFocusBadge: false, + paddingSize: 'none', + }} + renderOption={renderOption} + > + {(list) => list} + + ) : undefined } {...rest} /> diff --git a/src/components/suggest/suggest_input.tsx b/src/components/suggest/suggest_input.tsx index 62e724fa81a..f8b5adf06ba 100644 --- a/src/components/suggest/suggest_input.tsx +++ b/src/components/suggest/suggest_input.tsx @@ -36,7 +36,7 @@ export type EuiSuggestInputProps = CommonProps & /** * List element to show when open. */ - suggestions: JSX.Element; + suggestions?: JSX.Element; /** * Callback function called when the input changes. @@ -143,7 +143,7 @@ export const EuiSuggestInput: FunctionComponent = ({ Date: Wed, 29 Sep 2021 14:23:36 -0500 Subject: [PATCH 05/60] WIP: useCombobox --- src/components/form/form_row/form_row.tsx | 3 + src/components/selectable/selectable.tsx | 95 +++++++++------ .../selectable_list/selectable_list.tsx | 1 + src/components/suggest/suggest.tsx | 111 ++++++++++++------ src/components/suggest/suggest_input.tsx | 11 +- src/services/hooks/index.ts | 1 + src/services/hooks/useCombobox.ts | 110 +++++++++++++++++ src/services/index.ts | 8 +- 8 files changed, 263 insertions(+), 77 deletions(-) create mode 100644 src/services/hooks/useCombobox.ts diff --git a/src/components/form/form_row/form_row.tsx b/src/components/form/form_row/form_row.tsx index d83b8a31e48..ca4d03b9877 100644 --- a/src/components/form/form_row/form_row.tsx +++ b/src/components/form/form_row/form_row.tsx @@ -220,18 +220,21 @@ export class EuiFormRow extends Component { let optionalLabel; const isLegend = label && labelType === 'legend' ? true : false; + const labelId = `${id}_label`; if (label || labelAppend) { let labelProps = {}; if (isLegend) { labelProps = { type: labelType, + id: labelId, }; } else { labelProps = { htmlFor: hasChildLabel ? id : undefined, ...(!isDisabled && { isFocused: this.state.isFocused }), // If the row is disabled, don't pass the isFocused state. type: labelType, + id: labelId, }; } optionalLabel = ( diff --git a/src/components/selectable/selectable.tsx b/src/components/selectable/selectable.tsx index 83da8e18337..439b77ee828 100644 --- a/src/components/selectable/selectable.tsx +++ b/src/components/selectable/selectable.tsx @@ -131,6 +131,8 @@ export type EuiSelectableProps = CommonProps & * Default: false */ isPreFiltered?: boolean; + onActiveOptionIndexChange?: (activeOptionIndex?: number) => void; + optionIdGenerator?: (index?: number) => string; }; export interface EuiSelectableState { @@ -153,10 +155,19 @@ export class EuiSelectable extends Component< private containerRef = createRef(); private optionsListRef = createRef>(); private preventOnFocus = false; - rootId = htmlIdGenerator(); + rootId: (suffix?: string) => string; + messageContentId: string; + listId: string; constructor(props: EuiSelectableProps) { super(props); + this.rootId = props.id + ? (suffix) => `${props.id}${suffix ? `_${suffix}` : ''}` + : htmlIdGenerator(); + + this.listId = this.rootId(); + this.messageContentId = this.rootId('messageContent'); + const { options, singleSelection, isPreFiltered } = props; const initialSearchValue = ''; @@ -213,6 +224,11 @@ export class EuiSelectable extends Component< return this.state.activeOptionIndex != null; }; + onActiveOptionIndexChange = () => { + this.props.onActiveOptionIndexChange && + this.props.onActiveOptionIndexChange(this.state.activeOptionIndex); + }; + onMouseDown = () => { // Bypass onFocus when a click event originates from this.containerRef. // Prevents onFocus from scrolling away from a clicked option and negating the selection event. @@ -235,14 +251,20 @@ export class EuiSelectable extends Component< ); if (firstSelected > -1) { - this.setState({ activeOptionIndex: firstSelected, isFocused: true }); + this.setState( + { activeOptionIndex: firstSelected, isFocused: true }, + this.onActiveOptionIndexChange + ); } else { - this.setState({ - activeOptionIndex: this.state.visibleOptions.findIndex( - (option) => !option.disabled && !option.isGroupLabel - ), - isFocused: true, - }); + this.setState( + { + activeOptionIndex: this.state.visibleOptions.findIndex( + (option) => !option.disabled && !option.isGroupLabel + ), + isFocused: true, + }, + this.onActiveOptionIndexChange + ); } }; @@ -317,7 +339,7 @@ export class EuiSelectable extends Component< } return { activeOptionIndex: nextActiveOptionIndex }; - }); + }, this.onActiveOptionIndexChange); }; onSearchChange = ( @@ -344,16 +366,18 @@ export class EuiSelectable extends Component< onContainerBlur = (e: React.FocusEvent) => { // Ignore blur events when moving from search to option to avoid activeOptionIndex conflicts if ( - ((e.relatedTarget as Node)?.firstChild as HTMLElement)?.id === - this.rootId('listbox') + ((e.relatedTarget as Node)?.firstChild as HTMLElement)?.id === this.listId ) { return; } - this.setState({ - activeOptionIndex: undefined, - isFocused: false, - }); + this.setState( + { + activeOptionIndex: undefined, + isFocused: false, + }, + this.onActiveOptionIndexChange + ); }; onOptionClick = (options: Array>) => { @@ -380,6 +404,14 @@ export class EuiSelectable extends Component< this.optionsListRef.current?.listRef?.scrollToItem(index, align); }; + makeOptionId = (index?: number) => { + if (this.props.optionIdGenerator) { + return this.props.optionIdGenerator(index); + } + + return index != null ? `${this.listId}_option-${index}` : ''; + }; + render() { const { id, @@ -401,6 +433,8 @@ export class EuiSelectable extends Component< noMatchesMessage, emptyMessage, isPreFiltered, + onActiveOptionIndexChange, + optionIdGenerator, ...rest } = this.props; @@ -435,17 +469,6 @@ export class EuiSelectable extends Component< className ); - /** Create Id's */ - let messageContentId = this.rootId('messageContent'); - const listId = this.rootId('listbox'); - const makeOptionId = (index: number | undefined) => { - if (typeof index === 'undefined') { - return ''; - } - - return `${listId}_option-${index}`; - }; - /** Create message content that replaces the list if no options are available (yet) */ let messageContent: ReactNode | undefined; if (isLoading) { @@ -466,7 +489,7 @@ export class EuiSelectable extends Component< ); } else { messageContent = React.cloneElement(loadingMessage, { - id: messageContentId, + id: this.messageContentId, ...loadingMessage.props, }); } @@ -488,7 +511,7 @@ export class EuiSelectable extends Component< ); } else { messageContent = React.cloneElement(noMatchesMessage, { - id: messageContentId, + id: this.messageContentId, ...noMatchesMessage.props, }); } @@ -506,12 +529,12 @@ export class EuiSelectable extends Component< ); } else { messageContent = React.cloneElement(emptyMessage, { - id: messageContentId, + id: this.messageContentId, ...emptyMessage.props, }); } } else { - messageContentId = ''; + this.messageContentId = ''; } /** @@ -558,7 +581,7 @@ export class EuiSelectable extends Component< const searchAccessibleName = getAccessibleName( searchProps, - messageContentId + this.messageContentId ); const searchHasAccessibleName = Boolean( Object.keys(searchAccessibleName).length @@ -570,8 +593,8 @@ export class EuiSelectable extends Component< key="listSearch" options={options} onChange={this.onSearchChange} - listId={this.optionsListRef.current ? listId : undefined} // Only pass the listId if it exists on the page - aria-activedescendant={makeOptionId(activeOptionIndex)} // the current faux-focused option + listId={this.optionsListRef.current ? this.listId : undefined} // Only pass the listId if it exists on the page + aria-activedescendant={this.makeOptionId(activeOptionIndex)} // the current faux-focused option placeholder={placeholderName} isPreFiltered={isPreFiltered ?? false} {...(searchHasAccessibleName @@ -589,7 +612,7 @@ export class EuiSelectable extends Component< ); const list = messageContent ? ( {messageContent} @@ -613,8 +636,8 @@ export class EuiSelectable extends Component< height={height} allowExclusions={allowExclusions} searchable={searchable} - makeOptionId={makeOptionId} - listId={listId} + makeOptionId={this.makeOptionId} + listId={this.listId} {...(listHasAccessibleName ? listAccessibleName : searchable && { 'aria-label': placeholderName })} diff --git a/src/components/selectable/selectable_list/selectable_list.tsx b/src/components/selectable/selectable_list/selectable_list.tsx index 888d7e52743..fc030df6db9 100644 --- a/src/components/selectable/selectable_list/selectable_list.tsx +++ b/src/components/selectable/selectable_list/selectable_list.tsx @@ -288,6 +288,7 @@ export class EuiSelectableList extends Component> { 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby, + role, ...rest } = this.props; diff --git a/src/components/suggest/suggest.tsx b/src/components/suggest/suggest.tsx index c58bc9034bb..24e963bbebd 100644 --- a/src/components/suggest/suggest.tsx +++ b/src/components/suggest/suggest.tsx @@ -8,8 +8,11 @@ import React, { FunctionComponent } from 'react'; import classNames from 'classnames'; -import { CommonProps } from '../common'; +import { CommonProps, ExclusiveUnion } from '../common'; +import { useCombobox } from '../../services'; + +import { EuiScreenReaderOnly } from '../accessibility'; import { EuiSelectable, EuiSelectableListItemProps } from '../selectable'; import { EuiSuggestItem, _EuiSuggestItemPropsBase } from './suggest_item'; @@ -21,7 +24,7 @@ export interface EuiSuggestionProps onClick?: EuiSelectableListItemProps['onClick']; } -export type EuiSuggestProps = CommonProps & +type _EuiSuggestProps = CommonProps & Omit & { /** * List of suggestions to display using EuiSuggestItem. @@ -37,6 +40,18 @@ export type EuiSuggestProps = CommonProps & onInputChange?: (target: EventTarget) => void; }; +export type EuiSuggestProps = _EuiSuggestProps & + ExclusiveUnion< + { + 'aria-label': string; + 'aria-labelledby'?: string; + }, + { + 'aria-label'?: string; + 'aria-labelledby': string; + } + >; + export const EuiSuggest: FunctionComponent = ({ onItemClick, onInputChange, @@ -44,8 +59,25 @@ export const EuiSuggest: FunctionComponent = ({ append, tooltipContent, suggestions, + id, + 'aria-label': ariaLabel, + 'aria-labelledby': labelId, ...rest }) => { + const { + containerAttributes, + inputAttributes, + listAttributes: { id: listAttrsId, ...listAttrs }, + infoAttributes, + instructionsAttributes, + methods: { optionIdGenerator, setFocusedOptionIndex, setListBoxOpen }, + state: { isListBoxOpen }, + } = useCombobox({ + id, + ariaLabel, + labelId, + }); + const onChange = (e: React.FormEvent) => { onInputChange ? onInputChange(e.target) : null; }; @@ -68,36 +100,49 @@ export const EuiSuggest: FunctionComponent = ({ }; return ( -
    - 0 ? ( - - singleSelection={true} - options={suggestionList} - listProps={{ - bordered: true, - showIcons: false, - onFocusBadge: false, - paddingSize: 'none', - }} - renderOption={renderOption} - > - {(list) => list} - - ) : undefined - } - {...rest} - /> -
    + <> +
    + 0 ? ( + + id={listAttrsId} + singleSelection={true} + options={suggestionList} + listProps={{ + bordered: true, + showIcons: false, + onFocusBadge: false, + paddingSize: 'none', + ...listAttrs, + }} + renderOption={renderOption} + optionIdGenerator={optionIdGenerator} + onActiveOptionIndexChange={setFocusedOptionIndex} + > + {(list) => list} + + ) : undefined + } + {...rest} + {...inputAttributes} + /> +
    + +
    +

    + State: {isListBoxOpen ? 'list exanded' : 'list collapsed'}. +

    +

    + Use up and down arrows to move focus over options. Enter to select. + Escape to collapse options. +

    +
    +
    + ); }; diff --git a/src/components/suggest/suggest_input.tsx b/src/components/suggest/suggest_input.tsx index f8b5adf06ba..08367af0637 100644 --- a/src/components/suggest/suggest_input.tsx +++ b/src/components/suggest/suggest_input.tsx @@ -42,6 +42,7 @@ export type EuiSuggestInputProps = CommonProps & * Callback function called when the input changes. */ sendValue?: (value: string) => void; + onListOpen?: (isOpen: boolean) => void; }; interface Status { @@ -82,6 +83,7 @@ export const EuiSuggestInput: FunctionComponent = ({ tooltipContent, suggestions, sendValue, + onListOpen, ...rest }) => { const [value, setValue] = useState(''); @@ -89,12 +91,18 @@ export const EuiSuggestInput: FunctionComponent = ({ const onFieldChange = (e: any) => { setValue(e.target.value); - setIsPopoverOpen(e.target.value !== '' ? true : false); + e.target.value !== '' ? openPopover() : closePopover(); if (sendValue) sendValue(e.target.value); }; + const openPopover = () => { + setIsPopoverOpen(true); + onListOpen && onListOpen(true); + }; + const closePopover = () => { setIsPopoverOpen(false); + onListOpen && onListOpen(false); }; let icon = ''; @@ -147,6 +155,7 @@ export const EuiSuggestInput: FunctionComponent = ({ panelPaddingSize="none" fullWidth closePopover={closePopover} + title="gkgkgk" > {suggestions}
    diff --git a/src/services/hooks/index.ts b/src/services/hooks/index.ts index 7c6224ff242..3a2bdcafbb6 100644 --- a/src/services/hooks/index.ts +++ b/src/services/hooks/index.ts @@ -7,6 +7,7 @@ */ export * from './useCombinedRefs'; +export * from './useCombobox'; export * from './useDependentState'; export * from './useIsWithinBreakpoints'; export * from './useMouseMove'; diff --git a/src/services/hooks/useCombobox.ts b/src/services/hooks/useCombobox.ts new file mode 100644 index 00000000000..86cf8bbafa4 --- /dev/null +++ b/src/services/hooks/useCombobox.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + HTMLAttributes, + InputHTMLAttributes, + LabelHTMLAttributes, + useState, +} from 'react'; + +import { useGeneratedHtmlId } from '../accessibility'; + +// https://www.w3.org/TR/wai-aria-practices/#wai-aria-roles-states-and-properties-6 +// Combobox implementing the ARIA 1.1 pattern + +export interface UseComboBoxInterface { + id?: string; + ariaLabel?: string; + labelId?: string; +} +export interface UseComboBox { + containerAttributes: HTMLAttributes; + inputAttributes: InputHTMLAttributes; + listAttributes: HTMLAttributes; + labelAttributes: LabelHTMLAttributes; + infoAttributes: HTMLAttributes; + instructionsAttributes: HTMLAttributes; + methods: { + optionIdGenerator: (index?: number) => string; + setListBoxOpen: (isOpen: boolean) => void; + setFocusedOptionIndex: (index?: number) => void; + }; + state: { + isListBoxOpen: boolean; + focusedOptionIndex?: number; + }; +} +export const useCombobox = ({ + id, + ariaLabel, + labelId: _labelId, +}: UseComboBoxInterface): UseComboBox => { + const [isListBoxOpen, setListBoxOpen] = useState(false); + const [focusedOptionIndex, setFocusedOptionIndex] = useState(); + + const rootId = useGeneratedHtmlId({ + prefix: id ? 'euiSuggest' : undefined, + conditionalId: id, + }); + + const collectionId = `${rootId}_collection`; + const infoId = `${rootId}_info`; + const inputId = rootId; + const instructionsId = `${rootId}_instructions`; + const labelId = _labelId; + + const optionIdGenerator = (index?: number) => + index != null ? `${collectionId}_option-${index}` : ''; + + return { + containerAttributes: { + role: 'combobox', + 'aria-expanded': isListBoxOpen, + 'aria-haspopup': isListBoxOpen ? 'listbox' : undefined, + 'aria-owns': isListBoxOpen ? collectionId : undefined, + }, + inputAttributes: { + role: 'textbox', + id: inputId, + autoComplete: 'off', + 'aria-activedescendant': optionIdGenerator(focusedOptionIndex), + 'aria-autocomplete': 'list', + 'aria-controls': isListBoxOpen ? collectionId : undefined, + 'aria-describedby': `${infoId} ${instructionsId}`, + 'aria-label': !labelId ? ariaLabel : undefined, + 'aria-labelledby': labelId, + }, + listAttributes: { + role: 'listbox', + id: collectionId, + 'aria-label': !labelId ? ariaLabel : undefined, + 'aria-labelledby': labelId, + }, + // + labelAttributes: { + id: labelId, + htmlFor: inputId, + }, + infoAttributes: { + id: infoId, + }, + instructionsAttributes: { + id: instructionsId, + }, + methods: { + optionIdGenerator, + setListBoxOpen, + setFocusedOptionIndex, + }, + state: { + isListBoxOpen, + focusedOptionIndex, + }, + }; +}; diff --git a/src/services/index.ts b/src/services/index.ts index a0d5677f501..7efd59e13bf 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -123,13 +123,7 @@ export { export { EuiWindowEvent } from './window_event'; -export { - useCombinedRefs, - useDependentState, - useIsWithinBreakpoints, - useMouseMove, - isMouseEvent, -} from './hooks'; +export * from './hooks'; export { throttle } from './throttle'; From 9abae26234f801b4e9f6463bf27072c96515b7fb Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 29 Sep 2021 14:24:14 -0500 Subject: [PATCH 06/60] WIP: temporary docs --- src-docs/src/views/suggest/suggest.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src-docs/src/views/suggest/suggest.js b/src-docs/src/views/suggest/suggest.js index e233d08a0db..96bdfc08b2a 100644 --- a/src-docs/src/views/suggest/suggest.js +++ b/src-docs/src/views/suggest/suggest.js @@ -4,6 +4,7 @@ import { EuiRadioGroup, EuiSuggest, EuiSpacer, + EuiFormRow, } from '../../../../src/components'; import { htmlIdGenerator } from '../../../../src/services'; @@ -70,7 +71,19 @@ export default () => { onChange={(id) => onChange(id)} /> + + {}} + onItemClick={onItemClick} + placeholder="Enter query to display suggestions" + suggestions={sampleItems} + /> + + {}} onItemClick={onItemClick} From 58eb516cd7dc1e5ac70f1cfb249de8247dd249d9 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Thu, 30 Sep 2021 10:19:47 -0500 Subject: [PATCH 07/60] snapshots --- CHANGELOG.md | 6 +- .../described_form_group.test.tsx.snap | 2 + .../__snapshots__/form_row.test.tsx.snap | 12 + .../__snapshots__/selectable.test.tsx.snap | 4 +- .../__snapshots__/suggest.test.tsx.snap | 802 ++++++++++++------ src/components/suggest/suggest_input.tsx | 1 - 6 files changed, 570 insertions(+), 257 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33e8b576dc4..59fa5ad0fef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## [`master`](https://github.com/elastic/eui/tree/master) -No public interface changes since `38.2.0`. +- Added `useCombobox` hook for creating custom combobox components ([#5157](https://github.com/elastic/eui/pull/5157)) +- Added `paddingSize` prop to `EuiSelectableList` ([#5157](https://github.com/elastic/eui/pull/5157)) +- Refactored `EuiSuggest` to use `EuiSelectable` ([#5157](https://github.com/elastic/eui/pull/5157)) ## [`38.2.0`](https://github.com/elastic/eui/tree/v38.2.0) @@ -73,8 +75,6 @@ No public interface changes since `38.2.0`. - Added the `repositionOnScroll` prop to `EuiSuperSelect` ([#5155](https://github.com/elastic/eui/pull/5155)) - Added `useGeneratedHtmlId` utility, which memoizes the randomly generated ID on mount and prevents regenerated IDs on component rerender ([#5133](https://github.com/elastic/eui/pull/5133)) - Fixed `z-index` styles that were causing parts of `EuiResizableContainer` to overlap `EuiHeader` ([#5164](https://github.com/elastic/eui/pull/5164)) -- Added `paddingSize` prop to `EuiSelectableList` ([#5157](https://github.com/elastic/eui/pull/5157)) -- Refactored `EuiSuggest` to use `EuiSelectable` ([#5157](https://github.com/elastic/eui/pull/5157)) **Bug fixes** diff --git a/src/components/form/described_form_group/__snapshots__/described_form_group.test.tsx.snap b/src/components/form/described_form_group/__snapshots__/described_form_group.test.tsx.snap index c8117412922..68555458dde 100644 --- a/src/components/form/described_form_group/__snapshots__/described_form_group.test.tsx.snap +++ b/src/components/form/described_form_group/__snapshots__/described_form_group.test.tsx.snap @@ -677,6 +677,7 @@ exports[`EuiDescribedFormGroup ties together parts for accessibility 1`] = ` aria-invalid={true} className="euiFormRow__label" htmlFor="generated-id" + id="generated-id_label" isFocused={false} isInvalid={true} type="label" @@ -685,6 +686,7 @@ exports[`EuiDescribedFormGroup ties together parts for accessibility 1`] = ` aria-invalid={true} className="euiFormLabel euiFormRow__label euiFormLabel-isInvalid" htmlFor="generated-id" + id="generated-id_label" > Label diff --git a/src/components/form/form_row/__snapshots__/form_row.test.tsx.snap b/src/components/form/form_row/__snapshots__/form_row.test.tsx.snap index 49bf5f2de39..b7799aadd35 100644 --- a/src/components/form/form_row/__snapshots__/form_row.test.tsx.snap +++ b/src/components/form/form_row/__snapshots__/form_row.test.tsx.snap @@ -24,12 +24,14 @@ exports[`EuiFormRow behavior onBlur is called in child 1`] = `