fix(composer): don't blur when adding a link

Summary:
Refactor focusing behavior in floating toolbar controller

Fixes T3781
Fixes T3791

Test Plan: manual

Reviewers: bengotow

Reviewed By: bengotow

Maniphest Tasks: T3791, T3781

Differential Revision: https://phab.nylas.com/D2073
This commit is contained in:
Evan Morikawa 2015-09-25 16:14:01 -04:00
parent fc83f3cfab
commit 8760de1b0e
5 changed files with 60 additions and 31 deletions

View file

@ -153,7 +153,7 @@ class ComposerView extends React.Component
render: => render: =>
if @props.mode is "inline" if @props.mode is "inline"
<FocusTrackingRegion className={@_wrapClasses()} onFocus={@focus} tabIndex="-1"> <FocusTrackingRegion className={@_wrapClasses()} tabIndex="-1">
{@_renderComposer()} {@_renderComposer()}
</FocusTrackingRegion> </FocusTrackingRegion>
else else

View file

@ -230,14 +230,23 @@ class ContenteditableComponent extends React.Component
else else
document.execCommand("outdent") document.execCommand("outdent")
# The native document.execCommand('outdent')
_outdent: ->
_closestAtCursor: (selector) -> _closestAtCursor: (selector) ->
selection = document.getSelection() selection = document.getSelection()
return unless selection?.isCollapsed return unless selection?.isCollapsed
return selection.anchorNode?.closest(selector) return @_closest(selection.anchorNode, selector)
# https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
# Only Elements (not Text nodes) have the `closest` method
_closest: (node, selector) ->
el = if node instanceof HTMLElement then node else node.parentElement
return el.closest(selector)
_replaceFirstListItem: (li, replaceWith) -> _replaceFirstListItem: (li, replaceWith) ->
@_teardownSelectionListeners() @_teardownSelectionListeners()
list = li.closest("ul, ol") list = @_closest(li, "ul, ol")
if replaceWith.length is 0 if replaceWith.length is 0
replaceWith = replaceWith.replace /\s/g, "&nbsp;" replaceWith = replaceWith.replace /\s/g, "&nbsp;"
@ -269,19 +278,19 @@ class ContenteditableComponent extends React.Component
event.preventDefault() event.preventDefault()
selection = document.getSelection() selection = document.getSelection()
if selection?.isCollapsed if selection?.isCollapsed
# https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
if selection.anchorNode instanceof HTMLElement
anchorElement = selection.anchorNode
else
anchorElement = selection.anchorNode.parentElement
# Only Elements (not Text nodes) have the `closest` method # Only Elements (not Text nodes) have the `closest` method
if anchorElement.closest("li") li = @_closestAtCursor("li")
if li
if event.shiftKey if event.shiftKey
list = @_closestAtCursor("ul, ol")
# BUG: As of 9/25/15 if you outdent the first item in a list, it
# doesn't work :(
if list.querySelectorAll('li')?[0] is li # We're in first li
@_replaceFirstListItem(li, li.innerHTML)
else
document.execCommand("outdent") document.execCommand("outdent")
else else
document.execCommand("indent") document.execCommand("indent")
return
else if event.shiftKey else if event.shiftKey
if @_atTabChar() if @_atTabChar()
@_removeLastCharacter() @_removeLastCharacter()
@ -312,7 +321,7 @@ class ContenteditableComponent extends React.Component
return false if not selection.isCollapsed return false if not selection.isCollapsed
return true if anchor?.nodeName is "LI" return true if anchor?.nodeName is "LI"
return false if selection.anchorOffset > 0 return false if selection.anchorOffset > 0
li = anchor.closest("li") li = @_closest(anchor, "li")
return unless li return unless li
return DOMUtils.isFirstChild(li, anchor) return DOMUtils.isFirstChild(li, anchor)
@ -449,8 +458,6 @@ class ContenteditableComponent extends React.Component
_onBlur: (event) => _onBlur: (event) =>
@setInnerState dragging: false @setInnerState dragging: false
# The delay here is necessary to see if the blur was caused by us
# navigating to the toolbar and focusing on the set-url input.
return if @_editableNode().parentElement.contains event.relatedTarget return if @_editableNode().parentElement.contains event.relatedTarget
@setInnerState editableFocused: false @setInnerState editableFocused: false
@ -848,6 +855,8 @@ class ContenteditableComponent extends React.Component
else @_execCommand ["createLink", false, url] else @_execCommand ["createLink", false, url]
@_restoreSelection(force: true, collapse: "end") @_restoreSelection(force: true, collapse: "end")
return
_execCommand: (commandArgs=[], selectionRange={}) => _execCommand: (commandArgs=[], selectionRange={}) =>
{anchorNode, anchorOffset, focusNode, focusOffset} = selectionRange {anchorNode, anchorOffset, focusNode, focusOffset} = selectionRange
@_teardownSelectionListeners() @_teardownSelectionListeners()

View file

@ -91,20 +91,30 @@ class FloatingToolbarContainer extends React.Component
onDomMutator={@props.onDomMutator} onDomMutator={@props.onDomMutator}
linkToModify={@state.linkToModify} linkToModify={@state.linkToModify}
onChangeFocus={@_onChangeFocus} onChangeFocus={@_onChangeFocus}
editAreaWidth={@state.editAreaWidth}
contentPadding={@CONTENT_PADDING} contentPadding={@CONTENT_PADDING}
editAreaWidth={@state.editAreaWidth} /> onDoneWithLink={@_onDoneWithLink}
onClickLinkEditBtn={@_onClickLinkEditBtn} />
_onChangeFocus: (focus) => # Called when a user clicks the "link" icon on the FloatingToolbar
@componentWillReceiveInnerProps toolbarFocus: focus _onClickLinkEditBtn: =>
@setState toolbarMode: "edit-link"
_onChangeMode: (mode) => # A user could be done with a link because they're setting a new one, or
if mode is "buttons" # clearing one, or just canceling.
_onDoneWithLink: =>
@componentWillReceiveInnerProps linkHoveringOver: null @componentWillReceiveInnerProps linkHoveringOver: null
@setState @setState
toolbarMode: mode toolbarMode: "buttons"
toolbarVisible: false toolbarVisible: false
else return
@setState toolbarMode: mode
# We explicitly control the focus of the FloatingToolbar because we can
# do things like switch from "buttons" mode to "edit-link" mode (which
# natively fires focus change events) but not want to signify a "focus"
# change
_onChangeFocus: (focus) =>
@componentWillReceiveInnerProps toolbarFocus: focus
# We want the toolbar's state to be declaratively defined from other # We want the toolbar's state to be declaratively defined from other
# states. # states.

View file

@ -86,7 +86,7 @@ class FloatingToolbar extends React.Component
onClick={@_execCommand} onClick={@_execCommand}
data-command-name="underline"></button> data-command-name="underline"></button>
<button className="btn btn-link toolbar-btn" <button className="btn btn-link toolbar-btn"
onClick={@_showLink} onClick={@props.onClickLinkEditBtn}
data-command-name="link"></button> data-command-name="link"></button>
{@_toolbarExtensions()} {@_toolbarExtensions()}
</div> </div>
@ -164,7 +164,7 @@ class FloatingToolbar extends React.Component
_removeUrl: => _removeUrl: =>
@setState urlInputValue: "" @setState urlInputValue: ""
@props.onSaveUrl "", @props.linkToModify @props.onSaveUrl "", @props.linkToModify
@props.onChangeMode("buttons") @props.onDoneWithLink()
_onFocus: => _onFocus: =>
@props.onChangeFocus(true) @props.onChangeFocus(true)
@ -188,7 +188,7 @@ class FloatingToolbar extends React.Component
_saveUrl: => _saveUrl: =>
if (@state.urlInputValue ? "").trim().length > 0 if (@state.urlInputValue ? "").trim().length > 0
@props.onSaveUrl @state.urlInputValue, @props.linkToModify @props.onSaveUrl @state.urlInputValue, @props.linkToModify
@props.onChangeMode("buttons") @props.onDoneWithLink()
_execCommand: (event) => _execCommand: (event) =>
cmd = event.currentTarget.getAttribute 'data-command-name' cmd = event.currentTarget.getAttribute 'data-command-name'
@ -239,7 +239,4 @@ class FloatingToolbar extends React.Component
else else
return TOOLBAR_BUTTONS_WIDTH return TOOLBAR_BUTTONS_WIDTH
_showLink: =>
@props.onChangeMode("edit-link")
module.exports = FloatingToolbar module.exports = FloatingToolbar

View file

@ -24,6 +24,19 @@ class FocusTrackingRegion extends React.Component
@_goingout = true @_goingout = true
setTimeout => setTimeout =>
return unless @_goingout return unless @_goingout
# If we're unmounted the `@_goingout` flag will catch the unmount
# @_goingout is set to true when we umount
#
# It's posible for a focusout event to fire from within a region
# that we're actually focsued on.
#
# This happens when component that used to have the focus is
# unmounted. An example is the url input field of the
# FloatingToolbar in the Composer's Contenteditable
el = React.findDOMNode(@)
return if el.contains document.activeElement
# This prevents the strange effect of an input appearing to have focus # This prevents the strange effect of an input appearing to have focus
# when the element receiving focus does not support selection (like a # when the element receiving focus does not support selection (like a
# div with tabIndex=-1) # div with tabIndex=-1)