diff --git a/app/soapbox/actions/modal.js b/app/soapbox/actions/modal.js
index eaa5a315d..72604ecc6 100644
--- a/app/soapbox/actions/modal.js
+++ b/app/soapbox/actions/modal.js
@@ -9,8 +9,9 @@ export function openModal(type, props) {
};
}
-export function closeModal() {
+export function closeModal(type) {
return {
type: MODAL_CLOSE,
+ modalType: type,
};
}
diff --git a/app/soapbox/components/button.js b/app/soapbox/components/button.js
index 6705b77df..214ce7b53 100644
--- a/app/soapbox/components/button.js
+++ b/app/soapbox/components/button.js
@@ -66,7 +66,7 @@ export default class Button extends React.PureComponent {
if (this.props.to) {
return (
-
+
{btn}
);
diff --git a/app/soapbox/components/dropdown_menu.js b/app/soapbox/components/dropdown_menu.js
index 2f2412928..37f1a8c2b 100644
--- a/app/soapbox/components/dropdown_menu.js
+++ b/app/soapbox/components/dropdown_menu.js
@@ -45,7 +45,9 @@ class DropdownMenu extends React.PureComponent {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('keydown', this.handleKeyDown, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
- if (this.focusedItem && this.props.openedViaKeyboard) this.focusedItem.focus();
+ if (this.focusedItem && this.props.openedViaKeyboard) {
+ this.focusedItem.focus({ preventScroll: true });
+ }
this.setState({ mounted: true });
}
@@ -66,38 +68,42 @@ class DropdownMenu extends React.PureComponent {
handleKeyDown = e => {
const items = Array.from(this.node.getElementsByTagName('a'));
const index = items.indexOf(document.activeElement);
- let element;
+ let element = null;
switch(e.key) {
case 'ArrowDown':
- element = items[index+1];
- if (element) {
- element.focus();
- }
+ element = items[index+1] || items[0];
break;
case 'ArrowUp':
- element = items[index-1];
- if (element) {
- element.focus();
+ element = items[index-1] || items[items.length-1];
+ break;
+ case 'Tab':
+ if (e.shiftKey) {
+ element = items[index-1] || items[items.length-1];
+ } else {
+ element = items[index+1] || items[0];
}
break;
case 'Home':
element = items[0];
- if (element) {
- element.focus();
- }
break;
case 'End':
element = items[items.length-1];
- if (element) {
- element.focus();
- }
break;
+ case 'Escape':
+ this.props.onClose();
+ break;
+ }
+
+ if (element) {
+ element.focus();
+ e.preventDefault();
+ e.stopPropagation();
}
}
- handleItemKeyDown = e => {
- if (e.key === 'Enter') {
+ handleItemKeyPress = e => {
+ if (e.key === 'Enter' || e.key === ' ') {
this.handleClick(e);
}
}
@@ -151,7 +157,7 @@ class DropdownMenu extends React.PureComponent {
ref={i === 0 ? this.setFocusRef : null}
onClick={this.handleClick}
onAuxClick={this.handleAuxClick}
- onKeyDown={this.handleItemKeyDown}
+ onKeyPress={this.handleItemKeyPress}
data-index={i}
target={newTab ? '_blank' : null}
data-method={isLogout ? 'delete' : null}
@@ -226,19 +232,36 @@ export default class Dropdown extends React.PureComponent {
}
handleClose = () => {
+ if (this.activeElement) {
+ this.activeElement.focus();
+ this.activeElement = null;
+ }
this.props.onClose(this.state.id);
}
- handleKeyDown = e => {
+ handleMouseDown = () => {
+ if (!this.state.open) {
+ this.activeElement = document.activeElement;
+ }
+ }
+
+ handleButtonKeyDown = (e) => {
+ switch(e.key) {
+ case ' ':
+ case 'Enter':
+ this.handleMouseDown();
+ break;
+ }
+ }
+
+ handleKeyPress = (e) => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleClick(e);
+ e.stopPropagation();
e.preventDefault();
break;
- case 'Escape':
- this.handleClose();
- break;
}
}
@@ -276,7 +299,7 @@ export default class Dropdown extends React.PureComponent {
const open = this.state.id === openDropdownId;
return (
-
+
diff --git a/app/soapbox/components/icon_button.js b/app/soapbox/components/icon_button.js
index 21ed4ca95..0d290c781 100644
--- a/app/soapbox/components/icon_button.js
+++ b/app/soapbox/components/icon_button.js
@@ -13,8 +13,10 @@ export default class IconButton extends React.PureComponent {
title: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
onClick: PropTypes.func,
+ onMouseDown: PropTypes.func,
onKeyUp: PropTypes.func,
onKeyDown: PropTypes.func,
+ onKeyPress: PropTypes.func,
onMouseEnter: PropTypes.func,
onMouseLeave: PropTypes.func,
size: PropTypes.number,
@@ -54,6 +56,30 @@ export default class IconButton extends React.PureComponent {
}
}
+ handleMouseDown = (e) => {
+ if (!this.props.disabled && this.props.onMouseDown) {
+ this.props.onMouseDown(e);
+ }
+ }
+
+ handleKeyDown = (e) => {
+ if (!this.props.disabled && this.props.onKeyDown) {
+ this.props.onKeyDown(e);
+ }
+ }
+
+ handleKeyUp = (e) => {
+ if (!this.props.disabled && this.props.onKeyUp) {
+ this.props.onKeyUp(e);
+ }
+ }
+
+ handleKeyPress = (e) => {
+ if (this.props.onKeyPress && !this.props.disabled) {
+ this.props.onKeyPress(e);
+ }
+ }
+
render() {
const style = {
fontSize: `${this.props.size}px`,
@@ -98,8 +124,10 @@ export default class IconButton extends React.PureComponent {
title={title}
className={classes}
onClick={this.handleClick}
- onKeyUp={this.props.onKeyUp}
- onKeyDown={this.props.onKeyDown}
+ onMouseDown={this.handleMouseDown}
+ onKeyDown={this.handleKeyDown}
+ onKeyUp={this.handleKeyUp}
+ onKeyPress={this.handleKeyPress}
onMouseEnter={this.props.onMouseEnter}
onMouseLeave={this.props.onMouseLeave}
tabIndex={tabIndex}
@@ -125,8 +153,10 @@ export default class IconButton extends React.PureComponent {
title={title}
className={classes}
onClick={this.handleClick}
- onKeyUp={this.props.onKeyUp}
- onKeyDown={this.props.onKeyDown}
+ onMouseDown={this.handleMouseDown}
+ onKeyDown={this.handleKeyDown}
+ onKeyUp={this.handleKeyUp}
+ onKeyPress={this.handleKeyPress}
onMouseEnter={this.props.onMouseEnter}
onMouseLeave={this.props.onMouseLeave}
tabIndex={tabIndex}
diff --git a/app/soapbox/components/modal_root.js b/app/soapbox/components/modal_root.js
index 1ffb12d3c..a8e8fe746 100644
--- a/app/soapbox/components/modal_root.js
+++ b/app/soapbox/components/modal_root.js
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
+import 'wicg-inert';
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
import { connect } from 'react-redux';
import { openModal } from '../actions/modal';
@@ -74,8 +75,31 @@ class ModalRoot extends React.PureComponent {
}
};
+
+ handleKeyDown = (e) => {
+ if (e.key === 'Tab') {
+ const focusable = Array.from(this.node.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter((x) => window.getComputedStyle(x).display !== 'none');
+ const index = focusable.indexOf(e.target);
+
+ let element;
+
+ if (e.shiftKey) {
+ element = focusable[index - 1] || focusable[focusable.length - 1];
+ } else {
+ element = focusable[index + 1] || focusable[0];
+ }
+
+ if (element) {
+ element.focus();
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+ }
+
componentDidMount() {
window.addEventListener('keyup', this.handleKeyUp, false);
+ window.addEventListener('keydown', this.handleKeyDown, false);
}
componentDidUpdate(prevProps) {
@@ -101,6 +125,7 @@ class ModalRoot extends React.PureComponent {
componentWillUnmount() {
window.removeEventListener('keyup', this.handleKeyUp);
+ window.removeEventListener('keydown', this.handleKeyDown);
}
getSiblings = () => {
diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js
index 151a3e926..95c7ffb0c 100644
--- a/app/soapbox/components/status.js
+++ b/app/soapbox/components/status.js
@@ -213,6 +213,21 @@ class Status extends ImmutablePureComponent {
this.props.OnOpenAudio(media, startTime);
}
+ handleHotkeyOpenMedia = e => {
+ const { onOpenMedia, onOpenVideo } = this.props;
+ const status = this._properStatus();
+
+ e.preventDefault();
+
+ if (status.get('media_attachments').size > 0) {
+ if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
+ onOpenVideo(status.getIn(['media_attachments', 0]), 0);
+ } else {
+ onOpenMedia(status.get('media_attachments'), 0);
+ }
+ }
+ }
+
handleHotkeyReply = e => {
e.preventDefault();
this.props.onReply(this._properStatus(), this.context.router.history);
@@ -461,6 +476,7 @@ class Status extends ImmutablePureComponent {
moveDown: this.handleHotkeyMoveDown,
toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
+ openMedia: this.handleHotkeyOpenMedia,
react: this.handleHotkeyReact,
};
diff --git a/app/soapbox/containers/dropdown_menu_container.js b/app/soapbox/containers/dropdown_menu_container.js
index 73c8a1e53..f79b19202 100644
--- a/app/soapbox/containers/dropdown_menu_container.js
+++ b/app/soapbox/containers/dropdown_menu_container.js
@@ -20,7 +20,7 @@ const mapDispatchToProps = (dispatch, { status, items }) => ({
}) : openDropdownMenu(id, dropdownPlacement, keyboard));
},
onClose(id) {
- dispatch(closeModal());
+ dispatch(closeModal('ACTIONS'));
dispatch(closeDropdownMenu(id));
},
});
diff --git a/app/soapbox/containers/soapbox.js b/app/soapbox/containers/soapbox.js
index 27f7ef614..8a0c327da 100644
--- a/app/soapbox/containers/soapbox.js
+++ b/app/soapbox/containers/soapbox.js
@@ -15,6 +15,8 @@ import UI from '../features/ui';
// import Introduction from '../features/introduction';
import { preload } from '../actions/preload';
import { IntlProvider } from 'react-intl';
+import { previewState as previewMediaState } from 'soapbox/features/ui/components/media_modal';
+import { previewState as previewVideoState } from 'soapbox/features/ui/components/video_modal';
import ErrorBoundary from '../components/error_boundary';
import { fetchInstance } from 'soapbox/actions/instance';
import { fetchSoapboxConfig } from 'soapbox/actions/soapbox';
@@ -104,6 +106,10 @@ class SoapboxMount extends React.PureComponent {
this.maybeUpdateMessages(prevProps);
}
+ shouldUpdateScroll(_, { location }) {
+ return location.state !== previewMediaState && location.state !== previewVideoState;
+ }
+
render() {
const { me, themeCss, locale, customCss } = this.props;
if (me === null) return null;
@@ -137,7 +143,7 @@ class SoapboxMount extends React.PureComponent {
-
+
{!me && }
diff --git a/app/soapbox/features/compose/components/compose_form.js b/app/soapbox/features/compose/components/compose_form.js
index 502fbd7f8..0f34cc24d 100644
--- a/app/soapbox/features/compose/components/compose_form.js
+++ b/app/soapbox/features/compose/components/compose_form.js
@@ -314,10 +314,6 @@ export default class ComposeForm extends ImmutablePureComponent {
/>
-
-
-
-
+
{
!condensed &&
diff --git a/app/soapbox/features/compose/components/privacy_dropdown.js b/app/soapbox/features/compose/components/privacy_dropdown.js
index 2e5f4e5f4..f6fd27d28 100644
--- a/app/soapbox/features/compose/components/privacy_dropdown.js
+++ b/app/soapbox/features/compose/components/privacy_dropdown.js
@@ -50,7 +50,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
const index = items.findIndex(item => {
return (item.value === value);
});
- let element;
+ let element = null;
switch(e.key) {
case 'Escape':
@@ -60,34 +60,32 @@ class PrivacyDropdownMenu extends React.PureComponent {
this.handleClick(e);
break;
case 'ArrowDown':
- element = this.node.childNodes[index + 1];
- if (element) {
- element.focus();
- this.props.onChange(element.getAttribute('data-index'));
- }
+ element = this.node.childNodes[index + 1] || this.node.firstChild;
break;
case 'ArrowUp':
- element = this.node.childNodes[index - 1];
- if (element) {
- element.focus();
- this.props.onChange(element.getAttribute('data-index'));
+ element = this.node.childNodes[index - 1] || this.node.lastChild;
+ break;
+ case 'Tab':
+ if (e.shiftKey) {
+ element = this.node.childNodes[index - 1] || this.node.lastChild;
+ } else {
+ element = this.node.childNodes[index + 1] || this.node.firstChild;
}
break;
case 'Home':
element = this.node.firstChild;
- if (element) {
- element.focus();
- this.props.onChange(element.getAttribute('data-index'));
- }
break;
case 'End':
element = this.node.lastChild;
- if (element) {
- element.focus();
- this.props.onChange(element.getAttribute('data-index'));
- }
break;
}
+
+ if (element) {
+ element.focus();
+ this.props.onChange(element.getAttribute('data-index'));
+ e.preventDefault();
+ e.stopPropagation();
+ }
}
handleClick = e => {
@@ -102,7 +100,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
componentDidMount() {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
- if (this.focusedItem) this.focusedItem.focus();
+ if (this.focusedItem) this.focusedItem.focus({ preventScroll: true });
this.setState({ mounted: true });
}
@@ -192,6 +190,9 @@ class PrivacyDropdown extends React.PureComponent {
}
} else {
const { top } = target.getBoundingClientRect();
+ if (this.state.open && this.activeElement) {
+ this.activeElement.focus();
+ }
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
this.setState({ open: !this.state.open });
}
@@ -214,7 +215,25 @@ class PrivacyDropdown extends React.PureComponent {
}
}
+ handleMouseDown = () => {
+ if (!this.state.open) {
+ this.activeElement = document.activeElement;
+ }
+ }
+
+ handleButtonKeyDown = (e) => {
+ switch(e.key) {
+ case ' ':
+ case 'Enter':
+ this.handleMouseDown();
+ break;
+ }
+ }
+
handleClose = () => {
+ if (this.state.open && this.activeElement) {
+ this.activeElement.focus();
+ }
this.setState({ open: false });
}
@@ -240,6 +259,8 @@ class PrivacyDropdown extends React.PureComponent {
active={open}
inverted
onClick={this.handleToggle}
+ onMouseDown={this.handleMouseDown}
+ onKeyDown={this.handleButtonKeyDown}
style={{ height: null, lineHeight: '27px' }}
/>
diff --git a/app/soapbox/features/status/index.js b/app/soapbox/features/status/index.js
index 5bac4fff9..03890cb26 100644
--- a/app/soapbox/features/status/index.js
+++ b/app/soapbox/features/status/index.js
@@ -263,6 +263,21 @@ class Status extends ImmutablePureComponent {
this.props.dispatch(openModal('VIDEO', { media, time }));
}
+ handleHotkeyOpenMedia = e => {
+ const { onOpenMedia, onOpenVideo } = this.props;
+ const status = this._properStatus();
+
+ e.preventDefault();
+
+ if (status.get('media_attachments').size > 0) {
+ if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
+ onOpenVideo(status.getIn(['media_attachments', 0]), 0);
+ } else {
+ onOpenMedia(status.get('media_attachments'), 0);
+ }
+ }
+ }
+
handleMuteClick = (account) => {
this.props.dispatch(initMuteModal(account));
}
@@ -548,6 +563,7 @@ class Status extends ImmutablePureComponent {
openProfile: this.handleHotkeyOpenProfile,
toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
+ openMedia: this.handleHotkeyOpenMedia,
react: this.handleHotkeyReact,
};
diff --git a/app/soapbox/features/ui/components/hotkeys_modal.js b/app/soapbox/features/ui/components/hotkeys_modal.js
index c646a0923..6d1434135 100644
--- a/app/soapbox/features/ui/components/hotkeys_modal.js
+++ b/app/soapbox/features/ui/components/hotkeys_modal.js
@@ -62,12 +62,8 @@ class HotkeysModal extends ImmutablePureComponent {
|
- x |
- |
-
-
- h |
- |
+ a |
+ |
@@ -78,6 +74,14 @@ class HotkeysModal extends ImmutablePureComponent {
+
+ x |
+ |
+
+
+ h |
+ |
+
up, k |
|
@@ -106,10 +110,6 @@ class HotkeysModal extends ImmutablePureComponent {
esc |
|
-
- g + h |
- |
-
@@ -119,6 +119,10 @@ class HotkeysModal extends ImmutablePureComponent {
+
+ g + h |
+ |
+
g + n |
|
diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js
index 35c768593..010242b0d 100644
--- a/app/soapbox/features/ui/index.js
+++ b/app/soapbox/features/ui/index.js
@@ -155,6 +155,7 @@ const keyMap = {
goToRequests: 'g r',
toggleHidden: 'x',
toggleSensitive: 'h',
+ openMedia: 'a',
};
class SwitchingColumnsArea extends React.PureComponent {
diff --git a/app/soapbox/features/video/index.js b/app/soapbox/features/video/index.js
index 2b70d6db3..286b9755e 100644
--- a/app/soapbox/features/video/index.js
+++ b/app/soapbox/features/video/index.js
@@ -461,7 +461,7 @@ class Video extends React.PureComponent {
-
+
diff --git a/app/soapbox/reducers/modal.js b/app/soapbox/reducers/modal.js
index 6572d619e..69a991fa7 100644
--- a/app/soapbox/reducers/modal.js
+++ b/app/soapbox/reducers/modal.js
@@ -10,7 +10,7 @@ export default function modal(state = initialState, action) {
case MODAL_OPEN:
return { modalType: action.modalType, modalProps: action.modalProps };
case MODAL_CLOSE:
- return initialState;
+ return (action.modalType === undefined || action.modalType === state.modalType) ? initialState : state;
default:
return state;
}
diff --git a/app/styles/components/account-header.scss b/app/styles/components/account-header.scss
index 55386987a..cc3cbd95e 100644
--- a/app/styles/components/account-header.scss
+++ b/app/styles/components/account-header.scss
@@ -221,6 +221,7 @@
}
&:hover,
+ &:focus,
&.active {
border-bottom: 2px solid var(--primary-text-color);
}
diff --git a/app/styles/components/columns.scss b/app/styles/components/columns.scss
index 5bd24e1e2..205546845 100644
--- a/app/styles/components/columns.scss
+++ b/app/styles/components/columns.scss
@@ -480,7 +480,8 @@
color: var(--primary-text-color--faint);
background: transparent;
- &:hover {
+ &:hover,
+ &:focus {
color: hsla(var(--primary-text-color_hsl), 0.8);
}
diff --git a/app/styles/components/compose-form.scss b/app/styles/components/compose-form.scss
index 9daba415c..23c2ae359 100644
--- a/app/styles/components/compose-form.scss
+++ b/app/styles/components/compose-form.scss
@@ -62,9 +62,8 @@
.emoji-picker-dropdown {
position: absolute;
- top: 5px;
+ top: 10px;
right: 5px;
- z-index: 1;
}
.compose-form__autosuggest-wrapper {
@@ -141,7 +140,6 @@
}
}
- .emoji-picker-wrapper,
.autosuggest-textarea__suggestions-wrapper {
position: relative;
height: 0;
diff --git a/app/styles/ui.scss b/app/styles/ui.scss
index f96f26aae..bf8dd9f7e 100644
--- a/app/styles/ui.scss
+++ b/app/styles/ui.scss
@@ -43,7 +43,7 @@
&:hover,
&:active,
&:focus {
- color: var(--primary-text-color--faint);
+ color: var(--primary-text-color);
}
&.disabled {
@@ -93,7 +93,7 @@
&:hover,
&:active,
&:focus {
- color: var(--primary-text-color--faint);
+ color: var(--primary-text-color);
transition: color 200ms ease-out;
}
@@ -148,10 +148,6 @@
padding: 20px;
margin-bottom: 20px;
- .emoji-picker-wrapper {
- .emoji-picker-dropdown { top: 10px; }
- }
-
.compose-form {
flex: 1 1;
padding: 0 0 0 20px !important;
diff --git a/package.json b/package.json
index cc252e867..6ce9d397e 100644
--- a/package.json
+++ b/package.json
@@ -146,7 +146,8 @@
"webpack-bundle-analyzer": "^4.0.0",
"webpack-cli": "^3.3.2",
"webpack-merge": "^5.2.0",
- "websocket.js": "^0.1.12"
+ "websocket.js": "^0.1.12",
+ "wicg-inert": "^3.1.1"
},
"devDependencies": {
"axios-mock-adapter": "^1.18.1",
diff --git a/yarn.lock b/yarn.lock
index 031ea8ab9..edbe92d7d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13062,6 +13062,11 @@ which@^2.0.1:
dependencies:
isexe "^2.0.0"
+wicg-inert@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/wicg-inert/-/wicg-inert-3.1.1.tgz#b033fd4fbfb9e3fd709e5d84becbdf2e06e5c229"
+ integrity sha512-PhBaNh8ur9Xm4Ggy4umelwNIP6pPP1bv3EaWaKqfb/QNme2rdLjm7wIInvV4WhxVHhzA4Spgw9qNSqWtB/ca2A==
+
wide-align@^1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"