Mailspring/internal_packages/composer/lib/composer-header.jsx
Annie 43ea21e3bf add(composer): Turn emails into mailto for debugging purposes
Summary:
fix(subject-field): Fix subject text field focus
Before, if you clicked a mailto link, once the composer loaded, the subject field (which was focused in composer-preload) blurred. This was not ideal as users would want to type once the popout loaded and instead they would have to click first. We identified that this was coming from the composer header _renderSubject injected component. We fixed the focus within mail merge subject text field and additionally added the onComponentDidChange method to injected component. This allowed us to only update the component when the header field changed.

Test Plan: tested on my machine for mailto links

Reviewers: juan

Reviewed By: juan

Subscribers: juan

Differential Revision: https://phab.nylas.com/D3150
2016-08-12 16:06:22 -07:00

306 lines
8.4 KiB
JavaScript

import _ from 'underscore';
import React from 'react';
import ReactDOM from 'react-dom';
import AccountContactField from './account-contact-field';
import {Utils, DraftHelpers, Actions, AccountStore} from 'nylas-exports';
import {
InjectedComponent,
KeyCommandsRegion,
ParticipantsTextField,
ListensToFluxStore,
} from 'nylas-component-kit';
import CollapsedParticipants from './collapsed-participants';
import ComposerHeaderActions from './composer-header-actions';
import SubjectTextField from './subject-text-field';
import Fields from './fields';
const ScopedFromField = ListensToFluxStore(AccountContactField, {
stores: [AccountStore],
getStateFromStores: (props) => {
const savedOrReplyToThread = !!props.draft.threadId;
if (savedOrReplyToThread) {
return {accounts: [AccountStore.accountForId(props.draft.accountId)]};
}
return {accounts: AccountStore.accounts()}
},
});
export default class ComposerHeader extends React.Component {
static displayName = "ComposerHeader";
static propTypes = {
draft: React.PropTypes.object.isRequired,
session: React.PropTypes.object.isRequired,
initiallyFocused: React.PropTypes.bool,
// Subject text field injected component needs to call this function
// when it is rendered with a new header component
onNewHeaderComponents: React.PropTypes.func,
}
static contextTypes = {
parentTabGroup: React.PropTypes.object,
}
constructor(props = {}) {
super(props)
this.state = this._initialStateForDraft(this.props.draft, props);
}
componentWillReceiveProps(nextProps) {
if (this.props.session !== nextProps.session) {
this.setState(this._initialStateForDraft(nextProps.draft, nextProps));
} else {
this._ensureFilledFieldsEnabled(nextProps.draft);
}
}
showAndFocusField = (fieldName) => {
const enabledFields = _.uniq([].concat(this.state.enabledFields, [fieldName]));
const participantsFocused = this.state.participantsFocused || Fields.ParticipantFields.includes(fieldName);
Utils.waitFor(() => this.refs[fieldName]).then(() =>
this.refs[fieldName].focus()
).catch(() => {
})
this.setState({enabledFields, participantsFocused});
}
hideField = (fieldName) => {
if (ReactDOM.findDOMNode(this.refs[fieldName]).contains(document.activeElement)) {
this.context.parentTabGroup.shiftFocus(-1)
}
const enabledFields = _.without(this.state.enabledFields, fieldName)
this.setState({enabledFields})
}
_ensureFilledFieldsEnabled(draft) {
let enabledFields = this.state.enabledFields;
if (!_.isEmpty(draft.cc)) {
enabledFields = enabledFields.concat([Fields.Cc]);
}
if (!_.isEmpty(draft.bcc)) {
enabledFields = enabledFields.concat([Fields.Bcc]);
}
if (enabledFields !== this.state.enabledFields) {
this.setState({enabledFields});
}
}
_initialStateForDraft(draft, props) {
const enabledFields = [Fields.To];
if (!_.isEmpty(draft.cc)) {
enabledFields.push(Fields.Cc);
}
if (!_.isEmpty(draft.bcc)) {
enabledFields.push(Fields.Bcc);
}
enabledFields.push(Fields.From);
if (this._shouldEnableSubject()) {
enabledFields.push(Fields.Subject);
}
return {
enabledFields,
participantsFocused: props.initiallyFocused,
};
}
_shouldEnableSubject = () => {
if (_.isEmpty(this.props.draft.subject)) {
return true;
}
if (DraftHelpers.isForwardedMessage(this.props.draft)) {
return true;
}
if (this.props.draft.replyToMessageId) {
return false;
}
return true;
}
_onChangeParticipants = (changes) => {
this.props.session.changes.add(changes);
Actions.draftParticipantsChanged(this.props.draft.clientId, changes);
}
_onSubjectChange = (value) => {
this.props.session.changes.add({subject: value});
}
_onFocusInParticipants = () => {
const fieldName = this.state.participantsLastActiveField || Fields.To;
Utils.waitFor(() =>
this.refs[fieldName]
).then(() =>
this.refs[fieldName].focus()
).catch(() => {
});
this.setState({
participantsFocused: true,
participantsLastActiveField: null,
});
}
_onFocusOutParticipants = (lastFocusedEl) => {
const active = Fields.ParticipantFields.find((fieldName) => {
return this.refs[fieldName] ? ReactDOM.findDOMNode(this.refs[fieldName]).contains(lastFocusedEl) : false
}
);
this.setState({
participantsFocused: false,
participantsLastActiveField: active,
});
}
_onDragCollapsedParticipants({isDropping}) {
if (isDropping) {
this.setState({
participantsFocused: true,
enabledFields: [...Fields.ParticipantFields, Fields.From, Fields.Subject],
})
}
}
_renderParticipants = () => {
let content = null;
if (this.state.participantsFocused) {
content = this._renderFields();
} else {
content = (
<CollapsedParticipants
to={this.props.draft.to}
cc={this.props.draft.cc}
bcc={this.props.draft.bcc}
onDragChange={::this._onDragCollapsedParticipants}
/>
)
}
// When the participants field collapses, we store the field that was last
// focused onto our state, so that we can restore focus to it when the fields
// are expanded again.
return (
<KeyCommandsRegion
tabIndex={-1}
ref="participantsContainer"
className="expanded-participants"
onFocusIn={this._onFocusInParticipants}
onFocusOut={this._onFocusOutParticipants}
>
{content}
</KeyCommandsRegion>
);
}
_renderSubject = () => {
if (!this.state.enabledFields.includes(Fields.Subject)) {
return false;
}
const {draft, session} = this.props
return (
<InjectedComponent
ref={Fields.Subject}
key="subject-wrap"
matching={{role: 'Composer:SubjectTextField'}}
exposedProps={{
draft,
session,
value: draft.subject,
draftClientId: draft.clientId,
onSubjectChange: this._onSubjectChange,
}}
requiredMethods={['focus']}
fallback={SubjectTextField}
onComponentDidChange={this.props.onNewHeaderComponents}
/>
)
}
_renderFields = () => {
const {to, cc, bcc, from} = this.props.draft;
// Note: We need to physically add and remove these elements, not just hide them.
// If they're hidden, shift-tab between fields breaks.
const fields = [];
fields.push(
<ParticipantsTextField
ref={Fields.To}
key="to"
field="to"
change={this._onChangeParticipants}
className="composer-participant-field to-field"
participants={{to, cc, bcc}}
draft={this.props.draft}
session={this.props.session}
/>
)
if (this.state.enabledFields.includes(Fields.Cc)) {
fields.push(
<ParticipantsTextField
ref={Fields.Cc}
key="cc"
field="cc"
change={this._onChangeParticipants}
onEmptied={() => this.hideField(Fields.Cc)}
className="composer-participant-field cc-field"
participants={{to, cc, bcc}}
draft={this.props.draft}
session={this.props.session}
/>
)
}
if (this.state.enabledFields.includes(Fields.Bcc)) {
fields.push(
<ParticipantsTextField
ref={Fields.Bcc}
key="bcc"
field="bcc"
change={this._onChangeParticipants}
onEmptied={() => this.hideField(Fields.Bcc)}
className="composer-participant-field bcc-field"
participants={{to, cc, bcc}}
draft={this.props.draft}
session={this.props.session}
/>
)
}
if (this.state.enabledFields.includes(Fields.From)) {
fields.push(
<ScopedFromField
key="from"
ref={Fields.From}
draft={this.props.draft}
session={this.props.session}
onChange={this._onChangeParticipants}
value={from[0]}
/>
)
}
return fields;
}
render() {
return (
<div className="composer-header">
<ComposerHeaderActions
draftClientId={this.props.draft.clientId}
enabledFields={this.state.enabledFields}
participantsFocused={this.state.participantsFocused}
onShowAndFocusField={this.showAndFocusField}
/>
{this._renderParticipants()}
{this._renderSubject()}
</div>
)
}
}