Mailspring/internal_packages/composer/lib/send-action-button.jsx
Ben Gotow b4434f6617 fix(focus): Remove focusedField in favor of imperative focus, break apart ComposerView
Summary:
- Removes controlled focus in the composer!
  - No React components ever perfom focus in lifecycle methods. Never again.
  - A new `Utils.schedule({action, after, timeout})` helper makes it easy to say "setState or load draft, etc. and then focus"
  - The DraftStore issues a focusDraft action after creating a draft, which causes the MessageList to focus and scroll to the desired composer, which itself decides which field to focus.
  - The MessageList never focuses anything automatically.
- Refactors ComposerView apart — ComposerHeader handles all top fields, DraftSessionContainer handles draft session initialization and exposes props to ComposerView
  - ComposerHeader now uses a KeyCommandRegion (with focusIn and focusOut) to do the expanding and collapsing of the participants fields. May rename that container very soon.
- Removes all CommandRegistry handling of tab and shift-tab. Unless you preventDefault, the browser does it's thing.
- Removes all tabIndexes greater than 1. This is an anti-pattern—assigning everything a tabIndex of 0 tells the browser to move between them based on their order in the DOM, and is almost always what you want.
- Adds "TabGroupRegion" which allows you to create a tab/shift-tabbing group, (so tabbing does not leave the active composer). Can't believe this isn't a browser feature.

Todos:
- Occasionally, clicking out of the composer contenteditable requires two clicks. This is because atomicEdit is restoring selection within the contenteditable and breaking blur.
- Because the ComposerView does not render until it has a draft, we're back to it being white in popout composers for a brief moment. We will fix this another way - all the "return unless draft" statements were untenable.
- Clicking a row in the thread list no longer shifts focus to the message list and focuses the last draft. This will be restored soon.

Test Plan: Broken

Reviewers: juan, evan

Reviewed By: juan, evan

Differential Revision: https://phab.nylas.com/D2814
2016-04-04 15:22:01 -07:00

184 lines
4.7 KiB
JavaScript

import _ from 'underscore'
import _str from 'underscore.string'
import {React, Actions, ExtensionRegistry} from 'nylas-exports'
import {Menu, RetinaImg, ButtonDropdown} from 'nylas-component-kit'
const CONFIG_KEY = "core.sending.defaultSendType";
export default class SendActionButton extends React.Component {
static displayName = "SendActionButton";
static propTypes = {
draft: React.PropTypes.object,
style: React.PropTypes.object,
isValidDraft: React.PropTypes.func,
};
static defaultProps = {
style: {},
};
constructor(props) {
super(props)
this.state = {
actionConfigs: this._actionConfigs(this.props),
};
}
componentDidMount() {
this.unsub = ExtensionRegistry.Composer.listen(this._onExtensionsChanged);
}
componentWillReceiveProps(newProps) {
this.setState({actionConfigs: this._actionConfigs(newProps)});
}
componentWillUnmount() {
this.unsub();
}
primaryClick = () => {
this._onPrimaryClick();
}
_configKeyFromTitle(title) {
return _str.dasherize(title.toLowerCase());
}
_defaultActionConfig() {
return ({
title: "Send",
iconUrl: null,
onSend: ({draft}) => Actions.sendDraft(draft.clientId),
configKey: "send",
});
}
_actionConfigs(props) {
const actionConfigs = [this._defaultActionConfig()]
for (const extension of ExtensionRegistry.Composer.extensions()) {
if (!extension.sendActionConfig) {
continue;
}
try {
const actionConfig = extension.sendActionConfig({draft: props.draft});
if (actionConfig) {
this._verifyConfig(actionConfig, extension);
actionConfig.configKey = this._configKeyFromTitle(actionConfig.title);
actionConfigs.push(actionConfig);
}
} catch (err) {
NylasEnv.reportError(err);
}
}
return actionConfigs;
}
_verifyConfig(config = {}, extension) {
const name = extension.name;
if (!_.isString(config.title)) {
throw new Error(`${name}.sendActionConfig must return a string "title"`);
}
if (!_.isFunction(config.onSend)) {
throw new Error(`${name}.sendActionConfig must return a "onSend" function that will be called when the action is selected`);
}
return true;
}
_onExtensionsChanged = () => {
this.setState({actionConfigs: this._actionConfigs(this.props)});
}
_onPrimaryClick = () => {
const {preferred} = this._orderedActionConfigs();
this._onSendWithAction(preferred);
}
_onSendWithAction = ({onSend}) => {
if (this.props.isValidDraft()) {
try {
onSend({draft: this.props.draft});
} catch (err) {
NylasEnv.reportError(err)
}
}
}
_orderedActionConfigs() {
const configKeys = _.pluck(this.state.actionConfigs, "configKey");
let preferredKey = NylasEnv.config.get(CONFIG_KEY);
if (!preferredKey || !configKeys.includes(preferredKey)) {
preferredKey = this._defaultActionConfig().configKey;
}
const preferred = _.findWhere(this.state.actionConfigs, {configKey: preferredKey});
const rest = _.without(this.state.actionConfigs, preferred);
return {preferred, rest};
}
_contentForAction = ({iconUrl}) => {
let plusHTML = "";
let additionalImg = false;
if (iconUrl) {
plusHTML = (<span>&nbsp;+&nbsp;</span>);
additionalImg = (<RetinaImg url={iconUrl} mode={RetinaImg.Mode.ContentIsMask} />);
}
return (
<span>
<RetinaImg name="icon-composer-send.png" mode={RetinaImg.Mode.ContentIsMask} />
<span className="text">Send{plusHTML}</span>{additionalImg}
</span>
);
}
_renderSingleButton() {
return (
<button
tabIndex={-1}
className={"btn btn-toolbar btn-normal btn-emphasis btn-text btn-send"}
style={{order: -100}}
onClick={this._onPrimaryClick}>
{this._contentForAction(this.state.actionConfigs[0])}
</button>
);
}
_renderButtonDropdown() {
const {preferred, rest} = this._orderedActionConfigs()
const menu = (
<Menu
items={rest}
itemKey={ (actionConfig) => actionConfig.configKey }
itemContent={this._contentForAction}
onSelect={this._onSendWithAction}
/>
);
return (
<ButtonDropdown
className={"btn-send btn-emphasis btn-text"}
style={{order: -100}}
primaryItem={this._contentForAction(preferred)}
primaryTitle={preferred.title}
primaryClick={this._onPrimaryClick}
closeOnMenuClick
menu={menu}
/>
);
}
render() {
if (this.state.actionConfigs.length === 1) {
return this._renderSingleButton();
}
return this._renderButtonDropdown();
}
}