diff --git a/__tests__/src/components/Workspace.test.js b/__tests__/src/components/Workspace.test.js index 3f1fd853e79f7aec16a9fad1ab6b05b789675b1b..493598f751109c49936e69dd95271179c79dbbdb 100644 --- a/__tests__/src/components/Workspace.test.js +++ b/__tests__/src/components/Workspace.test.js @@ -83,22 +83,6 @@ describe('Workspace', () => { }); }); - describe('when the workspace control panel is displayed', () => { - it('has the *-with-control-panel class applied', () => { - const { container } = createWrapper(); - - expect(container.querySelector('.mirador-workspace-with-control-panel')).toBeInTheDocument(); - }); - }); - - describe('when the workspace control panel is not displayed', () => { - it('does not have the *-with-control-panel class applied', () => { - const { container } = createWrapper({ isWorkspaceControlPanelVisible: false }); - - expect(container.querySelector('.mirador-workspace-with-control-panel')).not.toBeInTheDocument(); - }); - }); - describe('drag and drop', () => { it('adds a new catalog entry from a manifest', async () => { const manifestJson = '{ "data": "123" }'; @@ -106,7 +90,7 @@ describe('Workspace', () => { const addWindow = jest.fn(); const { container } = createWrapper({ addWindow }); - const dropTarget = container.querySelector('.mirador-workspace-with-control-panel'); + const dropTarget = container.querySelector('.mirador-workspace-viewport'); const file = new File([manifestJson], 'manifest.json', { type: 'application/json' }); const dataTransfer = { @@ -129,7 +113,7 @@ describe('Workspace', () => { const { container } = createWrapper({ addWindow, allowNewWindows: false }); - const dropTarget = container.querySelector('.mirador-workspace-with-control-panel'); + const dropTarget = container.querySelector('.mirador-workspace-viewport'); const file = new File([manifestJson], 'manifest.json', { type: 'application/json' }); const dataTransfer = { diff --git a/src/components/CompanionArea.js b/src/components/CompanionArea.js index 79e1a7acee616656c4df49800a85ecb9ac80a1c1..b73350d644204d6e36febe1491f0be5ff69431a9 100644 --- a/src/components/CompanionArea.js +++ b/src/components/CompanionArea.js @@ -4,16 +4,17 @@ import { styled } from '@mui/material/styles'; import Slide from '@mui/material/Slide'; import ArrowLeftIcon from '@mui/icons-material/ArrowLeftSharp'; import ArrowRightIcon from '@mui/icons-material/ArrowRightSharp'; +import classNames from 'classnames'; import CompanionWindowFactory from '../containers/CompanionWindowFactory'; import MiradorMenuButton from '../containers/MiradorMenuButton'; import ns from '../config/css-ns'; -const Root = styled('div', { name: 'CompanionArea', slot: 'root' })(({ position, theme }) => ({ +const Root = styled('div', { name: 'CompanionArea', slot: 'root' })(({ ownerState, theme }) => ({ display: 'flex', minHeight: 0, position: 'relative', zIndex: theme.zIndex.appBar - 2, - ...((position === 'bottom' || position === 'far-bottom') && { + ...((ownerState.position === 'bottom' || ownerState.position === 'far-bottom') && { flexDirection: 'column', width: '100%', }), @@ -89,12 +90,13 @@ export class CompanionArea extends Component { /** */ render() { const { + className, companionWindowIds, companionAreaOpen, setCompanionAreaOpen, position, sideBarOpen, t, windowId, } = this.props; - const className = [this.areaLayoutClass(), ns(`companion-area-${position}`)].join(' '); + const classes = classNames(this.areaLayoutClass(), ns(`companion-area-${position}`), className); return ( - <Root className={className}> + <Root ownerState={this.props} className={classes}> <Slide in={companionAreaOpen} direction={this.slideDirection()}> <Container ownerState={this.props} @@ -125,6 +127,7 @@ export class CompanionArea extends Component { CompanionArea.propTypes = { classes: PropTypes.objectOf(PropTypes.string), + className: PropTypes.string, companionAreaOpen: PropTypes.bool.isRequired, companionWindowIds: PropTypes.arrayOf(PropTypes.string).isRequired, direction: PropTypes.string.isRequired, @@ -137,6 +140,7 @@ CompanionArea.propTypes = { CompanionArea.defaultProps = { classes: {}, + className: undefined, setCompanionAreaOpen: () => {}, sideBarOpen: false, }; diff --git a/src/components/PrimaryWindow.js b/src/components/PrimaryWindow.js index 4d6344b3acb3f692e6a0511697751016ebeadb03..30c4d302eb4e9794fc9c9b73e318c02a24e78982 100644 --- a/src/components/PrimaryWindow.js +++ b/src/components/PrimaryWindow.js @@ -1,6 +1,7 @@ import { Component, lazy, Suspense } from 'react'; import PropTypes from 'prop-types'; import { styled } from '@mui/material/styles'; +import classNames from 'classnames'; import WindowSideBar from '../containers/WindowSideBar'; import CompanionArea from '../containers/CompanionArea'; import CollectionDialog from '../containers/CollectionDialog'; @@ -16,7 +17,7 @@ GalleryView.displayName = 'GalleryView'; SelectCollection.displayName = 'SelectCollection'; WindowViewer.displayName = 'WindowViewer'; -const StyledPrimaryWindowContainer = styled('div')(() => ({ +const Root = styled('div', { name: 'PrimaryWindow', slot: 'root' })(() => ({ display: 'flex', flex: 1, position: 'relative', @@ -80,17 +81,18 @@ export class PrimaryWindow extends Component { */ render() { const { - isCollectionDialogVisible, windowId, children, + isCollectionDialogVisible, windowId, children, className, } = this.props; + return ( - <StyledPrimaryWindowContainer data-testid="test-window" className={ns('primary-window')}> + <Root data-testid="test-window" className={classNames(ns('primary-window'), className)}> <WindowSideBar windowId={windowId} /> <CompanionArea windowId={windowId} position="left" /> { isCollectionDialogVisible && <CollectionDialog windowId={windowId} /> } <Suspense fallback={<div />}> {children || this.renderViewer()} </Suspense> - </StyledPrimaryWindowContainer> + </Root> ); } } @@ -98,6 +100,7 @@ export class PrimaryWindow extends Component { PrimaryWindow.propTypes = { audioResources: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types children: PropTypes.node, + className: PropTypes.string, isCollection: PropTypes.bool, isCollectionDialogVisible: PropTypes.bool, isFetching: PropTypes.bool, @@ -109,6 +112,7 @@ PrimaryWindow.propTypes = { PrimaryWindow.defaultProps = { audioResources: [], children: undefined, + className: undefined, isCollection: false, isCollectionDialogVisible: false, isFetching: false, diff --git a/src/components/Window.js b/src/components/Window.js index c2f6b3a5bb0527e3e68e77432bea9290c3c1ce10..4610fb6cf2d4116f9edd1901bf6988a88b9c5b05 100644 --- a/src/components/Window.js +++ b/src/components/Window.js @@ -1,4 +1,4 @@ -import { Component } from 'react'; +import { Component, useContext } from 'react'; import PropTypes from 'prop-types'; import { styled } from '@mui/material/styles'; import Paper from '@mui/material/Paper'; @@ -12,13 +12,25 @@ import ErrorContent from '../containers/ErrorContent'; import IIIFAuthentication from '../containers/IIIFAuthentication'; import { PluginHook } from './PluginHook'; -const Root = styled(Paper)(({ ownerState, theme }) => ({ - backgroundColor: theme.palette.shades?.dark, - borderRadius: 0, +const rowMixin = { display: 'flex', + flex: '1', + flexDirection: 'row', + minHeight: 0, +}; + +const columnMixin = { + display: 'flex', + flex: '1', flexDirection: 'column', - height: '100%', minHeight: 0, +}; + +const Root = styled(Paper, { name: 'Window', slot: 'root' })(({ ownerState, theme }) => ({ + ...columnMixin, + backgroundColor: theme.palette.shades?.dark, + borderRadius: 0, + height: '100%', overflow: 'hidden', width: '100%', ...(ownerState?.maximized && { @@ -29,41 +41,39 @@ const Root = styled(Paper)(({ ownerState, theme }) => ({ }), })); -const StyledMiddle = styled('div')(() => ({ - display: 'flex', - flex: '1', - flexDirection: 'row', - minHeight: 0, +const ContentRow = styled('div', { name: 'Window', slot: 'row' })(() => ({ + ...rowMixin, })); -const StyledMiddleLeft = styled('div')(() => ({ - display: 'flex', - flex: '1', - flexDirection: 'column', - minHeight: 0, +const ContentColumn = styled('div', { name: 'Window', slot: 'column' })(() => ({ + ...columnMixin, })); -const StyledPrimaryWindow = styled('div')(() => ({ - display: 'flex', - flex: '1', +const StyledPrimaryWindow = styled(PrimaryWindow, { name: 'Window', slot: 'primary' })(() => ({ + ...rowMixin, height: '300px', - minHeight: 0, position: 'relative', })); -const StyledCompanionAreaBottom = styled('div')(() => ({ - display: 'flex', +const StyledCompanionAreaBottom = styled(CompanionArea, { name: 'Window', slot: 'bottom' })(() => ({ + ...rowMixin, flex: '0', flexBasis: 'auto', - minHeight: 0, })); -const StyledCompanionAreaRight = styled('div')(() => ({ - display: 'flex', +const StyledCompanionAreaRight = styled('div', { name: 'Window', slot: 'right' })(() => ({ + ...rowMixin, flex: '0 1 auto', - minHeight: 0, })); +/** Window title bar wrapper for drag controls in the mosaic view */ +const DraggableNavBar = ({ children, ...props }) => { + const { mosaicWindowActions } = useContext(MosaicWindowContext); + return mosaicWindowActions.connectDragSource( + <nav {...props}>{children}</nav>, + ); +}; + /** * Represents a Window in the mirador workspace * @param {object} window @@ -81,40 +91,13 @@ export class Window extends Component { return { error, hasError: true }; } - /** - * wrappedTopBar - will conditionally wrap a WindowTopBar for needed - * additional functionality based on workspace type - */ - wrappedTopBar() { - const { - windowId, workspaceType, windowDraggable, - } = this.props; - - const topBar = ( - <div> - <WindowTopBar - windowId={windowId} - windowDraggable={windowDraggable} - /> - <IIIFAuthentication windowId={windowId} /> - </div> - ); - if (workspaceType === 'mosaic' && windowDraggable) { - const { mosaicWindowActions } = this.context; - return mosaicWindowActions.connectDragSource( - topBar, - ); - } - return topBar; - } - /** * Renders things */ render() { const { focusWindow, label, isFetching, sideBarOpen, - view, windowId, t, + view, windowDraggable, windowId, workspaceType, t, manifestError, } = this.props; @@ -138,27 +121,28 @@ export class Window extends Component { className={ns('window')} aria-label={t('window', { label })} > - {this.wrappedTopBar()} + <WindowTopBar + component={workspaceType === 'mosaic' && windowDraggable ? DraggableNavBar : undefined} + windowId={windowId} + windowDraggable={windowDraggable} + /> + <IIIFAuthentication windowId={windowId} /> { manifestError && <ErrorContent error={{ stack: manifestError }} windowId={windowId} /> } - <StyledMiddle> - <StyledMiddleLeft> - <StyledPrimaryWindow> - <PrimaryWindow - view={view} - windowId={windowId} - isFetching={isFetching} - sideBarOpen={sideBarOpen} - /> - </StyledPrimaryWindow> - <StyledCompanionAreaBottom> - <CompanionArea windowId={windowId} position="bottom" /> - </StyledCompanionAreaBottom> - </StyledMiddleLeft> + <ContentRow> + <ContentColumn> + <StyledPrimaryWindow + view={view} + windowId={windowId} + isFetching={isFetching} + sideBarOpen={sideBarOpen} + /> + <StyledCompanionAreaBottom windowId={windowId} position="bottom" /> + </ContentColumn> <StyledCompanionAreaRight> <CompanionArea windowId={windowId} position="right" /> <CompanionArea windowId={windowId} position="far-right" /> </StyledCompanionAreaRight> - </StyledMiddle> + </ContentRow> <CompanionArea windowId={windowId} position="far-bottom" /> <PluginHook {...this.props} /> </Root> diff --git a/src/components/WindowSideBar.js b/src/components/WindowSideBar.js index c036a8faa4bcf92630c0acc1714d230e922bb65e..7750eeb294d9e3e9e42651fbd7870da8a999e37b 100644 --- a/src/components/WindowSideBar.js +++ b/src/components/WindowSideBar.js @@ -6,11 +6,15 @@ import WindowSideBarButtons from '../containers/WindowSideBarButtons'; const Root = styled(Drawer, { name: 'WindowSideBar', slot: 'root' })(({ theme }) => ({ flexShrink: 0, - height: '100%', order: -1000, zIndex: theme.zIndex.appBar - 1, })); +const Nav = styled('nav', { name: 'WindowSideBar', slot: 'nav' })({ + position: 'relative !important', + width: 48, +}); + /** * WindowSideBar */ @@ -31,15 +35,7 @@ export class WindowSideBar extends Component { anchor={direction === 'rtl' ? 'right' : 'left'} PaperProps={{ 'aria-label': t('sidebarPanelsNavigation'), - component: 'nav', - sx: { - borderBlock: 0, - borderInlineStart: 0, - height: '100%', - overflowX: 'hidden', - position: 'relative', - width: 48, - }, + component: Nav, variant: 'outlined', }} SlideProps={{ direction: direction === 'rtl' ? 'left' : 'right', mountOnEnter: true, unmountOnExit: true }} diff --git a/src/components/WindowTopBar.js b/src/components/WindowTopBar.js index abc496c2ccc00d24b239dcd978d61303e4ef137d..5e32f8cd0787303091ddba23f3fb1843cf6d40d6 100644 --- a/src/components/WindowTopBar.js +++ b/src/components/WindowTopBar.js @@ -45,10 +45,11 @@ export class WindowTopBar extends Component { removeWindow, windowId, toggleWindowSideBar, t, maximizeWindow, maximized, minimizeWindow, allowClose, allowMaximize, focusWindow, allowFullscreen, allowTopMenuButton, allowWindowSideBar, + component, } = this.props; return ( - <Root component="nav" aria-label={t('windowNavigation')} position="relative" color="default" enableColorOnDark> + <Root component={component} aria-label={t('windowNavigation')} position="relative" color="default" enableColorOnDark> <StyledToolbar disableGutters onMouseDown={focusWindow} @@ -106,6 +107,7 @@ WindowTopBar.propTypes = { allowMaximize: PropTypes.bool, allowTopMenuButton: PropTypes.bool, allowWindowSideBar: PropTypes.bool, + component: PropTypes.elementType, focused: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types focusWindow: PropTypes.func, maximized: PropTypes.bool, @@ -124,6 +126,7 @@ WindowTopBar.defaultProps = { allowMaximize: true, allowTopMenuButton: true, allowWindowSideBar: true, + component: 'nav', focused: false, focusWindow: () => {}, maximized: false, diff --git a/src/components/Workspace.js b/src/components/Workspace.js index 8963e182326b98de4e1e60800af67ba4e2c20faf..c94f8a30b3fc1eb1550ff0fe3a90211bedc14c5b 100644 --- a/src/components/Workspace.js +++ b/src/components/Workspace.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import { styled } from '@mui/material/styles'; import classNames from 'classnames'; import Grid from '@mui/material/Grid'; -import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import { visuallyHidden } from '@mui/utils'; import Window from '../containers/Window'; @@ -12,23 +11,10 @@ import WorkspaceElastic from '../containers/WorkspaceElastic'; import ns from '../config/css-ns'; import { IIIFDropTarget } from './IIIFDropTarget'; -const Root = styled('div', { name: 'Workspace', slot: 'root' })(({ ownerState, theme }) => ({ - '@media (min-width: 600px)': { - ...(ownerState.isWorkspaceControlPanelVisible && { - paddingLeft: theme.spacing(8.5), - paddingTop: 0, - }), - }, - ...(ownerState.isWorkspaceControlPanelVisible && { - paddingTop: theme.spacing(9.25), - }), - bottom: 0, - left: 0, - margin: 0, - overflow: 'hidden', - position: 'absolute', - right: 0, - top: 0, +const Root = styled('div', { name: 'Workspace', slot: 'root' })(() => ({ + height: '100%', + position: 'relative', + width: '100%', })); /** @@ -84,11 +70,7 @@ export class Workspace extends Component { const { t } = this.props; return ( - <Paper - style={{ - height: '100%', - }} - > + <Root> <Grid alignItems="center" container @@ -109,7 +91,7 @@ export class Workspace extends Component { </Typography> </Grid> </Grid> - </Paper> + </Root> ); } @@ -135,7 +117,7 @@ export class Workspace extends Component { * render */ render() { - const { isWorkspaceControlPanelVisible, t } = this.props; + const { t } = this.props; return ( <IIIFDropTarget onDrop={this.handleDrop}> @@ -144,7 +126,6 @@ export class Workspace extends Component { className={ classNames( ns('workspace-viewport'), - (isWorkspaceControlPanelVisible && ns('workspace-with-control-panel')), ) } > @@ -159,7 +140,6 @@ export class Workspace extends Component { Workspace.propTypes = { addWindow: PropTypes.func, allowNewWindows: PropTypes.bool, - isWorkspaceControlPanelVisible: PropTypes.bool.isRequired, maximizedWindowIds: PropTypes.arrayOf(PropTypes.string), t: PropTypes.func.isRequired, windowIds: PropTypes.arrayOf(PropTypes.string), diff --git a/src/components/WorkspaceAdd.js b/src/components/WorkspaceAdd.js index cced34789c9e064d3fdc0d71deb7297847360639..6e744878c40c1503314274002e1fe6cca2c6de97 100644 --- a/src/components/WorkspaceAdd.js +++ b/src/components/WorkspaceAdd.js @@ -25,13 +25,6 @@ const StyledWorkspaceAdd = styled('div')(() => ({ height: '100%', overflowX: 'hidden', overflowY: 'auto', - paddingTop: 68, - // injection order matters - // eslint-disable-next-line sort-keys - '@media (min-width: 600px)': { - paddingLeft: 68, - paddingTop: 0, - }, })); const StyledMiradorMenuButton = styled(MiradorMenuButton)(() => ({ diff --git a/src/components/WorkspaceArea.js b/src/components/WorkspaceArea.js index 886118f616052b1d36396b19def21e3144d240e9..16200f8d817f18c107737aa30cdf3f7703f082fc 100644 --- a/src/components/WorkspaceArea.js +++ b/src/components/WorkspaceArea.js @@ -8,20 +8,29 @@ import WorkspaceAdd from '../containers/WorkspaceAdd'; import BackgroundPluginArea from '../containers/BackgroundPluginArea'; import ns from '../config/css-ns'; -const StyledMain = styled('main')(({ theme }) => { +const Root = styled('div', { name: 'WorkspaceArea', slot: 'root' })(({ theme }) => { const getBackgroundColor = theme.palette.mode === 'light' ? darken : lighten; return { - background: getBackgroundColor(theme.palette.grey.A200, 0.1), + background: getBackgroundColor(theme.palette.shades.light, 0.1), bottom: 0, + display: 'flex', + flexDirection: 'column', left: 0, - overflow: 'hidden', position: 'absolute', right: 0, top: 0, + [theme.breakpoints.up('sm')]: { + flexDirection: 'row', + }, }; }); +const ViewerArea = styled('main', { name: 'WorkspaceArea', slot: 'viewer' })(() => ({ + flexGrow: 1, + position: 'relative', +})); + /** * This is the top level Mirador component. * @prop {Object} manifests @@ -42,12 +51,12 @@ export class WorkspaceArea extends Component { } = this.props; return ( - <> + <Root ownerState={this.props}> { isWorkspaceControlPanelVisible && <WorkspaceControlPanel variant={controlPanelVariant} /> } - <StyledMain + <ViewerArea className={ns('viewer')} lang={lang} aria-label={t('workspace')} @@ -60,8 +69,8 @@ export class WorkspaceArea extends Component { } <ErrorDialog /> <BackgroundPluginArea /> - </StyledMain> - </> + </ViewerArea> + </Root> ); } } diff --git a/src/components/WorkspaceControlPanel.js b/src/components/WorkspaceControlPanel.js index ddcd967284ca3a0b4000c4288316f3fe22c12e37..1331341330cc8ff29e1199eb1f21f98ad6b2b325 100644 --- a/src/components/WorkspaceControlPanel.js +++ b/src/components/WorkspaceControlPanel.js @@ -14,10 +14,9 @@ const Root = styled(AppBar, { name: 'WorkspaceControlPanel', slot: 'root' })(({ height: 64, padding: theme.spacing(1), paddingBottom: 0, + position: 'relative', [theme.breakpoints.up('sm')]: { - height: '100%', - left: 0, - right: 'auto', + height: 'auto', width: ownerState.variant === 'wide' ? 'auto' : 64, }, ...(ownerState.variant === 'wide' && { diff --git a/src/components/WorkspaceElastic.js b/src/components/WorkspaceElastic.js index c910f0d30166df12a43a1ae5df8456d44bc53e24..f871ab9983bac48fd8445862cfda64de337265b3 100644 --- a/src/components/WorkspaceElastic.js +++ b/src/components/WorkspaceElastic.js @@ -6,6 +6,12 @@ import ResizeObserver from 'react-resize-observer'; import WorkspaceElasticWindow from '../containers/WorkspaceElasticWindow'; import ns from '../config/css-ns'; +const Root = styled('div', { name: 'WorkspaceElastic', slot: 'root' })({ + height: '100%', + position: 'relative', + width: '100%', +}); + const StyledRnd = styled(Rnd)({ boxSizing: 'border-box', margin: 0, @@ -13,7 +19,7 @@ const StyledRnd = styled(Rnd)({ transitionDuration: '.7s', // order matters // eslint-disable-next-line sort-keys - '& .react-draggable-dragging': { + '&.react-draggable-dragging': { transitionDuration: 'unset', }, }); @@ -39,7 +45,7 @@ class WorkspaceElastic extends Component { const offsetY = workspace.height / 2; return ( - <div style={{ height: '100%', position: 'relative', width: '100%' }}> + <Root> <ResizeObserver onReflow={() => {}} onResize={(rect) => { setWorkspaceViewportDimensions(rect); }} @@ -79,7 +85,7 @@ class WorkspaceElastic extends Component { )) } </StyledRnd> - </div> + </Root> ); } } diff --git a/src/containers/Workspace.js b/src/containers/Workspace.js index b7a8881f87923414cebff803a0923557c40ba953..f9288e8d94c750a2e8bcafb881abbbea7779bf55 100644 --- a/src/containers/Workspace.js +++ b/src/containers/Workspace.js @@ -17,7 +17,6 @@ import * as actions from '../state/actions'; const mapStateToProps = state => ( { allowNewWindows: getConfig(state).workspace.allowNewWindows, - isWorkspaceControlPanelVisible: getConfig(state).workspaceControlPanel.enabled, maximizedWindowIds: getMaximizedWindowsIds(state), windowIds: getWindowIds(state), workspaceId: getWorkspace(state).id,