about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2022-10-09 15:55:32 +0200
committerGitHub <noreply@github.com>2022-10-09 15:55:32 +0200
commit07653246223251052f5150e1e74139bf8ff41ec4 (patch)
treeb61d5697d912e366feadbf49ce44d4e57fa848f3
parent2b00ccdbd555f9391aa87778f8b41eedcb7f8a04 (diff)
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>
-rw-r--r--app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap4
-rw-r--r--app/javascript/mastodon/components/avatar.js2
-rw-r--r--app/javascript/mastodon/components/logo.js3
-rw-r--r--app/javascript/mastodon/features/ui/components/column_link.js20
-rw-r--r--app/javascript/mastodon/features/ui/components/follow_requests_column_link.js (renamed from app/javascript/mastodon/features/ui/components/follow_requests_nav_link.js)24
-rw-r--r--app/javascript/mastodon/features/ui/components/header.js3
-rw-r--r--app/javascript/mastodon/features/ui/components/navigation_panel.js59
-rw-r--r--app/javascript/styles/mastodon/components.scss34
-rw-r--r--app/javascript/styles/mastodon/variables.scss2
9 files changed, 109 insertions, 42 deletions
diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap
index 1c200b184..f5c10aa37 100644
--- a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap
@@ -2,9 +2,11 @@
 
 exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
 <div
+  aria-label="alice"
   className="account__avatar"
   onMouseEnter={[Function]}
   onMouseLeave={[Function]}
+  role="img"
   style={
     {
       "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`] = `
 <div
+  aria-label="alice"
   className="account__avatar"
   onMouseEnter={[Function]}
   onMouseLeave={[Function]}
+  role="img"
   style={
     {
       "backgroundImage": "url(/static/alice.jpg)",
diff --git a/app/javascript/mastodon/components/avatar.js b/app/javascript/mastodon/components/avatar.js
index 12ab7d2df..976d85cdb 100644
--- a/app/javascript/mastodon/components/avatar.js
+++ b/app/javascript/mastodon/components/avatar.js
@@ -63,6 +63,8 @@ export default class Avatar extends React.PureComponent {
         onMouseEnter={this.handleMouseEnter}
         onMouseLeave={this.handleMouseLeave}
         style={style}
+        role='img'
+        aria-label={account.get('acct')}
       />
     );
   }
diff --git a/app/javascript/mastodon/components/logo.js b/app/javascript/mastodon/components/logo.js
index 3570b3644..ee5c22496 100644
--- a/app/javascript/mastodon/components/logo.js
+++ b/app/javascript/mastodon/components/logo.js
@@ -1,7 +1,8 @@
 import React from 'react';
 
 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' />
   </svg>
 );
diff --git a/app/javascript/mastodon/features/ui/components/column_link.js b/app/javascript/mastodon/features/ui/components/column_link.js
index 0a25f1ea2..42da05c0a 100644
--- a/app/javascript/mastodon/features/ui/components/column_link.js
+++ b/app/javascript/mastodon/features/ui/components/column_link.js
@@ -1,37 +1,41 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { Link } from 'react-router-dom';
+import { NavLink } from 'react-router-dom';
 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 iconElement = typeof icon === 'string' ? <Icon id={icon} fixedWidth className='column-link__icon' /> : icon;
 
   if (href) {
     return (
-      <a href={href} className='column-link' data-method={method}>
-        <Icon id={icon} fixedWidth className='column-link__icon' />
+      <a href={href} className={className} data-method={method} title={text} {...other}>
+        {iconElement}
         {text}
         {badgeElement}
       </a>
     );
   } else {
     return (
-      <Link to={to} className='column-link'>
-        <Icon id={icon} fixedWidth className='column-link__icon' />
+      <NavLink to={to} className={className} title={text} {...other}>
+        {iconElement}
         {text}
         {badgeElement}
-      </Link>
+      </NavLink>
     );
   }
 };
 
 ColumnLink.propTypes = {
-  icon: PropTypes.string.isRequired,
+  icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
   text: PropTypes.string.isRequired,
   to: PropTypes.string,
   href: PropTypes.string,
   method: PropTypes.string,
   badge: PropTypes.node,
+  transparent: PropTypes.bool,
 };
 
 export default ColumnLink;
diff --git a/app/javascript/mastodon/features/ui/components/follow_requests_nav_link.js b/app/javascript/mastodon/features/ui/components/follow_requests_column_link.js
index 950ed7b27..8d4057782 100644
--- a/app/javascript/mastodon/features/ui/components/follow_requests_nav_link.js
+++ b/app/javascript/mastodon/features/ui/components/follow_requests_column_link.js
@@ -2,22 +2,27 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { fetchFollowRequests } from 'mastodon/actions/accounts';
 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 { 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 => ({
   count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
 });
 
-export default @withRouter
+export default @injectIntl
 @connect(mapStateToProps)
-class FollowRequestsNavLink extends React.Component {
+class FollowRequestsColumnLink extends React.Component {
 
   static propTypes = {
     dispatch: PropTypes.func.isRequired,
     count: PropTypes.number.isRequired,
+    intl: PropTypes.object.isRequired,
   };
 
   componentDidMount () {
@@ -27,13 +32,20 @@ class FollowRequestsNavLink extends React.Component {
   }
 
   render () {
-    const { count } = this.props;
+    const { count, intl } = this.props;
 
     if (count === 0) {
       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)}
+      />
+    );
   }
 
 }
diff --git a/app/javascript/mastodon/features/ui/components/header.js b/app/javascript/mastodon/features/ui/components/header.js
index cddab820c..c49f48cc9 100644
--- a/app/javascript/mastodon/features/ui/components/header.js
+++ b/app/javascript/mastodon/features/ui/components/header.js
@@ -11,8 +11,7 @@ import { connect } from 'react-redux';
 const Account = connect(state => ({
   account: state.getIn(['accounts', me]),
 }))(({ account }) => (
-  <Permalink href={account.get('url')} to={`/@${account.get('acct')}`}>
-    <span style={{ display: 'none' }}>{account.get('acct')}</span>
+  <Permalink href={account.get('url')} to={`/@${account.get('acct')}`} title={account.get('acct')}>
     <Avatar account={account} size={35} />
   </Permalink>
 ));
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js
index a9b80e71d..aa917c1ca 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.js
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js
@@ -1,24 +1,45 @@
-import PropTypes from 'prop-types';
 import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { Link, NavLink } from 'react-router-dom';
-import Icon from 'mastodon/components/icon';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl } from 'react-intl';
+import { Link } from 'react-router-dom';
 import Logo from 'mastodon/components/logo';
 import TrendsContainer from 'mastodon/features/getting_started/containers/trends_container';
 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 NotificationsCounterIcon from './notifications_counter_icon';
 import SignInBanner from './sign_in_banner';
+import ColumnLink from 'mastodon/features/ui/components/column_link';
+
+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 class NavigationPanel extends React.Component {
+export default @injectIntl
+class NavigationPanel extends React.Component {
 
   static contextTypes = {
     router: PropTypes.object.isRequired,
     identity: PropTypes.object.isRequired,
   };
 
+  static propTypes = {
+    intl: PropTypes.object.isRequired,
+  };
+
   render () {
+    const { intl } = this.props;
     const { signedIn } = this.context.identity;
 
     return (
@@ -30,17 +51,17 @@ export default class NavigationPanel extends React.Component {
 
         {signedIn && (
           <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>
-            <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>
-            <FollowRequestsNavLink />
+            <ColumnLink transparent to='/home' icon='home' text={intl.formatMessage(messages.home)} />
+            <ColumnLink transparent to='/notifications' icon={<NotificationsCounterIcon className='column-link__icon' />} text={intl.formatMessage(messages.notifications)} />
+            <FollowRequestsColumnLink />
           </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 && (
           <>
-            <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>
-            <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 to='/public/local' icon='users' text={intl.formatMessage(messages.local)} />
+            <ColumnLink transparent exact to='/public' icon='globe' text={intl.formatMessage(messages.federated)} />
           </>
         )}
 
@@ -53,23 +74,23 @@ export default class NavigationPanel extends React.Component {
 
         {signedIn && (
           <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>
-            <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>
-            <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>
-            <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='/conversations' icon='at' text={intl.formatMessage(messages.direct)} />
+            <ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
+            <ColumnLink transparent to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} />
+            <ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} />
 
             <ListPanel />
 
             <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>
-            <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='/settings/preferences' icon='cog' text={intl.formatMessage(messages.preferences)} />
+            <ColumnLink transparent href='/relationships' icon='users' text={intl.formatMessage(messages.followsAndFollowers)} />
           </React.Fragment>
         )}
 
         <div className='navigation-panel__legal'>
           <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>
 
         {showTrends && (
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 02c2a14bf..039d0b904 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -2604,12 +2604,14 @@ $ui-header-height: 55px;
 }
 
 @media screen and (max-width: $no-gap-breakpoint - 1px) {
+  $sidebar-width: 285px;
+
   .with-fab .scrollable .item-list:last-child {
     padding-bottom: 5.25rem;
   }
 
   .columns-area__panels__main {
-    width: calc(100% - 55px);
+    width: calc(100% - $sidebar-width);
   }
 
   .columns-area__panels {
@@ -2617,10 +2619,10 @@ $ui-header-height: 55px;
   }
 
   .columns-area__panels__pane--navigational {
-    min-width: 55px;
+    min-width: $sidebar-width;
 
     .columns-area__panels__pane__inner {
-      width: 55px;
+      width: $sidebar-width;
     }
 
     .navigation-panel {
@@ -2630,7 +2632,6 @@ $ui-header-height: 55px;
       height: 100vh;
     }
 
-    .column-link span,
     .navigation-panel__sign-in-banner,
     .navigation-panel__logo,
     .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 {
   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 {
     display: none;
   }
@@ -3145,6 +3166,9 @@ $ui-header-height: 55px;
   font-size: 16px;
   padding: 15px;
   text-decoration: none;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 
   &:hover,
   &:focus,
diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss
index 775a12e68..2f6c41d5f 100644
--- a/app/javascript/styles/mastodon/variables.scss
+++ b/app/javascript/styles/mastodon/variables.scss
@@ -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.
 $media-modal-media-max-height: 80%;
 
-$no-gap-breakpoint: 890px;
+$no-gap-breakpoint: 1175px;
 
 $font-sans-serif: 'mastodon-font-sans-serif' !default;
 $font-display: 'mastodon-font-display' !default;