UNPKG

ndla-ui

Version:

UI component library for NDLA.

475 lines (447 loc) 17 kB
/** * Copyright (c) 2016-present, NDLA. * * This source code is licensed under the GPLv3 license found in the * LICENSE file in the root directory of this source tree. * */ // Can be removed when updating to jsx-a11y 6.x /* eslint jsx-a11y/no-noninteractive-element-to-interactive-role: 1 */ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import BEMHelper from 'react-bem-helper'; import { Trans } from 'ndla-i18n'; import debounce from 'lodash/debounce'; import { Home, Back, Additional, ChevronRight } from 'ndla-icons/common'; import { Cross } from 'ndla-icons/action'; import { SafeLink, Tooltip } from 'ndla-ui'; import { ModalHeader } from 'ndla-modal'; import Button from 'ndla-button'; import SubtopicLinkList from './SubtopicLinkList'; import { TopicShape } from '../shapes'; import Logo from '../Logo'; import { FilterListPhone } from '../Filter'; const classes = new BEMHelper({ name: 'topic-menu', prefix: 'c-', }); export const renderAdditionalIcon = (isAdditional, label) => { if (isAdditional && label) { return ( <Tooltip tooltip={label} tooltipContainerClass="c-topic-menu__tooltipContainer"> <Additional className="c-icon--20" /> </Tooltip> ); } if (isAdditional) { return <Additional className="c-icon--20 c-topic-menu__tooltipContainer" />; } return null; }; export default class TopicMenu extends Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); this.handleBtnKeyPress = this.handleBtnKeyPress.bind(this); this.handleSubtopicExpand = this.handleSubtopicExpand.bind(this); this.handleOnGoBack = this.handleOnGoBack.bind(this); this.setScreenSize = this.setScreenSize.bind(this); this.setScreenSizeDebounced = debounce(() => this.setScreenSize(false), 50); this.state = { isNarrowScreen: false, competenceGoalsOpen: false, }; } componentDidMount() { this.setScreenSize(true); window.addEventListener('resize', this.setScreenSizeDebounced); } componentWillUnmount() { this.setScreenSizeDebounced.cancel(); window.removeEventListener('resize', this.setScreenSizeDebounced); } setScreenSize(initial = false) { const isNarrowScreen = (window.innerWidth || document.documentElement.clientWidth) < 768; /* eslint react/no-did-mount-set-state: 0 */ if ((initial && isNarrowScreen) || !initial) { this.setState({ isNarrowScreen, }); } /* eslint react/no-did-mount-set-state: 1 */ } handleClick(event, topicId) { this.props.onNavigate(topicId, null); } handleSubtopicExpand(subtopicId, index) { this.props.onNavigate(this.props.expandedTopicId, subtopicId, index); } handleOnGoBack() { this.props.onNavigate( this.props.expandedSubtopicsId.length ? this.props.expandedTopicId : null, null, ); } handleBtnKeyPress(event, topicId) { if (event.charCode === 32 || event.charCode === 13) { // space or enter event.preventDefault(); this.props.onNavigate(topicId, null); } } renderCompentenceGoals(competenceGoalsOpen, t) { return ( <Button className={ this.state.isNarrowScreen ? 'c-button c-button--lighter c-topic-menu__competence-open-button' : 'c-topic-menu__competence-toggle-button' } stripped={!this.state.isNarrowScreen} onClick={() => this.setState({ competenceGoalsOpen: !competenceGoalsOpen, }) }> {competenceGoalsOpen ? ( <span> {t('competenceGoals.closeCompetenceGoals')} <Cross /> </span> ) : ( t('competenceGoals.showCompetenceGoals') )} </Button> ); } render() { const { topics, toTopic, subjectTitle, toSubject, close: closeMenu, expandedTopicId, expandedSubtopicsId, filterOptions, filterValues, onFilterClick, resourceToLinkProps, hideSearch, competenceGoals, searchFieldComponent, toFrontpage, locale, } = this.props; const { competenceGoalsOpen } = this.state; const expandedTopic = topics.find(topic => topic.id === expandedTopicId); const currentlyExpandedSubTopics = []; if (expandedTopic) { let currentSubtopic; let foundMatch; expandedSubtopicsId.forEach((id, index) => { if (index === 0) { currentSubtopic = expandedTopic.subtopics.find( topic => topic.id === id, ); foundMatch = currentSubtopic ? 0 : undefined; } else { currentSubtopic = currentSubtopic.subtopics.find( topic => topic.id === id, ); foundMatch += currentSubtopic ? 1 : 0; } if (foundMatch === index) { currentlyExpandedSubTopics[index] = currentSubtopic; } }); } const hasExpandedSubtopics = currentlyExpandedSubTopics.length > 0; const subTopicModifiers = ['sub-topic']; if (!hasExpandedSubtopics) { subTopicModifiers.push('no-border'); } const disableMain = this.state.isNarrowScreen && expandedTopic; const disableSubTopic = disableMain && hasExpandedSubtopics; const disableHeaderNavigation = this.state.isNarrowScreen && competenceGoalsOpen; const sliderCounter = !expandedTopicId ? 0 : expandedSubtopicsId.length + 1; return ( <Trans> {({ t }) => ( <nav> <ModalHeader modifier={['white', 'menu']}> <div {...classes('masthead-left')}> <button type="button" {...classes('close-button')} onClick={closeMenu}> <Cross /> <span>{t('masthead.menu.close')}</span> </button> </div> <div {...classes('masthead-right')}> {!hideSearch && searchFieldComponent} <Logo to="#" isBeta={this.props.isBeta} label={t('logo.altText')} locale={locale} /> </div> </ModalHeader> <div {...classes('content')}> <div {...classes('back', 'wide')}> <SafeLink {...classes('back-link')} to={toFrontpage()}> <Home {...classes('home-icon', '', 'c-icon--20')} /> {t('masthead.menu.subjectOverview')} </SafeLink> </div> <div {...classes('back', { 'hidden-phone': competenceGoalsOpen, narrow: true, })}> <SafeLink {...classes('back-link')} to={toFrontpage()}> <Home {...classes('home-icon', '', 'c-icon--20')} /> {t('masthead.menu.subjectOverview')} </SafeLink> </div> {!disableMain && ( <Fragment> {!disableHeaderNavigation && ( <div {...classes('subject', { hasFilter: filterOptions && filterOptions.length > 0 && !competenceGoalsOpen, })}> <div {...classes('subject__header')}> <h1> <SafeLink to={toSubject()}> {subjectTitle} <ChevronRight /> </SafeLink> </h1> {competenceGoals && !this.state.isNarrowScreen && this.renderCompentenceGoals(competenceGoalsOpen, t)} </div> {!competenceGoalsOpen && filterOptions && filterOptions.length > 1 && ( <div {...classes('filter-wrapper')}> <FilterListPhone activeFiltersNarrow alignedGroup options={filterOptions} values={filterValues} onChange={onFilterClick} messages={{ useFilter: t('masthead.menu.useFilter'), openFilter: t('masthead.menu.openFilter'), closeFilter: t('masthead.menu.closeFilter'), }} label={`${subjectTitle}:`} /> </div> )} {!competenceGoalsOpen && ( <div {...classes('back-button-slide-wrapper')}> <button type="button" {...classes( 'back-button-slides', `slide-${sliderCounter}`, )} onClick={this.handleOnGoBack}> <Back /> <span>{t('masthead.menu.back')}</span> </button> </div> )} </div> )} </Fragment> )} {competenceGoalsOpen && ( <div {...classes('competence')}> <button type="button" {...classes( this.state.isNarrowScreen ? 'competence-close-button' : 'competence-close-button', )} onClick={() => this.setState({ competenceGoalsOpen: false, }) }> <Back /> {t('competenceGoals.competenceGoalsNarrowBackButton')} </button> {competenceGoals} </div> )} {!competenceGoalsOpen && ( <div {...classes('subject-navigation', `slide-${sliderCounter}`)}> {!disableMain && ( <Fragment> <div {...classes('section', 'main')}> <SafeLink to={toSubject()} className={classes('link', 'big').className}> <span {...classes('link-label')}> {t('masthead.menu.goTo')}: </span> <span {...classes('link-target')}> {t('masthead.menu.subjectPage')} <span {...classes('arrow')}>›</span> </span> </SafeLink> <ul {...classes('list')}> {topics.map(topic => { const active = topic.id === expandedTopicId; return ( <li {...classes('topic-item', active && 'active')} key={topic.id}> <button type="button" {...classes('link')} onClick={event => this.handleClick(event, topic.id) } onKeyPress={event => this.handleBtnKeyPress(event, topic.id) }> <span> {topic.name} {renderAdditionalIcon( topic.additional, t('resource.additionalTooltip'), )} </span> <ChevronRight /> </button> </li> ); })} </ul> {competenceGoals && this.state.isNarrowScreen && this.renderCompentenceGoals(false, t)} </div> </Fragment> )} {expandedTopic && !disableSubTopic && ( <SubtopicLinkList classes={classes} className={ classes('section', subTopicModifiers).className } closeMenu={closeMenu} topic={expandedTopic} backLabel={ !hasExpandedSubtopics ? subjectTitle : currentlyExpandedSubTopics[ currentlyExpandedSubTopics.length - 1 ].name } goToTitle={t('masthead.menu.goTo')} toTopic={toTopic} expandedSubtopicId={ currentlyExpandedSubTopics[0] && currentlyExpandedSubTopics[0].id } onSubtopicExpand={id => { this.handleSubtopicExpand(id, 0); }} onGoBack={this.handleOnGoBack} resourceToLinkProps={resourceToLinkProps} competenceButton={ competenceGoals && this.state.isNarrowScreen && this.renderCompentenceGoals(false, t) } /> )} {currentlyExpandedSubTopics.map((subTopic, index) => ( <SubtopicLinkList key={subTopic.id} classes={classes} className={ classes('section', ['sub-topic', 'no-border']).className } closeMenu={closeMenu} topic={subTopic} backLabel={ index === 0 ? this.props.topics.find( topic => topic.id === this.props.expandedTopicId, ).name : currentlyExpandedSubTopics[index - 1].name } toTopic={toTopic} expandedSubtopicId={ currentlyExpandedSubTopics[index + 1] ? currentlyExpandedSubTopics[index + 1].id : 'no way' } onSubtopicExpand={id => { this.handleSubtopicExpand(id, index + 1); }} onGoBack={this.handleOnGoBack} resourceToLinkProps={resourceToLinkProps} defaultCount={this.props.defaultCount} competenceButton={ competenceGoals && this.state.isNarrowScreen && this.renderCompentenceGoals(false, t) } /> ))} </div> )} </div> </nav> )} </Trans> ); } } TopicMenu.propTypes = { topics: PropTypes.arrayOf(TopicShape).isRequired, toFrontpage: PropTypes.func.isRequired, toTopic: PropTypes.func.isRequired, toSubject: PropTypes.func.isRequired, close: PropTypes.func, defaultCount: PropTypes.number, filterOptions: PropTypes.arrayOf( PropTypes.shape({ title: PropTypes.string.isRequired, value: PropTypes.string.isRequired, }), ), onFilterClick: PropTypes.func, filterValues: PropTypes.arrayOf(PropTypes.string), subjectTitle: PropTypes.string.isRequired, resourceToLinkProps: PropTypes.func.isRequired, onNavigate: PropTypes.func.isRequired, expandedTopicId: PropTypes.string, expandedSubtopicsId: PropTypes.arrayOf(PropTypes.string).isRequired, isBeta: PropTypes.bool, hideSearch: PropTypes.bool, competenceGoals: PropTypes.node, searchFieldComponent: PropTypes.node, locale: PropTypes.string, }; TopicMenu.defaultProps = { defaultCount: 12, };