Skip to content

Commit

Permalink
Popover improvements (Graylog2#4690)
Browse files Browse the repository at this point in the history
* Pass callback to hide popover on select

In this way, consumers of the SelectPopover can decide if the popover
should hide when an item is selected.

* Pass callback to hide popover on select

In this way, consumers of ColorPickerPopover can decide if the popover
should hide when an item is selected.

* Allow to customize clear selection text

* Allow multiple selections in SelectPopover
  • Loading branch information
edmundoa authored and Marius Sturm committed Mar 27, 2018
1 parent d8a97f0 commit 2df062f
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ const ColorPickerPopover = createReactClass({
triggerNode: PropTypes.node.isRequired,
/** Event that will show/hide the popover. */
triggerAction: PropTypes.oneOf(['click', 'hover', 'focus']),
/**
* Function that will be called when the selected color changes.
* The function receives the color in hexadecimal format as first argument,
* the event as the second argument, and a callback function to hide the
* overlay as third argument.
*/
onChange: PropTypes.func.isRequired,
},

getDefaultProps() {
Expand All @@ -37,16 +44,24 @@ const ColorPickerPopover = createReactClass({
};
},

handleChange(color, event) {
this.props.onChange(color, event, () => this.overlay.hide());
},

render() {
const { id, placement, title, triggerNode, triggerAction, ...colorPickerProps } = this.props;
const popover = (
<Popover id={id} title={title} className={style.customPopover}>
<ColorPicker {...colorPickerProps} />
<ColorPicker {...colorPickerProps} onChange={this.handleChange} />
</Popover>
);

return (
<OverlayTrigger trigger={triggerAction} placement={placement} overlay={popover} rootClose>
<OverlayTrigger ref={(c) => { this.overlay = c; }}
trigger={triggerAction}
placement={placement}
overlay={popover}
rootClose>
{triggerNode}
</OverlayTrigger>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const ColorPickerOverlayExample = createReactClass({
};
},

handleColorChange(color) {
handleColorChange(color, _, hidePopover) {
hidePopover();
this.setState({ color: color });
},

Expand Down
57 changes: 42 additions & 15 deletions graylog2-web-interface/src/components/common/SelectPopover.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,22 @@ const SelectPopover = createReactClass({
* and must return a React node that will be displayed on screen.
*/
itemFormatter: PropTypes.func,
/** Indicates which is the selected item. This should be the same string that appears in the `items` list. */
selectedItem: PropTypes.string,
/** Indicates whether the component will allow multiple selected items or not. */
multiple: PropTypes.bool,
/** Indicates which items are selected. This should be the same string that appears in the `items` list. */
selectedItems: PropTypes.arrayOf(PropTypes.string),
/**
* Function that will be called when the item selection changes.
* The function will receive the selected item as argument or `undefined` if the selection
* is cleared.
* The function will receive the selected item as first argument or `undefined` if the selection
* is cleared, and a callback function to hide the popover as a second argument.
*/
onItemSelect: PropTypes.func.isRequired,
/** Indicates whether the component should display a text filter or not. */
displayDataFilter: PropTypes.bool,
/** Placeholder to display in the filter text input. */
filterPlaceholder: PropTypes.string,
/** Text to display in the entry to clear the current selection. */
clearSelectionText: PropTypes.string,
},

getDefaultProps() {
Expand All @@ -54,34 +58,51 @@ const SelectPopover = createReactClass({
triggerAction: 'click',
items: [],
itemFormatter: item => item,
selectedItem: undefined,
multiple: false,
selectedItems: [],
onItemSelect: () => {},
displayDataFilter: true,
filterPlaceholder: 'Type to filter',
clearSelectionText: 'Clear selection',
};
},

getInitialState() {
return {
filterText: '',
filteredItems: this.props.items,
selectedItem: this.props.selectedItem,
selectedItems: this.props.selectedItems,
};
},

componentWillReceiveProps(nextProps) {
if (this.props.selectedItem !== nextProps.selectedItem) {
this.setState({ selectedItem: nextProps.selectedItem });
if (lodash.isEqual(this.props.selectedItems, nextProps.selectedItems)) {
this.setState({ selectedItems: nextProps.selectedItems });
}
if (this.props.items !== nextProps.items) {
this.filterData(this.state.filterText, nextProps.items);
}
},

handleSelectionChange(nextSelection) {
this.setState({ selectedItems: nextSelection });
this.props.onItemSelect(nextSelection, () => this.overlay.hide());
},

clearItemSelection() {
this.handleSelectionChange([]);
},

handleItemSelection(item) {
return () => {
this.setState({ selectedItem: item });
this.props.onItemSelect(item);
const selectedItems = this.state.selectedItems;
let nextSelectedItems;
if (selectedItems.includes(item)) {
nextSelectedItems = lodash.without(selectedItems, item);
} else {
nextSelectedItems = this.props.multiple ? lodash.concat(selectedItems, item) : [item];
}
this.handleSelectionChange(nextSelectedItems);
};
},

Expand Down Expand Up @@ -112,26 +133,28 @@ const SelectPopover = createReactClass({

renderClearSelectionItem() {
return (
<ListGroupItem onClick={this.handleItemSelection()}><i className="fa fa-fw fa-times text-danger" /> Clear selection</ListGroupItem>
<ListGroupItem onClick={this.clearItemSelection}>
<i className="fa fa-fw fa-times text-danger" /> {this.props.clearSelectionText}
</ListGroupItem>
);
},

render() {
const { displayDataFilter, itemFormatter, items, placement, triggerAction, triggerNode, ...otherProps } = this.props;
const popoverProps = this.pickPopoverProps(otherProps);
const { filteredItems, selectedItem } = this.state;
const { filteredItems, selectedItems } = this.state;

const popover = (
<Popover {...popoverProps} className={style.customPopover}>
{displayDataFilter && this.renderDataFilter(items)}
{selectedItem && this.renderClearSelectionItem()}
{selectedItems.length > 0 && this.renderClearSelectionItem()}
<IsolatedScroll className={style.scrollableList}>
<ListGroup>
{filteredItems.map((item) => {
return (
<ListGroupItem key={item}
onClick={this.handleItemSelection(item)}
active={this.state.selectedItem === item}>
active={this.state.selectedItems.includes(item)}>
{itemFormatter(item)}
</ListGroupItem>
);
Expand All @@ -142,7 +165,11 @@ const SelectPopover = createReactClass({
);

return (
<OverlayTrigger trigger={triggerAction} placement={placement} overlay={popover} rootClose>
<OverlayTrigger ref={(c) => { this.overlay = c; }}
trigger={triggerAction}
placement={placement}
overlay={popover}
rootClose>
{triggerNode}
</OverlayTrigger>
);
Expand Down
26 changes: 14 additions & 12 deletions graylog2-web-interface/src/components/common/SelectPopover.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ const items = [
const SelectPopoverExample = createReactClass({
getInitialState() {
return {
selectedItem: undefined,
selectedColor: undefined,
};
},

handleItemSelect(item) {
this.setState({ selectedItem: item });
this.setState({ selectedColor: item[0] });
},

render() {
const selectedItem = this.state.selectedItem;
const selectedColor = this.state.selectedColor;

return (
<div>
Expand All @@ -32,12 +32,13 @@ const SelectPopoverExample = createReactClass({
title="Filter by color"
triggerNode={<Button bsStyle="info" bsSize="small">Select color</Button>}
items={items}
selectedItem={selectedItem}
selectedItems={selectedColor ? [selectedColor] : []}
onItemSelect={this.handleItemSelect}
displayDataFilter={false} />
displayDataFilter={false}
clearSelectionText="Clear color selection"/>
</div>

{selectedItem ? `You have selected ${selectedItem}` : 'Please select a color!'}
{selectedColor ? `You have selected ${selectedColor}` : 'Please select a color!'}
</div>
);
}
Expand Down Expand Up @@ -73,12 +74,12 @@ const items = [
const SelectPopoverFormattedExample = createReactClass({
getInitialState() {
return {
selectedItem: undefined,
selectedColors: [],
};
},

handleItemSelect(item) {
this.setState({ selectedItem: item });
this.setState({ selectedColors: item });
},

formatItem(item) {
Expand All @@ -90,7 +91,7 @@ const SelectPopoverFormattedExample = createReactClass({
},

render() {
const selectedItem = this.state.selectedItem;
const selectedColors = this.state.selectedColors;

return (
<div>
Expand All @@ -100,12 +101,13 @@ const SelectPopoverFormattedExample = createReactClass({
triggerNode={<Button bsStyle="info" bsSize="small">Select color</Button>}
items={items}
itemFormatter={this.formatItem}
selectedItem={selectedItem}
onItemSelect={this.handleItemSelect}
filterPlaceholder="Filter by color" />
filterPlaceholder="Filter by color"
multiple={true}
selectedItems={selectedColors} />
</div>

{selectedItem ? `You have selected ${selectedItem}` : 'Please select another color!'}
{selectedColors.length > 0 ? `You have selected ${selectedColors.join(', ')}` : 'Please select some colors!'}
</div>
);
}
Expand Down

0 comments on commit 2df062f

Please sign in to comment.