mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-12-29 11:52:34 +08:00
43ea21e3bf
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
306 lines
8.4 KiB
JavaScript
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>
|
|
)
|
|
}
|
|
}
|