Skip to content

Commit

Permalink
Cards and CommentThreadItems scroll into view when needed
Browse files Browse the repository at this point in the history
  • Loading branch information
cbothner committed Dec 13, 2017
1 parent b01fcd9 commit 4ef1970
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 89 deletions.
3 changes: 3 additions & 0 deletions app/javascript/card/CardContents.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import CitationTooltip from './CitationTooltip'
import CommentThreadsTag from 'comments/CommentThreadsTag'
import { OnScreenTracker } from 'utility/Tracker'
import { FocusContainer } from 'utility/A11y'
import { ScrollIntoView } from 'utility/ScrollView'

import type { ContextRouter, Match } from 'react-router-dom'

Expand Down Expand Up @@ -142,6 +143,8 @@ class CardContents extends React.Component<Props, *> {
transition: 'padding-top 0.1s, flex 0.3s',
}}
>
{theseCommentThreadsOpen ? <ScrollIntoView /> : null}

{editing && <EditorToolbar cardId={id} />}
{title}
<FocusContainer
Expand Down
3 changes: 2 additions & 1 deletion app/javascript/comments/CommentThreadsCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import CommentThreadItem from 'conversation/CommentThreadItem'
import CommentsCard from 'comments/CommentsCard'
import NewCommentButton from 'comments/NewCommentButton'
import { FocusContainer } from 'utility/A11y'
import { CommentThreadBreadcrumbs, ScrollView } from 'conversation/shared'
import { CommentThreadBreadcrumbs } from 'conversation/shared'
import ScrollView from 'utility/ScrollView'

import { Link, Route, matchPath } from 'react-router-dom'
import { elementOpen, commentsOpen } from 'shared/routes'
Expand Down
13 changes: 11 additions & 2 deletions app/javascript/conversation/CommentThreadItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
CommentThreadBreadcrumb,
} from 'conversation/shared'
import Identicon from 'shared/Identicon'
import { ScrollIntoView } from 'utility/ScrollView'

import { commentsOpen, commentThreadsOpen } from 'shared/routes'
import { hoverCommentThread, deleteCommentThread } from 'redux/actions'
Expand Down Expand Up @@ -69,9 +70,15 @@ function mapDispatchToProps (
dispatch: Dispatch,
{ id, history, location }: OwnProps
) {
const inSitu = /cards/.test(location.pathname)
const hoverHandlers = inSitu
? {
handleMouseEnter: () => dispatch(hoverCommentThread(id)),
handleMouseLeave: () => dispatch(hoverCommentThread(null)),
}
: {}
return {
handleMouseEnter: () => dispatch(hoverCommentThread(id)),
handleMouseLeave: () => dispatch(hoverCommentThread(null)),
...hoverHandlers,
handleDeleteThread: (e: SyntheticMouseEvent<*>) => {
e.preventDefault()
const promise = dispatch(deleteCommentThread(id))
Expand Down Expand Up @@ -106,6 +113,8 @@ const CommentThreadItem = ({
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{open && <ScrollIntoView />}

<CommentThreadBreadcrumbs>
<CommentThreadBreadcrumb>
<FormattedMessage
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/conversation/RecentCommentThreads.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { NonIdealState } from '@blueprintjs/core'
import { CoverImageContainer } from 'overview/BillboardTitle'
import CommunityChooser from 'overview/CommunityChooser'
import CommentThreadItem from 'conversation/CommentThreadItem'
import { ScrollView } from 'conversation/shared'
import ScrollView from 'utility/ScrollView'

import type { State } from 'redux/state'

Expand Down
3 changes: 2 additions & 1 deletion app/javascript/conversation/SelectedCommentThread.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import Responses from 'conversation/Responses'
import ResponseForm, {
EmptyResponseFormContainer,
} from 'conversation/ResponseForm'
import { ScrollView, NoSelectedCommentThread } from 'conversation/shared'
import { NoSelectedCommentThread } from 'conversation/shared'
import ScrollView from 'utility/ScrollView'
import { LabelForScreenReaders, FocusContainer } from 'utility/A11y'

import { deleteCommentThread } from 'redux/actions'
Expand Down
63 changes: 0 additions & 63 deletions app/javascript/conversation/shared.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ import styled, { css } from 'styled-components'
import { FormattedDate } from 'react-intl'
import ReactMarkdown from 'react-markdown'

function cancelScrollEvent (e: WheelEvent) {
e.stopImmediatePropagation()
e.preventDefault()
return false
}

export const CommentThreadBreadcrumbs = styled.ul.attrs({
className: 'pt-breadcrumbs',
})`
Expand Down Expand Up @@ -88,63 +82,6 @@ const OptionalUnderline = styled.span.attrs({
`};
`

export class ScrollView extends React.Component<{
maxHeightOffset: string,
innerRef: HTMLDivElement => any,
children: React.Node,
}> {
static defaultProps = { innerRef: (_: HTMLDivElement) => {} }

container: ?HTMLDivElement

componentDidMount () {
this.container &&
this.container.addEventListener('wheel', this.handleScroll, false)
this.container && this.props.innerRef(this.container)
}

componentWillUnmount () {
this.container &&
this.container.removeEventListener('wheel', this.handleScroll, false)
}

render () {
const { children, ...rest } = this.props
return (
<ScrollViewDiv {...rest} innerRef={el => (this.container = el)}>
{children}
</ScrollViewDiv>
)
}

handleScroll = (e: WheelEvent) => {
const target = ((e.target: any): HTMLElement)
if (this.container && this.container.contains(target)) {
var scrollTop = this.container.scrollTop
var scrollHeight = this.container.scrollHeight
var height = this.container.clientHeight
var wheelDelta = e.deltaY
var isDeltaPositive = wheelDelta > 0

if (isDeltaPositive && wheelDelta > scrollHeight - height - scrollTop) {
this.container.scrollTop = scrollHeight
return cancelScrollEvent(e)
} else if (!isDeltaPositive && -wheelDelta > scrollTop) {
this.container.scrollTop = 0
return cancelScrollEvent(e)
}
}
}
}

const ScrollViewDiv = styled.div.attrs({ className: 'ScrollView' })`
max-height: ${({ maxHeightOffset }) =>
`calc(100vh - (${maxHeightOffset}))` || '100vh'};
overflow-x: hidden;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
`

export const SmallGreyText = styled.span`
font-size: 14px;
color: #5c7080;
Expand Down
21 changes: 0 additions & 21 deletions app/javascript/utility/ScrollLock.jsx

This file was deleted.

114 changes: 114 additions & 0 deletions app/javascript/utility/ScrollView.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* @providesModule ScrollView
* @flow
*/

import * as React from 'react'
import styled from 'styled-components'

class ScrollView extends React.Component<{
maxHeightOffset: string,
innerRef: HTMLDivElement => any,
children: React.Node,
}> {
static defaultProps = { innerRef: (_: HTMLDivElement) => {} }

container: ?HTMLDivElement

componentDidMount () {
this.container &&
this.container.addEventListener('wheel', this.handleScroll, false)
this.container && this.props.innerRef(this.container)
}

componentWillUnmount () {
this.container &&
this.container.removeEventListener('wheel', this.handleScroll, false)
}

render () {
const { children, ...rest } = this.props
return (
<ScrollViewDiv {...rest} innerRef={el => (this.container = el)}>
{children}
</ScrollViewDiv>
)
}

handleScroll = (e: WheelEvent) => {
const target = ((e.target: any): HTMLElement)
if (this.container && this.container.contains(target)) {
var scrollTop = this.container.scrollTop
var scrollHeight = this.container.scrollHeight
var height = this.container.clientHeight
var wheelDelta = e.deltaY
var isDeltaPositive = wheelDelta > 0

if (isDeltaPositive && wheelDelta > scrollHeight - height - scrollTop) {
this.container.scrollTop = scrollHeight
return cancelScrollEvent(e)
} else if (!isDeltaPositive && -wheelDelta > scrollTop) {
this.container.scrollTop = 0
return cancelScrollEvent(e)
}
}
}
}
export default ScrollView

function cancelScrollEvent (e: WheelEvent) {
e.stopImmediatePropagation()
e.preventDefault()
return false
}

const ScrollViewDiv = styled.div.attrs({ className: 'ScrollView' })`
max-height: ${({ maxHeightOffset }) =>
`calc(100vh - (${maxHeightOffset}))` || '100vh'};
overflow-x: hidden;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
`

export class ScrollIntoView extends React.Component<{}> {
ref: ?HTMLDivElement

componentDidMount () {
setTimeout(() => {
const rect = this.ref && this.ref.getBoundingClientRect()
const windowHeight =
document.documentElement && document.documentElement.clientHeight

if (
rect &&
windowHeight &&
(rect.top < 0 || rect.bottom > windowHeight)
) {
this.ref &&
this.ref.scrollIntoView &&
this.ref.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}, 50)
}

render () {
return <ScrollTarget innerRef={ref => (this.ref = ref)} />
}
}
const ScrollTarget = styled.div`
transform: translateY(-50px);
`

export class ScrollLock extends React.Component<{ children: React.Node }> {
componentDidMount () {
document.body && document.body.classList.add('pt-overlay-open')
}

componentWillUnmount () {
document.body && document.body.classList.remove('pt-overlay-open')
}

render () {
return this.props.children
}
}

0 comments on commit 4ef1970

Please sign in to comment.