diff --git a/exports/nylas-component-kit.coffee b/exports/nylas-component-kit.coffee
index f975800c8..b0f0a1feb 100644
--- a/exports/nylas-component-kit.coffee
+++ b/exports/nylas-component-kit.coffee
@@ -16,6 +16,7 @@ module.exports =
MultiselectList: require '../src/components/multiselect-list'
MultiselectActionBar: require '../src/components/multiselect-action-bar'
ResizableRegion: require '../src/components/resizable-region'
+ ScrollRegion: require '../src/components/scroll-region'
InjectedComponentSet: require '../src/components/injected-component-set'
InjectedComponent: require '../src/components/injected-component'
TokenizingTextField: require '../src/components/tokenizing-text-field'
diff --git a/internal_packages/account-sidebar/lib/account-sidebar.cjsx b/internal_packages/account-sidebar/lib/account-sidebar.cjsx
index ba1e38d63..7581db6fb 100644
--- a/internal_packages/account-sidebar/lib/account-sidebar.cjsx
+++ b/internal_packages/account-sidebar/lib/account-sidebar.cjsx
@@ -1,5 +1,6 @@
React = require 'react'
{Actions} = require("nylas-exports")
+{ScrollRegion} = require("nylas-component-kit")
SidebarDividerItem = require("./account-sidebar-divider-item")
SidebarTagItem = require("./account-sidebar-tag-item")
SidebarSheetItem = require("./account-sidebar-sheet-item")
@@ -26,11 +27,11 @@ class AccountSidebar extends React.Component
@unsubscribe() if @unsubscribe
render: =>
-
+
_sections: =>
return @state.sections.map (section) =>
diff --git a/internal_packages/account-sidebar/stylesheets/account-sidebar.less b/internal_packages/account-sidebar/stylesheets/account-sidebar.less
index 7e32e6be4..4b5161332 100644
--- a/internal_packages/account-sidebar/stylesheets/account-sidebar.less
+++ b/internal_packages/account-sidebar/stylesheets/account-sidebar.less
@@ -4,8 +4,6 @@
#account-sidebar {
order: 1;
height: 100%;
- overflow-y: auto;
- overflow-x: hidden;
background-color: @source-list-bg;
section {
diff --git a/internal_packages/message-list/lib/message-list.cjsx b/internal_packages/message-list/lib/message-list.cjsx
index 187d4007e..27115c9b8 100755
--- a/internal_packages/message-list/lib/message-list.cjsx
+++ b/internal_packages/message-list/lib/message-list.cjsx
@@ -4,11 +4,46 @@ classNames = require 'classnames'
MessageItem = require "./message-item"
{Utils, Actions, MessageStore, ComponentRegistry} = require("nylas-exports")
{Spinner,
+ ScrollRegion,
ResizableRegion,
RetinaImg,
InjectedComponentSet,
InjectedComponent} = require('nylas-component-kit')
+class MessageListScrollTooltip extends React.Component
+ @displayName: 'MessageListScrollTooltip'
+ @propTypes:
+ viewportCenter: React.PropTypes.number.isRequired
+ totalHeight: React.PropTypes.number.isRequired
+
+ componentWillMount: =>
+ @setupForProps(@props)
+
+ componentWillReceiveProps: (newProps) =>
+ @setupForProps(newProps)
+
+ shouldComponentUpdate: (newProps, newState) =>
+ not _.isEqual(@state,newState)
+
+ setupForProps: (props) ->
+ # Technically, we could have MessageList provide the currently visible
+ # item index, but the DOM approach is simple and self-contained.
+ #
+ els = document.querySelectorAll('.message-item-wrap')
+ idx = _.findIndex els, (el) -> el.offsetTop > props.viewportCenter
+ if idx is -1
+ idx = els.length
+
+ @setState
+ idx: idx
+ count: els.length
+
+ render: ->
+
+ {@state.idx} of {@state.count}
+
+
+
class MessageList extends React.Component
@displayName: 'MessageList'
@containerRequired: false
@@ -74,22 +109,23 @@ class MessageList extends React.Component
"ready": @state.ready
-
-
-
-
-
+
+
+
+
{@_messageComponents()}
-
+
{@_renderReplyArea()}
diff --git a/internal_packages/thread-list/lib/thread-list.cjsx b/internal_packages/thread-list/lib/thread-list.cjsx
index 9b89d408a..7124fc735 100644
--- a/internal_packages/thread-list/lib/thread-list.cjsx
+++ b/internal_packages/thread-list/lib/thread-list.cjsx
@@ -12,6 +12,32 @@ classNames = require 'classnames'
ThreadListParticipants = require './thread-list-participants'
ThreadListStore = require './thread-list-store'
+class ThreadListScrollTooltip extends React.Component
+ @displayName: 'ThreadListScrollTooltip'
+ @propTypes:
+ viewportCenter: React.PropTypes.number.isRequired
+ totalHeight: React.PropTypes.number.isRequired
+
+ componentWillMount: =>
+ @setupForProps(@props)
+
+ componentWillReceiveProps: (newProps) =>
+ @setupForProps(newProps)
+
+ shouldComponentUpdate: (newProps, newState) =>
+ @state?.idx isnt newState.idx
+
+ setupForProps: (props) ->
+ idx = Math.floor(ThreadListStore.view().count() / @props.totalHeight * @props.viewportCenter)
+ @setState
+ idx: idx
+ item: ThreadListStore.view().get(idx)
+
+ render: ->
+
+ {timestamp(@state.item?.lastMessageTimestamp)}
+
+
class ThreadList extends React.Component
@displayName: 'ThreadList'
@@ -99,6 +125,7 @@ class ThreadList extends React.Component
commands={@commands}
itemPropsProvider={@itemPropsProvider}
className="thread-list"
+ scrollTooltipComponent={ThreadListScrollTooltip}
collection="thread" />
# Additional Commands
diff --git a/internal_packages/thread-list/stylesheets/thread-list.less b/internal_packages/thread-list/stylesheets/thread-list.less
index d1b244e0f..0a2491bbd 100644
--- a/internal_packages/thread-list/stylesheets/thread-list.less
+++ b/internal_packages/thread-list/stylesheets/thread-list.less
@@ -8,7 +8,9 @@
.thread-list, .draft-list {
order: 3;
flex: 1;
- position:relative;
+ position:absolute;
+ width:100%;
+ height:100%;
-webkit-font-smoothing: subpixel-antialiased;
.list-item {
diff --git a/src/components/list-tabular.cjsx b/src/components/list-tabular.cjsx
index c78bd00e3..a21bb7318 100644
--- a/src/components/list-tabular.cjsx
+++ b/src/components/list-tabular.cjsx
@@ -1,5 +1,6 @@
_ = require 'underscore'
React = require 'react/addons'
+ScrollRegion = require './scroll-region'
RangeChunkSize = 10
@@ -85,14 +86,11 @@ class ListTabular extends React.Component
# If our view has been swapped out for an entirely different one,
# reset our scroll position to the top.
if prevProps.dataView isnt @props.dataView
- container = React.findDOMNode(@refs.container)
- container.scrollTop = 0
+ @refs.container.scrollTop = 0
@updateRangeState()
updateScrollState: =>
window.requestAnimationFrame =>
- container = React.findDOMNode(@refs.container)
-
# Create an event that fires when we stop receiving scroll events.
# There is no "scrollend" event, but we really need one.
clearTimeout(@_scrollTimer) if @_scrollTimer
@@ -104,7 +102,7 @@ class ListTabular extends React.Component
# If we've shifted enough pixels from our previous scrollTop to require
# new rows to be rendered, update our state!
- if Math.abs(@state.scrollTop - container.scrollTop) >= @_rowHeight() * RangeChunkSize
+ if Math.abs(@state.scrollTop - @refs.container.scrollTop) >= @_rowHeight() * RangeChunkSize
@updateRangeState()
onDoneReceivingScrollEvents: =>
@@ -113,8 +111,7 @@ class ListTabular extends React.Component
@updateRangeState()
updateRangeState: =>
- container = @refs.container
- scrollTop = React.findDOMNode(container)?.scrollTop
+ scrollTop = @refs.container.scrollTop
rowHeight = @_rowHeight()
@@ -159,12 +156,12 @@ class ListTabular extends React.Component
height: @props.dataView.count() * @_rowHeight()
pointerEvents: if @state.scrollInProgress then 'none' else 'auto'
-
+
{@_headers()}
{@_rows()}
-
+
_rowHeight: =>
39
diff --git a/src/components/multiselect-list.cjsx b/src/components/multiselect-list.cjsx
index f87b7ec5c..35e756d42 100644
--- a/src/components/multiselect-list.cjsx
+++ b/src/components/multiselect-list.cjsx
@@ -32,6 +32,7 @@ class MultiselectList extends React.Component
columns: React.PropTypes.array.isRequired
dataStore: React.PropTypes.object.isRequired
itemPropsProvider: React.PropTypes.func.isRequired
+ scrollTooltipComponent: React.PropTypes.func
constructor: (@props) ->
@state = @_getStateFromStores()
@@ -120,6 +121,7 @@ class MultiselectList extends React.Component
+ @state =
+ totalHeight:0
+ viewportHeight: 0
+ viewportOffset: 0
+ dragging: false
+ scrolling: false
+
+ Object.defineProperty(@, 'scrollTop', {
+ get: -> React.findDOMNode(@refs.content).scrollTop
+ set: (val) -> React.findDOMNode(@refs.content).scrollTop = val
+ })
+
+ componentDidMount: =>
+ @_recomputeDimensions()
+
+ componentDidUpdate: =>
+ @_recomputeDimensions()
+
+ componentWillUnmount: =>
+ @_onHandleUp()
+
+ shouldComponentUpdate: (newProps, newState) =>
+ not Utils.isEqualReact(newProps, @props) or not Utils.isEqualReact(newState, @state)
+
+ render: =>
+ containerClasses = "#{@props.className ? ''} " + classNames
+ 'scroll-region': true
+ 'dragging': @state.dragging
+ 'scrolling': @state.scrolling
+
+ otherProps = _.omit(@props, _.keys(@constructor.propTypes))
+
+ tooltip = []
+ if @props.scrollTooltipComponent
+ tooltip = <@props.scrollTooltipComponent viewportCenter={@state.viewportOffset + @state.viewportHeight / 2} totalHeight={@state.totalHeight} />
+
+
+
+
+ {@props.children}
+
+
+
+ _scrollbarWrapStyles: =>
+ position:'absolute'
+ top: 0
+ bottom: 0
+ right: 0
+ zIndex: 2
+
+ _scrollbarHandleStyles: =>
+ handleHeight = @_getHandleHeight()
+ handleTop = (@state.viewportOffset / (@state.totalHeight - @state.viewportHeight)) * (@state.trackHeight - handleHeight)
+
+ position:'relative'
+ height: handleHeight
+ top: handleTop
+
+ _getHandleHeight: =>
+ Math.min(@state.totalHeight, Math.max(40, (@state.trackHeight / @state.totalHeight) * @state.trackHeight))
+
+ _recomputeDimensions: =>
+ return unless @refs.content
+
+ contentNode = React.findDOMNode(@refs.content)
+ trackNode = React.findDOMNode(@refs.track)
+
+ totalHeight = contentNode.scrollHeight
+ trackHeight = trackNode.clientHeight
+ viewportHeight = contentNode.clientHeight
+ viewportOffset = contentNode.scrollTop
+
+ if @state.totalHeight != totalHeight or
+ @state.trackHeight != trackHeight or
+ @state.viewportOffset != viewportOffset or
+ @state.viewportHeight != viewportHeight
+ @setState({totalHeight, trackHeight, viewportOffset, viewportHeight})
+
+ _onHandleDown: (event) =>
+ handleNode = React.findDOMNode(@refs.handle)
+ @_trackOffset = React.findDOMNode(@refs.track).getBoundingClientRect().top
+ @_mouseOffsetWithinHandle = event.pageY - handleNode.getBoundingClientRect().top
+ window.addEventListener("mousemove", @_onHandleMove)
+ window.addEventListener("mouseup", @_onHandleUp)
+ @setState(dragging: true)
+
+ _onHandleMove: (event) =>
+ trackY = event.pageY - @_trackOffset - @_mouseOffsetWithinHandle
+ trackPxToViewportPx = (@state.totalHeight - @state.viewportHeight) / (@state.trackHeight - @_getHandleHeight())
+
+ contentNode = React.findDOMNode(@refs.content)
+ contentNode.scrollTop = trackY * trackPxToViewportPx
+
+ _onHandleUp: (event) =>
+ window.removeEventListener("mousemove", @_onHandleMove)
+ window.removeEventListener("mouseup", @_onHandleUp)
+ @setState(dragging: false)
+
+ _onHandleClick: (event) =>
+ # Avoid event propogating up to track
+ event.stopPropagation()
+
+ _onScrollJump: (event) =>
+ @_mouseOffsetWithinHandle = @_getHandleHeight() / 2
+ @_onHandleMove(event)
+
+ _onScroll: (event) =>
+ @_recomputeDimensions()
+ @props.onScroll?(event)
+
+ if not @state.scrolling
+ @setState(scrolling: true)
+
+ @_onStoppedScroll ?= _.debounce =>
+ @setState(scrolling: false)
+ , 250
+ @_onStoppedScroll()
+
+
+module.exports = ScrollRegion
diff --git a/static/components/list-tabular.less b/static/components/list-tabular.less
index 0e514b779..7c52f08c5 100644
--- a/static/components/list-tabular.less
+++ b/static/components/list-tabular.less
@@ -71,11 +71,6 @@
.list-container {
- position: absolute;
- height: 100%;
- width: 100%;
- overflow-y: scroll;
-
.list-item {
font-size: @font-size-base;
line-height: @line-height-computed;
@@ -107,6 +102,7 @@
.list-tabular {
flex: 1;
width: 100%;
+ height: 100%;
.list-tabular-item {
position: relative;
diff --git a/static/components/scroll-region.less b/static/components/scroll-region.less
new file mode 100644
index 000000000..be5f6c8bd
--- /dev/null
+++ b/static/components/scroll-region.less
@@ -0,0 +1,104 @@
+@import "ui-variables";
+
+@tooltipBorderColor: rgba(54, 56, 57, 0.9);
+@tooltipBackground: -webkit-gradient(linear, left top, left bottom, from(rgba(99, 102, 103, 0.9)), to(rgba(82, 85, 86, 0.9)));
+
+.scroll-tooltip {
+ background: @tooltipBackground;
+ color: white;
+ box-shadow: 0 2px 7px rgba(0, 0, 0, 0.25);
+ padding: @padding-base-vertical @padding-base-horizontal @padding-base-vertical @padding-base-horizontal;
+ border: 1px solid @tooltipBorderColor;
+ border-radius: @border-radius-base;
+ transform: translate(-15px, 0);
+ position: relative;
+ white-space:nowrap;
+}
+.scroll-tooltip:after, .scroll-tooltip:before {
+ left: 100%;
+ top: 50%;
+ border: solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+}
+.scroll-tooltip:after {
+ border-color: transparent;
+ border-left-color: lighten(rgba(99, 102, 103, 1), 3%);
+ border-width: 8px;
+ margin-top: -8px;
+}
+.scroll-tooltip:before {
+ border-color: transparent;
+ border-left-color: darken(@tooltipBorderColor, 20%);
+ border-width: 9px;
+ margin-top: -9px;
+}
+
+::-webkit-scrollbar {
+ display: none;
+}
+
+.scroll-region {
+ position:relative;
+
+ .scroll-region-content {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ overflow-y: scroll;
+ }
+
+ .scrollbar-track {
+ opacity: 0;
+ transition: opacity 0.3s;
+ transition-delay: 0.5s;
+ padding:3px;
+ width:17px;
+ background: @list-bg;
+ border-left: 1px solid @border-color-divider;
+ &:hover {
+ opacity: 1;
+ transition-delay: 0s;
+ }
+
+ /* Used to read the track height with padding applied. */
+ .scrollbar-track-inner {
+ height:100%;
+ }
+
+ .scrollbar-handle {
+ background-color: lighten(@gray, 40%);
+ border:1px solid lighten(@gray, 30%);
+ border-radius:8px;
+ .tooltip {
+ opacity: 0;
+ transition: opacity 0.3s;
+ top: 50%;
+ transform: translate(-100%, -50%);
+ position: absolute;
+ }
+ }
+ }
+}
+.scroll-region.scrolling {
+ .scrollbar-track {
+ opacity: 1;
+ transition-delay: 0s;
+ }
+}
+.scroll-region.dragging {
+ .scrollbar-track {
+ opacity: 1;
+ .scrollbar-handle {
+ cursor: default;
+ background-color: lighten(@gray, 30%);
+ border:1px solid lighten(@gray, 20%);
+ .tooltip {
+ opacity: 1;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/static/index.less b/static/index.less
index 264e6b0f5..875ca8b59 100644
--- a/static/index.less
+++ b/static/index.less
@@ -18,6 +18,7 @@
@import "components/tokenizing-text-field";
@import "components/extra";
@import "components/list-tabular";
+@import "components/scroll-region";
@import "components/spinner";
@import "components/generated-form";
@import "components/empty-state";
diff --git a/static/workspace.less b/static/workspace.less
index 7fcc1f8ff..749ecc666 100644
--- a/static/workspace.less
+++ b/static/workspace.less
@@ -198,7 +198,7 @@ body.is-blurred {
}
&.flexbox-handle-right {
- right:-3px;
+ right:-4px;
padding-right:3px;
}
&.flexbox-handle-left {