mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-27 01:25:52 -05:00
Merge pull request #7299 from bocoup/issue-7198
CE-292: Confusing tab navigation on explore page for screen readers
This commit is contained in:
commit
7dff6e3a5b
7 changed files with 226 additions and 92 deletions
|
@ -20,6 +20,7 @@ const SubNavigation = props => (
|
||||||
'sub-nav-align-right': props.align === 'right'
|
'sub-nav-align-right': props.align === 'right'
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
role={props.role}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,6 +28,7 @@ const SubNavigation = props => (
|
||||||
|
|
||||||
SubNavigation.propTypes = {
|
SubNavigation.propTypes = {
|
||||||
align: PropTypes.string,
|
align: PropTypes.string,
|
||||||
|
role: PropTypes.string,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
className: PropTypes.string
|
className: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const classNames = require('classnames');
|
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
|
const {useRef} = require('react');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
|
||||||
const SubNavigation = require('../../components/subnavigation/subnavigation.jsx');
|
const SubNavigation = require('../../components/subnavigation/subnavigation.jsx');
|
||||||
|
@ -10,17 +10,94 @@ require('./tabs.scss');
|
||||||
* Container for a custom, horizontal list of navigation elements
|
* Container for a custom, horizontal list of navigation elements
|
||||||
* that can be displayed within a view or component.
|
* that can be displayed within a view or component.
|
||||||
*/
|
*/
|
||||||
const Tabs = props => (
|
const Tabs = ({items, activeTabName}) => {
|
||||||
<div className="tab-background">
|
const tabElementRefs = useRef({});
|
||||||
<SubNavigation className={classNames('tabs', props.className)}>
|
|
||||||
{props.children}
|
const itemsRendered = items.map(({name, onTrigger, getContent}) => {
|
||||||
</SubNavigation>
|
const isActive = name === activeTabName;
|
||||||
</div>
|
|
||||||
);
|
let tabRef;
|
||||||
|
if (tabElementRefs.current[name]) {
|
||||||
|
tabRef = tabElementRefs.current[name];
|
||||||
|
} else {
|
||||||
|
tabRef = React.createRef();
|
||||||
|
tabElementRefs.current[name] = tabRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
role="tab"
|
||||||
|
aria-selected={`${isActive ? 'true' : 'false'}`}
|
||||||
|
className={`${isActive ? 'active' : ''}`}
|
||||||
|
onClick={onTrigger}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
|
key={name}
|
||||||
|
ref={tabRef}
|
||||||
|
>
|
||||||
|
{getContent(isActive)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className="tab-background"
|
||||||
|
onKeyDown={handleKeyDown}// eslint-disable-line
|
||||||
|
>
|
||||||
|
<SubNavigation
|
||||||
|
role="tablist"
|
||||||
|
className="tabs"
|
||||||
|
>
|
||||||
|
{itemsRendered}
|
||||||
|
</SubNavigation>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
Tabs.propTypes = {
|
Tabs.propTypes = {
|
||||||
children: PropTypes.node,
|
items: PropTypes.arrayOf(
|
||||||
className: PropTypes.string
|
PropTypes.shape({
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
onTrigger: PropTypes.func.isRequired,
|
||||||
|
getContent: PropTypes.func.isRequired
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
|
activeTabName: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Tabs;
|
module.exports = Tabs;
|
||||||
|
|
|
@ -14,13 +14,14 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs li {
|
.tabs button {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
width: $cols2;
|
width: $cols2;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: $header-gray;
|
color: $header-gray;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
border-bottom: 3px solid $ui-aqua;
|
border-bottom: 3px solid $ui-aqua;
|
||||||
|
|
|
@ -74,6 +74,8 @@
|
||||||
"general.download": "Download",
|
"general.download": "Download",
|
||||||
"general.password": "Password",
|
"general.password": "Password",
|
||||||
"general.press": "Press",
|
"general.press": "Press",
|
||||||
|
"general.projectsSelected": "Projects Tab Selected",
|
||||||
|
"general.projectsNotS": "Projects",
|
||||||
"general.privacyPolicy": "Privacy Policy",
|
"general.privacyPolicy": "Privacy Policy",
|
||||||
"general.projects": "Projects",
|
"general.projects": "Projects",
|
||||||
"general.profile": "Profile",
|
"general.profile": "Profile",
|
||||||
|
@ -91,6 +93,8 @@
|
||||||
"general.startOver": "Start over",
|
"general.startOver": "Start over",
|
||||||
"general.statistics": "Statistics",
|
"general.statistics": "Statistics",
|
||||||
"general.studios": "Studios",
|
"general.studios": "Studios",
|
||||||
|
"general.studiosSelected": "Studios Tab Selected",
|
||||||
|
"general.studiosNotS": "Studios",
|
||||||
"general.support": "Resources",
|
"general.support": "Resources",
|
||||||
"general.ideas": "Ideas",
|
"general.ideas": "Ideas",
|
||||||
"general.tipsWindow": "Tips Window",
|
"general.tipsWindow": "Tips Window",
|
||||||
|
@ -111,13 +115,21 @@
|
||||||
"general.seeAllComments": "See all comments",
|
"general.seeAllComments": "See all comments",
|
||||||
|
|
||||||
"general.all": "All",
|
"general.all": "All",
|
||||||
|
"general.allSelected": "All Selected",
|
||||||
"general.animations": "Animations",
|
"general.animations": "Animations",
|
||||||
|
"general.animationsSelected": "Animations Selected",
|
||||||
"general.art": "Art",
|
"general.art": "Art",
|
||||||
|
"general.artSelected": "Art Selected",
|
||||||
"general.games": "Games",
|
"general.games": "Games",
|
||||||
|
"general.gamesSelected": "Games Selected",
|
||||||
"general.music": "Music",
|
"general.music": "Music",
|
||||||
|
"general.musicSelected": "Music Selected",
|
||||||
"general.results": "Results",
|
"general.results": "Results",
|
||||||
|
"general.resultsSelected": "Results Selected",
|
||||||
"general.stories": "Stories",
|
"general.stories": "Stories",
|
||||||
|
"general.storiesSelected": "Stories Selected",
|
||||||
"general.tutorials": "Tutorials",
|
"general.tutorials": "Tutorials",
|
||||||
|
"general.tutorialsSelected": "Tutorials Selected",
|
||||||
|
|
||||||
"general.teacherAccounts": "Teacher Accounts",
|
"general.teacherAccounts": "Teacher Accounts",
|
||||||
|
|
||||||
|
|
|
@ -26,10 +26,8 @@ class Explore extends React.Component {
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'getExploreState',
|
'getExploreState',
|
||||||
'handleGetExploreMore',
|
'handleGetExploreMore',
|
||||||
'changeItemType',
|
|
||||||
'handleChangeSortMode',
|
'handleChangeSortMode',
|
||||||
'getBubble',
|
'getBubble'
|
||||||
'getTab'
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.state = this.getExploreState();
|
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) {
|
handleChangeSortMode (name, value) {
|
||||||
if (this.state.acceptableModes.indexOf(value) !== -1) {
|
if (this.state.acceptableModes.indexOf(value) !== -1) {
|
||||||
window.location =
|
window.location =
|
||||||
|
@ -124,31 +113,7 @@ class Explore extends React.Component {
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
getTab (type) {
|
|
||||||
const classes = classNames({
|
|
||||||
active: (this.state.itemType === type)
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<a href={`/explore/${type}/${this.state.category}/${this.state.mode}`}>
|
|
||||||
<li className={classes}>
|
|
||||||
{this.state.itemType === type ? [
|
|
||||||
<img
|
|
||||||
className={`tab-icon ${type}`}
|
|
||||||
key={`tab-${type}`}
|
|
||||||
src={`/svgs/tabs/${type}-active.svg`}
|
|
||||||
/>
|
|
||||||
] : [
|
|
||||||
<img
|
|
||||||
className={`tab-icon ${type}`}
|
|
||||||
key={`tab-${type}`}
|
|
||||||
src={`/svgs/tabs/${type}-inactive.svg`}
|
|
||||||
/>
|
|
||||||
]}
|
|
||||||
<FormattedMessage id={`general.${type}`} />
|
|
||||||
</li>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -160,10 +125,63 @@ class Explore extends React.Component {
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</TitleBanner>
|
</TitleBanner>
|
||||||
<Tabs>
|
<Tabs
|
||||||
{this.getTab('projects')}
|
items={[
|
||||||
{this.getTab('studios')}
|
{
|
||||||
</Tabs>
|
name: 'projects',
|
||||||
|
onTrigger: () => {
|
||||||
|
window.location = `${window.location.origin}/explore/projects/` +
|
||||||
|
`${this.state.category}/${this.state.mode}`;
|
||||||
|
},
|
||||||
|
getContent: isActive => (
|
||||||
|
<div>
|
||||||
|
{isActive ? (
|
||||||
|
<img
|
||||||
|
className="tab-icon projects"
|
||||||
|
src="/svgs/tabs/projects-active.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
className="tab-icon projects"
|
||||||
|
src="/svgs/tabs/projects-inactive.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<FormattedMessage id="general.projects" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'studios',
|
||||||
|
onTrigger: () => {
|
||||||
|
window.location = `${window.location.origin}/explore/studios/` +
|
||||||
|
`${this.state.category}/${this.state.mode}`;
|
||||||
|
},
|
||||||
|
getContent: isActive => (
|
||||||
|
<div>
|
||||||
|
{isActive ? (
|
||||||
|
<img
|
||||||
|
className="tab-icon studios"
|
||||||
|
src="/svgs/tabs/studios-active.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
className="tab-icon studios"
|
||||||
|
src="/svgs/tabs/studios-inactive.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<FormattedMessage id="general.studios" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
activeTabName={this.state.itemType}
|
||||||
|
/>
|
||||||
<div className="sort-controls">
|
<div className="sort-controls">
|
||||||
<SubNavigation className="categories">
|
<SubNavigation className="categories">
|
||||||
{this.getBubble('all')}
|
{this.getBubble('all')}
|
||||||
|
|
|
@ -31,8 +31,7 @@ class Search extends React.Component {
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'getSearchState',
|
'getSearchState',
|
||||||
'handleChangeSortMode',
|
'handleChangeSortMode',
|
||||||
'handleGetSearchMore',
|
'handleGetSearchMore'
|
||||||
'getTab'
|
|
||||||
]);
|
]);
|
||||||
this.state = this.getSearchState();
|
this.state = this.getSearchState();
|
||||||
this.state.loaded = [];
|
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 = (
|
|
||||||
<a href={targetUrl}>
|
|
||||||
<li>
|
|
||||||
<img
|
|
||||||
className={`tab-icon ${type}`}
|
|
||||||
src={`/svgs/tabs/${type}-inactive.svg`}
|
|
||||||
/>
|
|
||||||
<FormattedMessage id={`general.${type}`} />
|
|
||||||
</li>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
if (this.state.tab === type) {
|
|
||||||
allTab = (
|
|
||||||
<a href={targetUrl}>
|
|
||||||
<li className="active">
|
|
||||||
<img
|
|
||||||
className={`tab-icon ${type}`}
|
|
||||||
src={`/svgs/tabs/${type}-active.svg`}
|
|
||||||
/>
|
|
||||||
<FormattedMessage id={`general.${type}`} />
|
|
||||||
</li>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return allTab;
|
|
||||||
}
|
|
||||||
getProjectBox () {
|
getProjectBox () {
|
||||||
const results = (
|
const results = (
|
||||||
<Grid
|
<Grid
|
||||||
|
@ -228,10 +195,67 @@ class Search extends React.Component {
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</TitleBanner>
|
</TitleBanner>
|
||||||
<Tabs>
|
<Tabs
|
||||||
{this.getTab('projects')}
|
items={[
|
||||||
{this.getTab('studios')}
|
{
|
||||||
</Tabs>
|
name: 'projects',
|
||||||
|
onTrigger: () => {
|
||||||
|
const termText = this.encodeSearchTerm();
|
||||||
|
let targetUrl = `/search/projects`;
|
||||||
|
if (termText) targetUrl += `?q=${termText}`;
|
||||||
|
window.location = targetUrl;
|
||||||
|
},
|
||||||
|
getContent: isActive => (
|
||||||
|
<div>
|
||||||
|
{isActive ? (
|
||||||
|
<img
|
||||||
|
className="tab-icon projects"
|
||||||
|
src="/svgs/tabs/projects-active.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
className="tab-icon projects"
|
||||||
|
src="/svgs/tabs/projects-inactive.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<FormattedMessage id="general.projects" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'studios',
|
||||||
|
onTrigger: () => {
|
||||||
|
const termText = this.encodeSearchTerm();
|
||||||
|
let targetUrl = `/search/studios`;
|
||||||
|
if (termText) targetUrl += `?q=${termText}`;
|
||||||
|
window.location = targetUrl;
|
||||||
|
},
|
||||||
|
getContent: isActive => (
|
||||||
|
<div>
|
||||||
|
{isActive ? (
|
||||||
|
<img
|
||||||
|
className="tab-icon studios"
|
||||||
|
src="/svgs/tabs/studios-active.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
className="tab-icon studios"
|
||||||
|
src="/svgs/tabs/studios-inactive.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<FormattedMessage id="general.studios" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
activeTabName={this.state.tab}
|
||||||
|
/>
|
||||||
<div className="sort-controls">
|
<div className="sort-controls">
|
||||||
<Form className="sort-mode">
|
<Form className="sort-mode">
|
||||||
<Select
|
<Select
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>Icons</title><path d="M13.39,3.29V9.4a0.41,0.41,0,0,1-.14.31,4.18,4.18,0,0,1-5.51,0,3.42,3.42,0,0,0-2.23-.84,3.35,3.35,0,0,0-2.07.72v4.05a0.42,0.42,0,1,1-.84,0V3.29A0.41,0.41,0,0,1,2.87,2.9,4.17,4.17,0,0,1,8.27,3a3.39,3.39,0,0,0,4.45,0,0.39,0.39,0,0,1,.43-0.06A0.4,0.4,0,0,1,13.39,3.29Z" fill="#26d9bb" stroke="#22b296" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
<svg aria-label="Check Check" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>Icons</title><path d="M13.39,3.29V9.4a0.41,0.41,0,0,1-.14.31,4.18,4.18,0,0,1-5.51,0,3.42,3.42,0,0,0-2.23-.84,3.35,3.35,0,0,0-2.07.72v4.05a0.42,0.42,0,1,1-.84,0V3.29A0.41,0.41,0,0,1,2.87,2.9,4.17,4.17,0,0,1,8.27,3a3.39,3.39,0,0,0,4.45,0,0.39,0.39,0,0,1,.43-0.06A0.4,0.4,0,0,1,13.39,3.29Z" fill="#26d9bb" stroke="#22b296" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
Before Width: | Height: | Size: 474 B After Width: | Height: | Size: 499 B |
Loading…
Reference in a new issue