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

View File

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

View File

@ -1,4 +1,5 @@
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react' import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
import _ from 'lodash'
import {ClickOutside} from 'src/shared/components/ClickOutside' import {ClickOutside} from 'src/shared/components/ClickOutside'
import FuncList from 'src/ifql/components/FuncList' import FuncList from 'src/ifql/components/FuncList'
@ -6,6 +7,8 @@ import FuncList from 'src/ifql/components/FuncList'
interface State { interface State {
isOpen: boolean isOpen: boolean
inputText: string inputText: string
selectedFunc: string
availableFuncs: string[]
} }
interface Props { interface Props {
@ -20,71 +23,128 @@ export class FuncSelector extends PureComponent<Props, State> {
this.state = { this.state = {
isOpen: false, isOpen: false,
inputText: '', inputText: '',
selectedFunc: '',
availableFuncs: [],
} }
} }
public render() { public render() {
const {isOpen, inputText} = this.state const {isOpen, inputText, selectedFunc, availableFuncs} = this.state
return ( return (
<ClickOutside onClickOutside={this.handleClickOutside}> <ClickOutside onClickOutside={this.handleClickOutside}>
<div className={`dropdown dashboard-switcher ${this.openClass}`}> <div className="ifql-func--selector">
<button {isOpen ? (
className="btn btn-square btn-default btn-sm dropdown-toggle"
onClick={this.handleClick}
>
<span className="icon plus" />
</button>
<FuncList <FuncList
inputText={inputText} inputText={inputText}
onAddNode={this.handleAddNode} onAddNode={this.handleAddNode}
isOpen={isOpen} funcs={availableFuncs}
funcs={this.availableFuncs}
onInputChange={this.handleInputChange} onInputChange={this.handleInputChange}
onKeyDown={this.handleKeyDown} 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> </div>
</ClickOutside> </ClickOutside>
) )
} }
private get openClass(): string { private handleCloseList = () => {
if (this.state.isOpen) { this.setState({isOpen: false, selectedFunc: '', availableFuncs: []})
return 'open'
}
return ''
} }
private handleAddNode = (name: string) => { private handleAddNode = (name: string) => {
this.setState({isOpen: false}) this.handleCloseList()
this.props.onAddNode(name) this.props.onAddNode(name)
} }
private get availableFuncs(): string[] { private setAvailableFuncs = inputText => {
return this.props.funcs.filter(f => const {funcs} = this.props
f.toLowerCase().includes(this.state.inputText) 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>) => { private handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({inputText: e.target.value}) this.setAvailableFuncs(e.target.value)
} }
private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => { private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key !== 'Escape') { const {selectedFunc, availableFuncs} = this.state
return 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()
} }
private handleClick = () => { if (e.key === 'ArrowUp' && selectedFuncExists) {
this.setState({isOpen: !this.state.isOpen}) // 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 handleSetSelectedFunc = funcName => {
this.setState({selectedFunc: funcName})
}
private handleOpenList = () => {
const {funcs} = this.props
this.setState({
isOpen: true,
inputText: '',
availableFuncs: funcs,
selectedFunc: funcs[0],
})
} }
private handleClickOutside = () => { 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-h,
.fancy-scroll--track-v { .fancy-scroll--track-v {
&:hover {cursor: pointer;} &:hover {
cursor: pointer;
}
} }
/* Horizontal Scrollbar Styles */ /* Horizontal Scrollbar Styles */
.fancy-scroll--track-h { .fancy-scroll--track-h {
@ -45,14 +47,35 @@ $scrollbar-color-kap-a: $c-rainforest;
$scrollbar-color-kap-b: $c-pool; $scrollbar-color-kap-b: $c-pool;
.fancy-scroll--kapacitor { .fancy-scroll--kapacitor {
.fancy-scroll--thumb-h { @include gradient-h($scrollbar-color-kap-a,$scrollbar-color-kap-b); } .fancy-scroll--thumb-h {
.fancy-scroll--thumb-v { @include gradient-v($scrollbar-color-kap-a,$scrollbar-color-kap-b); } @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 */ /* Dropdown Theme Scrollbars */
ul.dropdown-menu { ul.dropdown-menu {
.fancy-scroll--thumb-h { @include gradient-h($c-neutrino,$c-laser); } .fancy-scroll--thumb-h {
.fancy-scroll--thumb-v { @include gradient-v($c-neutrino,$c-laser); } @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 */ /* Hacky Fix to make fancy scrollbars work in Safari */
.query-builder--list { .query-builder--list {