Skip to content
Snippets Groups Projects
Unverified Commit 414f532a authored by Jack Reed's avatar Jack Reed Committed by GitHub
Browse files

Merge pull request #2182 from ProjectMirador/2029-window-top-menu-keyboard-nav

Initial pass at a more keyboard accessible WindowTopMenu
parents e5a3f6b1 80e85b29
No related branches found
No related tags found
No related merge requests found
import React from 'react';
import { shallow } from 'enzyme';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import RadioGroup from '@material-ui/core/RadioGroup';
import Typography from '@material-ui/core/Typography';
import ListSubheader from '@material-ui/core/ListSubheader';
import MenuItem from '@material-ui/core/MenuItem';
import { WindowThumbnailSettings } from '../../../src/components/WindowThumbnailSettings';
/** create wrapper */
function createWrapper(props) {
return shallow(
<WindowThumbnailSettings
classes={{}}
windowId="xyz"
setWindowThumbnailPosition={() => {}}
thumbnailNavigationPosition="off"
......@@ -20,8 +21,7 @@ function createWrapper(props) {
describe('WindowThumbnailSettings', () => {
it('renders all elements correctly', () => {
const wrapper = createWrapper();
expect(wrapper.find(Typography).length).toBe(1);
expect(wrapper.find(RadioGroup).length).toBe(1);
expect(wrapper.find(ListSubheader).length).toBe(1);
const labels = wrapper.find(FormControlLabel);
expect(labels.length).toBe(3);
expect(labels.at(0).props().value).toBe('off');
......@@ -29,19 +29,22 @@ describe('WindowThumbnailSettings', () => {
expect(labels.at(2).props().value).toBe('far-right');
});
it('should set the correct label active', () => {
it('should set the correct label active (by setting the secondary color)', () => {
let wrapper = createWrapper({ thumbnailNavigationPosition: 'far-bottom' });
expect(wrapper.find(RadioGroup).props().value).toBe('far-bottom');
expect(wrapper.find(FormControlLabel).at(1).props().control.props.color).toEqual('secondary');
expect(wrapper.find(FormControlLabel).at(2).props().control.props.color).not.toEqual('secondary');
wrapper = createWrapper({ thumbnailNavigationPosition: 'far-right' });
expect(wrapper.find(RadioGroup).props().value).toBe('far-right');
expect(wrapper.find(FormControlLabel).at(2).props().control.props.color).toEqual('secondary');
});
it('updates state when the thumbnail config selection changes', () => {
const setWindowThumbnailPosition = jest.fn();
const wrapper = createWrapper({ setWindowThumbnailPosition });
wrapper.find(RadioGroup).first().simulate('change', { target: { value: 'off' } });
wrapper.find(MenuItem).at(0).simulate('click');
expect(setWindowThumbnailPosition).toHaveBeenCalledWith('xyz', 'off');
wrapper.find(RadioGroup).first().simulate('change', { target: { value: 'far-right' } });
wrapper.find(MenuItem).at(2).simulate('click');
expect(setWindowThumbnailPosition).toHaveBeenCalledWith('xyz', 'far-right');
});
});
import React from 'react';
import { shallow } from 'enzyme';
import ListItem from '@material-ui/core/ListItem';
import Menu from '@material-ui/core/Menu';
import WindowThumbnailSettings from '../../../src/containers/WindowThumbnailSettings';
import WindowViewSettings from '../../../src/containers/WindowViewSettings';
......@@ -23,7 +22,6 @@ describe('WindowTopMenu', () => {
it('renders all needed elements', () => {
const wrapper = createWrapper();
expect(wrapper.find(Menu).length).toBe(1);
expect(wrapper.find(ListItem).length).toBe(2);
expect(wrapper.find(WindowThumbnailSettings).length).toBe(1);
expect(wrapper.find(WindowViewSettings).length).toBe(1);
});
......
import React from 'react';
import { shallow } from 'enzyme';
import { mount, shallow } from 'enzyme';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import RadioGroup from '@material-ui/core/RadioGroup';
import Typography from '@material-ui/core/Typography';
import ListSubheader from '@material-ui/core/ListSubheader';
import MenuItem from '@material-ui/core/MenuItem';
import { WindowViewSettings } from '../../../src/components/WindowViewSettings';
/** create wrapper */
function createWrapper(props) {
return shallow(
<WindowViewSettings
classes={{}}
windowId="xyz"
setWindowViewType={() => {}}
windowViewType="single"
......@@ -20,27 +21,55 @@ function createWrapper(props) {
describe('WindowViewSettings', () => {
it('renders all elements correctly', () => {
const wrapper = createWrapper();
expect(wrapper.find(Typography).length).toBe(1);
expect(wrapper.find(RadioGroup).length).toBe(1);
expect(wrapper.find(ListSubheader).length).toBe(1);
const labels = wrapper.find(FormControlLabel);
expect(labels.length).toBe(2);
expect(labels.at(0).props().value).toBe('single');
expect(labels.at(1).props().value).toBe('book');
});
it('should set the correct label active', () => {
it('should set the correct label active (by setting the secondary color)', () => {
let wrapper = createWrapper({ windowViewType: 'single' });
expect(wrapper.find(RadioGroup).props().value).toBe('single');
expect(wrapper.find(FormControlLabel).at(0).props().control.props.color).toEqual('secondary');
expect(wrapper.find(FormControlLabel).at(1).props().control.props.color).not.toEqual('secondary');
wrapper = createWrapper({ windowViewType: 'book' });
expect(wrapper.find(RadioGroup).props().value).toBe('book');
expect(wrapper.find(FormControlLabel).at(1).props().control.props.color).toEqual('secondary');
});
it('updates state when the view config selection changes', () => {
const setWindowViewType = jest.fn();
const wrapper = createWrapper({ setWindowViewType });
wrapper.find(RadioGroup).first().simulate('change', { target: { value: 'book' } });
expect(setWindowViewType).toHaveBeenCalledWith('xyz', 'book');
wrapper.find(RadioGroup).first().simulate('change', { target: { value: 'single' } });
wrapper.find(MenuItem).at(0).simulate('click');
expect(setWindowViewType).toHaveBeenCalledWith('xyz', 'single');
wrapper.find(MenuItem).at(1).simulate('click');
expect(setWindowViewType).toHaveBeenCalledWith('xyz', 'book');
});
it('sets the selected ref to a MenuItem in the component (when mounting)', () => {
const wrapper = mount(
<WindowViewSettings
classes={{}}
windowId="xyz"
setWindowViewType={() => {}}
windowViewType="single"
/>,
);
expect(
wrapper // eslint-disable-line no-underscore-dangle
.instance()
.selectedRef
._reactInternalFiber
.type
.displayName,
).toEqual('WithStyles(MenuItem)');
// The document's ActiveElement is an li
expect(
document
.activeElement[Object.keys(document.activeElement)[0]]
.elementType,
).toEqual('li');
});
});
import React, { Component } from 'react';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Radio from '@material-ui/core/Radio';
import RadioGroup from '@material-ui/core/RadioGroup';
import Typography from '@material-ui/core/Typography';
import ListSubheader from '@material-ui/core/ListSubheader';
import MenuItem from '@material-ui/core/MenuItem';
import ThumbnailsOffIcon from '@material-ui/icons/CropDinSharp';
import PropTypes from 'prop-types';
import ThumbnailNavigationBottomIcon from './icons/ThumbnailNavigationBottomIcon';
......@@ -22,10 +21,10 @@ export class WindowThumbnailSettings extends Component {
/**
* @private
*/
handleChange(event) {
handleChange(value) {
const { windowId, setWindowThumbnailPosition } = this.props;
setWindowThumbnailPosition(windowId, event.target.value);
setWindowThumbnailPosition(windowId, value);
}
/**
......@@ -34,42 +33,61 @@ export class WindowThumbnailSettings extends Component {
* @return {type} description
*/
render() {
const { thumbnailNavigationPosition, t } = this.props;
const {
classes, handleClose, t, thumbnailNavigationPosition,
} = this.props;
return (
<>
<Typography>{t('thumbnails')}</Typography>
<RadioGroup aria-label={t('position')} name="position" value={thumbnailNavigationPosition} onChange={this.handleChange} row>
<ListSubheader role="presentation" tabIndex="-1">{t('thumbnails')}</ListSubheader>
<MenuItem className={classes.MenuItem} onClick={() => { this.handleChange('off'); handleClose(); }}>
<FormControlLabel
value="off"
control={<Radio color="secondary" icon={<ThumbnailsOffIcon />} checkedIcon={<ThumbnailsOffIcon />} />}
classes={{ label: thumbnailNavigationPosition === 'off' ? classes.selectedLabel : undefined }}
control={
<ThumbnailsOffIcon color={thumbnailNavigationPosition === 'off' ? 'secondary' : undefined} />
}
label={t('off')}
labelPlacement="bottom"
/>
</MenuItem>
<MenuItem className={classes.MenuItem} onClick={() => { this.handleChange('far-bottom'); handleClose(); }}>
<FormControlLabel
value="far-bottom"
control={<Radio color="secondary" icon={<ThumbnailNavigationBottomIcon />} checkedIcon={<ThumbnailNavigationBottomIcon />} />}
classes={{ label: thumbnailNavigationPosition === 'far-bottom' ? classes.selectedLabel : undefined }}
control={
<ThumbnailNavigationBottomIcon color={thumbnailNavigationPosition === 'far-bottom' ? 'secondary' : undefined} />
}
label={t('bottom')}
labelPlacement="bottom"
/>
</MenuItem>
<MenuItem className={classes.MenuItem} onClick={() => { this.handleChange('far-right'); handleClose(); }}>
<FormControlLabel
value="far-right"
control={<Radio color="secondary" icon={<ThumbnailNavigationRightIcon />} checkedIcon={<ThumbnailNavigationRightIcon />} />}
classes={{ label: thumbnailNavigationPosition === 'far-right' ? classes.selectedLabel : undefined }}
control={
<ThumbnailNavigationRightIcon color={thumbnailNavigationPosition === 'far-right' ? 'secondary' : undefined} />
}
label={t('right')}
labelPlacement="bottom"
/>
</RadioGroup>
</MenuItem>
</>
);
}
}
WindowThumbnailSettings.propTypes = {
classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
handleClose: PropTypes.func,
windowId: PropTypes.string.isRequired,
setWindowThumbnailPosition: PropTypes.func.isRequired,
thumbnailNavigationPosition: PropTypes.string.isRequired,
t: PropTypes.func,
};
WindowThumbnailSettings.defaultProps = {
handleClose: () => {},
t: key => key,
};
import React, { Component } from 'react';
import ListItem from '@material-ui/core/ListItem';
import Menu from '@material-ui/core/Menu';
import PropTypes from 'prop-types';
import WindowThumbnailSettings from '../containers/WindowThumbnailSettings';
......@@ -36,13 +35,10 @@ export class WindowTopMenu extends Component {
getContentAnchorEl={null}
open={Boolean(anchorEl)}
onClose={handleClose}
disableAutoFocusItem
>
<ListItem divider>
<WindowViewSettings windowId={windowId} />
</ListItem>
<ListItem divider>
<WindowThumbnailSettings windowId={windowId} />
</ListItem>
<WindowViewSettings windowId={windowId} handleClose={handleClose} />
<WindowThumbnailSettings windowId={windowId} handleClose={handleClose} />
</Menu>
</>
);
......
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Radio from '@material-ui/core/Radio';
import RadioGroup from '@material-ui/core/RadioGroup';
import Typography from '@material-ui/core/Typography';
import MenuItem from '@material-ui/core/MenuItem';
import ListSubheader from '@material-ui/core/ListSubheader';
import SingleIcon from '@material-ui/icons/CropOriginalSharp';
import PropTypes from 'prop-types';
import BookViewIcon from './icons/BookViewIcon';
......@@ -19,13 +19,32 @@ export class WindowViewSettings extends Component {
this.handleChange = this.handleChange.bind(this);
}
/**
* Take action when the component mounts for the first time
*/
componentDidMount() {
if (this.selectedRef) {
// MUI uses ReactDOM.findDOMNode and refs for handling focus
ReactDOM.findDOMNode(this.selectedRef).focus(); // eslint-disable-line react/no-find-dom-node
}
}
/**
* @private
*/
handleSelectedRef(ref) {
if (this.selectedRef) return;
this.selectedRef = ref;
}
/**
* @private
*/
handleChange(event) {
handleChange(value) {
const { windowId, setWindowViewType } = this.props;
setWindowViewType(windowId, event.target.value);
setWindowViewType(windowId, value);
}
/**
......@@ -34,36 +53,50 @@ export class WindowViewSettings extends Component {
* @return {type} description
*/
render() {
const { windowViewType, t } = this.props;
const {
classes, handleClose, t, windowViewType,
} = this.props;
return (
<>
<Typography>{t('view')}</Typography>
<RadioGroup aria-label={t('position')} name="position" value={windowViewType} onChange={this.handleChange} row>
<ListSubheader role="presentation" tabIndex="-1">{t('view')}</ListSubheader>
<MenuItem
className={classes.MenuItem}
ref={ref => this.handleSelectedRef(ref)}
onClick={() => { this.handleChange('single'); handleClose(); }}
>
<FormControlLabel
value="single"
control={<Radio color="secondary" icon={<SingleIcon />} checkedIcon={<SingleIcon />} />}
classes={{ label: windowViewType === 'single' ? classes.selectedLabel : undefined }}
control={<SingleIcon color={windowViewType === 'single' ? 'secondary' : undefined} />}
label={t('single')}
labelPlacement="bottom"
/>
</MenuItem>
<MenuItem className={classes.MenuItem} onClick={() => { this.handleChange('book'); handleClose(); }}>
<FormControlLabel
value="book"
control={<Radio color="secondary" icon={<BookViewIcon />} checkedIcon={<BookViewIcon />} />}
classes={{ label: windowViewType === 'book' ? classes.selectedLabel : undefined }}
control={<BookViewIcon color={windowViewType === 'book' ? 'secondary' : undefined} />}
label={t('book')}
labelPlacement="bottom"
/>
</RadioGroup>
</MenuItem>
</>
);
}
}
WindowViewSettings.propTypes = {
classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
handleClose: PropTypes.func,
windowId: PropTypes.string.isRequired,
setWindowViewType: PropTypes.func.isRequired,
windowViewType: PropTypes.string.isRequired,
t: PropTypes.func,
};
WindowViewSettings.defaultProps = {
handleClose: () => {},
t: key => key,
};
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import { withStyles } from '@material-ui/core/styles';
import * as actions from '../state/actions';
import { getThumbnailNavigationPosition } from '../state/selectors';
import { WindowThumbnailSettings } from '../components/WindowThumbnailSettings';
......@@ -23,7 +24,18 @@ const mapStateToProps = (state, props) => (
}
);
/** */
const styles = theme => ({
selectedLabel: {
color: theme.palette.secondary.main,
},
MenuItem: {
display: 'inline',
},
});
const enhance = compose(
withStyles(styles),
withTranslation(),
connect(mapStateToProps, mapDispatchToProps),
// further HOC go here
......
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import { withStyles } from '@material-ui/core/styles';
import * as actions from '../state/actions';
import { getWindowViewType } from '../state/selectors';
import { WindowViewSettings } from '../components/WindowViewSettings';
......@@ -23,7 +24,18 @@ const mapStateToProps = (state, props) => (
}
);
/** */
const styles = theme => ({
selectedLabel: {
color: theme.palette.secondary.main,
},
MenuItem: {
display: 'inline',
},
});
const enhance = compose(
withStyles(styles),
withTranslation(),
connect(mapStateToProps, mapDispatchToProps),
);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment