Skip to content

Commit

Permalink
Place options list inside of an EuiPortal.
Browse files Browse the repository at this point in the history
- Improve calculatePopoverPosition service by accepting a 'positions' argument.
  • Loading branch information
cjcenizal committed Mar 28, 2018
1 parent b0b9c19 commit dce7ebf
Show file tree
Hide file tree
Showing 14 changed files with 324 additions and 203 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- Add support for expandable rows to `EuiBasicTable` ([#585](https://github.com/elastic/eui/pull/585))
- Added `EuiComboBox` for selecting many options from a list of options ([567](https://github.com/elastic/eui/pull/567))
- Added `EuiHighlight` for highlighting a substring within text ([567](https://github.com/elastic/eui/pull/567))
- `calculatePopoverPosition` service now accepts a `positions` argument so you can specify which positions are acceptable.

**Bug fixes**

Expand Down
39 changes: 0 additions & 39 deletions src/components/combo_box/_combo_box.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,46 +43,7 @@
}
}

.euiComboBox__panel {
z-index: 1;
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 {
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:
Expand Down
83 changes: 61 additions & 22 deletions src/components/combo_box/combo_box.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import tabbable from 'tabbable';

import { comboBoxKeyCodes } from '../../services';
import { comboBoxKeyCodes, calculatePopoverPosition } from '../../services';
import { BACKSPACE, TAB, ESCAPE } from '../../services/key_codes';
import { EuiPortal } from '../portal';
import { EuiComboBoxInput } from './combo_box_input';
import { EuiComboBoxOptionsList } from './combo_box_options_list';

Expand Down Expand Up @@ -47,18 +48,23 @@ export class EuiComboBox extends Component {

this.state = {
searchValue: initialSearchValue,
isListOpen: this.props.isListOpen,
isListOpen: false,
listPosition: 'bottom',
listStyles: {},
};

// Cached derived state.
this.matchingOptions = matchingOptions;
this.optionToGroupMap = optionToGroupMap;
this.activeOptionIndex = undefined;
this.listBounds = undefined;

// Refs.
this.options = [];
this.comboBox = undefined;
this.autoSizeInput = undefined;
this.searchInput = undefined;
this.optionsList = undefined;
this.options = [];
}

getMatchingOptions = (options, selectedOptions, searchValue) => {
Expand All @@ -80,6 +86,27 @@ export class EuiComboBox extends Component {
});
};

updateListPosition = (listBounds = this.listBounds) => {
if (!this.state.isListOpen) {
return;
}

// Cache for future calls.
this.listBounds = listBounds;
const comboBoxBounds = this.comboBox.getBoundingClientRect();
const { position, left, top } = calculatePopoverPosition(comboBoxBounds, listBounds, 'bottom', 0, ['bottom', 'top']);

const listStyles = {
top: top + window.scrollY,
left,
};

this.setState({
listPosition: position,
listStyles,
});
};

tabAway = amount => {
const tabbableItems = tabbable(document);
const comboBoxIndex = tabbableItems.indexOf(this.searchInput);
Expand Down Expand Up @@ -287,14 +314,15 @@ export class EuiComboBox extends Component {
}
};

onComboBoxBlur = () => {
onComboBoxBlur = (e) => {
if (e.relatedTarget && this.comboBox.contains(e.relatedTarget)) return;
// This callback generally handles cases when the user has taken focus away by clicking outside
// of the combo box.

// Wait for the DOM to update.
requestAnimationFrame(() => {
// If the user has placed focus somewhere outside of the combo box, close it.
const hasFocus = this.comboBox.contains(document.activeElement);
const hasFocus = this.comboBox.contains(document.activeElement) || this.optionsList.contains(document.activeElement);
if (!hasFocus) {
this.closeList();
}
Expand All @@ -320,6 +348,10 @@ export class EuiComboBox extends Component {
this.searchInput = node;
};

optionsListRef = node => {
this.optionsList = node;
};

optionRef = (index, node) => {
// Sometimes the node is null.
if (node) {
Expand Down Expand Up @@ -376,32 +408,38 @@ export class EuiComboBox extends Component {
...rest
} = this.props;

const { searchValue } = this.state;
const { searchValue, isListOpen, listStyles, listPosition } = this.state;

const classes = classNames('euiComboBox', className, {
'euiComboBox-isOpen': this.state.isListOpen,
'euiComboBox-isOpen': isListOpen,
});

const value = selectedOptions.map(selectedOption => selectedOption.label).join(', ');

let optionsList;

if (onChange) {
if (onChange && isListOpen) {
optionsList = (
<EuiComboBoxOptionsList
isLoading={isLoading}
options={options}
selectedOptions={selectedOptions}
onCreateOption={onCreateOption}
searchValue={searchValue}
matchingOptions={this.matchingOptions}
optionToGroupMap={this.optionToGroupMap}
optionRef={this.optionRef}
onOptionClick={this.onOptionClick}
onOptionEnterKey={this.onOptionEnterKey}
areAllOptionsSelected={this.areAllOptionsSelected()}
getSelectedOptionForSearchValue={getSelectedOptionForSearchValue}
/>
<EuiPortal>
<EuiComboBoxOptionsList
isLoading={isLoading}
options={options}
selectedOptions={selectedOptions}
onCreateOption={onCreateOption}
searchValue={searchValue}
matchingOptions={this.matchingOptions}
optionToGroupMap={this.optionToGroupMap}
listRef={this.optionsListRef}
optionRef={this.optionRef}
onOptionClick={this.onOptionClick}
onOptionEnterKey={this.onOptionEnterKey}
areAllOptionsSelected={this.areAllOptionsSelected()}
getSelectedOptionForSearchValue={getSelectedOptionForSearchValue}
updatePosition={this.updateListPosition}
position={listPosition}
style={listStyles}
/>
</EuiPortal>
);
}

Expand All @@ -424,6 +462,7 @@ export class EuiComboBox extends Component {
searchValue={searchValue}
autoSizeInputRef={this.autoSizeInputRef}
inputRef={this.searchInputRef}
updatePosition={this.updateListPosition}
/>

{optionsList}
Expand Down
42 changes: 30 additions & 12 deletions src/components/combo_box/combo_box_input/combo_box_input.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ import { htmlIdGenerator } from '../../../services';
const makeId = htmlIdGenerator();

export class EuiComboBoxInput extends Component {
static propTypes = {
selectedOptions: PropTypes.array,
onRemoveOption: PropTypes.func,
onClick: PropTypes.func,
onFocus: PropTypes.func,
onChange: PropTypes.func,
value: PropTypes.string,
searchValue: PropTypes.string,
autoSizeInputRef: PropTypes.func,
inputRef: PropTypes.func,
updatePosition: PropTypes.func.isRequired,
}

constructor(props) {
super(props);

Expand All @@ -18,6 +31,13 @@ export class EuiComboBoxInput extends Component {
};
}

updatePosition = () => {
// Wait a beat for the DOM to update, since we depend on DOM elements' bounds.
requestAnimationFrame(() => {
this.props.updatePosition();
});
};

onFocus = () => {
this.props.onFocus();
this.setState({
Expand All @@ -31,6 +51,16 @@ export class EuiComboBoxInput extends Component {
});
};

componentWillUpdate(nextProps) {
const { searchValue } = nextProps;

// We need to update the position of everything if the user enters enough input to change
// the size of the input.
if (searchValue !== this.props.searchValue) {
this.updatePosition();
}
}

render() {
const {
selectedOptions,
Expand Down Expand Up @@ -119,15 +149,3 @@ export class EuiComboBoxInput extends Component {
);
}
}

EuiComboBoxInput.propTypes = {
selectedOptions: PropTypes.array,
onRemoveOption: PropTypes.func,
onClick: PropTypes.func,
onFocus: PropTypes.func,
onChange: PropTypes.func,
value: PropTypes.string,
searchValue: PropTypes.string,
autoSizeInputRef: PropTypes.func,
inputRef: PropTypes.func,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* 1. Make width match that of the input.
*/
.euiComboBoxOptionsList {
@include euiFormControlSize;
box-sizing: content-box; /* 1 */
z-index: 1;
position: absolute;
}

.euiComboBoxOptionsList--bottom {
border-radius: 0 0 $euiBorderRadius $euiBorderRadius !important;
border-top: none !important;
}

.euiComboBoxOptionsList--top {
border-radius: $euiBorderRadius $euiBorderRadius 0 0 !important;
box-shadow: none !important;
}

.euiComboBoxOptionsList__empty {
padding: $euiSizeS;
text-align: center;
color: $euiColorDarkShade;
}

.euiComboBoxOptionsList__rowWrap {
@include euiScrollBar;

padding: $euiSizeS;
max-height: 200px;
overflow-y: auto;
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
@import 'combo_box_options_list';
@import 'combo_box_option';
@import 'combo_box_title';
Loading

0 comments on commit dce7ebf

Please sign in to comment.