Skip to content
Snippets Groups Projects
TopBar.js 13.6 KiB
Newer Older
import React from 'react'
import PropTypes from 'prop-types'
import intl from 'react-intl-universal'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import IconButton from '@material-ui/core/IconButton'
esikkala's avatar
esikkala committed
import Typography from '@material-ui/core/Typography'
import MenuItem from '@material-ui/core/MenuItem'
import Menu from '@material-ui/core/Menu'
esikkala's avatar
esikkala committed
import { makeStyles } from '@material-ui/core/styles'
import MoreIcon from '@material-ui/icons/MoreVert'
import Button from '@material-ui/core/Button'
import { Link, NavLink } from 'react-router-dom'
import TopBarSearchField from './TopBarSearchField'
import TopBarInfoButton from './TopBarInfoButton'
import TopBarLanguageButton from './TopBarLanguageButton'
import Divider from '@material-ui/core/Divider'
import { has } from 'lodash'
import secoLogo from '../../img/logos/seco-logo-48x50.png'
esikkala's avatar
esikkala committed
const useStyles = makeStyles(theme => ({
    flexGrow: 1
esikkala's avatar
esikkala committed
  topBarToolbar: props => ({
    minHeight: props.layoutConfig.topBar.reducedHeight,
    [theme.breakpoints.up(props.layoutConfig.reducedHeightBreakpoint)]: {
      minHeight: props.layoutConfig.topBar.defaultHeight
    },
    paddingLeft: theme.spacing(1.5),
    paddingRight: theme.spacing(1.5)
esikkala's avatar
esikkala committed
  }),
  sectionDesktop: props => ({
esikkala's avatar
esikkala committed
    [theme.breakpoints.up(props.layoutConfig.topBar.mobileMenuBreakpoint)]: {
      display: 'flex'
    }
esikkala's avatar
esikkala committed
  }),
  link: {
    textDecoration: 'none'
  },
esikkala's avatar
esikkala committed
  sectionMobile: props => ({
esikkala's avatar
esikkala committed
    [theme.breakpoints.up(props.layoutConfig.topBar.mobileMenuBreakpoint)]: {
      display: 'none'
    }
esikkala's avatar
esikkala committed
  }),
    whiteSpace: 'nowrap',
esikkala's avatar
esikkala committed
    color: 'white !important',
    border: `1px solid ${theme.palette.primary.main}`
  appBarButtonActive: {
    border: '1px solid white'
esikkala's avatar
esikkala committed
  },
  appBarDivider: {
    marginLeft: theme.spacing(1),
    marginRight: theme.spacing(1),
    borderLeft: '2px solid white'
esikkala's avatar
esikkala committed
  secoLogo: props => ({
    marginLeft: theme.spacing(1),
esikkala's avatar
esikkala committed
    [theme.breakpoints.down(props.layoutConfig.topBar.mobileMenuBreakpoint)]: {
      display: 'none'
    }
esikkala's avatar
esikkala committed
  }),
  secoLogoImage: props => ({
    height: 32,
    [theme.breakpoints.up(props.layoutConfig.reducedHeightBreakpoint)]: {
      height: 50
    }
  }),
  mainLogo: props => ({
    height: 23,
    [theme.breakpoints.up(props.layoutConfig.reducedHeightBreakpoint)]: {
      height: 40
    },
    marginRight: theme.spacing(1)
  }),
  mainLogoButtonRoot: {
    paddingLeft: 0,
    [theme.breakpoints.down('xs')]: {
      minWidth: 48
    }
  },
  mainLogoButtonLabel: {
    justifyContent: 'left'
  },
esikkala's avatar
esikkala committed
  mainLogoTypography: props => ({
    // set color and background explicitly to keep Google Lighthouse happy
    color: '#fff',
    background: theme.palette.primary.main,
esikkala's avatar
esikkala committed
    whiteSpace: 'nowrap',
esikkala's avatar
esikkala committed
    textTransform: props.layoutConfig.topBar.logoTextTransform,
esikkala's avatar
esikkala committed
    [theme.breakpoints.down('sm')]: {
esikkala's avatar
esikkala committed
      fontSize: '1.5rem'
    },
    ...(props.layoutConfig.topBar.hideLogoTextOnMobile && {
      [theme.breakpoints.down('xs')]: {
        display: 'none'
      }
    })
  }),
esikkala's avatar
esikkala committed
  mobileMenuButton: {
    padding: 12
esikkala's avatar
esikkala committed
}))
esikkala's avatar
esikkala committed
/**
 * Responsive app bar with a search field, perspective links, info links and a language
 * selector. Based on Material-UI's App Bar component.
 */
esikkala's avatar
esikkala committed
const TopBar = props => {
  const [mobileMoreAnchorEl, setMobileMoreAnchorEl] = React.useState(null)
  const isMobileMenuOpen = Boolean(mobileMoreAnchorEl)
esikkala's avatar
esikkala committed
  const { perspectives, currentLocale, availableLocales, rootUrl, layoutConfig } = props
  const { topBar } = layoutConfig
esikkala's avatar
esikkala committed
  const classes = useStyles(props)
esikkala's avatar
esikkala committed
  const handleMobileMenuOpen = event => setMobileMoreAnchorEl(event.currentTarget)
  const handleMobileMenuClose = () => setMobileMoreAnchorEl(null)
esikkala's avatar
esikkala committed
  const clientFSMode = props.location.pathname.indexOf('clientFS') !== -1
esikkala's avatar
esikkala committed
  let showSearchField = true
  if (has(layoutConfig.topBar, 'showSearchField')) {
    showSearchField = layoutConfig.topBar.showSearchField
  }
  // https://material-ui.com/components/buttons/#third-party-routing-library
esikkala's avatar
esikkala committed
  const AdapterLink = React.forwardRef((props, ref) => <Link innerRef={ref} {...props} />)
  const AdapterNavLink = React.forwardRef((props, ref) => <NavLink innerRef={ref} {...props} />)
esikkala's avatar
esikkala committed
  const getInternalLink = perspective => {
    const searchMode = has(perspective, 'searchMode') ? perspective.searchMode : 'faceted-search'
    let link = null
    if (searchMode === 'dummy-internal') {
      link = `${props.rootUrl}${perspective.internalLink}`
    }
    if (searchMode !== 'dummy-internal') {
      link = `${props.rootUrl}/${perspective.id}/${searchMode}`
    }
    return link
  }

esikkala's avatar
esikkala committed
  const renderMobileMenuItem = perspective => {
    if (has(perspective, 'externalUrl')) {
esikkala's avatar
esikkala committed
          className={classes.link}
          key={perspective.id}
          href={perspective.externalUrl}
          target='_blank'
          rel='noopener noreferrer'
        >
          <MenuItem>
            {perspective.label
              ? perspective.label.toUpperCase()
              : intl.get(`perspectives.${perspective.id}.label`).toUpperCase()}
          </MenuItem>
        </a>
    } else {
        <MenuItem
          key={perspective.id}
esikkala's avatar
esikkala committed
          component={AdapterLink}
esikkala's avatar
esikkala committed
          to={getInternalLink(perspective)}
          onClick={handleMobileMenuClose}
          {intl.get(`perspectives.${perspective.id}.label`).toUpperCase()}
        </MenuItem>
esikkala's avatar
esikkala committed
  const renderDesktopTopMenuItem = perspective => {
    if (has(perspective, 'externalUrl')) {
esikkala's avatar
esikkala committed
          className={classes.link}
          key={perspective.id}
          href={perspective.externalUrl}
          target='_blank'
          rel='noopener noreferrer'
        >
          <Button
esikkala's avatar
esikkala committed
            className={classes.appBarButton}
esikkala's avatar
esikkala committed
          >
            {perspective.label
              ? perspective.label
              : intl.get(`perspectives.${perspective.id}.label`).toUpperCase()}
          </Button>
        </a>
    } else {
        <Button
          key={perspective.id}
esikkala's avatar
esikkala committed
          className={classes.appBarButton}
          component={AdapterNavLink}
esikkala's avatar
esikkala committed
          to={getInternalLink(perspective)}
esikkala's avatar
esikkala committed
          isActive={(match, location) => location.pathname.startsWith(`${props.rootUrl}/${perspective.id}`)}
          activeClassName={classes.appBarButtonActive}
          {intl.get(`perspectives.${perspective.id}.label`).toUpperCase()}
        </Button>
esikkala's avatar
esikkala committed
  const renderInfoItem = item => {
    let jsx
    if (item.externalLink) {
      jsx = (
        <a
          className={classes.link}
          key={item.id}
          href={intl.get(`topBar.info.${item.translatedUrl}`)}
          target='_blank'
          rel='noopener noreferrer'
        >
          <MenuItem onClick={handleMobileMenuClose}>
            {intl.get(`topBar.info.${item.translatedText}`).toUpperCase()}
          </MenuItem>
        </a>
      )
    } else {
      jsx = (
        <MenuItem
          key={item.id}
          component={AdapterLink}
          to={`${props.rootUrl}${item.internalLink}`}
          onClick={handleMobileMenuClose}
        >
          {intl.get(`topBar.info.${item.translatedText}`).toUpperCase()}
esikkala's avatar
esikkala committed
        </MenuItem>
esikkala's avatar
esikkala committed
      )
    }
    return jsx
  }

  const renderMobileMenu = perspectives => {
    const { topBar } = props.layoutConfig
    const { infoDropdown } = topBar
esikkala's avatar
esikkala committed
    return (
      <Menu
        anchorEl={mobileMoreAnchorEl}
        anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
        transformOrigin={{ vertical: 'top', horizontal: 'right' }}
        open={isMobileMenuOpen}
        onClose={handleMobileMenuClose}
esikkala's avatar
esikkala committed
        {perspectives.map(perspective => perspective.hideTopPerspectiveButton ? null : renderMobileMenuItem(perspective))}
        <Divider />
        {renderMobileMenuItem({
          id: 'feedback',
          externalUrl: props.layoutConfig.topBar.feedbackLink,
          label: intl.get('topBar.feedback')
        })}
        {infoDropdown.map(item => renderInfoItem(item))}
        {topBar.externalInstructions && renderMobileMenuItem({
          id: 'instructions',
          externalUrl: intl.get('topBar.instructionsUrl'),
          label: intl.get('topBar.instructions')
        })}
        {!topBar.externalInstructions &&
          <MenuItem
            key='instructions'
            component={AdapterLink}
esikkala's avatar
esikkala committed
            to={`${props.rootUrl}/instructions`}
            onClick={handleMobileMenuClose}
esikkala's avatar
esikkala committed
          >
            {intl.get('topBar.instructions').toUpperCase()}
          </MenuItem>}
esikkala's avatar
esikkala committed
      </Menu>
    )
  }
esikkala's avatar
esikkala committed
  return (
    <div className={classes.root}>
      {/* Add an empty Typography element to ensure that that the MuiTypography class is loaded for
esikkala's avatar
esikkala committed
         any lower level components that use MuiTypography class only in translation files */}
esikkala's avatar
esikkala committed
      <Typography />
esikkala's avatar
esikkala committed
      <AppBar position='static'>
        <Toolbar className={classes.topBarToolbar}>
esikkala's avatar
esikkala committed
          <Button
            component={AdapterLink} to='/'
            classes={{
              root: classes.mainLogoButtonRoot,
              label: classes.mainLogoButtonLabel
            }}
            onClick={() => clientFSMode ? props.clientFSClearResults() : null}
esikkala's avatar
esikkala committed
            {topBar.logoImage &&
              <img
                className={classes.mainLogo}
                src={topBar.logoImage}
                alt={`${intl.get('appTitle.short')} logo`}
              />}
            <Typography className={classes.mainLogoTypography} variant='h5'>
esikkala's avatar
esikkala committed
              {props.xsScreen ? intl.get('appTitle.mobile') : intl.get('appTitle.short')}
            </Typography>
esikkala's avatar
esikkala committed
          </Button>
esikkala's avatar
esikkala committed
          {showSearchField &&
esikkala's avatar
esikkala committed
            <TopBarSearchField
              fetchFullTextResults={props.fetchFullTextResults}
              clearResults={props.clearResults}
              xsScreen={props.xsScreen}
              rootUrl={rootUrl}
            />}
esikkala's avatar
esikkala committed
          <div className={classes.grow} />
          <div className={classes.sectionDesktop}>
            {perspectives.map((perspective, index) => perspective.hideTopPerspectiveButton ? null : renderDesktopTopMenuItem(perspective, index))}
esikkala's avatar
esikkala committed
            <div className={classes.appBarDivider} />
            {renderDesktopTopMenuItem({
              id: 'feedback',
esikkala's avatar
esikkala committed
              externalUrl: props.layoutConfig.topBar.feedbackLink,
              label: intl.get('topBar.feedback')
            })}
esikkala's avatar
esikkala committed
            <TopBarInfoButton rootUrl={props.rootUrl} layoutConfig={layoutConfig} />
            {topBar.externalInstructions && renderDesktopTopMenuItem({
              id: 'instructions',
              externalUrl: intl.get('topBar.instructionsUrl'),
              label: intl.get('topBar.instructions')
            })}
            {!topBar.externalInstructions &&
              <Button
                className={classes.appBarButton}
                component={AdapterNavLink}
                to={`${props.rootUrl}/instructions`}
                isActive={(match, location) => location.pathname.startsWith(`${props.rootUrl}/instructions`)}
                activeClassName={classes.appBarButtonActive}
              >
                {intl.get('topBar.instructions')}
              </Button>}
esikkala's avatar
esikkala committed
            {props.layoutConfig.topBar.showLanguageButton &&
esikkala's avatar
esikkala committed
              <TopBarLanguageButton
                currentLocale={currentLocale}
                availableLocales={availableLocales}
                loadLocales={props.loadLocales}
                location={props.location}
              />}
          </div>
          <a
            className={classes.secoLogo}
            href='https://seco.cs.aalto.fi'
            target='_blank'
            rel='noopener noreferrer'
          >
            <Button aria-label='link to Semantic Computing research group homepage'>
              <img
                className={classes.secoLogoImage}
                src={secoLogo}
                alt='Semantic Computing research group logo'
              />
            </Button>
esikkala's avatar
esikkala committed
          </a>
          <div className={classes.sectionMobile}>
esikkala's avatar
esikkala committed
            {props.layoutConfig.topBar.showLanguageButton &&
              <TopBarLanguageButton
                currentLocale={currentLocale}
                availableLocales={availableLocales}
                loadLocales={props.loadLocales}
                location={props.location}
              />}
esikkala's avatar
esikkala committed
            <IconButton
              aria-label='display more actions' color='inherit'
              className={classes.mobileMenuButton}
              onClick={handleMobileMenuOpen}
            >
esikkala's avatar
esikkala committed
              <MoreIcon />
            </IconButton>
          </div>
        </Toolbar>
      </AppBar>
      {renderMobileMenu(perspectives)}
    </div>
  )
}

TopBar.propTypes = {
esikkala's avatar
esikkala committed
  /**
   * Redux action for full text search results using the search field.
   */
  fetchFullTextResults: PropTypes.func.isRequired,
esikkala's avatar
esikkala committed
  /**
   * Redux action for clearing the full text results.
   */
  clearResults: PropTypes.func.isRequired,
esikkala's avatar
esikkala committed
  /**
   * Redux action for loading translations.
   */
  loadLocales: PropTypes.func.isRequired,
esikkala's avatar
esikkala committed
  /**
   * Current locale as a two-letter code
   */
  currentLocale: PropTypes.string.isRequired,
esikkala's avatar
esikkala committed
  /**
   * Available locales as an array of objects with two-letter codes as keys.
   */
  availableLocales: PropTypes.array.isRequired,
esikkala's avatar
esikkala committed
  /**
   * Perspective config as an array of objects.
   */
  perspectives: PropTypes.array.isRequired,
  /**
   * Flag for checking if the screen is extra small.
   */
esikkala's avatar
esikkala committed
  xsScreen: PropTypes.bool.isRequired,
esikkala's avatar
esikkala committed
  /**
   * React Router's location object. The perspective links are highlighted based on this.
   */
  location: PropTypes.object.isRequired,
esikkala's avatar
esikkala committed
  /**
   * Root url of the application.
   */
esikkala's avatar
esikkala committed
  rootUrl: PropTypes.string.isRequired,
  layoutConfig: PropTypes.object.isRequired
esikkala's avatar
esikkala committed
export default TopBar