Improve UX: accessibility, field guidance, and error prevention (#1010)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
This commit is contained in:
@@ -8,15 +8,29 @@ function ConnectButton(props: { connecting: boolean; classes: any; toggle: () =>
|
||||
|
||||
if (connecting) {
|
||||
return (
|
||||
<Button variant="contained" color="primary" className={classes.button} onClick={toggle} data-testid="abort-button">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.button}
|
||||
onClick={toggle}
|
||||
data-testid="abort-button"
|
||||
aria-label="Cancel connection attempt"
|
||||
>
|
||||
<ConnectionHealthIndicator />
|
||||
Abort
|
||||
Cancel
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="contained" color="primary" className={classes.button} onClick={toggle} data-testid="connect-button">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.button}
|
||||
onClick={toggle}
|
||||
data-testid="connect-button"
|
||||
aria-label="Connect to MQTT broker"
|
||||
>
|
||||
<PowerSettingsNew /> Connect
|
||||
</Button>
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff'
|
||||
import { AppState } from '../../reducers'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { connect } from 'react-redux'
|
||||
import { connectionActions, connectionManagerActions } from '../../actions'
|
||||
import { connectionActions, connectionManagerActions, globalActions } from '../../actions'
|
||||
import { ConnectionOptions, toMqttConnection } from '../../model/ConnectionOptions'
|
||||
import { KeyCodes } from '../../utils/KeyCodes'
|
||||
import { Theme } from '@mui/material/styles'
|
||||
@@ -17,14 +17,12 @@ import { ToggleSwitch } from './ToggleSwitch'
|
||||
import { useGlobalKeyEventHandler } from '../../effects/useGlobalKeyEventHandler'
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Grid,
|
||||
IconButton,
|
||||
Input,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from '@mui/material'
|
||||
|
||||
interface Props {
|
||||
@@ -32,6 +30,7 @@ interface Props {
|
||||
classes: { [s: string]: string }
|
||||
actions: typeof connectionActions
|
||||
managerActions: typeof connectionManagerActions
|
||||
globalActions: typeof globalActions
|
||||
connected: boolean
|
||||
connecting: boolean
|
||||
}
|
||||
@@ -41,6 +40,17 @@ const protocols = ['mqtt', 'ws']
|
||||
function ConnectionSettings(props: Props) {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
const confirmed = await props.globalActions.requestConfirmation(
|
||||
'Delete Connection',
|
||||
`Are you sure you want to delete the connection "${props.connection.name}"?\n\nThis action cannot be undone.`
|
||||
)
|
||||
|
||||
if (confirmed) {
|
||||
props.managerActions.deleteConnection(props.connection.id)
|
||||
}
|
||||
}, [props.connection.id, props.connection.name, props.globalActions, props.managerActions])
|
||||
|
||||
const toggleConnect = useCallback(() => {
|
||||
if (props.connecting) {
|
||||
props.actions.disconnect()
|
||||
@@ -76,7 +86,7 @@ function ConnectionSettings(props: Props) {
|
||||
className={props.classes.textField}
|
||||
value={props.connection.basePath}
|
||||
onChange={handleChange('basePath')}
|
||||
margin="normal"
|
||||
margin="dense"
|
||||
/>
|
||||
</Grid>
|
||||
)
|
||||
@@ -101,21 +111,26 @@ function ConnectionSettings(props: Props) {
|
||||
|
||||
const protocolItems = protocols.map((value: string) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{value}://
|
||||
{value}:// {value === 'mqtt' ? '(Standard)' : '(WebSocket)'}
|
||||
</MenuItem>
|
||||
))
|
||||
|
||||
return (
|
||||
<TextField
|
||||
select={true}
|
||||
label="Protocol"
|
||||
className={classes.textField}
|
||||
value={connection.protocol}
|
||||
onChange={updateProtocol}
|
||||
margin="normal"
|
||||
>
|
||||
{protocolItems}
|
||||
</TextField>
|
||||
<Tooltip title="Use 'mqtt' for standard connections or 'ws' for WebSocket connections" arrow>
|
||||
<TextField
|
||||
select={true}
|
||||
label="Protocol"
|
||||
className={classes.textField}
|
||||
value={connection.protocol}
|
||||
onChange={updateProtocol}
|
||||
margin="dense"
|
||||
inputProps={{
|
||||
'aria-label': 'MQTT protocol'
|
||||
}}
|
||||
>
|
||||
{protocolItems}
|
||||
</TextField>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -144,9 +159,15 @@ function ConnectionSettings(props: Props) {
|
||||
function PasswordVisibilityButton(props: { showPassword: boolean; toggle: () => void }) {
|
||||
return (
|
||||
<InputAdornment position="end">
|
||||
<IconButton aria-label="Toggle password visibility" onClick={props.toggle}>
|
||||
{props.showPassword ? <Visibility /> : <VisibilityOff />}
|
||||
</IconButton>
|
||||
<Tooltip title={props.showPassword ? "Hide password" : "Show password"} arrow>
|
||||
<IconButton
|
||||
aria-label={props.showPassword ? "Hide password" : "Show password"}
|
||||
onClick={props.toggle}
|
||||
edge="end"
|
||||
>
|
||||
{props.showPassword ? <Visibility /> : <VisibilityOff />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
@@ -154,9 +175,9 @@ function ConnectionSettings(props: Props) {
|
||||
const { classes, connection } = props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form className={classes.container} noValidate={true} autoComplete="off">
|
||||
<Grid container={true} spacing={3}>
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<form className={classes.container} noValidate={true} autoComplete="off" style={{ flex: 1, overflow: 'auto' }}>
|
||||
<Grid container={true} spacing={2}>
|
||||
<Grid item={true} xs={5}>
|
||||
<TextField
|
||||
autoFocus={true}
|
||||
@@ -164,7 +185,11 @@ function ConnectionSettings(props: Props) {
|
||||
className={classes.textField}
|
||||
value={connection.name}
|
||||
onChange={handleChange('name')}
|
||||
margin="normal"
|
||||
margin="dense"
|
||||
placeholder="My MQTT Connection"
|
||||
inputProps={{
|
||||
'aria-label': 'Connection name'
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item={true} xs={4}>
|
||||
@@ -187,8 +212,12 @@ function ConnectionSettings(props: Props) {
|
||||
className={classes.textField}
|
||||
value={connection.host}
|
||||
onChange={handleChange('host')}
|
||||
margin="normal"
|
||||
inputProps={{ 'data-testid': 'host-input' }}
|
||||
margin="dense"
|
||||
placeholder="broker.example.com"
|
||||
inputProps={{
|
||||
'data-testid': 'host-input',
|
||||
'aria-label': 'MQTT broker host'
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item={true} xs={3}>
|
||||
@@ -197,7 +226,14 @@ function ConnectionSettings(props: Props) {
|
||||
className={classes.textField}
|
||||
value={connection.port}
|
||||
onChange={handleChange('port')}
|
||||
margin="normal"
|
||||
margin="dense"
|
||||
type="number"
|
||||
placeholder="1883"
|
||||
inputProps={{
|
||||
'aria-label': 'MQTT broker port',
|
||||
min: 1,
|
||||
max: 65535
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
{requiresBasePath() ? renderBasePathInput() : null}
|
||||
@@ -207,54 +243,74 @@ function ConnectionSettings(props: Props) {
|
||||
className={classes.textField}
|
||||
value={connection.username}
|
||||
onChange={handleChange('username')}
|
||||
margin="normal"
|
||||
margin="dense"
|
||||
placeholder="Optional"
|
||||
inputProps={{
|
||||
'aria-label': 'MQTT username',
|
||||
'autoComplete': 'username'
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item={true} xs={requiresBasePath() ? 4 : 6}>
|
||||
<FormControl className={`${classes.textField} ${classes.inputFormControl}`}>
|
||||
<InputLabel htmlFor="adornment-password">Password</InputLabel>
|
||||
<Input
|
||||
id="adornment-password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={connection.password}
|
||||
onChange={handleChange('password')}
|
||||
endAdornment={<PasswordVisibilityButton showPassword={showPassword} toggle={handleClickShowPassword} />}
|
||||
/>
|
||||
</FormControl>
|
||||
<TextField
|
||||
label="Password"
|
||||
className={classes.textField}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={connection.password}
|
||||
onChange={handleChange('password')}
|
||||
margin="dense"
|
||||
placeholder="Optional"
|
||||
InputProps={{
|
||||
endAdornment: <PasswordVisibilityButton showPassword={showPassword} toggle={handleClickShowPassword} />
|
||||
}}
|
||||
inputProps={{
|
||||
'aria-label': 'MQTT password',
|
||||
'autoComplete': 'current-password'
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<br />
|
||||
</form>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', paddingTop: '16px', borderTop: '1px solid rgba(0, 0, 0, 0.12)' }}>
|
||||
<div>
|
||||
<div style={{ float: 'left' }}>
|
||||
<Tooltip title="Delete this connection permanently" arrow>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
className={classes.button}
|
||||
onClick={() => props.managerActions.deleteConnection(props.connection.id)}
|
||||
onClick={handleDelete}
|
||||
aria-label="Delete connection"
|
||||
>
|
||||
Delete <Delete />
|
||||
<Delete /> Delete
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Advanced connection settings" arrow>
|
||||
<Button
|
||||
variant="contained"
|
||||
className={classes.button}
|
||||
onClick={props.managerActions.toggleAdvancedSettings}
|
||||
data-testid="advanced-button"
|
||||
aria-label="Show advanced settings"
|
||||
>
|
||||
<Settings /> Advanced
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ float: 'right' }}>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<Tooltip title="Save connection settings" arrow>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
className={classes.button}
|
||||
onClick={props.managerActions.saveConnectionSettings}
|
||||
aria-label="Save connection"
|
||||
>
|
||||
<Save /> Save
|
||||
</Button>
|
||||
<ConnectButton toggle={toggleConnect} connecting={props.connecting} classes={classes} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<ConnectButton toggle={toggleConnect} connecting={props.connecting} classes={classes} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -270,6 +326,7 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
actions: bindActionCreators(connectionActions, dispatch),
|
||||
managerActions: bindActionCreators(connectionManagerActions, dispatch),
|
||||
globalActions: bindActionCreators(globalActions, dispatch),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,25 @@ import { FormControlLabel, Switch } from '@mui/material'
|
||||
|
||||
export function ToggleSwitch(props: { value: boolean; classes: any; toggle: () => void; label: string }) {
|
||||
const { classes, value, toggle, label } = props
|
||||
const toggleSwitch = <Switch checked={value} onChange={toggle} color="primary" />
|
||||
const toggleSwitch = (
|
||||
<Switch
|
||||
checked={value}
|
||||
onChange={toggle}
|
||||
color="primary"
|
||||
role="switch"
|
||||
aria-checked={value}
|
||||
inputProps={{
|
||||
'aria-label': label
|
||||
}}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<div className={classes.switch}>
|
||||
<FormControlLabel control={toggleSwitch} label={label} labelPlacement="bottom" />
|
||||
<FormControlLabel
|
||||
control={toggleSwitch}
|
||||
label={`${label} (${value ? 'On' : 'Off'})`}
|
||||
labelPlacement="bottom"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user