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:
Copilot
2025-12-28 08:50:49 +01:00
committed by GitHub
parent 4de52aba7c
commit 36b4c0fce5
4 changed files with 545 additions and 51 deletions

View File

@@ -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 />
&nbsp;&nbsp;Abort
&nbsp;&nbsp;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>
)

View File

@@ -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),
}
}

View File

@@ -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>
)
}