feat(composer): participants collapse

Summary:
Participants now collapse Gmail style in the composer field.

New, more declarative system for how we deal with "focusedFields" on the
composer.

Extracted a `CollapsedParticipants` and `ExpandedParticipants` component.

Test Plan: TODO

Reviewers: dillon, bengotow

Reviewed By: bengotow

Differential Revision: https://phab.nylas.com/D2013
This commit is contained in:
Evan Morikawa 2015-09-14 10:37:00 -04:00
parent 95c23ed497
commit c03a63b9ea
11 changed files with 812 additions and 361 deletions

View file

@ -4,6 +4,8 @@ div[contenteditable]':
'cmd-C' : 'composer:show-and-focus-cc'
'ctrl-B' : 'composer:show-and-focus-bcc'
'ctrl-C' : 'composer:show-and-focus-cc'
'cmd-T' : 'composer:focus-to'
'ctrl-T' : 'composer:focus-to'
'cmd-enter' : 'composer:send-message'
'ctrl-enter' : 'composer:send-message'
@ -13,11 +15,6 @@ div[contenteditable]':
'.composer-outer-wrap, .composer-outer-wrap div[contenteditable]':
'escape' : 'composer:delete-empty-draft'
'.composer-outer-wrap .btn.btn-send':
'enter' : 'composer:send-message'
'tab' : 'composer:focus-to'
'shift-tab' : 'native!'
'body.platform-win32 .composer-outer-wrap *[contenteditable], body.platform-win32 .composer-outer-wrap input':
'ctrl-z': 'composer:undo'
'ctrl-Z': 'composer:redo'

View file

@ -25,8 +25,6 @@ class AccountContactField extends React.Component
accounts: AccountStore.items()
render: =>
return <span></span> unless @state.accounts.length > 1
<div className="composer-participant-field">
<div className="composer-field-label">{"From:"}</div>
{@_renderFromPicker()}

View file

@ -0,0 +1,99 @@
React = require 'react'
{Utils} = require 'nylas-exports'
class CollapsedParticipants extends React.Component
@displayName: "CollapsedParticipants"
@propTypes:
# Arrays of Contact objects.
to: React.PropTypes.array
cc: React.PropTypes.array
bcc: React.PropTypes.array
# Notifies parent when the component has been clicked. This is usually
# used to expand the participants field
onClick: React.PropTypes.func
@defaultProps:
to: []
cc: []
bcc: []
constructor: (@props={}) ->
@state =
numToDisplay: 999
numRemaining: 0
numBccRemaining: 0
componentDidMount: ->
@_setNumHiddenParticipants()
shouldComponentUpdate: (nextProps, nextState) =>
not Utils.isEqualReact(nextProps, @props) or
not Utils.isEqualReact(nextState, @state)
componentDidUpdate: ->
@_setNumHiddenParticipants()
render: ->
contacts = @props.to.concat(@props.cc).map(@_collapsedContact)
bcc = @props.bcc.map(@_collapsedBccContact)
toDisplay = contacts.concat(bcc)
toDisplay = toDisplay[0...@state.numToDisplay]
if toDisplay.length is 0 then toDisplay = "Recipients"
<div onClick={ => @props.onClick?()}
ref="participantsWrap"
className="collapsed-composer-participants">
{@_renderNumRemaining()}
{toDisplay}
</div>
_renderNumRemaining: ->
if @state.numRemaining is 0 and @state.numBccRemaining is 0
return null
else if @state.numRemaining > 0 and @state.numBccRemaining is 0
str = "#{@state.numRemaining} more"
else if @state.numRemaining is 0 and @state.numBccRemaining > 0
str = "#{@state.numBccRemaining} Bcc"
else if @state.numRemaining > 0 and @state.numBccRemaining > 0
str = "#{@state.numRemaining + @state.numBccRemaining} more (#{@state.numBccRemaining} Bcc)"
return <div className="num-remaining-wrap tokenizing-field"><div className="show-more-fade"></div><div className="num-remaining token">{str}</div></div>
_collapsedContact: (contact) ->
name = contact.displayName()
<span key={contact.id}
className="collapsed-contact regular-contact">{name}</span>
_collapsedBccContact: (contact, i) ->
name = contact.displayName()
if i is 0 then name = "Bcc: #{name}"
<span key={contact.id}
className="collapsed-contact bcc-contact">{name}</span>
_setNumHiddenParticipants: ->
$wrap = React.findDOMNode(@refs.participantsWrap)
$regulars = $wrap.getElementsByClassName("regular-contact")
$bccs = $wrap.getElementsByClassName("bcc-contact")
availableSpace = $wrap.getBoundingClientRect().width
numRemaining = @props.to.length + @props.cc.length
numBccRemaining = @props.bcc.length
numToDisplay = 0
widthAccumulator = 0
for $p in $regulars
widthAccumulator += $p.getBoundingClientRect().width
break if widthAccumulator >= availableSpace
numRemaining -= 1
numToDisplay += 1
for $p in $bccs
widthAccumulator += $p.getBoundingClientRect().width
break if widthAccumulator >= availableSpace
numBccRemaining -= 1
numToDisplay += 1
@setState {numToDisplay, numRemaining, numBccRemaining}
module.exports = CollapsedParticipants

View file

@ -1,32 +1,33 @@
React = require 'react'
_ = require 'underscore'
React = require 'react'
{Utils,
File,
{File,
Utils,
Actions,
DraftStore,
UndoManager,
ContactStore,
AccountStore,
UndoManager,
FileUploadStore,
QuotedHTMLParser,
FileDownloadStore} = require 'nylas-exports'
{ResizableRegion,
InjectedComponentSet,
{DropZone,
RetinaImg,
ScrollRegion,
InjectedComponent,
FocusTrackingRegion,
ScrollRegion,
ButtonDropdown,
DropZone,
Menu,
RetinaImg} = require 'nylas-component-kit'
InjectedComponentSet} = require 'nylas-component-kit'
FileUpload = require './file-upload'
ImageFileUpload = require './image-file-upload'
ExpandedParticipants = require './expanded-participants'
CollapsedParticipants = require './collapsed-participants'
ContenteditableComponent = require './contenteditable-component'
ParticipantsTextField = require './participants-text-field'
AccountContactField = require './account-contact-field'
Fields = require './fields'
# The ComposerView is a unique React component because it (currently) is a
# singleton. Normally, the React way to do things would be to re-render the
@ -61,9 +62,8 @@ class ComposerView extends React.Component
body: ""
files: []
subject: ""
showcc: false
showbcc: false
showsubject: false
focusedField: Fields.To # Gets updated in @_initiallyFocusedField
enabledFields: [] # Gets updated in @_initiallyEnabledFields
showQuotedText: false
uploads: FileUploadStore.uploadsForMessage(@props.draftClientId) ? []
@ -75,26 +75,25 @@ class ComposerView extends React.Component
not Utils.isEqualReact(nextState, @state)
componentDidMount: =>
@_uploadUnlisten = FileUploadStore.listen @_onFileUploadStoreChange
@_usubs = []
@_usubs.push FileUploadStore.listen @_onFileUploadStoreChange
@_usubs.push AccountStore.listen @_onAccountStoreChanged
@_keymapUnlisten = atom.commands.add '.composer-outer-wrap', {
'composer:show-and-focus-bcc': @_showAndFocusBcc
'composer:show-and-focus-cc': @_showAndFocusCc
'composer:focus-to': => @focus "textFieldTo"
'composer:send-message': => @_sendDraft()
'composer:delete-empty-draft': => @_deleteDraftIfEmpty()
'composer:show-and-focus-bcc': => @_onChangeEnabledFields(show: [Fields.Bcc], focus: Fields.Bcc)
'composer:show-and-focus-cc': => @_onChangeEnabledFields(show: [Fields.Cc], focus: Fields.Cc)
'composer:focus-to': => @_onChangeEnabledFields(show: [Fields.To], focus: Fields.To)
"composer:undo": @undo
"composer:redo": @redo
}
if @props.mode is "fullwindow"
# Need to delay so the component can be fully painted. Focus doesn't
# work unless the element is on the page.
@focus "textFieldTo"
@_applyFocusedField()
componentWillUnmount: =>
@_unmounted = true # rarf
@_teardownForDraft()
@_deleteDraftIfEmpty()
@_uploadUnlisten() if @_uploadUnlisten
usub() for usub in @_usubs
@_keymapUnlisten.dispose() if @_keymapUnlisten
componentDidUpdate: =>
@ -105,11 +104,15 @@ class ComposerView extends React.Component
# re-rendering.
@_recoveredSelection = null if @_recoveredSelection?
# We often can't focus until the component state has changed
# (so the desired field exists or we have a draft)
if @_focusOnUpdate and @_proxy
@focus(@_focusOnUpdate.field)
@_focusOnUpdate = false
@_applyFocusedField()
_applyFocusedField: ->
if @state.focusedField
return unless @refs[@state.focusedField]
if @refs[@state.focusedField].focus
@refs[@state.focusedField].focus()
else
React.findDOMNode(@refs[@state.focusedField]).focus()
componentWillReceiveProps: (newProps) =>
@_ignoreNextTrigger = false
@ -150,9 +153,7 @@ class ComposerView extends React.Component
render: =>
if @props.mode is "inline"
<FocusTrackingRegion className={@_wrapClasses()} onFocus={@focus} tabIndex="-1">
<ResizableRegion handle={ResizableRegion.Handle.Bottom}>
{@_renderComposer()}
</ResizableRegion>
{@_renderComposer()}
</FocusTrackingRegion>
else
<div className={@_wrapClasses()}>
@ -174,17 +175,8 @@ class ComposerView extends React.Component
</div>
</div>
<div className="composer-content-wrap">
{@_renderBodyScrollbar()}
<div className="composer-centered">
{@_renderFields()}
<div className="compose-body" ref="composeBody" onClick={@_onClickComposeBody}>
{@_renderBody()}
{@_renderFooterRegions()}
</div>
</div>
<div className="composer-content-wrap" onKeyDown={@_onKeyDown}>
{@_renderScrollRegion()}
</div>
<div className="composer-action-bar-wrap">
@ -192,111 +184,89 @@ class ComposerView extends React.Component
</div>
</DropZone>
_renderFields: =>
# Note: We need to physically add and remove these elements, not just hide them.
# If they're hidden, shift-tab between fields breaks.
fields = []
fields.push(
<div key="to">
<div className="composer-participant-actions">
<span className="header-action"
style={display: @state.showcc and 'none' or 'inline'}
onClick={@_showAndFocusCc}>Cc</span>
<span className="header-action"
style={display: @state.showbcc and 'none' or 'inline'}
onClick={@_showAndFocusBcc}>Bcc</span>
<span className="header-action"
style={display: @state.showsubject and 'none' or 'initial'}
onClick={@_showAndFocusSubject}>Subject</span>
<span className="header-action"
data-tooltip="Popout composer"
style={{display: ((@props.mode is "fullwindow") and 'none' or 'initial'), paddingLeft: "1.5em"}}
onClick={@_popoutComposer}>
<RetinaImg name="composer-popout.png"
mode={RetinaImg.Mode.ContentIsMask}
style={{position: "relative", top: "-2px"}}/>
</span>
</div>
<ParticipantsTextField
ref="textFieldTo"
field='to'
change={@_onChangeParticipants}
className="composer-participant-field"
participants={to: @state['to'], cc: @state['cc'], bcc: @state['bcc']}
tabIndex='102'/>
</div>
)
if @state.showcc
fields.push(
<ParticipantsTextField
ref="textFieldCc"
key="cc"
field='cc'
change={@_onChangeParticipants}
onEmptied={@_onEmptyCc}
className="composer-participant-field"
participants={to: @state['to'], cc: @state['cc'], bcc: @state['bcc']}
tabIndex='103'/>
)
if @state.showbcc
fields.push(
<ParticipantsTextField
ref="textFieldBcc"
key="bcc"
field='bcc'
change={@_onChangeParticipants}
onEmptied={@_onEmptyBcc}
className="composer-participant-field"
participants={to: @state['to'], cc: @state['cc'], bcc: @state['bcc']}
tabIndex='104'/>
)
if @state.showsubject
fields.push(
<div key="subject" className="compose-subject-wrap">
<input type="text"
key="subject"
name="subject"
tabIndex="108"
ref="textFieldSubject"
placeholder="Subject"
value={@state.subject}
onChange={@_onChangeSubject}/>
</div>
)
if @state.showfrom
fields.push(
<AccountContactField
key="from"
onChange={ (me) => @_onChangeParticipants(from: [me]) }
value={@state.from?[0]} />
)
fields
_renderBodyScrollbar: =>
_renderScrollRegion: ->
if @props.mode is "inline"
[]
@_renderContent()
else
<ScrollRegion.Scrollbar ref="scrollbar" getScrollRegion={ => @refs.scrollregion } />
<ScrollRegion className="compose-body-scroll" ref="scrollregion">
{@_renderContent()}
</ScrollRegion>
_renderContent: ->
<div className="composer-centered">
{if @state.focusedField in Fields.ParticipantFields
<ExpandedParticipants to={@state.to} cc={@state.cc}
bcc={@state.bcc}
from={@state.from}
ref="expandedParticipants"
mode={@props.mode}
focusedField={@state.focusedField}
enabledFields={@state.enabledFields}
onChangeParticipants={@_onChangeParticipants}
onChangeEnabledFields={@_onChangeEnabledFields}
/>
else
<CollapsedParticipants to={@state.to} cc={@state.cc}
bcc={@state.bcc}
onClick={@_focusParticipantField} />
}
{@_renderSubject()}
<div className="compose-body" ref="composeBody" onClick={@_onClickComposeBody}>
{@_renderBody()}
{@_renderFooterRegions()}
</div>
</div>
_onKeyDown: (event) =>
if event.key is "Tab"
@_onTabDown(event)
return
_onTabDown: (event) =>
event.preventDefault()
enabledFields = _.filter @state.enabledFields, (field) ->
Fields.Order[field] >= 0
enabledFields = _.sortBy enabledFields, (field) ->
Fields.Order[field]
i = enabledFields.indexOf @state.focusedField
dir = if event.shiftKey then -1 else 1
newI = Math.min(Math.max(i + dir, 0), enabledFields.length - 1)
@setState focusedField: enabledFields[newI]
event.stopPropagation()
_onChangeParticipantField: (focusedField) =>
@setState {focusedField}
@_lastFocusedParticipantField = focusedField
_focusParticipantField: =>
@setState focusedField: @_lastFocusedParticipantField ? Fields.To
_onChangeEnabledFields: ({show, hide, focus}={}) =>
show = show ? []; hide = hide ? []
newFields = _.difference(@state.enabledFields.concat(show), hide)
@setState
enabledFields: newFields
focusedField: (focus ? @state.focusedField)
_renderSubject: ->
if Fields.Subject in @state.enabledFields
<div key="subject-wrap" className="compose-subject-wrap">
<input type="text"
name="subject"
ref={Fields.Subject}
placeholder="Subject"
value={@state.subject}
onFocus={ => @setState focusedField: Fields.Subject}
onChange={@_onChangeSubject}/>
</div>
_renderBody: =>
if @props.mode is "inline"
<span>
{@_renderBodyContenteditable()}
{@_renderAttachments()}
</span>
else
<ScrollRegion className="compose-body-scroll" ref="scrollregion" getScrollbar={ => @refs.scrollbar }>
{@_renderBodyContenteditable()}
{@_renderAttachments()}
</ScrollRegion>
<span>
{@_renderBodyContenteditable()}
{@_renderAttachments()}
</span>
_renderBodyContenteditable: =>
onScrollToBottom = null
@ -304,8 +274,9 @@ class ComposerView extends React.Component
onScrollToBottom = =>
@props.onRequestScrollTo({clientId: @_proxy.draft().clientId})
<ContenteditableComponent ref="contentBody"
<ContenteditableComponent ref={Fields.Body}
html={@state.body}
onFocus={ => @setState focusedField: Fields.Body}
onChange={@_onChangeBody}
onFilePaste={@_onFilePaste}
style={@_precalcComposerCss}
@ -313,8 +284,7 @@ class ComposerView extends React.Component
mode={{showQuotedText: @state.showQuotedText}}
onChangeMode={@_onChangeEditableMode}
onScrollTo={@props.onRequestScrollTo}
onScrollToBottom={onScrollToBottom}
tabIndex="109" />
onScrollToBottom={onScrollToBottom} />
_renderFooterRegions: =>
return <div></div> unless @props.draftClientId
@ -423,29 +393,6 @@ class ComposerView extends React.Component
</InjectedComponentSet>
# Focus the composer view. Chooses the appropriate field to start
# focused depending on the draft type, or you can pass a field as
# the first parameter.
focus: (field = null) =>
if not @_proxy
@_focusOnUpdate = {field}
return
defaultField = "contentBody"
if @isForwardedMessage() # Note: requires _proxy
defaultField = "textFieldTo"
field ?= defaultField
if not @refs[field]
@_focusOnUpdate = {field}
return
if @refs[field].focus
@refs[field].focus()
else
node = React.findDOMNode(@refs[field])
node.focus?()
isForwardedMessage: =>
return false if not @_proxy
draft = @_proxy.draft()
@ -455,7 +402,7 @@ class ComposerView extends React.Component
# and simulate what happens when you click beneath the text *in* the
# contentEditable.
_onClickComposeBody: (event) =>
@refs.contentBody.selectEnd()
@refs[Fields.Body].selectEnd()
_onDraftChanged: =>
return if @_ignoreNextTrigger
@ -471,28 +418,67 @@ class ComposerView extends React.Component
cc: draft.cc
bcc: draft.bcc
from: draft.from
body: draft.body
files: draft.files
subject: draft.subject
body: draft.body
showfrom: not draft.replyToMessageId and draft.files.length is 0
if !@state.populated
_.extend state,
showcc: not _.isEmpty(draft.cc)
showbcc: not _.isEmpty(draft.bcc)
showsubject: @_shouldShowSubject()
showQuotedText: @isForwardedMessage()
populated: true
focusedField: @_initiallyFocusedField(draft)
enabledFields: @_initiallyEnabledFields(draft)
showQuotedText: @isForwardedMessage()
# It's possible for another part of the application to manipulate the draft
# we're displaying. If they've added a CC or BCC, make sure we show those fields.
# We should never be hiding the field if it's populated.
state.showcc = true if not _.isEmpty(draft.cc)
state.showbcc = true if not _.isEmpty(draft.bcc)
state = @_verifyEnabledFields(draft, state)
@setState(state)
_shouldShowSubject: =>
_initiallyFocusedField: (draft) ->
return Fields.To if draft.to.length is 0
return Fields.Subject if (draft.subject ? "").trim().length is 0
return Fields.Body
_verifyEnabledFields: (draft, state) ->
enabledFields = @state.enabledFields.concat(state.enabledFields)
updated = false
if draft.cc.length > 0
updated = true
enabledFields.push(Fields.Cc)
if draft.bcc.length > 0
updated = true
enabledFields.push(Fields.Bcc)
if updated
state.enabledFields = _.uniq(enabledFields)
return state
_initiallyEnabledFields: (draft) ->
enabledFields = [Fields.To]
enabledFields.push Fields.Cc if not _.isEmpty(draft.cc)
enabledFields.push Fields.Bcc if not _.isEmpty(draft.bcc)
enabledFields.push Fields.From if @_shouldShowFromField(draft)
enabledFields.push Fields.Subject if @_shouldEnableSubject()
enabledFields.push Fields.Body
return enabledFields
# When the account store changes, the From field may or may not still
# be in scope. We need to make sure to update our enabled fields.
_onAccountStoreChanged: =>
if @_shouldShowFromField(@_proxy?.draft())
enabledFields = @state.enabledFields.concat [Fields.From]
else
enabledFields = _.without(@state.enabledFields, Fields.From)
@setState {enabledFields}
_shouldShowFromField: (draft) ->
return false unless draft
return AccountStore.items().length > 1 and
not draft.replyToMessageId and
draft.files.length is 0
_shouldEnableSubject: =>
return false unless @_proxy
draft = @_proxy.draft()
if _.isEmpty(draft.subject ? "") then return true
@ -664,29 +650,6 @@ class ComposerView extends React.Component
_attachFile: =>
Actions.attachFile({messageClientId: @props.draftClientId})
_showAndFocusBcc: =>
@setState {showbcc: true}
@focus('textFieldBcc')
_showAndFocusCc: =>
@setState {showcc: true}
@focus('textFieldCc')
_showAndFocusSubject: =>
@setState {showsubject: true}
@focus('textFieldSubject')
_onEmptyCc: =>
@setState showcc: false
@focus('textFieldTo')
_onEmptyBcc: =>
@setState showbcc: false
if @state.showcc
@focus('textFieldCc')
else
@focus('textFieldTo')
undo: (event) =>
event.preventDefault()
event.stopPropagation()
@ -708,8 +671,8 @@ class ComposerView extends React.Component
@_recoveredSelection = null
_getSelections: =>
currentSelection: @refs.contentBody?.getCurrentSelection?()
previousSelection: @refs.contentBody?.getPreviousSelection?()
currentSelection: @refs[Fields.Body]?.getCurrentSelection?()
previousSelection: @refs[Fields.Body]?.getPreviousSelection?()
_saveToHistory: (selections) =>
return unless @_proxy

View file

@ -105,6 +105,7 @@ class ContenteditableComponent extends React.Component
tabIndex={@props.tabIndex}
style={@props.style ? {}}
onBlur={@_onBlur}
onFocus={@_onFocus}
onClick={@_onClick}
onPaste={@_onPaste}
onInput={@_onInput}
@ -204,7 +205,10 @@ class ContenteditableComponent extends React.Component
document.execCommand("indent")
return
else if event.shiftKey
if @_atTabChar() then @_removeLastCharacter()
if @_atTabChar()
@_removeLastCharacter()
else if @_atBeginning()
return # Don't stop propagation
else
document.execCommand("insertText", false, "\t")
else
@ -212,6 +216,7 @@ class ContenteditableComponent extends React.Component
document.execCommand("insertText", false, "")
else
document.execCommand("insertText", false, "\t")
event.stopPropagation()
_selectionInText: (selection) ->
return false unless selection
@ -223,6 +228,16 @@ class ContenteditableComponent extends React.Component
return selection.anchorNode.textContent[selection.anchorOffset - 1] is "\t"
else return false
_atBeginning: ->
selection = document.getSelection()
return false if not selection.isCollapsed
return false if selection.anchorOffset > 0
el = @_editableNode()
return true if el.childNodes.length is 0
return true if selection.anchorNode is el
firstChild = el.childNodes[0]
return selection.anchorNode is firstChild
_removeLastCharacter: ->
selection = document.getSelection()
if @_selectionInText(selection)
@ -341,6 +356,9 @@ class ContenteditableComponent extends React.Component
@_hideToolbar()
, 50
_onFocus: (event) =>
@props.onFocus?(event)
_editableNode: =>
React.findDOMNode(@refs.contenteditable)

View file

@ -0,0 +1,172 @@
_ = require 'underscore'
React = require 'react'
AccountContactField = require './account-contact-field'
ParticipantsTextField = require './participants-text-field'
{RetinaImg} = require 'nylas-component-kit'
Fields = require './fields'
class ExpandedParticipants extends React.Component
@displayName: "ExpandedParticipants"
@propTypes:
# Arrays of Contact objects.
to: React.PropTypes.array
cc: React.PropTypes.array
bcc: React.PropTypes.array
from: React.PropTypes.array
# Either "fullwindow" or "inline"
mode: React.PropTypes.string
# The field that should be focused
focusedField: React.PropTypes.string
# An enum array of visible fields. Can be any constant in the `Fields`
# dict. We are passed these as props instead of holding it as state
# since this component is frequently unmounted and re-mounted every
# time it is displayed
enabledFields: React.PropTypes.array
# Callback for when a user changes which fields should be visible
onChangeEnabledFields: React.PropTypes.func
# Callback for the participants change
onChangeParticipants: React.PropTypes.func
@defaultProps:
to: []
cc: []
bcc: []
from: []
enabledFields: []
constructor: (@props={}) ->
componentDidMount: =>
@_applyFocusedField()
componentDidUpdate: ->
@_applyFocusedField()
render: ->
<div className="expanded-participants"
ref="participantWrap">
{@_renderFields()}
</div>
_applyFocusedField: ->
if @props.focusedField
return unless @refs[@props.focusedField]
if @refs[@props.focusedField].focus
@refs[@props.focusedField].focus()
else
React.findDOMNode(@refs[@props.focusedField]).focus()
_renderFields: =>
# Note: We need to physically add and remove these elements, not just hide them.
# If they're hidden, shift-tab between fields breaks.
fields = []
fields.push(
<div key="to">
<div className="composer-participant-actions">
{if Fields.Cc not in @props.enabledFields
<span className="header-action show-cc"
onClick={@_showAndFocusCc}>Cc</span>
}
{ if Fields.Bcc not in @props.enabledFields
<span className="header-action show-bcc"
onClick={@_showAndFocusBcc}>Bcc</span>
}
{ if Fields.Subject not in @props.enabledFields
<span className="header-action show-subject"
onClick={@_showAndFocusSubject}>Subject</span>
}
{ if @props.mode is "inline"
<span className="header-action show-popout"
data-tooltip="Popout composer"
style={paddingLeft: "1.5em"}
onClick={@_popoutComposer}>
<RetinaImg name="composer-popout.png"
mode={RetinaImg.Mode.ContentIsMask}
style={{position: "relative", top: "-2px"}}/>
</span>
}
</div>
<ParticipantsTextField
ref={Fields.To}
field='to'
change={@props.onChangeParticipants}
className="composer-participant-field to-field"
participants={to: @props['to'], cc: @props['cc'], bcc: @props['bcc']} />
</div>
)
if Fields.Cc in @props.enabledFields
fields.push(
<ParticipantsTextField
ref={Fields.Cc}
key="cc"
field='cc'
change={@props.onChangeParticipants}
onEmptied={@_onEmptyCc}
className="composer-participant-field cc-field"
participants={to: @props['to'], cc: @props['cc'], bcc: @props['bcc']} />
)
if Fields.Bcc in @props.enabledFields
fields.push(
<ParticipantsTextField
ref={Fields.Bcc}
key="bcc"
field='bcc'
change={@props.onChangeParticipants}
onEmptied={@_onEmptyBcc}
className="composer-participant-field bcc-field"
participants={to: @props['to'], cc: @props['cc'], bcc: @props['bcc']} />
)
if Fields.From in @props.enabledFields
fields.push(
<AccountContactField
key="from"
ref={Fields.From}
onChange={ (me) => @props.onChangeParticipants(from: [me]) }
value={@props.from?[0]} />
)
fields
_showAndFocusBcc: =>
@props.onChangeEnabledFields
show: [Fields.Bcc]
focus: Fields.Bcc
_showAndFocusCc: =>
@props.onChangeEnabledFields
show: [Fields.Cc]
focus: Fields.Cc
_showAndFocusSubject: =>
@props.onChangeEnabledFields
show: [Fields.Subject]
focus: Fields.Subject
_onEmptyCc: =>
@props.onChangeEnabledFields
hide: [Fields.Cc]
focus: Fields.To
_onEmptyBcc: =>
if Fields.Cc in @props.enabledFields
focus = Fields.Cc
else
focus = Fields.To
@props.onChangeEnabledFields
hide: [Fields.Bcc]
focus: focus
module.exports = ExpandedParticipants

View file

@ -0,0 +1,17 @@
Fields =
To: "textFieldTo"
Cc: "textFieldCc"
Bcc: "textFieldBcc"
From: "fromField"
Subject: "textFieldSubject"
Body: "contentBody"
Fields.ParticipantFields = [Fields.To, Fields.Cc, Fields.Bcc, Fields.From]
Fields.Order =
"textFieldTo": 1
"textFieldCc": 2
"textFieldBcc": 3
"fromField": -1 # Not selectable
"textFieldSubject": 5
"contentBody": 6
module.exports = Fields

View file

@ -0,0 +1,50 @@
_ = require "underscore"
React = require "react/addons"
Fields = require '../lib/fields'
ReactTestUtils = React.addons.TestUtils
CollapsedParticipants = require '../lib/collapsed-participants'
{Contact} = require 'nylas-exports'
describe "CollapsedParticipants", ->
makeField = (props={}) ->
@onClick = jasmine.createSpy("onClick")
props.onClick = @onClick
@fields = ReactTestUtils.renderIntoDocument(
<CollapsedParticipants {...props} />
)
it "fires callback when clicked", ->
makeField.call(@)
ReactTestUtils.Simulate.click React.findDOMNode(@fields)
expect(@onClick).toHaveBeenCalled()
expect(@onClick.calls.length).toBe 1
numStr = ->
React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "num-remaining")).innerHTML
it "doesn't render num remaining when nothing remains", ->
makeField.call(@)
els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@fields, "num-remaining")
expect(els.length).toBe 0
it "renders num remaining when remaining with no bcc", ->
makeField.call(@)
spyOn(@fields, "_setNumHiddenParticipants")
@fields.setState numRemaining: 10, numBccRemaining: 0
str = numStr.call(@)
expect(str).toBe "10 more"
it "renders num remaining when only bcc", ->
makeField.call(@)
spyOn(@fields, "_setNumHiddenParticipants")
@fields.setState numRemaining: 0, numBccRemaining: 5
str = numStr.call(@)
expect(str).toBe "5 Bcc"
it "renders num remaining when both remaining andj bcc", ->
makeField.call(@)
spyOn(@fields, "_setNumHiddenParticipants")
@fields.setState numRemaining: 10, numBccRemaining: 5
str = numStr.call(@)
expect(str).toBe "15 more (5 Bcc)"

View file

@ -20,6 +20,7 @@ ReactTestUtils = React.addons.TestUtils
{InjectedComponent} = require 'nylas-component-kit'
ParticipantsTextField = require '../lib/participants-text-field'
Fields = require '../lib/fields'
u1 = new Contact(name: "Christine Spang", email: "spang@nylas.com")
u2 = new Contact(name: "Michael Grinich", email: "mg@nylas.com")
@ -27,7 +28,8 @@ u3 = new Contact(name: "Evan Morikawa", email: "evan@nylas.com")
u4 = new Contact(name: "Zoë Leiper", email: "zip@nylas.com")
u5 = new Contact(name: "Ben Gotow", email: "ben@nylas.com")
file = new File(id: 'file_1_id', filename: 'a.png', contentType: 'image/png', size: 10, object: "file")
f1 = new File(id: 'file_1_id', filename: 'a.png', contentType: 'image/png', size: 10, object: "file")
f2 = new File(id: 'file_2_id', filename: 'b.pdf', contentType: '', size: 999999, object: "file")
users = [u1, u2, u3, u4, u5]
@ -69,18 +71,6 @@ ComposerView = proxyquire "../lib/composer-view",
isValidContact: isValidContactStub
DraftStore: DraftStore
beforeEach ->
# The AccountStore isn't set yet in the new window, populate it first.
AccountStore.populateItems().then ->
draft = new Message
from: [AccountStore.current().me()]
date: (new Date)
draft: true
accountId: AccountStore.current().id
DatabaseStore.persistModel(draft).then ->
return draft
describe "A blank composer view", ->
beforeEach ->
@composer = ReactTestUtils.renderIntoDocument(
@ -128,6 +118,7 @@ useFullDraft = ->
to: [u2]
cc: [u3, u4]
bcc: [u5]
files: [f1, f2]
subject: "Test Message 1"
body: "Hello <b>World</b><br/> This is a test"
replyToMessageId: null
@ -221,180 +212,174 @@ describe "populated composer", ->
expect(@draft).toBeDefined()
expect(@composer._proxy.draft()).toBe @draft
it "set the state based on the draft", ->
it "sets the basic draft state", ->
expect(@composer.state.from).toEqual [u1]
expect(@composer.state.showfrom).toEqual true
expect(@composer.state.to).toEqual [u2]
expect(@composer.state.cc).toEqual [u3, u4]
expect(@composer.state.bcc).toEqual [u5]
expect(@composer.state.subject).toEqual "Test Message 1"
expect(@composer.state.files).toEqual [f1, f2]
expect(@composer.state.body).toEqual "Hello <b>World</b><br/> This is a test"
describe "when deciding whether or not to show the subject", ->
it "shows the subject when the subject is empty", ->
it "sets first-time initial state about focused fields", ->
expect(@composer.state.populated).toBe true
expect(@composer.state.focusedField).toBeDefined()
expect(@composer.state.enabledFields).toBeDefined()
it "sets first-time initial state about showing quoted text", ->
expect(@composer.state.showQuotedText).toBe false
describe "deciding which field is initially focused", ->
it "focuses the To field if there's nobody in the 'to' field", ->
useDraft.call @
makeComposer.call @
expect(@composer.state.focusedField).toBe Fields.To
it "focuses the subject if there's no subject already", ->
useDraft.call @, to: [u1]
makeComposer.call @
expect(@composer.state.focusedField).toBe Fields.Subject
it "focuses the body otherwise", ->
useDraft.call @, to: [u1], subject: "Yo"
makeComposer.call @
expect(@composer.state.focusedField).toBe Fields.Body
describe "when deciding whether or not to enable the subject", ->
it "enables the subject when the subject is empty", ->
useDraft.call @, subject: ""
makeComposer.call @
expect(@composer._shouldShowSubject()).toBe true
expect(@composer._shouldEnableSubject()).toBe true
it "shows the subject when the subject looks like a fwd", ->
it "enables the subject when the subject looks like a fwd", ->
useDraft.call @, subject: "Fwd: This is the message"
makeComposer.call @
expect(@composer._shouldShowSubject()).toBe true
expect(@composer._shouldEnableSubject()).toBe true
it "shows the subject when the subject looks like a fwd", ->
it "enables the subject when the subject looks like a fwd", ->
useDraft.call @, subject: "fwd foo"
makeComposer.call @
expect(@composer._shouldShowSubject()).toBe true
expect(@composer._shouldEnableSubject()).toBe true
it "doesn't show subject when replyToMessageId exists", ->
it "doesn't enable subject when replyToMessageId exists", ->
useDraft.call @, subject: "should hide", replyToMessageId: "some-id"
makeComposer.call @
expect(@composer._shouldShowSubject()).toBe false
expect(@composer._shouldEnableSubject()).toBe false
it "shows the subject otherwise", ->
it "enables the subject otherwise", ->
useDraft.call @, subject: "Foo bar baz"
makeComposer.call @
expect(@composer._shouldShowSubject()).toBe true
expect(@composer._shouldEnableSubject()).toBe true
describe "when deciding whether or not to show cc and bcc", ->
it "doesn't show cc when there's no one to cc", ->
describe "when deciding whether or not to enable cc and bcc", ->
it "doesn't enable cc when there's no one to cc", ->
useDraft.call @, cc: []
makeComposer.call @
expect(@composer.state.showcc).toBe false
expect(@composer.state.enabledFields).not.toContain Fields.Cc
it "shows cc when populated", ->
it "enables cc when populated", ->
useDraft.call @, cc: [u1,u2]
makeComposer.call @
expect(@composer.state.showcc).toBe true
expect(@composer.state.enabledFields).toContain Fields.Cc
it "doesn't show bcc when there's no one to bcc", ->
it "doesn't enable bcc when there's no one to bcc", ->
useDraft.call @, bcc: []
makeComposer.call @
expect(@composer.state.showbcc).toBe false
expect(@composer.state.enabledFields).not.toContain Fields.Bcc
it "shows bcc when populated", ->
it "enables bcc when populated", ->
useDraft.call @, bcc: [u2,u3]
makeComposer.call @
expect(@composer.state.showbcc).toBe true
expect(@composer.state.enabledFields).toContain Fields.Bcc
describe "when focus() is called", ->
describe "if a field name is provided", ->
it "should focus that field", ->
useDraft.call(@, cc: [u2])
makeComposer.call(@)
spyOn(@composer.refs['textFieldCc'], 'focus')
@composer.focus('textFieldCc')
advanceClock(1000)
expect(@composer.refs['textFieldCc'].focus).toHaveBeenCalled()
describe "when deciding whether or not to enable the from field", ->
it "disables if there's no draft", ->
useDraft.call @
makeComposer.call @
expect(@composer._shouldShowFromField()).toBe false
describe "if the draft is a forward", ->
it "should focus the to field", ->
useDraft.call(@, {subject: 'Fwd: This is a test'})
makeComposer.call(@)
spyOn(@composer.refs['textFieldTo'], 'focus')
@composer.focus()
advanceClock(1000)
expect(@composer.refs['textFieldTo'].focus).toHaveBeenCalled()
it "disables if there's 1 account item", ->
spyOn(AccountStore, 'items').andCallFake -> [{id: 1}]
useDraft.call @, replyToMessageId: null, files: []
makeComposer.call @
expect(@composer.state.enabledFields).not.toContain Fields.From
describe "if the draft is a normal message", ->
it "should focus on the body", ->
useDraft.call(@)
makeComposer.call(@)
spyOn(@composer.refs['contentBody'], 'focus')
@composer.focus()
advanceClock(1000)
expect(@composer.refs['contentBody'].focus).toHaveBeenCalled()
it "disables if it's a reply-to message", ->
spyOn(AccountStore, 'items').andCallFake -> [{id: 1}, {id: 2}]
useDraft.call @, replyToMessageId: "local-123", files: []
makeComposer.call @
expect(@composer.state.enabledFields).not.toContain Fields.From
describe "if the draft has not yet loaded", ->
it "should set _focusOnUpdate and focus after the next render", ->
@draft = new Message(draft: true, body: "")
proxy = draftStoreProxyStub(DRAFT_CLIENT_ID, @draft)
proxyResolve = null
spyOn(DraftStore, "sessionForClientId").andCallFake ->
new Promise (resolve, reject) ->
proxyResolve = resolve
it "disables if there are attached files", ->
spyOn(AccountStore, 'items').andCallFake -> [{id: 1}, {id: 2}]
useDraft.call @, replyToMessageId: null, files: [f1]
makeComposer.call @
expect(@composer.state.enabledFields).not.toContain Fields.From
makeComposer.call(@)
it "enables if requirements are met", ->
a1 = new Account()
a2 = new Account()
spyOn(AccountStore, 'items').andCallFake -> [a1, a2]
useDraft.call @, replyToMessageId: null, files: []
makeComposer.call @
expect(@composer.state.enabledFields).toContain Fields.From
spyOn(@composer.refs['contentBody'], 'focus')
@composer.focus()
advanceClock(1000)
expect(@composer.refs['contentBody'].focus).not.toHaveBeenCalled()
proxyResolve(proxy)
advanceClock(1000)
expect(@composer.refs['contentBody'].focus).toHaveBeenCalled()
describe "when emptying cc/bcc fields", ->
it "focuses on to when bcc is emptied and there's no cc field", ->
useDraft.call(@, bcc: [u1])
describe "when enabling fields", ->
it "always enables the To and Body fields on empty composers", ->
useDraft.apply @
makeComposer.call(@)
spyOn(@composer.refs['textFieldTo'], 'focus')
spyOn(@composer.refs['textFieldBcc'], 'focus')
expect(@composer.state.enabledFields).toContain Fields.To
expect(@composer.state.enabledFields).toContain Fields.Body
bcc = ReactTestUtils.scryRenderedComponentsWithTypeAndProps(@composer, ParticipantsTextField, field: "bcc")[0]
@draft.bcc = []
bcc.props.onEmptied()
expect(@composer.state.showbcc).toBe false
advanceClock(1000)
expect(@composer.refs['textFieldTo'].focus).toHaveBeenCalled()
expect(@composer.refs['textFieldCc']).not.toBeDefined()
expect(@composer.refs['textFieldBcc']).not.toBeDefined()
it "focuses on cc when bcc is emptied and cc field is available", ->
useDraft.call(@, cc: [u2], bcc: [u1])
it "always enables the To and Body fields on full composers", ->
useFullDraft.apply(@)
makeComposer.call(@)
spyOn(@composer.refs['textFieldTo'], 'focus')
spyOn(@composer.refs['textFieldCc'], 'focus')
spyOn(@composer.refs['textFieldBcc'], 'focus')
expect(@composer.state.enabledFields).toContain Fields.To
expect(@composer.state.enabledFields).toContain Fields.Body
bcc = ReactTestUtils.scryRenderedComponentsWithTypeAndProps(@composer, ParticipantsTextField, field: "bcc")[0]
@draft.bcc = []
bcc.props.onEmptied()
expect(@composer.state.showbcc).toBe false
advanceClock(1000)
expect(@composer.refs['textFieldTo'].focus).not.toHaveBeenCalled()
expect(@composer.refs['textFieldCc'].focus).toHaveBeenCalled()
expect(@composer.refs['textFieldBcc']).not.toBeDefined()
it "focuses on to when cc is emptied", ->
useDraft.call(@, cc: [u1], bcc: [u2])
describe "applying the focused field", ->
beforeEach ->
useFullDraft.apply(@)
makeComposer.call(@)
spyOn(@composer.refs['textFieldTo'], 'focus')
spyOn(@composer.refs['textFieldCc'], 'focus')
spyOn(@composer.refs['textFieldBcc'], 'focus')
@composer.setState focusedField: Fields.Cc
@body = @composer.refs[Fields.Body]
spyOn(@body, "focus")
spyOn(React, "findDOMNode").andCallThrough()
cc = ReactTestUtils.scryRenderedComponentsWithTypeAndProps(@composer, ParticipantsTextField, field: "cc")[0]
@draft.cc = []
cc.props.onEmptied()
expect(@composer.state.showcc).toBe false
advanceClock(1000)
expect(@composer.refs['textFieldTo'].focus).toHaveBeenCalled()
expect(@composer.refs['textFieldCc']).not.toBeDefined()
expect(@composer.refs['textFieldBcc'].focus).not.toHaveBeenCalled()
it "can focus on the subject", ->
@composer.setState focusedField: Fields.Subject
expect(React.findDOMNode).toHaveBeenCalled()
expect(React.findDOMNode.calls.length).toBe 3
it "can focus on the body", ->
@composer.setState focusedField: Fields.Body
expect(@body.focus).toHaveBeenCalled()
expect(@body.focus.calls.length).toBe 1
it "ignores focuses to participant fields", ->
@composer.setState focusedField: Fields.To
expect(@body.focus).not.toHaveBeenCalled()
expect(React.findDOMNode.calls.length).toBe 1
describe "when participants are added during a draft update", ->
it "shows the cc fields and bcc fields to ensure participants are never hidden", ->
useDraft.call(@, cc: [], bcc: [])
makeComposer.call(@)
expect(@composer.state.showbcc).toBe(false)
expect(@composer.state.showcc).toBe(false)
expect(@composer.state.enabledFields).not.toContain Fields.Bcc
expect(@composer.state.enabledFields).not.toContain Fields.Cc
# Simulate a change event fired by the DraftStoreProxy
@draft.cc = [u1]
@composer._onDraftChanged()
expect(@composer.state.showbcc).toBe(false)
expect(@composer.state.showcc).toBe(true)
expect(@composer.state.enabledFields).not.toContain Fields.Bcc
expect(@composer.state.enabledFields).toContain Fields.Cc
# Simulate a change event fired by the DraftStoreProxy
@draft.bcc = [u2]
@composer._onDraftChanged()
expect(@composer.state.showbcc).toBe(true)
expect(@composer.state.showcc).toBe(true)
expect(@composer.state.enabledFields).toContain Fields.Bcc
expect(@composer.state.enabledFields).toContain Fields.Cc
describe "When sending a message", ->
beforeEach ->
@ -459,7 +444,7 @@ describe "populated composer", ->
to: [u1]
subject: "Hello World"
body: ""
files: [file]
files: [f1]
makeComposer.call(@)
@composer._sendDraft()
expect(Actions.sendDraft).toHaveBeenCalled()
@ -509,7 +494,7 @@ describe "populated composer", ->
subject: "Subject"
to: [u1]
body: "Check out attached file"
files: [file]
files: [f1]
makeComposer.call(@); @composer._sendDraft()
expect(Actions.sendDraft).toHaveBeenCalled()
expect(@dialog.showMessageBox).not.toHaveBeenCalled()
@ -542,26 +527,23 @@ describe "populated composer", ->
beforeEach ->
useFullDraft.apply(@)
makeComposer.call(@)
spyOn(@composer, "_sendDraft")
NylasTestUtils.loadKeymap("internal_packages/composer/keymaps/composer")
it "sends the draft on cmd-enter", ->
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@composer))
expect(@composer._sendDraft).toHaveBeenCalled()
expect(Actions.sendDraft).toHaveBeenCalled()
it "does not send the draft on enter if the button isn't in focus", ->
NylasTestUtils.keyPress("enter", React.findDOMNode(@composer))
expect(@composer._sendDraft).not.toHaveBeenCalled()
it "sends the draft on enter when the button is in focus", ->
sendBtn = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, "btn-send")
NylasTestUtils.keyPress("enter", React.findDOMNode(sendBtn))
expect(@composer._sendDraft).toHaveBeenCalled()
expect(Actions.sendDraft).not.toHaveBeenCalled()
it "doesn't let you send twice", ->
sendBtn = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, "btn-send")
NylasTestUtils.keyPress("enter", React.findDOMNode(sendBtn))
expect(@composer._sendDraft).toHaveBeenCalled()
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@composer))
@isSending.state = true
DraftStore.trigger()
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@composer))
expect(Actions.sendDraft).toHaveBeenCalled()
expect(Actions.sendDraft.calls.length).toBe 1
describe "drag and drop", ->
beforeEach ->
@ -569,7 +551,7 @@ describe "populated composer", ->
to: [u1]
subject: "Hello World"
body: ""
files: [file]
files: [f1]
makeComposer.call(@)
describe "_shouldAcceptDrop", ->

View file

@ -0,0 +1,108 @@
_ = require "underscore"
React = require "react/addons"
Fields = require '../lib/fields'
ReactTestUtils = React.addons.TestUtils
AccountContactField = require '../lib/account-contact-field'
ExpandedParticipants = require '../lib/expanded-participants'
describe "ExpandedParticipants", ->
makeField = (props={}) ->
@onChangeParticipants = jasmine.createSpy("onChangeParticipants")
@onChangeEnabledFields = jasmine.createSpy("onChangeEnabledFields")
props.onChangeParticipants = @onChangeParticipants
props.onChangeEnabledFields = @onChangeEnabledFields
@fields = ReactTestUtils.renderIntoDocument(
<ExpandedParticipants {...props} />
)
it "always renders to field", ->
makeField.call(@)
el = ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "to-field")
expect(el).toBeDefined()
it "renders cc when enabled", ->
makeField.call(@, enabledFields: [Fields.Cc])
el = ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "cc-field")
expect(el).toBeDefined()
it "renders bcc when enabled", ->
makeField.call(@, enabledFields: [Fields.Bcc])
el = ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "bcc-field")
expect(el).toBeDefined()
it "renders from when enabled", ->
makeField.call(@, enabledFields: [Fields.From])
el = ReactTestUtils.findRenderedComponentWithType(@fields, AccountContactField)
expect(el).toBeDefined()
it "renders all 'show' fields", ->
makeField.call(@)
showCc = ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "show-cc")
showBcc = ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "show-bcc")
showSubject = ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "show-subject")
expect(showCc).toBeDefined()
expect(showBcc).toBeDefined()
expect(showSubject).toBeDefined()
it "hides show cc if it's enabled", ->
makeField.call(@, enabledFields: [Fields.Cc])
els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@fields, "show-cc")
expect(els.length).toBe 0
it "hides show bcc if it's enabled", ->
makeField.call(@, enabledFields: [Fields.Bcc])
els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@fields, "show-bcc")
expect(els.length).toBe 0
it "hides show subject if it's enabled", ->
makeField.call(@, enabledFields: [Fields.Subject])
els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@fields, "show-subject")
expect(els.length).toBe 0
it "renders popout composer in the inline mode", ->
makeField.call(@, mode: "inline")
els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@fields, "show-popout")
expect(els.length).toBe 1
it "doesn't render popout composer in the fullwindow mode", ->
makeField.call(@, mode: "fullwindow")
els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@fields, "show-popout")
expect(els.length).toBe 0
it "shows and focuses cc when clicked", ->
makeField.call(@)
el = ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "show-cc")
ReactTestUtils.Simulate.click(React.findDOMNode(el))
expect(@onChangeEnabledFields).toHaveBeenCalledWith show: [Fields.Cc], focus: Fields.Cc
it "shows and focuses bcc when clicked", ->
makeField.call(@)
el = ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "show-bcc")
ReactTestUtils.Simulate.click(React.findDOMNode(el))
expect(@onChangeEnabledFields).toHaveBeenCalledWith show: [Fields.Bcc], focus: Fields.Bcc
it "shows subject when clicked", ->
makeField.call(@)
el = ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "show-subject")
ReactTestUtils.Simulate.click(React.findDOMNode(el))
expect(@onChangeEnabledFields).toHaveBeenCalledWith show: [Fields.Subject], focus: Fields.Subject
it "empties cc and focuses on to field", ->
makeField.call(@, enabledFields: [Fields.Cc, Fields.Bcc, Fields.Subject])
@fields.refs[Fields.Cc].props.onEmptied()
expect(@onChangeEnabledFields).toHaveBeenCalledWith hide: [Fields.Cc], focus: Fields.To
it "empties bcc and focuses on to field", ->
makeField.call(@, enabledFields: [Fields.Cc, Fields.Bcc, Fields.Subject])
@fields.refs[Fields.Bcc].props.onEmptied()
expect(@onChangeEnabledFields).toHaveBeenCalledWith hide: [Fields.Bcc], focus: Fields.Cc
it "empties bcc and focuses on cc field", ->
makeField.call(@, enabledFields: [Fields.Bcc, Fields.Subject])
@fields.refs[Fields.Bcc].props.onEmptied()
expect(@onChangeEnabledFields).toHaveBeenCalledWith hide: [Fields.Bcc], focus: Fields.To
it "notifies when participants change", ->
makeField.call(@, enabledFields: [Fields.Cc, Fields.Bcc, Fields.Subject])
@fields.refs[Fields.Cc].props.change()
expect(@onChangeParticipants).toHaveBeenCalled()

View file

@ -77,7 +77,7 @@
width: 100%;
max-width: @compose-width;
margin: 0 auto;
margin-top:@spacing-standard;
padding-top:@spacing-standard;
}
.text-actions {
text-align: right;
@ -125,6 +125,49 @@
display: block;
}
.collapsed-composer-participants {
position: relative;
margin: 0 23px;
border-bottom: 1px solid @border-color-divider;
flex-shrink:0;
color: @text-color-very-subtle;
padding: 16px 0 10px 0;
.collapsed-contact {
padding-right: 0.25em;
&:after {
content: ","
}
&:last-child:after {
content: ""
}
}
.num-remaining.token {
color: @text-color;
color: rgba(0,0,0,0.6);
padding-right: 12px;
margin-left: 0;
padding-top: 6px;
padding-bottom: 6px;
}
.num-remaining-wrap {
position: absolute;
right: 0;
z-index: 2;
top: 9px;
.show-more-fade {
position: absolute;
width: 220px;
height: 37px;
right: 0;
top: 0;
background: linear-gradient(to right, fade(@background-primary, 0%) 0%, fade(@background-primary, 100%) 40%);
}
}
}
.compose-subject-wrap {
position: relative;
margin: 0 23px;
@ -149,6 +192,10 @@
.compose-body-scroll {
position:initial;
.scroll-region-content .scroll-region-content-inner {
min-height: 100%;
display: flex;
}
}
.compose-body {
@ -275,7 +322,7 @@
margin: 0 8+@spacing-standard;
flex-shrink: 0;
border-bottom: 1px solid @border-color-divider;
min-height: 48px;
min-height: 49px;
.button-dropdown {
margin-left: 10px;