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: =>
if @props.mode is "inline"
<FocusTrackingRegion className={@_wrapClasses()} onFocus={@focus} tabIndex="-1">
<FocusTrackingRegion className={@_wrapClasses()} tabIndex="-1">
{@_renderComposer()}
</FocusTrackingRegion>
else

View file

@ -230,14 +230,23 @@ class ContenteditableComponent extends React.Component
else
document.execCommand("outdent")
# The native document.execCommand('outdent')
_outdent: ->
_closestAtCursor: (selector) ->
selection = document.getSelection()
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) ->
@_teardownSelectionListeners()
list = li.closest("ul, ol")
list = @_closest(li, "ul, ol")
if replaceWith.length is 0
replaceWith = replaceWith.replace /\s/g, "&nbsp;"
@ -269,19 +278,19 @@ class ContenteditableComponent extends React.Component
event.preventDefault()
selection = document.getSelection()
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
if anchorElement.closest("li")
li = @_closestAtCursor("li")
if li
if event.shiftKey
document.execCommand("outdent")
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")
else
document.execCommand("indent")
return
else if event.shiftKey
if @_atTabChar()
@_removeLastCharacter()
@ -312,7 +321,7 @@ class ContenteditableComponent extends React.Component
return false if not selection.isCollapsed
return true if anchor?.nodeName is "LI"
return false if selection.anchorOffset > 0
li = anchor.closest("li")
li = @_closest(anchor, "li")
return unless li
return DOMUtils.isFirstChild(li, anchor)
@ -449,8 +458,6 @@ class ContenteditableComponent extends React.Component
_onBlur: (event) =>
@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
@setInnerState editableFocused: false
@ -848,6 +855,8 @@ class ContenteditableComponent extends React.Component
else @_execCommand ["createLink", false, url]
@_restoreSelection(force: true, collapse: "end")
return
_execCommand: (commandArgs=[], selectionRange={}) =>
{anchorNode, anchorOffset, focusNode, focusOffset} = selectionRange
@_teardownSelectionListeners()

View file

@ -91,21 +91,31 @@ class FloatingToolbarContainer extends React.Component
onDomMutator={@props.onDomMutator}
linkToModify={@state.linkToModify}
onChangeFocus={@_onChangeFocus}
editAreaWidth={@state.editAreaWidth}
contentPadding={@CONTENT_PADDING}
editAreaWidth={@state.editAreaWidth} />
onDoneWithLink={@_onDoneWithLink}
onClickLinkEditBtn={@_onClickLinkEditBtn} />
# Called when a user clicks the "link" icon on the FloatingToolbar
_onClickLinkEditBtn: =>
@setState toolbarMode: "edit-link"
# A user could be done with a link because they're setting a new one, or
# clearing one, or just canceling.
_onDoneWithLink: =>
@componentWillReceiveInnerProps linkHoveringOver: null
@setState
toolbarMode: "buttons"
toolbarVisible: false
return
# 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
_onChangeMode: (mode) =>
if mode is "buttons"
@componentWillReceiveInnerProps linkHoveringOver: null
@setState
toolbarMode: mode
toolbarVisible: false
else
@setState toolbarMode: mode
# We want the toolbar's state to be declaratively defined from other
# states.
_setToolbarState: =>

View file

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

View file

@ -24,6 +24,19 @@ class FocusTrackingRegion extends React.Component
@_goingout = true
setTimeout =>
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
# when the element receiving focus does not support selection (like a
# div with tabIndex=-1)