diff --git a/src/components/subnavigation/subnavigation.jsx b/src/components/subnavigation/subnavigation.jsx index e9d964975..b04ae38cb 100644 --- a/src/components/subnavigation/subnavigation.jsx +++ b/src/components/subnavigation/subnavigation.jsx @@ -20,6 +20,7 @@ const SubNavigation = props => ( 'sub-nav-align-right': props.align === 'right' } )} + role={props.role} > {props.children} @@ -27,6 +28,7 @@ const SubNavigation = props => ( SubNavigation.propTypes = { align: PropTypes.string, + role: PropTypes.string, children: PropTypes.node, className: PropTypes.string }; diff --git a/src/components/tabs/tabs.jsx b/src/components/tabs/tabs.jsx index cc637ff56..07553ef13 100644 --- a/src/components/tabs/tabs.jsx +++ b/src/components/tabs/tabs.jsx @@ -1,5 +1,5 @@ -const classNames = require('classnames'); const PropTypes = require('prop-types'); +const {useRef} = require('react'); const React = require('react'); const SubNavigation = require('../../components/subnavigation/subnavigation.jsx'); @@ -10,17 +10,94 @@ require('./tabs.scss'); * Container for a custom, horizontal list of navigation elements * that can be displayed within a view or component. */ -const Tabs = props => ( -
- - {props.children} - -
-); +const Tabs = ({items, activeTabName}) => { + const tabElementRefs = useRef({}); + + const itemsRendered = items.map(({name, onTrigger, getContent}) => { + const isActive = name === activeTabName; + + let tabRef; + if (tabElementRefs.current[name]) { + tabRef = tabElementRefs.current[name]; + } else { + tabRef = React.createRef(); + tabElementRefs.current[name] = tabRef; + } + + return ( + + ); + }); + + const handleKeyDown = event => { + if (!['ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter', ' '].includes(event.key)) { + return; + } + event.preventDefault(); + const focusedIndex = Object.values(tabElementRefs.current) + .findIndex(tabElementRef => + document.activeElement === tabElementRef.current + ); + if (event.key === 'ArrowLeft') { + let nextIndex; + if (focusedIndex === 0) { + nextIndex = Object.values(tabElementRefs.current).length - 1; + } else { + nextIndex = focusedIndex - 1; + } + Object.values(tabElementRefs.current)[nextIndex].current.focus(); + } else if (event.key === 'ArrowRight') { + let nextIndex; + if (focusedIndex === Object.values(tabElementRefs.current).length - 1) { + nextIndex = 0; + } else { + nextIndex = focusedIndex + 1; + } + Object.values(tabElementRefs.current)[nextIndex].current.focus(); + } else if (event.key === 'Home') { + Object.values(tabElementRefs.current)[0].current.focus(); + } else if (event.key === 'End') { + const lastTab = Object.values(tabElementRefs.current).length - 1; + Object.values(tabElementRefs.current)[lastTab].current.focus(); + } else if (event.key === 'Enter' || event.key === ' ') { + items[focusedIndex].onTrigger(); + } + }; + + return ( +
+ + {itemsRendered} + +
+ ); +}; Tabs.propTypes = { - children: PropTypes.node, - className: PropTypes.string + items: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + onTrigger: PropTypes.func.isRequired, + getContent: PropTypes.func.isRequired + }) + ).isRequired, + activeTabName: PropTypes.string.isRequired }; module.exports = Tabs; diff --git a/src/components/tabs/tabs.scss b/src/components/tabs/tabs.scss index a612c3f76..791e9723f 100644 --- a/src/components/tabs/tabs.scss +++ b/src/components/tabs/tabs.scss @@ -14,13 +14,14 @@ justify-content: center; } -.tabs li { +.tabs button { margin: 0; border: 0; border-radius: 0; width: $cols2; text-align: center; color: $header-gray; + background-color: transparent; &.active { border-bottom: 3px solid $ui-aqua; diff --git a/src/l10n.json b/src/l10n.json index 41ce896c0..82ebe2b4b 100644 --- a/src/l10n.json +++ b/src/l10n.json @@ -74,6 +74,8 @@ "general.download": "Download", "general.password": "Password", "general.press": "Press", + "general.projectsSelected": "Projects Tab Selected", + "general.projectsNotS": "Projects", "general.privacyPolicy": "Privacy Policy", "general.projects": "Projects", "general.profile": "Profile", @@ -91,6 +93,8 @@ "general.startOver": "Start over", "general.statistics": "Statistics", "general.studios": "Studios", + "general.studiosSelected": "Studios Tab Selected", + "general.studiosNotS": "Studios", "general.support": "Resources", "general.ideas": "Ideas", "general.tipsWindow": "Tips Window", @@ -111,13 +115,21 @@ "general.seeAllComments": "See all comments", "general.all": "All", + "general.allSelected": "All Selected", "general.animations": "Animations", + "general.animationsSelected": "Animations Selected", "general.art": "Art", + "general.artSelected": "Art Selected", "general.games": "Games", + "general.gamesSelected": "Games Selected", "general.music": "Music", + "general.musicSelected": "Music Selected", "general.results": "Results", + "general.resultsSelected": "Results Selected", "general.stories": "Stories", + "general.storiesSelected": "Stories Selected", "general.tutorials": "Tutorials", + "general.tutorialsSelected": "Tutorials Selected", "general.teacherAccounts": "Teacher Accounts", diff --git a/src/views/explore/explore.jsx b/src/views/explore/explore.jsx index 4bd84f2f9..7d4e9dd42 100644 --- a/src/views/explore/explore.jsx +++ b/src/views/explore/explore.jsx @@ -26,10 +26,8 @@ class Explore extends React.Component { bindAll(this, [ 'getExploreState', 'handleGetExploreMore', - 'changeItemType', 'handleChangeSortMode', - 'getBubble', - 'getTab' + 'getBubble' ]); this.state = this.getExploreState(); @@ -96,16 +94,7 @@ class Explore extends React.Component { } }); } - changeItemType () { - let newType; - for (const t of this.state.acceptableTypes) { - if (this.state.itemType !== t) { - newType = t; - break; - } - } - window.location = `${window.location.origin}/explore/${newType}/${this.state.tab}/${this.state.mode}`; - } + handleChangeSortMode (name, value) { if (this.state.acceptableModes.indexOf(value) !== -1) { window.location = @@ -124,31 +113,7 @@ class Explore extends React.Component { ); } - getTab (type) { - const classes = classNames({ - active: (this.state.itemType === type) - }); - return ( - -
  • - {this.state.itemType === type ? [ - - ] : [ - - ]} - -
  • -
    - ); - } + render () { return (
    @@ -160,10 +125,63 @@ class Explore extends React.Component {
    - - {this.getTab('projects')} - {this.getTab('studios')} - + { + window.location = `${window.location.origin}/explore/projects/` + + `${this.state.category}/${this.state.mode}`; + }, + getContent: isActive => ( +
    + {isActive ? ( + + ) : ( + + ) + } + +
    + ) + }, + { + name: 'studios', + onTrigger: () => { + window.location = `${window.location.origin}/explore/studios/` + + `${this.state.category}/${this.state.mode}`; + }, + getContent: isActive => ( +
    + {isActive ? ( + + ) : ( + + ) + } + +
    + ) + } + ]} + activeTabName={this.state.itemType} + />
    {this.getBubble('all')} diff --git a/src/views/search/search.jsx b/src/views/search/search.jsx index c5d1d4c27..74d18f99f 100644 --- a/src/views/search/search.jsx +++ b/src/views/search/search.jsx @@ -31,8 +31,7 @@ class Search extends React.Component { bindAll(this, [ 'getSearchState', 'handleChangeSortMode', - 'handleGetSearchMore', - 'getTab' + 'handleGetSearchMore' ]); this.state = this.getSearchState(); this.state.loaded = []; @@ -151,38 +150,6 @@ class Search extends React.Component { }); }); } - getTab (type) { - const termText = this.encodeSearchTerm(); - let targetUrl = `/search/${type}`; - if (termText) { - targetUrl += `?q=${termText}`; - } - let allTab = ( - -
  • - - -
  • -
    - ); - if (this.state.tab === type) { - allTab = ( - -
  • - - -
  • -
    - ); - } - return allTab; - } getProjectBox () { const results = (
    - - {this.getTab('projects')} - {this.getTab('studios')} - + { + const termText = this.encodeSearchTerm(); + let targetUrl = `/search/projects`; + if (termText) targetUrl += `?q=${termText}`; + window.location = targetUrl; + }, + getContent: isActive => ( +
    + {isActive ? ( + + ) : ( + + ) + } + +
    + ) + }, + { + name: 'studios', + onTrigger: () => { + const termText = this.encodeSearchTerm(); + let targetUrl = `/search/studios`; + if (termText) targetUrl += `?q=${termText}`; + window.location = targetUrl; + }, + getContent: isActive => ( +
    + {isActive ? ( + + ) : ( + + ) + } + +
    + ) + } + ]} + activeTabName={this.state.tab} + />