diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b6912cc787..573012faaff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,8 @@
- Modifying drop shadow intensities and color. ([607](https://github.com/elastic/eui/pull/607))
- Add Sass color functions. Make `$euiColorWarning` color usage more accessible while still being "yellow". ([628](https://github.com/elastic/eui/pull/628))
- Removed extraneous `global_styling/mixins/_forms.scss` file and importing the correct files in the `filter_group.scss` and `combo_box.scss` files. ([#609](https://github.com/elastic/eui/pull/609))
+- Added `isInvalid` prop to `EuiComboBox` ([#631](https://github.com/elastic/eui/pull/631))
+- Added support for rejecting user input by returning `false` from the `onCreateOption` prop of `EuiComboBox` ([#631](https://github.com/elastic/eui/pull/631))
**Bug fixes**
@@ -10,6 +12,9 @@
- `EuiSelect` can pass any node as a value rather than just a string ([#603](https://github.com/elastic/eui/pull/603))
- Fixed a typo in the flex TypeScript definition ([#629](https://github.com/elastic/eui/pull/629))
- Fixed `EuiComboBox` bug in which the options list wouldn't always match the width of the input ([#611](https://github.com/elastic/eui/pull/611))
+- Fixed `EuiComboBox` bug in which opening the combo box when there's no scrollbar on the window would result in the list being positioned incorrectly ([#631](https://github.com/elastic/eui/pull/631))
+- Fixed `EuiComboBox` bug in which clicking a pill's close button would close the list ([#631](https://github.com/elastic/eui/pull/631))
+- Fixed `EuiComboBox` bug in which moving focus from one combo box to another would remove the `euiBody-hasPortalContent` class from the body. ([#631](https://github.com/elastic/eui/pull/631))
# [`0.0.37`](https://github.com/elastic/eui/tree/v0.0.37)
diff --git a/src-docs/src/views/combo_box/combo_box_example.js b/src-docs/src/views/combo_box/combo_box_example.js
index e35a35c5ec0..f13e55ba154 100644
--- a/src-docs/src/views/combo_box/combo_box_example.js
+++ b/src-docs/src/views/combo_box/combo_box_example.js
@@ -198,7 +198,7 @@ export const ComboBoxExample = {
props: { EuiComboBox },
demo: ,
}, {
- title: 'Hiding suggestions',
+ title: 'Custom options only, with validation',
source: [{
type: GuideSectionTypes.JS,
code: customOptionsOnlySource,
diff --git a/src-docs/src/views/combo_box/custom_options_only.js b/src-docs/src/views/combo_box/custom_options_only.js
index a2d6bed25c0..dd150bb6086 100644
--- a/src-docs/src/views/combo_box/custom_options_only.js
+++ b/src-docs/src/views/combo_box/custom_options_only.js
@@ -2,22 +2,28 @@ import React, { Component } from 'react';
import {
EuiComboBox,
+ EuiFormRow,
} from '../../../../src/components';
+const isValid = (value) => {
+ // Only allow letters. No spaces, numbers, or special characters.
+ return value.match(/^[a-zA-Z]+$/) !== null;
+};
+
export default class extends Component {
constructor(props) {
super(props);
this.state = {
+ isInvalid: false,
selectedOptions: [],
};
}
onCreateOption = (searchValue) => {
- const normalizedSearchValue = searchValue.trim().toLowerCase();
-
- if (!normalizedSearchValue) {
- return;
+ if (!isValid(searchValue)) {
+ // Return false to explicitly reject the user's input.
+ return false;
}
const newOption = {
@@ -30,22 +36,45 @@ export default class extends Component {
}));
};
+ onSearchChange = (searchValue) => {
+ if (!searchValue) {
+ this.setState({
+ isInvalid: false,
+ });
+
+ return;
+ }
+
+ this.setState({
+ isInvalid: !isValid(searchValue),
+ });
+ };
+
onChange = (selectedOptions) => {
this.setState({
selectedOptions,
+ isInvalid: false,
});
};
render() {
- const { selectedOptions } = this.state;
+ const { selectedOptions, isInvalid } = this.state;
return (
-
+
+
+
);
}
}
diff --git a/src/components/combo_box/_combo_box.scss b/src/components/combo_box/_combo_box.scss
index 6447beb2f3b..e42df2a9601 100644
--- a/src/components/combo_box/_combo_box.scss
+++ b/src/components/combo_box/_combo_box.scss
@@ -54,4 +54,10 @@
inset 0 -2px 0 0 $euiColorPrimary;
}
}
+
+ &.euiComboBox-isInvalid {
+ .euiComboBox__inputWrap {
+ @include euiFormControlInvalidStyle;
+ }
+ }
}
diff --git a/src/components/combo_box/combo_box.js b/src/components/combo_box/combo_box.js
index 87b08d21667..49851787e4f 100644
--- a/src/components/combo_box/combo_box.js
+++ b/src/components/combo_box/combo_box.js
@@ -37,6 +37,7 @@ export class EuiComboBox extends Component {
onSearchChange: PropTypes.func,
onCreateOption: PropTypes.func,
renderOption: PropTypes.func,
+ isInvalid: PropTypes.bool,
}
static defaultProps = {
@@ -190,6 +191,11 @@ export class EuiComboBox extends Component {
}
};
+ focusSearchInput = () => {
+ this.clearActiveOption();
+ this.searchInput.focus();
+ };
+
clearSearchValue = () => {
this.onSearchChange('');
};
@@ -229,7 +235,13 @@ export class EuiComboBox extends Component {
// Add new custom pill if this is custom input, even if it partially matches an option..
if (!this.hasActiveOption() || this.doesSearchMatchOnlyOption()) {
- this.props.onCreateOption(this.state.searchValue, flattenOptionGroups(this.props.options));
+ const isOptionCreated = this.props.onCreateOption(this.state.searchValue, flattenOptionGroups(this.props.options));
+
+ // Expect the consumer to be explicit in rejecting a custom option.
+ if (isOptionCreated === false) {
+ return;
+ }
+
this.clearSearchValue();
}
};
@@ -251,7 +263,19 @@ export class EuiComboBox extends Component {
return flattenOptionGroups(options).length === selectedOptions.length;
};
- onFocusChange = event => {
+ onFocus = () => {
+ document.addEventListener('click', this.onDocumentFocusChange);
+ document.addEventListener('focusin', this.onDocumentFocusChange);
+ this.openList();
+ }
+
+ onBlur = () => {
+ document.removeEventListener('click', this.onDocumentFocusChange);
+ document.removeEventListener('focusin', this.onDocumentFocusChange);
+ this.closeList();
+ }
+
+ onDocumentFocusChange = event => {
// Close the list if the combo box has lost focus.
if (
this.comboBox === event.target
@@ -264,7 +288,11 @@ export class EuiComboBox extends Component {
// Wait for the DOM to update.
requestAnimationFrame(() => {
- this.closeList();
+ if (document.activeElement === this.searchInput) {
+ return;
+ }
+
+ this.onBlur();
});
};
@@ -287,8 +315,7 @@ export class EuiComboBox extends Component {
case ESCAPE:
// Move focus from options list to input.
if (this.hasActiveOption()) {
- this.clearActiveOption();
- this.searchInput.focus();
+ this.focusSearchInput();
}
break;
@@ -319,14 +346,14 @@ export class EuiComboBox extends Component {
onAddOption = (addedOption) => {
const { onChange, selectedOptions, singleSelection } = this.props;
onChange(singleSelection ? [addedOption] : selectedOptions.concat(addedOption));
- this.clearActiveOption();
this.clearSearchValue();
- this.searchInput.focus();
+ this.focusSearchInput();
};
onRemoveOption = (removedOption) => {
const { onChange, selectedOptions } = this.props;
onChange(selectedOptions.filter(option => option !== removedOption));
+ this.focusSearchInput();
};
onComboBoxClick = () => {
@@ -381,9 +408,6 @@ export class EuiComboBox extends Component {
};
componentDidMount() {
- document.addEventListener('click', this.onFocusChange);
- document.addEventListener('focusin', this.onFocusChange);
-
// TODO: This will need to be called once the actual stylesheet loads.
setTimeout(() => {
this.autoSizeInput.copyInputStyles();
@@ -419,8 +443,8 @@ export class EuiComboBox extends Component {
}
componentWillUnmount() {
- document.removeEventListener('click', this.onFocusChange);
- document.removeEventListener('focusin', this.onFocusChange);
+ document.removeEventListener('click', this.onDocumentFocusChange);
+ document.removeEventListener('focusin', this.onDocumentFocusChange);
}
render() {
@@ -438,6 +462,7 @@ export class EuiComboBox extends Component {
onChange, // eslint-disable-line no-unused-vars
onSearchChange, // eslint-disable-line no-unused-vars
async, // eslint-disable-line no-unused-vars
+ isInvalid,
...rest
} = this.props;
@@ -445,6 +470,7 @@ export class EuiComboBox extends Component {
const classes = classNames('euiComboBox', className, {
'euiComboBox-isOpen': isListOpen,
+ 'euiComboBox-isInvalid': isInvalid,
});
const value = selectedOptions.map(selectedOption => selectedOption.label).join(', ');
@@ -491,7 +517,7 @@ export class EuiComboBox extends Component {
onRemoveOption={this.onRemoveOption}
onClick={this.onComboBoxClick}
onChange={this.onSearchChange}
- onFocus={this.openList}
+ onFocus={this.onFocus}
value={value}
searchValue={searchValue}
autoSizeInputRef={this.autoSizeInputRef}
diff --git a/src/components/combo_box/combo_box_input/combo_box_input.js b/src/components/combo_box/combo_box_input/combo_box_input.js
index 0944c876668..244d08dd4c3 100644
--- a/src/components/combo_box/combo_box_input/combo_box_input.js
+++ b/src/components/combo_box/combo_box_input/combo_box_input.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import AutosizeInput from 'react-input-autosize';
import { EuiScreenReaderOnly } from '../../accessibility';
-import { EuiFormControlLayout, EuiValidatableControl } from '../../form';
+import { EuiFormControlLayout } from '../../form';
import { EuiComboBoxPill } from './combo_box_pill';
import { htmlIdGenerator } from '../../../services';
@@ -143,20 +143,18 @@ export class EuiComboBoxInput extends Component {
>
{pills}
{placeholderMessage}
-
- onChange(e.target.value)}
- value={searchValue}
- ref={autoSizeInputRef}
- inputRef={inputRef}
- />
-
+ onChange(e.target.value)}
+ value={searchValue}
+ ref={autoSizeInputRef}
+ inputRef={inputRef}
+ />
{removeOptionMessage}
diff --git a/src/components/combo_box/combo_box_input/combo_box_pill.js b/src/components/combo_box/combo_box_input/combo_box_pill.js
index 697ed24c281..bb637b668e7 100644
--- a/src/components/combo_box/combo_box_input/combo_box_pill.js
+++ b/src/components/combo_box/combo_box_input/combo_box_pill.js
@@ -21,19 +21,16 @@ export class EuiComboBoxPill extends Component {
color: 'hollow',
};
- onCloseButtonClick(option, e) {
- // Prevent the combo box from losing focus.
- e.preventDefault();
- e.stopPropagation();
- e.nativeEvent.stopImmediatePropagation();
- this.props.onClose(option)
- }
+ onCloseButtonClick = () => {
+ const { onClose, option } = this.props;
+ onClose(option);
+ };
render() {
const {
children,
className,
- option,
+ option, // eslint-disable-line no-unused-vars
onClose, // eslint-disable-line no-unused-vars
color,
...rest
@@ -44,7 +41,7 @@ export class EuiComboBoxPill extends Component {
{
- // Prevent the combo box from losing focus.
- e.preventDefault();
- e.stopPropagation();
- e.nativeEvent.stopImmediatePropagation();
+ onClick = () => {
const { onClick, option } = this.props;
onClick(option);
};
diff --git a/src/components/combo_box/combo_box_options_list/combo_box_options_list.js b/src/components/combo_box/combo_box_options_list/combo_box_options_list.js
index 9f9e6d62b3d..e23be602202 100644
--- a/src/components/combo_box/combo_box_options_list/combo_box_options_list.js
+++ b/src/components/combo_box/combo_box_options_list/combo_box_options_list.js
@@ -46,11 +46,13 @@ export class EuiComboBoxOptionsList extends Component {
};
componentDidMount() {
- document.body.classList.add('euiBody-hasPortalContent');
-
+ // Wait a frame, otherwise moving focus from one combo box to another will result in the class
+ // being removed from the body.
+ requestAnimationFrame(() => {
+ document.body.classList.add('euiBody-hasPortalContent');
+ });
this.updatePosition();
window.addEventListener('resize', this.updatePosition);
- window.addEventListener('scroll', this.updatePosition);
}
componentWillUpdate(nextProps) {
@@ -69,7 +71,6 @@ export class EuiComboBoxOptionsList extends Component {
componentWillUnmount() {
document.body.classList.remove('euiBody-hasPortalContent');
window.removeEventListener('resize', this.updatePosition);
- window.removeEventListener('scroll', this.updatePosition);
}
listRef = node => {