diff --git a/__tests__/src/actions/window.test.js b/__tests__/src/actions/window.test.js index 3977bd6597fb8334fd263b157787aece80ac70b0..11a5055027b2d9b9baf16f65d106d413bc24c132 100644 --- a/__tests__/src/actions/window.test.js +++ b/__tests__/src/actions/window.test.js @@ -58,4 +58,17 @@ describe('window actions', () => { expect(actions.setWindowThumbnailPosition(id, 'right')).toEqual(expectedAction); }); }); + + describe('toggleWindowSideBarPanel', () => { + it('returns the appropriate action type', () => { + const windowId = 'abc123'; + const panelType = 'panelType'; + const expectedAction = { + type: ActionTypes.TOGGLE_WINDOW_SIDE_BAR_PANEL, + windowId, + panelType, + }; + expect(actions.toggleWindowSideBarPanel(windowId, 'panelType')).toEqual(expectedAction); + }); + }); }); diff --git a/__tests__/src/components/WindowSideBar.test.js b/__tests__/src/components/WindowSideBar.test.js index 6c651dbf435354f7abf9098120a75cd904dca848..74d0d015084cf699b32a91edb3d3548978b1a04c 100644 --- a/__tests__/src/components/WindowSideBar.test.js +++ b/__tests__/src/components/WindowSideBar.test.js @@ -9,7 +9,7 @@ describe('WindowSideBar', () => { }); it('renders without an error', () => { - expect(wrapper.find('WithStyles(Drawer)').length).toBe(1); + expect(wrapper.find('WithStyles(Drawer)').length).toBe(2); expect(wrapper.find('WithStyles(List)').length).toBe(1); }); }); diff --git a/__tests__/src/components/WindowSideBarButtons.test.js b/__tests__/src/components/WindowSideBarButtons.test.js index cbb38135a5038940f2bc590bea77faaa96a37799..0ffde2da597f94e00f98d9e6751c72681307bb5f 100644 --- a/__tests__/src/components/WindowSideBarButtons.test.js +++ b/__tests__/src/components/WindowSideBarButtons.test.js @@ -11,4 +11,16 @@ describe('WindowSideBarButtons', () => { it('renders without an error', () => { expect(wrapper.find('Fragment').length).toBe(1); }); + + it('triggers the toggleWindowSideBarPanel prop on click', () => { + const toggleWindowSideBarPanel = jest.fn(); + wrapper = shallow( + <WindowSideBarButtons toggleWindowSideBarPanel={toggleWindowSideBarPanel} />, + ); + + const iconButton = wrapper.find('WithStyles(IconButton)[aria-label="Open information companion window"]'); + expect(iconButton.simulate('click')); + expect(toggleWindowSideBarPanel).toHaveBeenCalledTimes(1); + expect(toggleWindowSideBarPanel).toHaveBeenCalledWith('info'); + }); }); diff --git a/__tests__/src/components/WindowSideBarInfoPanel.test.js b/__tests__/src/components/WindowSideBarInfoPanel.test.js new file mode 100644 index 0000000000000000000000000000000000000000..e74a3e79929e997a818a56538cf1402fbf66c676 --- /dev/null +++ b/__tests__/src/components/WindowSideBarInfoPanel.test.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import createStore from '../../../src/state/createStore'; +import * as actions from '../../../src/state/actions'; +import WindowSideBarInfoPanel from '../../../src/components/WindowSideBarInfoPanel'; +import fixture from '../../fixtures/version-2/001.json'; + +describe('WindowSideBarInfoPanel', () => { + let wrapper; + let manifest; + const store = createStore(); + + beforeEach(() => { + store.dispatch(actions.receiveManifest('foo', fixture)); + manifest = store.getState().manifests.foo; + wrapper = shallow(<WindowSideBarInfoPanel manifest={manifest} />); + }); + + it('renders without an error', () => { + expect(wrapper.find('h2').text()).toBe('About this item'); + expect(wrapper.find('h3').text()).toBe('Bodleian Library Human Freaks 2 (33)'); + expect(wrapper.find('.mirador-window-sidebar-info-panel div').text()).toBe('[Handbill of Mr. Becket, [1787] ]'); + }); +}); diff --git a/__tests__/src/components/WindowSideBarPanel.test.js b/__tests__/src/components/WindowSideBarPanel.test.js new file mode 100644 index 0000000000000000000000000000000000000000..1478f900e36add28440a0f2a47a87ea14f07c015 --- /dev/null +++ b/__tests__/src/components/WindowSideBarPanel.test.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import createStore from '../../../src/state/createStore'; +import * as actions from '../../../src/state/actions'; +import WindowSideBarPanel from '../../../src/components/WindowSideBarPanel'; +import fixture from '../../fixtures/version-2/001.json'; + +describe('WindowSideBarPanel', () => { + let wrapper; + let manifest; + const store = createStore(); + + beforeEach(() => { + store.dispatch(actions.receiveManifest('foo', fixture)); + manifest = store.getState().manifests.foo; + }); + + describe('when the sideBarPanel is set to "info"', () => { + beforeEach(() => { + wrapper = shallow(<WindowSideBarPanel sideBarPanel="info" manifest={manifest} />); + }); + + it('renders the WindowSideBarInfoPanel', () => { + expect(wrapper.find('WindowSideBarInfoPanel').length).toBe(1); + }); + }); + + describe('when the sideBarPanel is set to "closed" (or any other unknown value)', () => { + beforeEach(() => { + wrapper = shallow(<WindowSideBarPanel sideBarPanel="closed" manifest={manifest} />); + }); + + it('does not render any panel component', () => { + expect(wrapper.find('WindowSideBarInfoPanel').length).toBe(0); + }); + }); +}); diff --git a/__tests__/src/reducers/windows.test.js b/__tests__/src/reducers/windows.test.js index ceb3e1936cfbf3b31642ebf4b1d53967f97c9efe..f7c09854bbb636c7aabe9650c6d75ed5609e9fc4 100644 --- a/__tests__/src/reducers/windows.test.js +++ b/__tests__/src/reducers/windows.test.js @@ -65,6 +65,44 @@ describe('windows reducer', () => { expect(reducer(before, action)).toEqual(after); }); + describe('TOGGLE_WINDOW_SIDE_BAR_PANEL', () => { + it('sets the sideBarPanel value to the given value when it was changed', () => { + const action = { + type: ActionTypes.TOGGLE_WINDOW_SIDE_BAR_PANEL, + windowId: 'abc123', + panelType: 'info', + }; + const before = { + abc123: { sideBarPanel: 'closed' }, + abc321: { sideBarPanel: 'closed' }, + }; + const after = { + abc123: { sideBarPanel: 'info' }, + abc321: { sideBarPanel: 'closed' }, + }; + + expect(reducer(before, action)).toEqual(after); + }); + + it('sets the sideBarPanel value to "closed" when trying to open a panel that already is open', () => { + const action = { + type: ActionTypes.TOGGLE_WINDOW_SIDE_BAR_PANEL, + windowId: 'abc123', + panelType: 'info', + }; + const before = { + abc123: { sideBarPanel: 'info' }, + abc321: { sideBarPanel: 'closed' }, + }; + const after = { + abc123: { sideBarPanel: 'closed' }, + abc321: { sideBarPanel: 'closed' }, + }; + + expect(reducer(before, action)).toEqual(after); + }); + }); + it('should handle NEXT_CANVAS', () => { expect(reducer({ abc123: { diff --git a/src/components/WindowMiddleContent.js b/src/components/WindowMiddleContent.js index 2b3f49b10754874a03ed50757af0735558f5022c..766547a3528a327168961ab22a4606fdd2360d6a 100644 --- a/src/components/WindowMiddleContent.js +++ b/src/components/WindowMiddleContent.js @@ -39,6 +39,7 @@ class WindowMiddleContent extends Component { windowId={window.id} manifest={manifest} sideBarOpen={window.sideBarOpen} + sideBarPanel={window.sideBarPanel} /> <CompanionWindow windowId={window.id} diff --git a/src/components/WindowSideBar.js b/src/components/WindowSideBar.js index e3c928846492afa1dd7d8faed226fbed703454a6..4221c0fcbda3c75a850e73c1c27a1a8ad5955bbe 100644 --- a/src/components/WindowSideBar.js +++ b/src/components/WindowSideBar.js @@ -5,6 +5,8 @@ import Drawer from '@material-ui/core/Drawer'; import { withStyles } from '@material-ui/core/styles'; import List from '@material-ui/core/List'; import WindowSideBarButtons from '../containers/WindowSideBarButtons'; +import WindowSideBarPanel from './WindowSideBarPanel'; +import ns from '../config/css-ns'; /** * WindowSideBar @@ -16,42 +18,67 @@ class WindowSideBar extends Component { */ render() { const { - windowId, classes, sideBarOpen, + classes, manifest, windowId, sideBarOpen, sideBarPanel, } = this.props; return ( - <Drawer - variant="temporary" - className={classNames(classes.drawer)} - classes={{ paper: classNames(classes.drawer) }} - open={sideBarOpen} - anchor="left" - PaperProps={{ style: { position: 'relative' } }} - ModalProps={{ - container: document.getElementById(windowId), - disablePortal: true, - hideBackdrop: true, - style: { position: 'absolute' }, - }} - > - <div className={classes.toolbar} /> - <List> - <WindowSideBarButtons windowId={windowId} /> - </List> - </Drawer> + <> + <Drawer + variant="temporary" + className={classNames(classes.drawer)} + classes={{ paper: classNames(classes.drawer) }} + open={sideBarOpen} + anchor="left" + PaperProps={{ style: { position: 'relative' } }} + ModalProps={{ + container: document.getElementById(windowId), + disablePortal: true, + hideBackdrop: true, + style: { position: 'absolute' }, + }} + > + <List> + <WindowSideBarButtons windowId={windowId} sideBarPanel={sideBarPanel} /> + </List> + </Drawer> + <Drawer + variant="temporary" + className={classNames(classes.drawer, ns('window-sidebar-panel-drawer'))} + classes={{ paper: classNames(classes.drawer) }} + open={sideBarOpen && sideBarPanel !== 'closed'} + anchor="left" + PaperProps={{ style: { position: 'relative', width: '200px' } }} + ModalProps={{ + container: document.getElementById(windowId), + disablePortal: true, + hideBackdrop: true, + style: { position: 'absolute', width: '200px' }, + }} + > + <WindowSideBarPanel + manifest={manifest} + windowId={windowId} + sideBarPanel={sideBarPanel} + /> + </Drawer> + </> ); } } WindowSideBar.propTypes = { - windowId: PropTypes.string.isRequired, classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types, + manifest: PropTypes.object, // eslint-disable-line react/forbid-prop-types + windowId: PropTypes.string.isRequired, sideBarOpen: PropTypes.bool, + sideBarPanel: PropTypes.string, }; WindowSideBar.defaultProps = { + manifest: {}, sideBarOpen: false, + sideBarPanel: 'closed', }; /** diff --git a/src/components/WindowSideBarButtons.js b/src/components/WindowSideBarButtons.js index 2cd1d2258a351be5737a41bcee11f07558a6af20..037349f635a591b5326c3a68edd4f5a82f4eaf01 100644 --- a/src/components/WindowSideBarButtons.js +++ b/src/components/WindowSideBarButtons.js @@ -1,17 +1,53 @@ import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import IconButton from '@material-ui/core/IconButton'; +import InfoIcon from '@material-ui/icons/Info'; /** * */ class WindowSideBarButtons extends Component { + /** + * sideBarPanelCurrentlySelected - return if the given sideBarPanel is currently selected + * @return Boolean + */ + sideBarPanelCurrentlySelected(panelType) { + const { sideBarPanel } = this.props; + + return sideBarPanel === panelType; + } + /** * render * * @return {type} description */ render() { - return (<></>); + const { toggleWindowSideBarPanel } = this.props; + return ( + <> + <IconButton + aria-label="Open information companion window" + color="inherit" + onClick={() => (toggleWindowSideBarPanel('info'))} + > + <InfoIcon + color={this.sideBarPanelCurrentlySelected('info') ? 'action' : 'inherit'} + /> + </IconButton> + </> + ); } } +WindowSideBarButtons.propTypes = { + toggleWindowSideBarPanel: PropTypes.func, + sideBarPanel: PropTypes.string, +}; + +WindowSideBarButtons.defaultProps = { + toggleWindowSideBarPanel: () => {}, + sideBarPanel: 'closed', +}; + export default WindowSideBarButtons; diff --git a/src/components/WindowSideBarInfoPanel.js b/src/components/WindowSideBarInfoPanel.js new file mode 100644 index 0000000000000000000000000000000000000000..83de8502b1559a935a636c4e4dc5bd816e61ef73 --- /dev/null +++ b/src/components/WindowSideBarInfoPanel.js @@ -0,0 +1,57 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ns from '../config/css-ns'; + +/** + * WindowSideBarInfoPanel + */ +export default class WindowSideBarInfoPanel extends Component { + /** + * manifestLabel - get the label from the manifesto manifestation + * @return String + */ + manifestLabel() { + const { manifest } = this.props; + + if (manifest.manifestation) { + return manifest.manifestation.getLabel().map(label => label.value)[0]; + } + return ''; + } + + /** + * manifestDescription - get the description from the manifesto manifestation + * @return String + */ + manifestDescription() { + const { manifest } = this.props; + + if (manifest.manifestation) { + return manifest.manifestation.getDescription().map(label => label.value); + } + return ''; + } + + /** + * render + * @return + */ + render() { + return ( + <div className={ns('window-sidebar-info-panel')}> + <h2>About this item</h2> + <h3>{this.manifestLabel()}</h3> + <div>{this.manifestDescription()}</div> + </div> + ); + } +} + +WindowSideBarInfoPanel.propTypes = { + manifest: PropTypes.object, // eslint-disable-line react/forbid-prop-types +}; + + +WindowSideBarInfoPanel.defaultProps = { + manifest: {}, +}; diff --git a/src/components/WindowSideBarPanel.js b/src/components/WindowSideBarPanel.js new file mode 100644 index 0000000000000000000000000000000000000000..a34d1622aaf65cd00ec322c677caeb5e1e2f67e5 --- /dev/null +++ b/src/components/WindowSideBarPanel.js @@ -0,0 +1,46 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import WindowSideBarInfoPanel from './WindowSideBarInfoPanel'; + +/** + * WindowSideBarPanel - the panel that pops out from the sidebar + * when various icons are clicked such as Info, Search, etc. + */ +class WindowSideBarPanel extends Component { + /** + * activePanelComponent + * @return React Component + */ + activePanelComponent() { + const { manifest, sideBarPanel } = this.props; + switch (sideBarPanel) { + case 'info': + return <WindowSideBarInfoPanel manifest={manifest} />; + default: + return null; + } + } + + /** + * render + * @return + */ + render() { + return ( + <div> + {this.activePanelComponent()} + </div> + ); + } +} + +WindowSideBarPanel.propTypes = { + manifest: PropTypes.object, // eslint-disable-line react/forbid-prop-types + sideBarPanel: PropTypes.string, +}; +WindowSideBarPanel.defaultProps = { + manifest: {}, + sideBarPanel: 'closed', // Closed will fall out to the default null case for the actiuve panel +}; + +export default WindowSideBarPanel; diff --git a/src/containers/WindowSideBarButtons.js b/src/containers/WindowSideBarButtons.js index eb200833775acd08a27b05a88425bc7190075324..93a58e23f8b1ab6288bda18cc31f7b730e02c641 100644 --- a/src/containers/WindowSideBarButtons.js +++ b/src/containers/WindowSideBarButtons.js @@ -1,4 +1,25 @@ +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import * as actions from '../state/actions'; import miradorWithPlugins from '../lib/miradorWithPlugins'; import WindowSideBarButtons from '../components/WindowSideBarButtons'; -export default miradorWithPlugins(WindowSideBarButtons); + +/** + * mapDispatchToProps - used to hook up connect to action creators + * @memberof WindowSideButtons + * @private + */ +const mapDispatchToProps = (dispatch, props) => ({ + toggleWindowSideBarPanel: panelType => dispatch( + actions.toggleWindowSideBarPanel(props.windowId, panelType), + ), +}); + +const enhance = compose( + connect(null, mapDispatchToProps), + miradorWithPlugins, + // further HOC go here +); + +export default enhance(WindowSideBarButtons); diff --git a/src/containers/WindowSideBarPanel.js b/src/containers/WindowSideBarPanel.js new file mode 100644 index 0000000000000000000000000000000000000000..1c271c43d54b3cad031d846a3a1a469e879beaf5 --- /dev/null +++ b/src/containers/WindowSideBarPanel.js @@ -0,0 +1,4 @@ +import miradorWithPlugins from '../lib/miradorWithPlugins'; +import WindowSideBarPanel from '../components/WindowSideBarPanel'; + +export default miradorWithPlugins(WindowSideBarPanel); diff --git a/src/state/actions/action-types.js b/src/state/actions/action-types.js index 302d40515fb883623162514d066515d585fe7a89..d21f58b39f2ea2ba9f63915cbde1c11e636abeb3 100644 --- a/src/state/actions/action-types.js +++ b/src/state/actions/action-types.js @@ -14,6 +14,7 @@ const ActionTypes = { SET_CONFIG: 'SET_CONFIG', SET_WINDOW_THUMBNAIL_POSITION: 'SET_WINDOW_THUMBNAIL_POSITION', TOGGLE_WINDOW_SIDE_BAR: 'TOGGLE_WINDOW_SIDE_BAR', + TOGGLE_WINDOW_SIDE_BAR_PANEL: 'TOGGLE_WINDOW_SIDE_BAR_PANEL', UPDATE_CONFIG: 'UPDATE_CONFIG', REMOVE_MANIFEST: 'REMOVE_MANIFEST', REQUEST_INFO_RESPONSE: 'REQUEST_INFO_RESPONSE', diff --git a/src/state/actions/window.js b/src/state/actions/window.js index 605b690b8b38bc86b52f8fbc33a3a1c8216c2bee..ee50b7827755f19190dcfd8d0969b5567505d899 100644 --- a/src/state/actions/window.js +++ b/src/state/actions/window.js @@ -52,9 +52,21 @@ export function toggleWindowSideBar(windowId) { } /** - * toggleWindowSideBar - action creator + * toggleWindowSideBarPanel - action creator + * + * @param {String} windowId + * @param {String} panelType + * @memberof ActionCreators + */ +export function toggleWindowSideBarPanel(windowId, panelType) { + return { type: ActionTypes.TOGGLE_WINDOW_SIDE_BAR_PANEL, windowId, panelType }; +} + +/** + * setWindowThumbnailPosition - action creator * * @param {String} windowId + * @param {String} position * @memberof ActionCreators */ export function setWindowThumbnailPosition(windowId, position) { diff --git a/src/state/reducers/windows.js b/src/state/reducers/windows.js index 8c851f8249148b26e4906cdc5ecd06c86d2a0a99..26c18f70220ba79a9a7d974e17e52434e7176679 100644 --- a/src/state/reducers/windows.js +++ b/src/state/reducers/windows.js @@ -22,6 +22,7 @@ const windowsReducer = (state = {}, action) => { sideBarOpen: !state[action.windowId].sideBarOpen, }, }; + case ActionTypes.SET_WINDOW_THUMBNAIL_POSITION: return { ...state, @@ -30,6 +31,18 @@ const windowsReducer = (state = {}, action) => { thumbnailNavigationPosition: action.position, }, }; + case ActionTypes.TOGGLE_WINDOW_SIDE_BAR_PANEL: + return { + ...state, + [action.windowId]: { + ...state[action.windowId], + sideBarPanel: ( + state[action.windowId].sideBarPanel === action.panelType + ? 'closed' + : action.panelType + ), + }, + }; case ActionTypes.NEXT_CANVAS: return setCanvasIndex(state, action.windowId, currentIndex => currentIndex + 1); case ActionTypes.PREVIOUS_CANVAS: diff --git a/src/styles/index.scss b/src/styles/index.scss index 53bd8bd43d86efe90949417827222e39568d5758..5056437008e94385d19f50f23e3284cd8a90ceee 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -36,6 +36,10 @@ body { position: relative; } + &-window-sidebar-panel-drawer { + margin-left: $window-sidebar-width; + } + &-window-companion-side { background: lighten($gray, 40%); min-width: 75px; diff --git a/src/styles/variables.scss b/src/styles/variables.scss index c3b54911915125a8fdec56927df34c5beccdc54e..a096be0ed29f6aa05ae582d23e81accb33c2e2de 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -4,3 +4,4 @@ $gray: #808080; $white: #fff; $window-top-bar-gradient-top: rgba(0, 0, 0, .65); $window-top-bar-gradient-bottom: rgba(0, 0, 0, 0); +$window-sidebar-width: 55px;