Add support for keyboard events
This commit is contained in:
136
app/src/components/Layout/SearchBar.tsx
Normal file
136
app/src/components/Layout/SearchBar.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import React, { useCallback, useState, useRef } from 'react'
|
||||
import ClearAdornment from '../helper/ClearAdornment'
|
||||
import Search from '@material-ui/icons/Search'
|
||||
import { AppState } from '../../reducers'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { connect } from 'react-redux'
|
||||
import { InputBase } from '@material-ui/core'
|
||||
import { settingsActions } from '../../actions'
|
||||
import { fade, Theme, withStyles } from '@material-ui/core/styles'
|
||||
import { useGlobalKeyEventHandler } from '../../effects/useGlobalKeyEventHandler'
|
||||
|
||||
function SearchBar(props: {
|
||||
classes: any
|
||||
topicFilter?: string
|
||||
hasConnection: boolean
|
||||
actions: {
|
||||
settings: typeof settingsActions
|
||||
}
|
||||
}) {
|
||||
const { actions, classes, hasConnection, topicFilter } = props
|
||||
|
||||
const [hasFocus, setHasFocus] = useState(false)
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
const onFocus = useCallback(() => setHasFocus(true), [])
|
||||
const onBlur = useCallback(() => setHasFocus(false), [])
|
||||
|
||||
const clearFilter = useCallback(() => {
|
||||
actions.settings.filterTopics('')
|
||||
}, [])
|
||||
|
||||
const onFilterChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
actions.settings.filterTopics(event.target.value)
|
||||
}, [])
|
||||
|
||||
useGlobalKeyEventHandler(undefined, event => {
|
||||
const isCharacter = event.key.length === 1
|
||||
const isBackspace = event.key === 'Backspace'
|
||||
const isBodyActiveElement = document.activeElement && document.activeElement.tagName === 'BODY'
|
||||
|
||||
if ((isCharacter || isBackspace) && !hasFocus && isBodyActiveElement && hasConnection) {
|
||||
// Focus input field, no preventDefault the event will reach the input element after it has been focussed
|
||||
inputRef.current && inputRef.current.focus()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={classes.search}>
|
||||
<div className={classes.searchIcon}>
|
||||
<Search />
|
||||
</div>
|
||||
<InputBase
|
||||
value={topicFilter}
|
||||
inputProps={{
|
||||
onFocus,
|
||||
onBlur,
|
||||
ref: inputRef,
|
||||
}}
|
||||
onChange={onFilterChange}
|
||||
placeholder="Search…"
|
||||
endAdornment={
|
||||
<div style={{ width: '24px', paddingRight: '8px' }}>
|
||||
<ClearAdornment variant="primary" action={clearFilter} value={topicFilter} />
|
||||
</div>
|
||||
}
|
||||
classes={{ root: classes.inputRoot, input: classes.inputInput }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
topicFilter: state.settings.get('topicFilter'),
|
||||
hasConnection: Boolean(state.connection.connectionId),
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
actions: {
|
||||
settings: bindActionCreators(settingsActions, dispatch),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => ({
|
||||
search: {
|
||||
position: 'relative' as 'relative',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: fade(theme.palette.common.white, 0.15),
|
||||
'&:hover': {
|
||||
backgroundColor: fade(theme.palette.common.white, 0.25),
|
||||
},
|
||||
marginRight: theme.spacing(2),
|
||||
marginLeft: 0,
|
||||
flexGrow: 1,
|
||||
maxWidth: '60%',
|
||||
|
||||
[theme.breakpoints.up('md')]: {
|
||||
maxWidth: '30%',
|
||||
|
||||
marginLeft: theme.spacing(4),
|
||||
width: 'auto' as 'auto',
|
||||
},
|
||||
[theme.breakpoints.up(750)]: {
|
||||
marginLeft: theme.spacing(4),
|
||||
width: 'auto' as 'auto',
|
||||
},
|
||||
},
|
||||
searchIcon: {
|
||||
width: theme.spacing(6),
|
||||
height: '100%',
|
||||
position: 'absolute' as 'absolute',
|
||||
pointerEvents: 'none' as 'none',
|
||||
display: 'flex' as 'flex',
|
||||
alignItems: 'center' as 'center',
|
||||
justifyContent: 'center' as 'center',
|
||||
},
|
||||
inputRoot: {
|
||||
color: 'inherit' as 'inherit',
|
||||
width: '100%',
|
||||
},
|
||||
inputInput: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingRight: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(6),
|
||||
transition: theme.transitions.create('width'),
|
||||
width: '100%',
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(SearchBar))
|
||||
@@ -1,17 +1,15 @@
|
||||
import * as React from 'react'
|
||||
import ClearAdornment from '../helper/ClearAdornment'
|
||||
import CloudOff from '@material-ui/icons/CloudOff'
|
||||
import ConnectionHealthIndicator from '../helper/ConnectionHealthIndicator'
|
||||
import Menu from '@material-ui/icons/Menu'
|
||||
import PauseButton from './PauseButton'
|
||||
import Search from '@material-ui/icons/Search'
|
||||
import SearchBar from './SearchBar'
|
||||
import { AppBar, Button, IconButton, Toolbar, Typography } from '@material-ui/core'
|
||||
import { AppState } from '../../reducers'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { connect } from 'react-redux'
|
||||
import { connectionActions, globalActions, settingsActions } from '../../actions'
|
||||
import { fade } from '@material-ui/core/styles/colorManipulator'
|
||||
import { Theme, withStyles } from '@material-ui/core/styles'
|
||||
import { AppBar, Button, IconButton, InputBase, Toolbar, Typography } from '@material-ui/core'
|
||||
|
||||
const styles = (theme: Theme) => ({
|
||||
title: {
|
||||
@@ -21,29 +19,6 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
whiteSpace: 'nowrap' as 'nowrap',
|
||||
},
|
||||
search: {
|
||||
position: 'relative' as 'relative',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: fade(theme.palette.common.white, 0.15),
|
||||
'&:hover': {
|
||||
backgroundColor: fade(theme.palette.common.white, 0.25),
|
||||
},
|
||||
marginRight: theme.spacing(2),
|
||||
marginLeft: 0,
|
||||
flexGrow: 1,
|
||||
maxWidth: '60%',
|
||||
|
||||
[theme.breakpoints.up('md')]: {
|
||||
maxWidth: '30%',
|
||||
|
||||
marginLeft: theme.spacing(4),
|
||||
width: 'auto' as 'auto',
|
||||
},
|
||||
[theme.breakpoints.up(750)]: {
|
||||
marginLeft: theme.spacing(4),
|
||||
width: 'auto' as 'auto',
|
||||
},
|
||||
},
|
||||
disconnectIcon: {
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
display: 'none' as 'none',
|
||||
@@ -51,27 +26,6 @@ const styles = (theme: Theme) => ({
|
||||
marginRight: '8px',
|
||||
paddingLeft: '8px',
|
||||
},
|
||||
searchIcon: {
|
||||
width: theme.spacing(6),
|
||||
height: '100%',
|
||||
position: 'absolute' as 'absolute',
|
||||
pointerEvents: 'none' as 'none',
|
||||
display: 'flex' as 'flex',
|
||||
alignItems: 'center' as 'center',
|
||||
justifyContent: 'center' as 'center',
|
||||
},
|
||||
inputRoot: {
|
||||
color: 'inherit' as 'inherit',
|
||||
width: '100%',
|
||||
},
|
||||
inputInput: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingRight: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(6),
|
||||
transition: theme.transitions.create('width'),
|
||||
width: '100%',
|
||||
},
|
||||
menuButton: {
|
||||
marginLeft: -12,
|
||||
marginRight: 20,
|
||||
@@ -98,37 +52,6 @@ class TitleBar extends React.Component<Props, {}> {
|
||||
this.state = {}
|
||||
}
|
||||
|
||||
private renderSearch() {
|
||||
const { classes, topicFilter } = this.props
|
||||
|
||||
return (
|
||||
<div className={classes.search}>
|
||||
<div className={classes.searchIcon}>
|
||||
<Search />
|
||||
</div>
|
||||
<InputBase
|
||||
value={topicFilter}
|
||||
onChange={this.onFilterChange}
|
||||
placeholder="Search…"
|
||||
endAdornment={
|
||||
<div style={{ width: '24px', paddingRight: '8px' }}>
|
||||
<ClearAdornment variant="primary" action={this.clearFilter} value={topicFilter} />
|
||||
</div>
|
||||
}
|
||||
classes={{ root: classes.inputRoot, input: classes.inputInput }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private clearFilter = () => {
|
||||
this.props.actions.settings.filterTopics('')
|
||||
}
|
||||
|
||||
private onFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.props.actions.settings.filterTopics(event.target.value)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { actions, classes } = this.props
|
||||
|
||||
@@ -146,7 +69,7 @@ class TitleBar extends React.Component<Props, {}> {
|
||||
<Typography className={classes.title} variant="h6" color="inherit">
|
||||
MQTT Explorer
|
||||
</Typography>
|
||||
{this.renderSearch()}
|
||||
<SearchBar />
|
||||
<PauseButton />
|
||||
<Button className={classes.disconnect} onClick={actions.connection.disconnect}>
|
||||
Disconnect <CloudOff className={classes.disconnectIcon} />
|
||||
|
||||
Reference in New Issue
Block a user