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()}
-
+ _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} /> + +
+
+
+
+
{tooltip}
+
+
+
+
+ {@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 {