about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/components
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours/glitch/components')
-rw-r--r--app/javascript/flavours/glitch/components/column.js5
-rw-r--r--app/javascript/flavours/glitch/components/dropdown_menu.js32
-rw-r--r--app/javascript/flavours/glitch/components/intersection_observer_article.js4
-rw-r--r--app/javascript/flavours/glitch/components/media_gallery.js6
-rw-r--r--app/javascript/flavours/glitch/components/status.js31
5 files changed, 62 insertions, 16 deletions
diff --git a/app/javascript/flavours/glitch/components/column.js b/app/javascript/flavours/glitch/components/column.js
index 57c4c7a40..dc87818a5 100644
--- a/app/javascript/flavours/glitch/components/column.js
+++ b/app/javascript/flavours/glitch/components/column.js
@@ -9,6 +9,7 @@ export default class Column extends React.PureComponent {
     children: PropTypes.node,
     extraClasses: PropTypes.string,
     name: PropTypes.string,
+    label: PropTypes.string,
   };
 
   scrollTop () {
@@ -42,10 +43,10 @@ export default class Column extends React.PureComponent {
   }
 
   render () {
-    const { children, extraClasses, name } = this.props;
+    const { children, extraClasses, name, label } = this.props;
 
     return (
-      <div role='region' data-column={name} className={`column ${extraClasses || ''}`} ref={this.setRef}>
+      <div role='region' aria-label={label} data-column={name} className={`column ${extraClasses || ''}`} ref={this.setRef}>
         {children}
       </div>
     );
diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js
index 1c2b0bf25..05611c135 100644
--- a/app/javascript/flavours/glitch/components/dropdown_menu.js
+++ b/app/javascript/flavours/glitch/components/dropdown_menu.js
@@ -23,6 +23,7 @@ class DropdownMenu extends React.PureComponent {
     placement: PropTypes.string,
     arrowOffsetLeft: PropTypes.string,
     arrowOffsetTop: PropTypes.string,
+    openedViaKeyboard: PropTypes.bool,
   };
 
   static defaultProps = {
@@ -42,13 +43,15 @@ class DropdownMenu extends React.PureComponent {
 
   componentDidMount () {
     document.addEventListener('click', this.handleDocumentClick, false);
+    document.addEventListener('keydown', this.handleKeyDown, false);
     document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
-    if (this.focusedItem) this.focusedItem.focus();
+    if (this.focusedItem && this.props.openedViaKeyboard) this.focusedItem.focus();
     this.setState({ mounted: true });
   }
 
   componentWillUnmount () {
     document.removeEventListener('click', this.handleDocumentClick, false);
+    document.removeEventListener('keydown', this.handleKeyDown, false);
     document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
   }
 
@@ -62,13 +65,10 @@ class DropdownMenu extends React.PureComponent {
 
   handleKeyDown = e => {
     const items = Array.from(this.node.getElementsByTagName('a'));
-    const index = items.indexOf(e.currentTarget);
+    const index = items.indexOf(document.activeElement);
     let element;
 
     switch(e.key) {
-    case 'Enter':
-      this.handleClick(e);
-      break;
     case 'ArrowDown':
       element = items[index+1];
       if (element) {
@@ -96,6 +96,12 @@ class DropdownMenu extends React.PureComponent {
     }
   }
 
+  handleItemKeyDown = e => {
+    if (e.key === 'Enter') {
+      this.handleClick(e);
+    }
+  }
+
   handleClick = e => {
     const i = Number(e.currentTarget.getAttribute('data-index'));
     const { action, to } = this.props.items[i];
@@ -120,7 +126,7 @@ class DropdownMenu extends React.PureComponent {
 
     return (
       <li className='dropdown-menu__item' key={`${text}-${i}`}>
-        <a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleKeyDown} data-index={i}>
+        <a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleItemKeyDown} data-index={i}>
           {text}
         </a>
       </li>
@@ -170,6 +176,7 @@ export default class Dropdown extends React.PureComponent {
     onClose: PropTypes.func.isRequired,
     dropdownPlacement: PropTypes.string,
     openDropdownId: PropTypes.number,
+    openedViaKeyboard: PropTypes.bool,
   };
 
   static defaultProps = {
@@ -180,14 +187,14 @@ export default class Dropdown extends React.PureComponent {
     id: id++,
   };
 
-  handleClick = ({ target }) => {
+  handleClick = ({ target, type }) => {
     if (this.state.id === this.props.openDropdownId) {
       this.handleClose();
     } else {
       const { top } = target.getBoundingClientRect();
       const placement = top * 2 < innerHeight ? 'bottom' : 'top';
 
-      this.props.onOpen(this.state.id, this.handleItemClick, placement);
+      this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
     }
   }
 
@@ -197,6 +204,11 @@ export default class Dropdown extends React.PureComponent {
 
   handleKeyDown = e => {
     switch(e.key) {
+    case ' ':
+    case 'Enter':
+      this.handleClick(e);
+      e.preventDefault();
+      break;
     case 'Escape':
       this.handleClose();
       break;
@@ -232,7 +244,7 @@ export default class Dropdown extends React.PureComponent {
   }
 
   render () {
-    const { icon, items, size, ariaLabel, disabled, dropdownPlacement, openDropdownId } = this.props;
+    const { icon, items, size, ariaLabel, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
     const open = this.state.id === openDropdownId;
 
     return (
@@ -248,7 +260,7 @@ export default class Dropdown extends React.PureComponent {
         />
 
         <Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
-          <DropdownMenu items={items} onClose={this.handleClose} />
+          <DropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} />
         </Overlay>
       </div>
     );
diff --git a/app/javascript/flavours/glitch/components/intersection_observer_article.js b/app/javascript/flavours/glitch/components/intersection_observer_article.js
index f7f6b0a53..6eeca5598 100644
--- a/app/javascript/flavours/glitch/components/intersection_observer_article.js
+++ b/app/javascript/flavours/glitch/components/intersection_observer_article.js
@@ -107,7 +107,7 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
       return (
         <article
           ref={this.handleRef}
-          aria-posinset={index}
+          aria-posinset={index + 1}
           aria-setsize={listLength}
           style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
           data-id={id}
@@ -119,7 +119,7 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
     }
 
     return (
-      <article ref={this.handleRef} aria-posinset={index} aria-setsize={listLength} data-id={id} tabIndex='0'>
+      <article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex='0'>
         {children && React.cloneElement(children, { hidden: false })}
       </article>
     );
diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js
index 1de12c5e0..605a2862b 100644
--- a/app/javascript/flavours/glitch/components/media_gallery.js
+++ b/app/javascript/flavours/glitch/components/media_gallery.js
@@ -78,6 +78,11 @@ class Item extends React.PureComponent {
     e.stopPropagation();
   }
 
+  handleMouseDown = (e) => {
+    e.preventDefault();
+    e.stopPropagation();
+  }
+
   render () {
     const { attachment, index, size, standalone, letterbox, displayWidth } = this.props;
 
@@ -181,6 +186,7 @@ class Item extends React.PureComponent {
             onClick={this.handleClick}
             onMouseEnter={this.handleMouseEnter}
             onMouseLeave={this.handleMouseLeave}
+            onMouseDown={this.handleMouseDown}
             autoPlay={autoPlay}
             loop
             muted
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index f3709f653..665aa457a 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -7,7 +7,7 @@ import StatusIcons from './status_icons';
 import StatusContent from './status_content';
 import StatusActionBar from './status_action_bar';
 import AttachmentList from './attachment_list';
-import { FormattedMessage } from 'react-intl';
+import { injectIntl, FormattedMessage } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { MediaGallery, Video } from 'flavours/glitch/util/async-components';
 import { HotKeys } from 'react-hotkeys';
@@ -19,6 +19,24 @@ import { autoUnfoldCW } from 'flavours/glitch/util/content_warning';
 // to use the progress bar to show download progress
 import Bundle from '../features/ui/components/bundle';
 
+export const textForScreenReader = (intl, status, rebloggedByText = false, expanded = false) => {
+  const displayName = status.getIn(['account', 'display_name']);
+
+  const values = [
+    displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
+    status.get('spoiler_text') && !expanded ? status.get('spoiler_text') : status.get('search_index').slice(status.get('spoiler_text').length),
+    intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
+    status.getIn(['account', 'acct']),
+  ];
+
+  if (rebloggedByText) {
+    values.push(rebloggedByText);
+  }
+
+  return values.join(', ');
+};
+
+@injectIntl
 export default class Status extends ImmutablePureComponent {
 
   static contextTypes = {
@@ -52,6 +70,7 @@ export default class Status extends ImmutablePureComponent {
     getScrollPosition: PropTypes.func,
     updateScrollBottom: PropTypes.func,
     expanded: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
   };
 
   state = {
@@ -337,6 +356,7 @@ export default class Status extends ImmutablePureComponent {
     } = this;
     const { router } = this.context;
     const {
+      intl,
       status,
       account,
       settings,
@@ -474,6 +494,12 @@ export default class Status extends ImmutablePureComponent {
       selectorAttribs[`data-${notifKind}-by`] = `@${account.get('acct')}`;
     }
 
+    let rebloggedByText;
+
+    if (prepend === 'reblog') {
+      rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: account.get('acct') });
+    }
+
     const handlers = {
       reply: this.handleHotkeyReply,
       favourite: this.handleHotkeyFavourite,
@@ -502,6 +528,7 @@ export default class Status extends ImmutablePureComponent {
           ref={handleRef}
           tabIndex='0'
           data-featured={featured ? 'true' : null}
+          aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}
         >
           <header className='status__info'>
             <span>
@@ -539,7 +566,7 @@ export default class Status extends ImmutablePureComponent {
             parseClick={parseClick}
             disabled={!router}
           />
-          {!isCollapsed || !muted ? (
+          {!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? (
             <StatusActionBar
               {...other}
               status={status}