From 4ef197085bc2b58685838db3d066cca027c51365 Mon Sep 17 00:00:00 2001 From: Cameron Bothner Date: Wed, 13 Dec 2017 12:36:53 -0500 Subject: [PATCH] Cards and CommentThreadItems scroll into view when needed --- app/javascript/card/CardContents.jsx | 3 + .../comments/CommentThreadsCard.jsx | 3 +- .../conversation/CommentThreadItem.jsx | 13 +- .../conversation/RecentCommentThreads.jsx | 2 +- .../conversation/SelectedCommentThread.jsx | 3 +- app/javascript/conversation/shared.jsx | 63 ---------- app/javascript/utility/ScrollLock.jsx | 21 ---- app/javascript/utility/ScrollView.jsx | 114 ++++++++++++++++++ 8 files changed, 133 insertions(+), 89 deletions(-) delete mode 100644 app/javascript/utility/ScrollLock.jsx create mode 100644 app/javascript/utility/ScrollView.jsx diff --git a/app/javascript/card/CardContents.jsx b/app/javascript/card/CardContents.jsx index 854df28e1..804cf259f 100644 --- a/app/javascript/card/CardContents.jsx +++ b/app/javascript/card/CardContents.jsx @@ -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' @@ -142,6 +143,8 @@ class CardContents extends React.Component { transition: 'padding-top 0.1s, flex 0.3s', }} > + {theseCommentThreadsOpen ? : null} + {editing && } {title} 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)) @@ -106,6 +113,8 @@ const CommentThreadItem = ({ onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} > + {open && } + 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 ( - (this.container = el)}> - {children} - - ) - } - - 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; diff --git a/app/javascript/utility/ScrollLock.jsx b/app/javascript/utility/ScrollLock.jsx deleted file mode 100644 index e7fe1126f..000000000 --- a/app/javascript/utility/ScrollLock.jsx +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @providesModule ScrollLock - * @flow - */ - -import * as React from 'react' - -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 - } -} -export default ScrollLock diff --git a/app/javascript/utility/ScrollView.jsx b/app/javascript/utility/ScrollView.jsx new file mode 100644 index 000000000..7965d6dfe --- /dev/null +++ b/app/javascript/utility/ScrollView.jsx @@ -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 ( + (this.container = el)}> + {children} + + ) + } + + 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 (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 + } +}