Skip to content
Snippets Groups Projects
Unverified Commit ee476935 authored by aeschylus's avatar aeschylus Committed by GitHub
Browse files

Merge pull request #1756 from ProjectMirador/1750-window-menu-button

Add a window menu button with thumbnail controls
parents 442a1aa1 fc4745ed
Branches
Tags
No related merge requests found
Showing
with 409 additions and 14 deletions
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
}, },
{ {
loadedManifest: 'https://media.nga.gov/public/manifests/nga_highlights.json', loadedManifest: 'https://media.nga.gov/public/manifests/nga_highlights.json',
thumbnailNavigationDisplayed: false, thumbnailNavigationPosition: 'off',
}] }]
}); });
</script> </script>
......
...@@ -17,7 +17,7 @@ describe('window actions', () => { ...@@ -17,7 +17,7 @@ describe('window actions', () => {
collectionIndex: 0, collectionIndex: 0,
manifestId: null, manifestId: null,
rangeId: null, rangeId: null,
thumbnailNavigationDisplayed: true, thumbnailNavigationPosition: 'bottom',
xywh: [0, 0, 400, 400], xywh: [0, 0, 400, 400],
rotation: null, rotation: null,
}, },
...@@ -46,4 +46,16 @@ describe('window actions', () => { ...@@ -46,4 +46,16 @@ describe('window actions', () => {
expect(actions.toggleWindowSideBar(id)).toEqual(expectedAction); expect(actions.toggleWindowSideBar(id)).toEqual(expectedAction);
}); });
}); });
describe('setWindowThumbnailPosition', () => {
it('returns the appropriate action type', () => {
const id = 'abc123';
const expectedAction = {
type: ActionTypes.SET_WINDOW_THUMBNAIL_POSITION,
windowId: id,
position: 'right',
};
expect(actions.setWindowThumbnailPosition(id, 'right')).toEqual(expectedAction);
});
});
}); });
...@@ -27,7 +27,7 @@ describe('ThumbnailNavigation', () => { ...@@ -27,7 +27,7 @@ describe('ThumbnailNavigation', () => {
window={{ window={{
id: 'foobar', id: 'foobar',
canvasIndex: 1, canvasIndex: 1,
thumbnailNavigationDisplayed: true, thumbnailNavigationPosition: 'bottom',
}} }}
config={{ thumbnailNavigation: { height: 150 } }} config={{ thumbnailNavigation: { height: 150 } }}
setCanvas={setCanvas} setCanvas={setCanvas}
......
import React from 'react';
import { shallow } from 'enzyme';
import WindowThumbnailSettings from '../../../src/components/WindowThumbnailSettings';
describe('WindowThumbnailSettings', () => {
let wrapper;
const setWindowThumbnailPosition = jest.fn();
beforeEach(() => {
wrapper = shallow(
<WindowThumbnailSettings
windowId="xyz"
setWindowThumbnailPosition={setWindowThumbnailPosition}
thumbnailNavigationPosition="bottom"
/>,
);
});
it('renders without an error', () => {
expect(wrapper.find('WithStyles(Typography)').dive().dive().text()).toBe('Thumbnails');
expect(wrapper.find('RadioGroup').props().value).toBe('bottom');
});
it('updates state when the thumbnail config selection changes', () => {
wrapper.find('RadioGroup').first().simulate('change', { target: { value: 'off' } });
expect(setWindowThumbnailPosition).toHaveBeenCalledWith('xyz', 'off');
});
});
import React from 'react';
import { shallow } from 'enzyme';
import WindowTopMenu from '../../../src/components/WindowTopMenu';
describe('WindowTopMenu', () => {
let wrapper;
let handleClose;
beforeEach(() => {
handleClose = jest.fn();
wrapper = shallow(<WindowTopMenu windowId="xyz" handleClose={handleClose} />).dive();
});
it('renders without an error', () => {
expect(wrapper.find('WithStyles(Menu)').length).toBe(1);
expect(wrapper.find('Connect(WindowThumbnailSettings)').length).toBe(1);
});
});
import React from 'react';
import { shallow } from 'enzyme';
import WindowTopMenuButton from '../../../src/components/WindowTopMenuButton';
describe('WindowTopMenuButton', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(
<WindowTopMenuButton classes={{}} windowId="xyz" />,
).dive();
});
it('renders without an error', () => {
expect(wrapper.find('WithStyles(IconButton)').length).toBe(1);
});
it('when clicked, updates the state', () => {
wrapper.find('WithStyles(IconButton)').simulate('click', { currentTarget: 'x' });
expect(wrapper.find('Connect(miradorWithPlugins(WithStyles(WindowTopMenu)))').props().anchorEl).toBe('x');
});
});
...@@ -49,7 +49,7 @@ describe('MiradorViewer', () => { ...@@ -49,7 +49,7 @@ describe('MiradorViewer', () => {
}, },
{ {
loadedManifest: 'https://iiif.harvardartmuseums.org/manifests/object/299843', loadedManifest: 'https://iiif.harvardartmuseums.org/manifests/object/299843',
thumbnailNavigationDisplayed: false, thumbnailNavigationPosition: 'off',
}, },
], ],
}); });
...@@ -57,8 +57,8 @@ describe('MiradorViewer', () => { ...@@ -57,8 +57,8 @@ describe('MiradorViewer', () => {
const { windows } = instance.store.getState(); const { windows } = instance.store.getState();
const windowIds = Object.keys(windows); const windowIds = Object.keys(windows);
expect(Object.keys(windowIds).length).toBe(2); expect(Object.keys(windowIds).length).toBe(2);
expect(windows[windowIds[0]].thumbnailNavigationDisplayed).toBe(true); expect(windows[windowIds[0]].thumbnailNavigationPosition).toBe('bottom');
expect(windows[windowIds[1]].thumbnailNavigationDisplayed).toBe(false); expect(windows[windowIds[1]].thumbnailNavigationPosition).toBe('off');
}); });
}); });
}); });
...@@ -47,6 +47,24 @@ describe('windows reducer', () => { ...@@ -47,6 +47,24 @@ describe('windows reducer', () => {
expect(reducer(before, action)).toEqual(after); expect(reducer(before, action)).toEqual(after);
}); });
it('should handle SET_WINDOW_THUMBNAIL_POSITION by changing the thumbnailNavigationPosition attribute', () => {
const action = {
type: ActionTypes.SET_WINDOW_THUMBNAIL_POSITION,
windowId: 'abc123',
position: 'right',
};
const before = {
abc123: { thumbnailNavigationPosition: 'bottom' },
abc321: { thumbnailNavigationPosition: 'off' },
};
const after = {
abc123: { thumbnailNavigationPosition: 'right' },
abc321: { thumbnailNavigationPosition: 'off' },
};
expect(reducer(before, action)).toEqual(after);
});
it('should handle NEXT_CANVAS', () => { it('should handle NEXT_CANVAS', () => {
expect(reducer({ expect(reducer({
abc123: { abc123: {
......
...@@ -102,7 +102,7 @@ class ThumbnailNavigation extends Component { ...@@ -102,7 +102,7 @@ class ThumbnailNavigation extends Component {
render() { render() {
const { config, window } = this.props; const { config, window } = this.props;
const { canvases } = this.state; const { canvases } = this.state;
if (!window.thumbnailNavigationDisplayed) { if (window.thumbnailNavigationPosition === 'off') {
return <></>; return <></>;
} }
return ( return (
......
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 CancelPresentationIcon from '@material-ui/icons/CancelPresentation';
import VideoLabelIcon from '@material-ui/icons/VideoLabel';
import PropTypes from 'prop-types';
/**
*
*/
export default class WindowThumbnailSettings extends Component {
/**
* constructor -
*/
constructor(props) {
super(props);
this.state = {
};
this.handleChange = this.handleChange.bind(this);
}
/**
* @private
*/
handleChange(event) {
const { windowId, setWindowThumbnailPosition } = this.props;
setWindowThumbnailPosition(windowId, event.target.value);
}
/**
* render
*
* @return {type} description
*/
render() {
const { thumbnailNavigationPosition } = this.props;
return (
<>
<Typography>Thumbnails</Typography>
<RadioGroup aria-label="position" name="position" value={thumbnailNavigationPosition} onChange={this.handleChange} row>
<FormControlLabel
value="off"
control={<Radio color="primary" icon={<CancelPresentationIcon />} checkedIcon={<CancelPresentationIcon />} />}
label="Off"
labelPlacement="bottom"
/>
<FormControlLabel
value="bottom"
control={<Radio color="primary" icon={<ThumbnailNavigationBottomIcon />} checkedIcon={<ThumbnailNavigationBottomIcon />} />}
label="Bottom"
labelPlacement="bottom"
/>
<FormControlLabel
value="right"
control={<Radio color="primary" icon={<ThumbnailNavigationRightIcon />} checkedIcon={<ThumbnailNavigationRightIcon />} />}
label="Right"
labelPlacement="bottom"
/>
</RadioGroup>
</>
);
}
}
/**
* @private
*/
function ThumbnailNavigationBottomIcon(props) {
return (
<VideoLabelIcon />
);
}
/**
* @private
*/
function ThumbnailNavigationRightIcon(props) {
return (
<VideoLabelIcon style={{ transform: 'rotate(-90deg)' }} />
);
}
WindowThumbnailSettings.propTypes = {
windowId: PropTypes.string.isRequired,
setWindowThumbnailPosition: PropTypes.func.isRequired,
thumbnailNavigationPosition: PropTypes.string.isRequired,
};
...@@ -7,6 +7,7 @@ import IconButton from '@material-ui/core/IconButton'; ...@@ -7,6 +7,7 @@ import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu'; import MenuIcon from '@material-ui/icons/Menu';
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from '@material-ui/core/Toolbar';
import classNames from 'classnames'; import classNames from 'classnames';
import WindowTopMenuButton from './WindowTopMenuButton';
import WindowTopBarButtons from '../containers/WindowTopBarButtons'; import WindowTopBarButtons from '../containers/WindowTopBarButtons';
import ns from '../config/css-ns'; import ns from '../config/css-ns';
...@@ -48,6 +49,7 @@ class WindowTopBar extends Component { ...@@ -48,6 +49,7 @@ class WindowTopBar extends Component {
{this.titleContent()} {this.titleContent()}
</Typography> </Typography>
<WindowTopBarButtons windowId={windowId} /> <WindowTopBarButtons windowId={windowId} />
<WindowTopMenuButton className={ns('window-menu-btn')} windowId={windowId} />
<Button color="inherit" className={ns('window-close')} aria-label="Close Window" onClick={removeWindow}>&times;</Button> <Button color="inherit" className={ns('window-close')} aria-label="Close Window" onClick={removeWindow}>&times;</Button>
</Toolbar> </Toolbar>
); );
......
import React, { Component } from 'react';
import { compose } from 'redux';
import ListItem from '@material-ui/core/ListItem';
import Menu from '@material-ui/core/Menu';
import Divider from '@material-ui/core/Divider';
import { withStyles } from '@material-ui/core/styles';
import PropTypes from 'prop-types';
import WindowThumbnailSettings from '../containers/WindowThumbnailSettings';
/**
*/
class WindowTopMenu extends Component {
/**
* constructor -
*/
constructor(props) {
super(props);
this.state = {
};
}
/**
* render
* @return
*/
render() {
const { handleClose, anchorEl, windowId } = this.props;
// const {} = this.state;
return (
<>
<Menu id={`window-menu_${windowId}`} anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose}>
<ListItem>
<WindowThumbnailSettings windowId={windowId} />
</ListItem>
<Divider />
</Menu>
</>
);
}
}
WindowTopMenu.propTypes = {
windowId: PropTypes.string.isRequired,
handleClose: PropTypes.func.isRequired,
anchorEl: PropTypes.object, // eslint-disable-line react/forbid-prop-types
};
WindowTopMenu.defaultProps = {
anchorEl: null,
};
/**
* @private
*/
const styles = theme => ({
});
const enhance = compose(
withStyles(styles),
// further HOC go here
);
export default enhance(WindowTopMenu);
import React, { Component } from 'react';
import { compose } from 'redux';
import IconButton from '@material-ui/core/IconButton';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import { withStyles } from '@material-ui/core/styles';
import PropTypes from 'prop-types';
import WindowTopMenu from '../containers/WindowTopMenu';
/**
*/
class WindowTopMenuButton extends Component {
/**
* constructor -
*/
constructor(props) {
super(props);
this.state = {
anchorEl: null,
};
this.handleMenuClick = this.handleMenuClick.bind(this);
this.handleMenuClose = this.handleMenuClose.bind(this);
}
/**
* @private
*/
handleMenuClick(event) {
this.setState({
anchorEl: event.currentTarget,
});
}
/**
* @private
*/
handleMenuClose() {
this.setState({
anchorEl: null,
});
}
/**
* render
* @return
*/
render() {
const { classes, windowId } = this.props;
const { anchorEl } = this.state;
return (
<>
<IconButton
color="primary"
aria-label="Menu"
className={classes.ctrlBtn}
aria-haspopup="true"
onClick={this.handleMenuClick}
aria-owns={anchorEl ? `window-menu_${windowId}` : undefined}
>
<MoreVertIcon />
</IconButton>
<WindowTopMenu
windowId={windowId}
anchorEl={anchorEl}
handleClose={this.handleMenuClose}
/>
</>
);
}
}
WindowTopMenuButton.propTypes = {
windowId: PropTypes.string.isRequired,
classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
/**
* @private
*/
const styles = theme => ({
ctrlBtn: {
margin: theme.spacing.unit,
},
});
const enhance = compose(
withStyles(styles),
// further HOC go here
);
export default enhance(WindowTopMenuButton);
export default { export default {
windows: [], windows: [],
thumbnailNavigation: { thumbnailNavigation: {
displayedByDefault: true, defaultPosition: 'bottom',
height: 150, height: 150,
}, },
}; };
import { compose } from 'redux';
import { connect } from 'react-redux';
import * as actions from '../state/actions';
import WindowThumbnailSettings from '../components/WindowThumbnailSettings';
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof ManifestListItem
* @private
*/
const mapDispatchToProps = { setWindowThumbnailPosition: actions.setWindowThumbnailPosition };
/**
* mapStateToProps - to hook up connect
* @memberof WindowViewer
* @private
*/
const mapStateToProps = (state, props) => (
{
thumbnailNavigationPosition: state.windows[props.windowId].thumbnailNavigationPosition,
}
);
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
// further HOC go here
);
export default enhance(WindowThumbnailSettings);
import miradorWithPlugins from '../lib/miradorWithPlugins';
import WindowTopMenu from '../components/WindowTopMenu';
export default miradorWithPlugins(WindowTopMenu);
...@@ -43,16 +43,16 @@ class MiradorViewer { ...@@ -43,16 +43,16 @@ class MiradorViewer {
store.dispatch(action); store.dispatch(action);
mergedConfig.windows.forEach((miradorWindow) => { mergedConfig.windows.forEach((miradorWindow) => {
let thumbnailNavigationDisplayed; let thumbnailNavigationPosition;
if (miradorWindow.thumbnailNavigationDisplayed !== undefined) { if (miradorWindow.thumbnailNavigationPosition !== undefined) {
({ thumbnailNavigationDisplayed } = miradorWindow); ({ thumbnailNavigationPosition } = miradorWindow);
} else { } else {
thumbnailNavigationDisplayed = mergedConfig.thumbnailNavigation.displayedByDefault; thumbnailNavigationPosition = mergedConfig.thumbnailNavigation.defaultPosition;
} }
store.dispatch(actions.fetchManifest(miradorWindow.loadedManifest)); store.dispatch(actions.fetchManifest(miradorWindow.loadedManifest));
store.dispatch(actions.addWindow({ store.dispatch(actions.addWindow({
manifestId: miradorWindow.loadedManifest, manifestId: miradorWindow.loadedManifest,
thumbnailNavigationDisplayed, thumbnailNavigationPosition,
})); }));
}); });
} }
......
...@@ -12,6 +12,7 @@ const ActionTypes = { ...@@ -12,6 +12,7 @@ const ActionTypes = {
RECEIVE_MANIFEST: 'RECEIVE_MANIFEST', RECEIVE_MANIFEST: 'RECEIVE_MANIFEST',
RECEIVE_MANIFEST_FAILURE: 'RECEIVE_MANIFEST_FAILURE', RECEIVE_MANIFEST_FAILURE: 'RECEIVE_MANIFEST_FAILURE',
SET_CONFIG: 'SET_CONFIG', SET_CONFIG: 'SET_CONFIG',
SET_WINDOW_THUMBNAIL_POSITION: 'SET_WINDOW_THUMBNAIL_POSITION',
TOGGLE_WINDOW_SIDE_BAR: 'TOGGLE_WINDOW_SIDE_BAR', TOGGLE_WINDOW_SIDE_BAR: 'TOGGLE_WINDOW_SIDE_BAR',
UPDATE_CONFIG: 'UPDATE_CONFIG', UPDATE_CONFIG: 'UPDATE_CONFIG',
REMOVE_MANIFEST: 'REMOVE_MANIFEST', REMOVE_MANIFEST: 'REMOVE_MANIFEST',
......
...@@ -24,7 +24,7 @@ export function addWindow(options) { ...@@ -24,7 +24,7 @@ export function addWindow(options) {
collectionIndex: 0, collectionIndex: 0,
manifestId: null, manifestId: null,
rangeId: null, rangeId: null,
thumbnailNavigationDisplayed: true, // True by default in settings.js thumbnailNavigationPosition: 'bottom', // bottom by default in settings.js
xywh: [0, 0, 400, 400], xywh: [0, 0, 400, 400],
rotation: null, rotation: null,
}; };
...@@ -50,3 +50,13 @@ export function removeWindow(windowId) { ...@@ -50,3 +50,13 @@ export function removeWindow(windowId) {
export function toggleWindowSideBar(windowId) { export function toggleWindowSideBar(windowId) {
return { type: ActionTypes.TOGGLE_WINDOW_SIDE_BAR, windowId }; return { type: ActionTypes.TOGGLE_WINDOW_SIDE_BAR, windowId };
} }
/**
* toggleWindowSideBar - action creator
*
* @param {String} windowId
* @memberof ActionCreators
*/
export function setWindowThumbnailPosition(windowId, position) {
return { type: ActionTypes.SET_WINDOW_THUMBNAIL_POSITION, windowId, position };
}
...@@ -22,6 +22,14 @@ const windowsReducer = (state = {}, action) => { ...@@ -22,6 +22,14 @@ const windowsReducer = (state = {}, action) => {
sideBarOpen: !state[action.windowId].sideBarOpen, sideBarOpen: !state[action.windowId].sideBarOpen,
}, },
}; };
case ActionTypes.SET_WINDOW_THUMBNAIL_POSITION:
return {
...state,
[action.windowId]: {
...state[action.windowId],
thumbnailNavigationPosition: action.position,
},
};
case ActionTypes.NEXT_CANVAS: case ActionTypes.NEXT_CANVAS:
return setCanvasIndex(state, action.windowId, currentIndex => currentIndex + 1); return setCanvasIndex(state, action.windowId, currentIndex => currentIndex + 1);
case ActionTypes.PREVIOUS_CANVAS: case ActionTypes.PREVIOUS_CANVAS:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment