diff --git a/docs/src/services/routes/routes.js b/docs/src/services/routes/routes.js index 812b9003219..d542b7b3edf 100644 --- a/docs/src/services/routes/routes.js +++ b/docs/src/services/routes/routes.js @@ -27,6 +27,9 @@ import CallOutExample import CodeExample from '../../views/code/code_example'; +import ComboBoxExample + from '../../views/combo_box/combo_box_example'; + import ContextMenuExample from '../../views/context_menu/context_menu_example'; @@ -135,6 +138,10 @@ const components = [{ name: 'Code', component: CodeExample, hasReact: true, +}, { + name: 'ComboBox', + component: ComboBoxExample, + hasReact: true, }, { name: 'ContextMenu', component: ContextMenuExample, diff --git a/docs/src/views/combo_box/combo_box.js b/docs/src/views/combo_box/combo_box.js new file mode 100644 index 00000000000..dd2ecb562fd --- /dev/null +++ b/docs/src/views/combo_box/combo_box.js @@ -0,0 +1,50 @@ +import React, { + Component, +} from 'react'; + +import { + EuiComboBox, +} from '../../../../src/components'; + +export default class extends Component { + constructor(props) { + super(props); + + this.state = { + isPopoverOpen: false, + }; + } + + closePopover() { + this.setState({ + isPopoverOpen: false, + }); + } + + render() { + return ( + + ); + } +} diff --git a/docs/src/views/combo_box/combo_box_example.js b/docs/src/views/combo_box/combo_box_example.js new file mode 100644 index 00000000000..212fb9d2151 --- /dev/null +++ b/docs/src/views/combo_box/combo_box_example.js @@ -0,0 +1,45 @@ +import React from 'react'; + +import { renderToHtml } from '../../services'; + +import { + GuidePage, + GuideSection, + GuideSectionTypes, +} from '../../components'; + +import { + EuiCode, + EuiCallOut, + EuiSpacer, +} from '../../../../src/components'; + +import ComboBox from './combo_box'; +const comboBoxSource = require('!!raw-loader!./combo_box'); +const comboBoxHtml = renderToHtml(ComboBox); + +export default props => ( + + + + + Design prototype of ComboBox component. It is not usable. +

+ } + demo={} + /> +
+); diff --git a/src/components/form/_index.scss b/src/components/form/_index.scss index f0661ab900c..57d0323e8fd 100644 --- a/src/components/form/_index.scss +++ b/src/components/form/_index.scss @@ -68,6 +68,7 @@ $euiFormBackgroundColor: tintOrShade($euiColorLightestShade, 60%, 25%); } @import 'checkbox/index'; +@import 'combo_box/index'; @import 'field_number/index'; @import 'field_password/index'; @import 'field_search/index'; diff --git a/src/components/form/combo_box/_combo_box.scss b/src/components/form/combo_box/_combo_box.scss new file mode 100644 index 00000000000..818149c890e --- /dev/null +++ b/src/components/form/combo_box/_combo_box.scss @@ -0,0 +1,74 @@ +.euiComboBox { + @include euiFormControlSize; + position: relative; + + .euiComboBox__inputWrap { + @include euiFormControlStyle; + @include euiFormControlWithIcon($isIconOptional: true); + padding: $euiSizeXS; + + &:hover { + cursor: text; + } + } + + .euiComboBox__input { + padding: 0; + border: none; + background: transparent; + font-size: $euiFontSizeS; + font-family: $euiFontFamily; + color: $euiTextColor; + margin: $euiSizeXS; + } + + .euiComboBox__panel { + position: absolute; + top: 100%; + left: -1px; + right: -1px; + border-radius: 0 0 $euiBorderRadius $euiBorderRadius; + visibility: hidden; + opacity: 0; + border-top: none; + transition: + visibility $euiAnimSpeedNormal $euiAnimSlightResistance, + opacity $euiAnimSpeedNormal $euiAnimSlightResistance; + } + + .euiComoboBox__empty { + @include euiFontSizeS; + padding: $euiSizeS; + text-align: center; + color: $euiColorDarkShade; + } + + .euiComboBox__rowWrap { + @include euiScrollBar; + + padding: $euiSizeS; + max-height: 200px; + overflow-y: auto; + } + + .euiComboBox__footer { + padding: $euiSizeS; + border-top: $euiBorderThin; + background: $euiFormBackgroundColor; + } + &.euiComboBox-isOpen { + .euiComboBox__panel { + visibility: visible; + opacity: 1; + } + + .euiComboBox__inputWrap { + background: $euiColorEmptyShade; + box-shadow: + 0 4px 4px -2px rgba(0, 0, 0, 0.1), + 0 0 0 1px rgba(0,0,0,0.16), + inset 0 0 0 0 $euiColorEmptyShade, + inset 0 -2px 0 0 $euiColorPrimary; + } + } +} diff --git a/src/components/form/combo_box/_combo_box_pill.scss b/src/components/form/combo_box/_combo_box_pill.scss new file mode 100644 index 00000000000..d3e1fe081f8 --- /dev/null +++ b/src/components/form/combo_box/_combo_box_pill.scss @@ -0,0 +1,40 @@ +.euiComboBoxPill { + font-size: $euiSizeM; + line-height: $euiSizeL; + display: inline-block; + text-decoration: none; + border: solid 1px transparent; + border-radius: $euiSizeL; + padding: 0 $euiSizeS; + background-color: transparent; + white-space: nowrap; + vertical-align: middle; + border-color: $euiColorLightShade; + background-color: $euiColorEmptyShade; + margin: $euiSizeXS; + margin-right: 0; + + .euiComboBoxPill__close { + width: $euiSizeM; + height: $euiSizeM; + margin-left: $euiSizeXS; + border: none; + vertical-align: middle; + + &:hover { + .euiComboBoxPill__closeIcon { + fill: $euiColorDanger; + } + } + + &:focus { + background: transparentize($euiColorDanger, .9); + } + + svg { + width: $euiSizeM; + height: $euiSizeM; + vertical-align: top !important; + } + } +} diff --git a/src/components/form/combo_box/_combo_box_row.scss b/src/components/form/combo_box/_combo_box_row.scss new file mode 100644 index 00000000000..5f6cfb86ed8 --- /dev/null +++ b/src/components/form/combo_box/_combo_box_row.scss @@ -0,0 +1,17 @@ +.euiComboBoxRow { + font-size: $euiFontSizeS; + padding: $euiSizeXS $euiSizeS; + width: 100%; + text-align: left; + border: $euiBorderThin; + border-color: transparent; + + &:hover { + text-decoration: underline; + } + &:focus { + cursor: pointer; + color: $euiColorPrimary; + background-color: $euiFocusBackgroundColor; + } +} diff --git a/src/components/form/combo_box/_index.scss b/src/components/form/combo_box/_index.scss new file mode 100644 index 00000000000..7fd95d6daa4 --- /dev/null +++ b/src/components/form/combo_box/_index.scss @@ -0,0 +1,3 @@ +@import 'combo_box'; +@import 'combo_box_pill'; +@import 'combo_box_row'; diff --git a/src/components/form/combo_box/combo_box.js b/src/components/form/combo_box/combo_box.js new file mode 100644 index 00000000000..01107a7c06c --- /dev/null +++ b/src/components/form/combo_box/combo_box.js @@ -0,0 +1,242 @@ +import React, { + Component, +} from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiTextColor, + EuiPanel, + EuiComboBoxPill, + EuiComboBoxRow, + EuiFormControlLayout, + EuiValidatableControl, + EuiOutsideClickDetector, +} from '../../../components'; + +export class EuiComboBox extends Component { + static propTypes = { + children: PropTypes.node, + className: PropTypes.string, + } + + constructor(props) { + super(props); + + this.state = { + isPopoverOpen: this.props.isPopoverOpen, + value: '', + matches: this.props.options, + focusedRow: -1, + }; + + this.handleSearchInputFocus = this.handleSearchInputFocus.bind(this); + this.handleShowPopover = this.handleShowPopover.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.isPopoverOpen !== this.state.isPopoverOpen) { + this.setState({ isPopoverOpen: nextProps.isPopoverOpen }); + } + } + + handleSearchInputFocus() { + this.searchInput.focus(); + } + + handleShowPopover() { + this.setState({ + isPopoverOpen: true, + }); + } + + filterItems(query) { + return this.props.options.filter(function(option) { + return option.text.toLowerCase().indexOf(query.toLowerCase()) !== -1; + }); + } + + handleChange(event) { + this.setState({ + value: event.target.value, + }); + } + + handleClosePopover() { + this.setState({ + isPopoverOpen: false, + }); + } + + handleKeyDown(e) { + switch (e.keyCode) { + case 40: // Down + this.focusRowNext(); + break; + case 38: // Up + break; + case 8: // Backspace + break; + case 13: // Enter + break; + } + } + + focusRowNext() { + + let rowIndex; + if (this.state.focusedRow >= this.props.options.length - 1) { + rowIndex = this.props.options.length - 1; + } else { + rowIndex = this.props.focusedRow + 1; + } + + this.setState({ + focusedRow: rowIndex, + }); + } + + render() { + const { + children, + options, + isInvalid, + isPopoverOpen, + className, + closePopover, + selectedOptions, + optionTypeName, + ...rest, + } = this.props; + + const searchString = this.state.value.toLowerCase(); + + const matches = + this.props.options.filter(option => ( + option.text.toLowerCase().indexOf(searchString) !== -1 + )).map((option, index) => { + return ( + {option.text} + ); + }); + + const exactMatches = this.props.options.filter(function (option) { + return (option.text.toLowerCase() === searchString); + }); + + let matchesOrEmpty = null; + if (matches.length === 0) { + matchesOrEmpty = ( +
+ No {optionTypeName} matches your search. +
+ ); + } else { + matchesOrEmpty = matches; + } + + const classes = classNames( + 'euiComboBox', + { + 'euiComboBox-isOpen': this.state.isPopoverOpen, + }, + className + ); + + const panelClasses = classNames( + 'euiComboBox__panel', + ); + + let footer = null; + + if ((exactMatches.length === 0) && (searchString !== '')) { + footer = ( +
+ + + + + Not listed? + + + + + Add {this.state.value} + + +
+ ); + } else if (searchString === '') { + footer = ( +
+ + + Start typing to add a new {optionTypeName}. + + +
+ ); + } + + return ( + +
+ +
+ {selectedOptions.map((option, index) => { + return {option.text}; + })} + + + { this.searchInput = input; }} + /> + +
+
+ +
+ {matchesOrEmpty} +
+ {footer} +
+
+
+ ); + } +} + +EuiComboBox.propTypes = { + name: PropTypes.string, + id: PropTypes.string, + // options: PropTypes.arrayOf(React.PropTypes.object).isRequired, + // selectedOptions: PropTypes.arrayOf(React.PropTypes.object).isRequired, + isInvalid: PropTypes.bool, + isPopoverOpen: PropTypes.bool, +}; + +EuiComboBox.defaultProps = { + options: [], + selectedOptions: [], +}; diff --git a/src/components/form/combo_box/combo_box_pill.js b/src/components/form/combo_box/combo_box_pill.js new file mode 100644 index 00000000000..9619158368b --- /dev/null +++ b/src/components/form/combo_box/combo_box_pill.js @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { + EuiIcon, +} from '../../../components'; + +export const EuiComboBoxPill = ({ + children, + className, + ...rest, +}) => { + const classes = classNames('euiComboBoxPill', className); + + return ( +
+ {children} + +
+ ); +}; + +EuiComboBoxPill.propTypes = { + children: PropTypes.node, + className: PropTypes.string, +}; diff --git a/src/components/form/combo_box/combo_box_row.js b/src/components/form/combo_box/combo_box_row.js new file mode 100644 index 00000000000..ec44a341f8d --- /dev/null +++ b/src/components/form/combo_box/combo_box_row.js @@ -0,0 +1,38 @@ +import React, { + Component, +} from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +export class EuiComboBoxRow extends Component { + static propTypes = { + children: PropTypes.node, + className: PropTypes.string, + } + + constructor(props) { + super(props); + } + + render() { + const { + children, + className, + ...rest, + } = this.props; + + const classes = classNames( + 'euiComboBoxRow', + className + ); + + return ( + + ); + } +} diff --git a/src/components/form/combo_box/index.js b/src/components/form/combo_box/index.js new file mode 100644 index 00000000000..d0ef23f2b81 --- /dev/null +++ b/src/components/form/combo_box/index.js @@ -0,0 +1,11 @@ +export { + EuiComboBox, +} from './combo_box'; + +export { + EuiComboBoxPill, +} from './combo_box_pill'; + +export { + EuiComboBoxRow, +} from './combo_box_row'; diff --git a/src/components/form/index.js b/src/components/form/index.js index 2c0c408108b..92cadf3bb43 100644 --- a/src/components/form/index.js +++ b/src/components/form/index.js @@ -2,6 +2,7 @@ export { EuiCheckbox, EuiCheckboxGroup, } from './checkbox'; +export { EuiComboBox, EuiComboBoxPill, EuiComboBoxRow } from './combo_box'; export { EuiFieldNumber } from './field_number'; export { EuiFieldPassword } from './field_password'; export { EuiFieldSearch } from './field_search'; diff --git a/src/components/index.js b/src/components/index.js index af424d801f8..08cc7bae0ae 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -52,6 +52,9 @@ export { export { EuiCheckbox, EuiCheckboxGroup, + EuiComboBox, + EuiComboBoxPill, + EuiComboBoxRow, EuiFieldNumber, EuiFieldPassword, EuiFieldSearch, diff --git a/src/global_styling/mixins/_helpers.scss b/src/global_styling/mixins/_helpers.scss index 52ccd801d08..5593155a2aa 100644 --- a/src/global_styling/mixins/_helpers.scss +++ b/src/global_styling/mixins/_helpers.scss @@ -29,12 +29,12 @@ */ @mixin euiScrollBar { &::-webkit-scrollbar { - width: 16px; - height: 16px; + width: $euiSize; + height: $euiSize; } &::-webkit-scrollbar-thumb { - background-color: rgba($euiColorDarkShade, 0.5); + background-color: $euiColorLightShade; border: 6px solid transparent; background-clip: content-box; }