Fix intermediary responsive layout, accessibility on navigation in web UI (#19324)
* Fix intermediary responsive layout, accessibility on navigation in web UI * `yarn test:jest -u` Co-authored-by: Yamagishi Kazutoshi <ykzts@desire.sh>
This commit is contained in:
parent
2b00ccdbd5
commit
0765324622
9 changed files with 109 additions and 42 deletions
|
@ -2,9 +2,11 @@
|
||||||
|
|
||||||
exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
|
exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
|
||||||
<div
|
<div
|
||||||
|
aria-label="alice"
|
||||||
className="account__avatar"
|
className="account__avatar"
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
|
role="img"
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"backgroundImage": "url(/animated/alice.gif)",
|
"backgroundImage": "url(/animated/alice.gif)",
|
||||||
|
@ -18,9 +20,11 @@ exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
|
||||||
|
|
||||||
exports[`<Avatar /> Still renders a still avatar 1`] = `
|
exports[`<Avatar /> Still renders a still avatar 1`] = `
|
||||||
<div
|
<div
|
||||||
|
aria-label="alice"
|
||||||
className="account__avatar"
|
className="account__avatar"
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
|
role="img"
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"backgroundImage": "url(/static/alice.jpg)",
|
"backgroundImage": "url(/static/alice.jpg)",
|
||||||
|
|
|
@ -63,6 +63,8 @@ export default class Avatar extends React.PureComponent {
|
||||||
onMouseEnter={this.handleMouseEnter}
|
onMouseEnter={this.handleMouseEnter}
|
||||||
onMouseLeave={this.handleMouseLeave}
|
onMouseLeave={this.handleMouseLeave}
|
||||||
style={style}
|
style={style}
|
||||||
|
role='img'
|
||||||
|
aria-label={account.get('acct')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const Logo = () => (
|
const Logo = () => (
|
||||||
<svg viewBox='0 0 261 66' className='logo'>
|
<svg viewBox='0 0 261 66' className='logo' role='img'>
|
||||||
|
<title>Mastodon</title>
|
||||||
<use xlinkHref='#logo-symbol-wordmark' />
|
<use xlinkHref='#logo-symbol-wordmark' />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,37 +1,41 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
const ColumnLink = ({ icon, text, to, href, method, badge }) => {
|
const ColumnLink = ({ icon, text, to, href, method, badge, transparent, ...other }) => {
|
||||||
|
const className = classNames('column-link', { 'column-link--transparent': transparent });
|
||||||
const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null;
|
const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null;
|
||||||
|
const iconElement = typeof icon === 'string' ? <Icon id={icon} fixedWidth className='column-link__icon' /> : icon;
|
||||||
|
|
||||||
if (href) {
|
if (href) {
|
||||||
return (
|
return (
|
||||||
<a href={href} className='column-link' data-method={method}>
|
<a href={href} className={className} data-method={method} title={text} {...other}>
|
||||||
<Icon id={icon} fixedWidth className='column-link__icon' />
|
{iconElement}
|
||||||
{text}
|
{text}
|
||||||
{badgeElement}
|
{badgeElement}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Link to={to} className='column-link'>
|
<NavLink to={to} className={className} title={text} {...other}>
|
||||||
<Icon id={icon} fixedWidth className='column-link__icon' />
|
{iconElement}
|
||||||
{text}
|
{text}
|
||||||
{badgeElement}
|
{badgeElement}
|
||||||
</Link>
|
</NavLink>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ColumnLink.propTypes = {
|
ColumnLink.propTypes = {
|
||||||
icon: PropTypes.string.isRequired,
|
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
||||||
text: PropTypes.string.isRequired,
|
text: PropTypes.string.isRequired,
|
||||||
to: PropTypes.string,
|
to: PropTypes.string,
|
||||||
href: PropTypes.string,
|
href: PropTypes.string,
|
||||||
method: PropTypes.string,
|
method: PropTypes.string,
|
||||||
badge: PropTypes.node,
|
badge: PropTypes.node,
|
||||||
|
transparent: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ColumnLink;
|
export default ColumnLink;
|
||||||
|
|
|
@ -2,22 +2,27 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { fetchFollowRequests } from 'mastodon/actions/accounts';
|
import { fetchFollowRequests } from 'mastodon/actions/accounts';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { NavLink, withRouter } from 'react-router-dom';
|
import ColumnLink from 'mastodon/features/ui/components/column_link';
|
||||||
import IconWithBadge from 'mastodon/components/icon_with_badge';
|
import IconWithBadge from 'mastodon/components/icon_with_badge';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
text: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||||
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
|
count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @withRouter
|
export default @injectIntl
|
||||||
@connect(mapStateToProps)
|
@connect(mapStateToProps)
|
||||||
class FollowRequestsNavLink extends React.Component {
|
class FollowRequestsColumnLink extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
count: PropTypes.number.isRequired,
|
count: PropTypes.number.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
@ -27,13 +32,20 @@ class FollowRequestsNavLink extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { count } = this.props;
|
const { count, intl } = this.props;
|
||||||
|
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <NavLink className='column-link column-link--transparent' to='/follow_requests'><IconWithBadge className='column-link__icon' id='user-plus' count={count} /><FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' /></NavLink>;
|
return (
|
||||||
|
<ColumnLink
|
||||||
|
transparent
|
||||||
|
to='/follow_requests'
|
||||||
|
icon={<IconWithBadge className='column-link__icon' id='user-plus' count={count} />}
|
||||||
|
text={intl.formatMessage(messages.text)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -11,8 +11,7 @@ import { connect } from 'react-redux';
|
||||||
const Account = connect(state => ({
|
const Account = connect(state => ({
|
||||||
account: state.getIn(['accounts', me]),
|
account: state.getIn(['accounts', me]),
|
||||||
}))(({ account }) => (
|
}))(({ account }) => (
|
||||||
<Permalink href={account.get('url')} to={`/@${account.get('acct')}`}>
|
<Permalink href={account.get('url')} to={`/@${account.get('acct')}`} title={account.get('acct')}>
|
||||||
<span style={{ display: 'none' }}>{account.get('acct')}</span>
|
|
||||||
<Avatar account={account} size={35} />
|
<Avatar account={account} size={35} />
|
||||||
</Permalink>
|
</Permalink>
|
||||||
));
|
));
|
||||||
|
|
|
@ -1,24 +1,45 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import PropTypes from 'prop-types';
|
||||||
import { Link, NavLink } from 'react-router-dom';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import Icon from 'mastodon/components/icon';
|
import { Link } from 'react-router-dom';
|
||||||
import Logo from 'mastodon/components/logo';
|
import Logo from 'mastodon/components/logo';
|
||||||
import TrendsContainer from 'mastodon/features/getting_started/containers/trends_container';
|
import TrendsContainer from 'mastodon/features/getting_started/containers/trends_container';
|
||||||
import { showTrends, timelinePreview } from 'mastodon/initial_state';
|
import { showTrends, timelinePreview } from 'mastodon/initial_state';
|
||||||
import FollowRequestsNavLink from './follow_requests_nav_link';
|
import FollowRequestsColumnLink from './follow_requests_column_link';
|
||||||
import ListPanel from './list_panel';
|
import ListPanel from './list_panel';
|
||||||
import NotificationsCounterIcon from './notifications_counter_icon';
|
import NotificationsCounterIcon from './notifications_counter_icon';
|
||||||
import SignInBanner from './sign_in_banner';
|
import SignInBanner from './sign_in_banner';
|
||||||
|
import ColumnLink from 'mastodon/features/ui/components/column_link';
|
||||||
|
|
||||||
export default class NavigationPanel extends React.Component {
|
const messages = defineMessages({
|
||||||
|
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||||
|
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
||||||
|
explore: { id: 'explore.title', defaultMessage: 'Explore' },
|
||||||
|
local: { id: 'tabs_bar.local_timeline', defaultMessage: 'Local' },
|
||||||
|
federated: { id: 'tabs_bar.federated_timeline', defaultMessage: 'Federated' },
|
||||||
|
direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
|
||||||
|
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||||
|
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
|
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||||
|
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||||
|
followsAndFollowers: { id: 'navigation_bar.follows_and_followers', defaultMessage: 'Follows and followers' },
|
||||||
|
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class NavigationPanel extends React.Component {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
router: PropTypes.object.isRequired,
|
router: PropTypes.object.isRequired,
|
||||||
identity: PropTypes.object.isRequired,
|
identity: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const { intl } = this.props;
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -30,17 +51,17 @@ export default class NavigationPanel extends React.Component {
|
||||||
|
|
||||||
{signedIn && (
|
{signedIn && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<NavLink className='column-link column-link--transparent' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>
|
<ColumnLink transparent to='/home' icon='home' text={intl.formatMessage(messages.home)} />
|
||||||
<NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>
|
<ColumnLink transparent to='/notifications' icon={<NotificationsCounterIcon className='column-link__icon' />} text={intl.formatMessage(messages.notifications)} />
|
||||||
<FollowRequestsNavLink />
|
<FollowRequestsColumnLink />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<NavLink className='column-link column-link--transparent' to='/explore' data-preview-title-id='explore.title' data-preview-icon='hashtag'><Icon className='column-link__icon' id='hashtag' fixedWidth /><FormattedMessage id='explore.title' defaultMessage='Explore' /></NavLink>
|
<ColumnLink transparent to='/explore' icon='hashtag' text={intl.formatMessage(messages.explore)} />
|
||||||
{signedIn || timelinePreview && (
|
{signedIn || timelinePreview && (
|
||||||
<>
|
<>
|
||||||
<NavLink className='column-link column-link--transparent' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
|
<ColumnLink transparent to='/public/local' icon='users' text={intl.formatMessage(messages.local)} />
|
||||||
<NavLink className='column-link column-link--transparent' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
|
<ColumnLink transparent exact to='/public' icon='globe' text={intl.formatMessage(messages.federated)} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -53,23 +74,23 @@ export default class NavigationPanel extends React.Component {
|
||||||
|
|
||||||
{signedIn && (
|
{signedIn && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<NavLink className='column-link column-link--transparent' to='/conversations'><Icon className='column-link__icon' id='at' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
|
<ColumnLink transparent to='/conversations' icon='at' text={intl.formatMessage(messages.direct)} />
|
||||||
<NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
|
<ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
|
||||||
<NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink>
|
<ColumnLink transparent to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} />
|
||||||
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
|
<ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} />
|
||||||
|
|
||||||
<ListPanel />
|
<ListPanel />
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<a className='column-link column-link--transparent' href='/settings/preferences'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>
|
<ColumnLink transparent href='/settings/preferences' icon='cog' text={intl.formatMessage(messages.preferences)} />
|
||||||
<a className='column-link column-link--transparent' href='/relationships'><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>
|
<ColumnLink transparent href='/relationships' icon='users' text={intl.formatMessage(messages.followsAndFollowers)} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='navigation-panel__legal'>
|
<div className='navigation-panel__legal'>
|
||||||
<hr />
|
<hr />
|
||||||
<NavLink className='column-link column-link--transparent' to='/about'><Icon className='column-link__icon' id='ellipsis-h' fixedWidth /><FormattedMessage id='navigation_bar.about' defaultMessage='About' /></NavLink>
|
<ColumnLink transparent to='/about' icon='ellipsis-h' text={intl.formatMessage(messages.about)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showTrends && (
|
{showTrends && (
|
||||||
|
|
|
@ -2604,12 +2604,14 @@ $ui-header-height: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $no-gap-breakpoint - 1px) {
|
@media screen and (max-width: $no-gap-breakpoint - 1px) {
|
||||||
|
$sidebar-width: 285px;
|
||||||
|
|
||||||
.with-fab .scrollable .item-list:last-child {
|
.with-fab .scrollable .item-list:last-child {
|
||||||
padding-bottom: 5.25rem;
|
padding-bottom: 5.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.columns-area__panels__main {
|
.columns-area__panels__main {
|
||||||
width: calc(100% - 55px);
|
width: calc(100% - $sidebar-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.columns-area__panels {
|
.columns-area__panels {
|
||||||
|
@ -2617,10 +2619,10 @@ $ui-header-height: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.columns-area__panels__pane--navigational {
|
.columns-area__panels__pane--navigational {
|
||||||
min-width: 55px;
|
min-width: $sidebar-width;
|
||||||
|
|
||||||
.columns-area__panels__pane__inner {
|
.columns-area__panels__pane__inner {
|
||||||
width: 55px;
|
width: $sidebar-width;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation-panel {
|
.navigation-panel {
|
||||||
|
@ -2630,7 +2632,6 @@ $ui-header-height: 55px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-link span,
|
|
||||||
.navigation-panel__sign-in-banner,
|
.navigation-panel__sign-in-banner,
|
||||||
.navigation-panel__logo,
|
.navigation-panel__logo,
|
||||||
.getting-started__trends {
|
.getting-started__trends {
|
||||||
|
@ -2655,11 +2656,31 @@ $ui-header-height: 55px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $no-gap-breakpoint - 285px - 1px) {
|
||||||
|
$sidebar-width: 55px;
|
||||||
|
|
||||||
|
.columns-area__panels__main {
|
||||||
|
width: calc(100% - $sidebar-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.columns-area__panels__pane--navigational {
|
||||||
|
min-width: $sidebar-width;
|
||||||
|
|
||||||
|
.columns-area__panels__pane__inner {
|
||||||
|
width: $sidebar-width;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-link span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.explore__search-header {
|
.explore__search-header {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $no-gap-breakpoint + 285px - 1px) {
|
@media screen and (max-width: $no-gap-breakpoint - 1px) {
|
||||||
.columns-area__panels__pane--compositional {
|
.columns-area__panels__pane--compositional {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -3145,6 +3166,9 @@ $ui-header-height: 55px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus,
|
||||||
|
|
|
@ -53,7 +53,7 @@ $media-modal-media-max-width: 100%;
|
||||||
// put margins on top and bottom of image to avoid the screen covered by image.
|
// put margins on top and bottom of image to avoid the screen covered by image.
|
||||||
$media-modal-media-max-height: 80%;
|
$media-modal-media-max-height: 80%;
|
||||||
|
|
||||||
$no-gap-breakpoint: 890px;
|
$no-gap-breakpoint: 1175px;
|
||||||
|
|
||||||
$font-sans-serif: 'mastodon-font-sans-serif' !default;
|
$font-sans-serif: 'mastodon-font-sans-serif' !default;
|
||||||
$font-display: 'mastodon-font-display' !default;
|
$font-display: 'mastodon-font-display' !default;
|
||||||
|
|
Loading…
Reference in a new issue