Introduce concept of "Selected Function" and allow arrow key selection

pull/10616/head
Alex P 2018-04-05 16:04:47 -07:00
parent ac480a9fdc
commit 98dfdb239c
4 changed files with 170 additions and 56 deletions

View File

@ -1,42 +1,58 @@
import React, {SFC, ChangeEvent, KeyboardEvent} from 'react'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import DropdownInput from 'src/shared/components/DropdownInput'
import FuncSelectorInput from 'src/shared/components/FuncSelectorInput'
import FuncListItem from 'src/ifql/components/FuncListItem'
interface Props {
inputText: string
isOpen: boolean
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void
onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void
onAddNode: (name: string) => void
funcs: string[]
selectedFunc: string
onSetSelectedFunc: (name: string) => void
}
const FuncList: SFC<Props> = ({
inputText,
isOpen,
onAddNode,
onKeyDown,
onInputChange,
funcs,
selectedFunc,
onSetSelectedFunc,
}) => {
return (
<ul className="dropdown-menu funcs">
<DropdownInput
buttonSize="btn-xs"
buttonColor="btn-default"
<div className="ifql-func--autocomplete">
<FuncSelectorInput
onFilterChange={onInputChange}
onFilterKeyPress={onKeyDown}
searchTerm={inputText}
/>
<FancyScrollbar autoHide={false} autoHeight={true} maxHeight={240}>
{isOpen &&
funcs.map((func, i) => (
<FuncListItem key={i} name={func} onAddNode={onAddNode} />
))}
</FancyScrollbar>
</ul>
<ul className="ifql-func--list">
<FancyScrollbar
autoHide={false}
autoHeight={true}
maxHeight={240}
className="fancy-scroll--func-selector"
>
{funcs.length > 0 ? (
funcs.map((func, i) => (
<FuncListItem
key={i}
name={func}
onAddNode={onAddNode}
selectedFunc={selectedFunc}
onSetSelectedFunc={onSetSelectedFunc}
/>
))
) : (
<div className="ifql-func--item empty">No results</div>
)}
</FancyScrollbar>
</ul>
</div>
)
}

View File

@ -3,6 +3,8 @@ import React, {PureComponent} from 'react'
interface Props {
name: string
onAddNode: (name: string) => void
selectedFunc: string
onSetSelectedFunc: (name: string) => void
}
export default class FuncListItem extends PureComponent<Props> {
@ -10,12 +12,25 @@ export default class FuncListItem extends PureComponent<Props> {
const {name} = this.props
return (
<li onClick={this.handleClick} className="dropdown-item func">
<a>{name}</a>
<li
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
className={`ifql-func--item ${this.getActiveClass()}`}
>
{name}
</li>
)
}
private getActiveClass(): string {
const {name, selectedFunc} = this.props
return name === selectedFunc ? 'active' : ''
}
private handleMouseEnter = () => {
this.props.onSetSelectedFunc(this.props.name)
}
private handleClick = () => {
this.props.onAddNode(this.props.name)
}

View File

@ -1,4 +1,5 @@
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
import _ from 'lodash'
import {ClickOutside} from 'src/shared/components/ClickOutside'
import FuncList from 'src/ifql/components/FuncList'
@ -6,6 +7,8 @@ import FuncList from 'src/ifql/components/FuncList'
interface State {
isOpen: boolean
inputText: string
selectedFunc: string
availableFuncs: string[]
}
interface Props {
@ -20,71 +23,128 @@ export class FuncSelector extends PureComponent<Props, State> {
this.state = {
isOpen: false,
inputText: '',
selectedFunc: '',
availableFuncs: [],
}
}
public render() {
const {isOpen, inputText} = this.state
const {isOpen, inputText, selectedFunc, availableFuncs} = this.state
return (
<ClickOutside onClickOutside={this.handleClickOutside}>
<div className={`dropdown dashboard-switcher ${this.openClass}`}>
<button
className="btn btn-square btn-default btn-sm dropdown-toggle"
onClick={this.handleClick}
>
<span className="icon plus" />
</button>
<FuncList
inputText={inputText}
onAddNode={this.handleAddNode}
isOpen={isOpen}
funcs={this.availableFuncs}
onInputChange={this.handleInputChange}
onKeyDown={this.handleKeyDown}
/>
<div className="ifql-func--selector">
{isOpen ? (
<FuncList
inputText={inputText}
onAddNode={this.handleAddNode}
funcs={availableFuncs}
onInputChange={this.handleInputChange}
onKeyDown={this.handleKeyDown}
selectedFunc={selectedFunc}
onSetSelectedFunc={this.handleSetSelectedFunc}
/>
) : (
<button
className="btn btn-square btn-primary btn-sm ifql-func--button"
onClick={this.handleOpenList}
tabIndex={0}
>
<span className="icon plus" />
</button>
)}
</div>
</ClickOutside>
)
}
private get openClass(): string {
if (this.state.isOpen) {
return 'open'
}
return ''
private handleCloseList = () => {
this.setState({isOpen: false, selectedFunc: '', availableFuncs: []})
}
private handleAddNode = (name: string) => {
this.setState({isOpen: false})
this.handleCloseList()
this.props.onAddNode(name)
}
private get availableFuncs(): string[] {
return this.props.funcs.filter(f =>
f.toLowerCase().includes(this.state.inputText)
private setAvailableFuncs = inputText => {
const {funcs} = this.props
const {selectedFunc} = this.state
const availableFuncs = funcs.filter(f =>
f.toLowerCase().includes(inputText)
)
const isSelectedVisible = !!availableFuncs.find(a => a === selectedFunc)
const newSelectedFunc = availableFuncs.length > 0 ? availableFuncs[0] : ''
this.setState({
inputText,
availableFuncs,
selectedFunc: isSelectedVisible ? selectedFunc : newSelectedFunc,
})
}
private handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({inputText: e.target.value})
this.setAvailableFuncs(e.target.value)
}
private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key !== 'Escape') {
return
const {selectedFunc, availableFuncs} = this.state
const selectedFuncExists = selectedFunc !== ''
if (e.key === 'Enter' && selectedFuncExists) {
return this.handleAddNode(selectedFunc)
}
this.setState({inputText: '', isOpen: false})
if (e.key === 'Escape' || e.key === 'Tab') {
return this.handleCloseList()
}
if (e.key === 'ArrowUp' && selectedFuncExists) {
// get index of selectedFunc in availableFuncs
const selectedIndex = _.findIndex(
availableFuncs,
func => func === selectedFunc
)
const previousIndex = selectedIndex - 1
// if there is selectedIndex - 1 in availableFuncs make that the new SelectedFunc
if (previousIndex >= 0) {
return this.setState({selectedFunc: availableFuncs[previousIndex]})
}
// if not then keep selectedFunc as is
}
if (e.key === 'ArrowDown' && selectedFuncExists) {
// get index of selectedFunc in availableFuncs
const selectedIndex = _.findIndex(
availableFuncs,
func => func === selectedFunc
)
const nextIndex = selectedIndex + 1
// if there is selectedIndex + 1 in availableFuncs make that the new SelectedFunc
if (nextIndex < availableFuncs.length) {
return this.setState({selectedFunc: availableFuncs[nextIndex]})
}
// if not then keep selectedFunc as is
}
}
private handleClick = () => {
this.setState({isOpen: !this.state.isOpen})
private handleSetSelectedFunc = funcName => {
this.setState({selectedFunc: funcName})
}
private handleOpenList = () => {
const {funcs} = this.props
this.setState({
isOpen: true,
inputText: '',
availableFuncs: funcs,
selectedFunc: funcs[0],
})
}
private handleClickOutside = () => {
this.setState({isOpen: false})
this.handleCloseList()
}
}

View File

@ -11,7 +11,9 @@ $scrollbar-color-b: $c-comet;
.fancy-scroll--track-h,
.fancy-scroll--track-v {
&:hover {cursor: pointer;}
&:hover {
cursor: pointer;
}
}
/* Horizontal Scrollbar Styles */
.fancy-scroll--track-h {
@ -24,7 +26,7 @@ $scrollbar-color-b: $c-comet;
.fancy-scroll--thumb-h {
height: $scrollbar-thumb-size !important;
border-radius: ($scrollbar-thumb-size / 2);
@include gradient-h($scrollbar-color-a,$scrollbar-color-b);
@include gradient-h($scrollbar-color-a, $scrollbar-color-b);
}
/* Vertical Scrollbar Styles */
.fancy-scroll--track-v {
@ -37,7 +39,7 @@ $scrollbar-color-b: $c-comet;
.fancy-scroll--thumb-v {
width: $scrollbar-thumb-size !important;
border-radius: ($scrollbar-thumb-size / 2);
@include gradient-v($scrollbar-color-a,$scrollbar-color-b);
@include gradient-v($scrollbar-color-a, $scrollbar-color-b);
}
/* Kapacitor Theme Scrollbars */
@ -45,14 +47,35 @@ $scrollbar-color-kap-a: $c-rainforest;
$scrollbar-color-kap-b: $c-pool;
.fancy-scroll--kapacitor {
.fancy-scroll--thumb-h { @include gradient-h($scrollbar-color-kap-a,$scrollbar-color-kap-b); }
.fancy-scroll--thumb-v { @include gradient-v($scrollbar-color-kap-a,$scrollbar-color-kap-b); }
.fancy-scroll--thumb-h {
@include gradient-h($scrollbar-color-kap-a, $scrollbar-color-kap-b);
}
.fancy-scroll--thumb-v {
@include gradient-v($scrollbar-color-kap-a, $scrollbar-color-kap-b);
}
}
/* Kapacitor Theme Scrollbars */
$scrollbar-color-kap-a: $c-rainforest;
$scrollbar-color-kap-b: $c-pool;
.fancy-scroll--func-selector {
.fancy-scroll--thumb-h {
@include gradient-h($c-neutrino, $c-hydrogen);
}
.fancy-scroll--thumb-v {
@include gradient-v($c-neutrino, $c-hydrogen);
}
}
/* Dropdown Theme Scrollbars */
ul.dropdown-menu {
.fancy-scroll--thumb-h { @include gradient-h($c-neutrino,$c-laser); }
.fancy-scroll--thumb-v { @include gradient-v($c-neutrino,$c-laser); }
.fancy-scroll--thumb-h {
@include gradient-h($c-neutrino, $c-laser);
}
.fancy-scroll--thumb-v {
@include gradient-v($c-neutrino, $c-laser);
}
}
/* Hacky Fix to make fancy scrollbars work in Safari */
.query-builder--list {