UI Usability Fixes

* Fallback to sessionStorage if localStorage is absent
* Disable auto-correct and auto-capitalize on username field (SignIn)
* Fix SignIn component name
* Improve support for low screen widths

Co-authored-by: kasedy <kasedy@gmail.com>
This commit is contained in:
rjwats 2020-05-16 12:39:18 +01:00 committed by GitHub
parent a1f4e57a21
commit 7d3bbf4240
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 84 additions and 99 deletions

View File

@ -39,17 +39,17 @@ const styles = (theme: Theme) => createStyles({
}
});
type SignInPageProps = WithSnackbarProps & WithStyles<typeof styles> & AuthenticationContextProps;
type SignInProps = WithSnackbarProps & WithStyles<typeof styles> & AuthenticationContextProps;
interface SignInPageState {
interface SignInState {
username: string,
password: string,
processing: boolean
}
class SignInPage extends Component<SignInPageProps, SignInPageState> {
class SignIn extends Component<SignInProps, SignInState> {
constructor(props: SignInPageProps) {
constructor(props: SignInProps) {
super(props);
this.state = {
username: '',
@ -115,6 +115,10 @@ class SignInPage extends Component<SignInPageProps, SignInPageState> {
value={username}
onChange={this.updateInputElement}
margin="normal"
inputProps={{
autoCapitalize: "none",
autoCorrect: "off",
}}
/>
<PasswordValidator
disabled={processing}
@ -140,4 +144,4 @@ class SignInPage extends Component<SignInPageProps, SignInPageState> {
}
export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignInPage)));
export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignIn)));

View File

@ -7,21 +7,28 @@ export const ACCESS_TOKEN = 'access_token';
export const LOGIN_PATHNAME = 'loginPathname';
export const LOGIN_SEARCH = 'loginSearch';
/**
* Fallback to sessionStorage if localStorage is absent. WebView may not have local storage enabled.
*/
export function getStorage() {
return localStorage || sessionStorage;
}
export function storeLoginRedirect(location?: H.Location) {
if (location) {
localStorage.setItem(LOGIN_PATHNAME, location.pathname);
localStorage.setItem(LOGIN_SEARCH, location.search);
getStorage().setItem(LOGIN_PATHNAME, location.pathname);
getStorage().setItem(LOGIN_SEARCH, location.search);
}
}
export function clearLoginRedirect() {
localStorage.removeItem(LOGIN_PATHNAME);
localStorage.removeItem(LOGIN_SEARCH);
getStorage().removeItem(LOGIN_PATHNAME);
getStorage().removeItem(LOGIN_SEARCH);
}
export function fetchLoginRedirect(): H.LocationDescriptorObject {
const loginPathname = localStorage.getItem(LOGIN_PATHNAME);
const loginSearch = localStorage.getItem(LOGIN_SEARCH);
const loginPathname = getStorage().getItem(LOGIN_PATHNAME);
const loginSearch = getStorage().getItem(LOGIN_SEARCH);
clearLoginRedirect();
return {
pathname: loginPathname || `/${PROJECT_PATH}/`,
@ -33,7 +40,7 @@ export function fetchLoginRedirect(): H.LocationDescriptorObject {
* Wraps the normal fetch routene with one with provides the access token if present.
*/
export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
const accessToken = localStorage.getItem(ACCESS_TOKEN);
const accessToken = getStorage().getItem(ACCESS_TOKEN);
if (accessToken) {
params = params || {};
params.credentials = 'include';
@ -63,7 +70,7 @@ export function redirectingAuthorizedFetch(url: RequestInfo, params?: RequestIni
}
export function addAccessTokenParameter(url: string) {
const accessToken = localStorage.getItem(ACCESS_TOKEN);
const accessToken = getStorage().getItem(ACCESS_TOKEN);
if (!accessToken) {
return url;
}

View File

@ -8,7 +8,7 @@ import { withStyles, Theme, createStyles, WithStyles } from '@material-ui/core/s
import history from '../history'
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
import { ACCESS_TOKEN, authorizedFetch } from './Authentication';
import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
import { AuthenticationContext, Me } from './AuthenticationContext';
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken);
@ -81,7 +81,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
}
refresh = () => {
const accessToken = localStorage.getItem(ACCESS_TOKEN)
const accessToken = getStorage().getItem(ACCESS_TOKEN)
if (accessToken) {
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
.then(response => {
@ -100,7 +100,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
signIn = (accessToken: string) => {
try {
localStorage.setItem(ACCESS_TOKEN, accessToken);
getStorage().setItem(ACCESS_TOKEN, accessToken);
const me: Me = decodeMeJWT(accessToken);
this.setState({ context: { ...this.state.context, me } });
this.props.enqueueSnackbar(`Logged in as ${me.username}`, { variant: 'success' });
@ -111,7 +111,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
}
signOut = () => {
localStorage.removeItem(ACCESS_TOKEN);
getStorage().removeItem(ACCESS_TOKEN);
this.setState({
context: {
...this.state.context,

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Typography, TableRow, TableBody, TableCell, TableHead, Table, Box } from '@material-ui/core';
import { Typography, Box, List, ListItem, ListItemText } from '@material-ui/core';
import { SectionContent } from '../components';
class DemoInformation extends Component {
@ -17,78 +17,52 @@ class DemoInformation extends Component {
simplify merges should you wish to update your project with future framework changes.
</Typography>
<Typography variant="body1" paragraph>
The demo project interface code stored in the interface/project directory:
</Typography>
<Table>
<TableHead>
<TableRow>
<TableCell>
File
</TableCell>
<TableCell>
Description
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>
ProjectMenu.tsx
</TableCell>
<TableCell>
You can add your project's screens to the side bar here.
</TableCell>
</TableRow>
<TableRow>
<TableCell>
ProjectRouting.tsx
</TableCell>
<TableCell>
The routing which controls the screens of your project.
</TableCell>
</TableRow>
<TableRow>
<TableCell>
DemoProject.tsx
</TableCell>
<TableCell>
This screen, with tabs and tab routing.
</TableCell>
</TableRow>
<TableRow>
<TableCell>
DemoInformation.tsx
</TableCell>
<TableCell>
The demo information page.
</TableCell>
</TableRow>
<TableRow>
<TableCell>
LightStateRestController.tsx
</TableCell>
<TableCell>
A form which lets the user control the LED over a REST service.
</TableCell>
</TableRow>
<TableRow>
<TableCell>
LightStateWebSocketController.tsx
</TableCell>
<TableCell>
A form which lets the user control and monitor the status of the LED over WebSockets.
</TableCell>
</TableRow>
<TableRow>
<TableCell>
LightMqttSettingsController.tsx
</TableCell>
<TableCell>
A form which lets the user change the MQTT settings for MQTT based control of the LED.
</TableCell>
</TableRow>
</TableBody>
</Table>
The demo project interface code is stored in the interface/project directory:
</Typography>
<List>
<ListItem>
<ListItemText
primary="ProjectMenu.tsx"
secondary="You can add your project's screens to the side bar here."
/>
</ListItem>
<ListItem>
<ListItemText
primary="ProjectRouting.tsx"
secondary="The routing which controls the screens of your project."
/>
</ListItem>
<ListItem>
<ListItemText
primary="DemoProject.tsx"
secondary="This screen, with tabs and tab routing."
/>
</ListItem>
<ListItem>
<ListItemText
primary="DemoInformation.tsx"
secondary="The demo information page."
/>
</ListItem>
<ListItem>
<ListItemText
primary="LightStateRestController.tsx"
secondary="A form which lets the user control the LED over a REST service."
/>
</ListItem>
<ListItem>
<ListItemText
primary="LightStateWebSocketController.tsx"
secondary="A form which lets the user control and monitor the status of the LED over WebSockets."
/>
</ListItem>
<ListItem>
<ListItemText
primary="LightMqttSettingsController.tsx"
secondary="A form which lets the user change the MQTT settings for MQTT based control of the LED."
/>
</ListItem>
</List>
<Box mt={2}>
<Typography variant="body1">
See the project <a href="https://github.com/rjwats/esp8266-react/">README</a> for a full description of the demo project.

View File

@ -1,7 +1,7 @@
import React, { Fragment } from 'react';
import { ValidatorForm } from 'react-material-ui-form-validator';
import { Table, TableBody, TableCell, TableHead, TableFooter, TableRow } from '@material-ui/core';
import { Table, TableBody, TableCell, TableHead, TableFooter, TableRow, withWidth, WithWidthProps, isWidthDown } from '@material-ui/core';
import { Box, Button, Typography, } from '@material-ui/core';
import EditIcon from '@material-ui/icons/Edit';
@ -28,7 +28,7 @@ function compareUsers(a: User, b: User) {
return 0;
}
type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps;
type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps & WithWidthProps;
type ManageUsersFormState = {
creating: boolean;
@ -106,12 +106,12 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
}
render() {
const { data, loadData } = this.props;
const { width, data, loadData } = this.props;
const { user, creating } = this.state;
return (
<Fragment>
<ValidatorForm onSubmit={this.onSubmit}>
<Table size="small">
<Table size="small" padding={isWidthDown('xs', width!) ? "none" : "default"}>
<TableHead>
<TableRow>
<TableCell>Username</TableCell>
@ -141,12 +141,12 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
</TableRow>
))}
</TableBody>
<TableFooter>
<TableFooter >
<TableRow>
<TableCell colSpan={2} />
<TableCell align="center">
<TableCell align="center" padding="default">
<Button startIcon={<PersonAddIcon />} variant="contained" color="secondary" onClick={this.createUser}>
Add User
Add
</Button>
</TableCell>
</TableRow>
@ -188,4 +188,4 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
}
export default withAuthenticatedContext(ManageUsersForm);
export default withAuthenticatedContext(withWidth()(ManageUsersForm));

View File

@ -79,7 +79,7 @@ class WiFiStatusForm extends Component<WiFiStatusFormProps> {
<SettingsInputComponentIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Gateway IP" secondary={data.gateway_ip ? data.gateway_ip : "none"} />
<ListItemText primary="Gateway IP" secondary={data.gateway_ip || "none"} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>