diff options
author | ThibG <thib@sitedethib.com> | 2019-10-07 12:02:20 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-07 12:02:20 +0200 |
commit | d2f7b8685cfd0ec9b69af505b56c791d9b5f1c82 (patch) | |
tree | 79e8fa613494ae37fe2adfb41bf9ba38f0ffd80d | |
parent | 3921125e5578fb3871fdcae0e8e8a77179f1ad72 (diff) | |
parent | 0fbe36e3fb4644945eeb0c142045a003e2793b19 (diff) |
Merge pull request #1229 from ThibG/glitch-soc/features/upstream-scroll-behavior
Merge upstream changes to scroll behavior in single column
41 files changed, 303 insertions, 87 deletions
diff --git a/app/javascript/flavours/glitch/components/column.js b/app/javascript/flavours/glitch/components/column.js index dc87818a5..5819d5362 100644 --- a/app/javascript/flavours/glitch/components/column.js +++ b/app/javascript/flavours/glitch/components/column.js @@ -10,10 +10,11 @@ export default class Column extends React.PureComponent { extraClasses: PropTypes.string, name: PropTypes.string, label: PropTypes.string, + bindToDocument: PropTypes.bool, }; scrollTop () { - const scrollable = this.node.querySelector('.scrollable'); + const scrollable = this.props.bindToDocument ? document.scrollingElement : this.node.querySelector('.scrollable'); if (!scrollable) { return; @@ -35,11 +36,19 @@ export default class Column extends React.PureComponent { } componentDidMount () { - this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + if (this.props.bindToDocument) { + document.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + } else { + this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + } } componentWillUnmount () { - this.node.removeEventListener('wheel', this.handleWheel); + if (this.props.bindToDocument) { + document.removeEventListener('wheel', this.handleWheel); + } else { + this.node.removeEventListener('wheel', this.handleWheel); + } } render () { diff --git a/app/javascript/flavours/glitch/components/column_back_button.js b/app/javascript/flavours/glitch/components/column_back_button.js index a0260e5af..8326cbb79 100644 --- a/app/javascript/flavours/glitch/components/column_back_button.js +++ b/app/javascript/flavours/glitch/components/column_back_button.js @@ -2,6 +2,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; import Icon from 'flavours/glitch/components/icon'; +import { createPortal } from 'react-dom'; export default class ColumnBackButton extends React.PureComponent { @@ -9,6 +10,10 @@ export default class ColumnBackButton extends React.PureComponent { router: PropTypes.object, }; + static propTypes = { + multiColumn: PropTypes.bool, + }; + handleClick = (event) => { // if history is exhausted, or we would leave mastodon, just go to root. if (window.history.state) { @@ -24,12 +29,20 @@ export default class ColumnBackButton extends React.PureComponent { } render () { - return ( + const { multiColumn } = this.props; + + const component = ( <button onClick={this.handleClick} className='column-back-button'> <Icon id='chevron-left' className='column-back-button__icon' fixedWidth /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> </button> ); + + if (multiColumn) { + return component; + } else { + return createPortal(component, document.getElementById('tabs-bar__portal')); + } } } diff --git a/app/javascript/flavours/glitch/components/column_header.js b/app/javascript/flavours/glitch/components/column_header.js index 3a064e90a..43c9f1144 100644 --- a/app/javascript/flavours/glitch/components/column_header.js +++ b/app/javascript/flavours/glitch/components/column_header.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { createPortal } from 'react-dom'; import classNames from 'classnames'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; @@ -36,6 +37,7 @@ class ColumnHeader extends React.PureComponent { onEnterCleaningMode: PropTypes.func, children: PropTypes.node, pinned: PropTypes.bool, + placeholder: PropTypes.bool, onPin: PropTypes.func, onMove: PropTypes.func, onClick: PropTypes.func, @@ -104,7 +106,7 @@ class ColumnHeader extends React.PureComponent { } render () { - const { intl, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive } = this.props; + const { intl, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive, placeholder } = this.props; const { collapsed, animating, animatingNCD } = this.state; let title = this.props.title; @@ -157,7 +159,7 @@ class ColumnHeader extends React.PureComponent { <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button> </div> ); - } else if (multiColumn) { + } else if (multiColumn && this.props.onPin) { pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>; } @@ -179,13 +181,13 @@ class ColumnHeader extends React.PureComponent { collapsedContent.push(pinButton); } - if (children || multiColumn) { + if (children || (multiColumn && this.props.onPin)) { collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>; } const hasTitle = icon && title; - return ( + const component = ( <div className={wrapperClassName}> <h1 className={buttonClassName}> {hasTitle && ( @@ -229,6 +231,12 @@ class ColumnHeader extends React.PureComponent { </div> </div> ); + + if (multiColumn || placeholder) { + return component; + } else { + return createPortal(component, document.getElementById('tabs-bar__portal')); + } } } diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js index 7c0b6d082..c022290a4 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.js +++ b/app/javascript/flavours/glitch/components/scrollable_list.js @@ -35,6 +35,7 @@ export default class ScrollableList extends PureComponent { alwaysPrepend: PropTypes.bool, emptyMessage: PropTypes.node, children: PropTypes.node, + bindToDocument: PropTypes.bool, }; static defaultProps = { @@ -50,7 +51,9 @@ export default class ScrollableList extends PureComponent { handleScroll = throttle(() => { if (this.node) { - const { scrollTop, scrollHeight, clientHeight } = this.node; + const scrollTop = this.getScrollTop(); + const scrollHeight = this.getScrollHeight(); + const clientHeight = this.getClientHeight(); const offset = scrollHeight - scrollTop - clientHeight; if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) { @@ -80,9 +83,14 @@ export default class ScrollableList extends PureComponent { scrollToTopOnMouseIdle = false; setScrollTop = newScrollTop => { - if (this.node.scrollTop !== newScrollTop) { + if (this.getScrollTop() !== newScrollTop) { this.lastScrollWasSynthetic = true; - this.node.scrollTop = newScrollTop; + + if (this.props.bindToDocument) { + document.scrollingElement.scrollTop = newScrollTop; + } else { + this.node.scrollTop = newScrollTop; + } } }; @@ -100,7 +108,7 @@ export default class ScrollableList extends PureComponent { this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY); - if (!this.mouseMovedRecently && this.node.scrollTop === 0) { + if (!this.mouseMovedRecently && this.getScrollTop() === 0) { // Only set if we just started moving and are scrolled to the top. this.scrollToTopOnMouseIdle = true; } @@ -132,15 +140,27 @@ export default class ScrollableList extends PureComponent { } getScrollPosition = () => { - if (this.node && (this.node.scrollTop > 0 || this.mouseMovedRecently)) { - return {height: this.node.scrollHeight, top: this.node.scrollTop}; + if (this.node && (this.getScrollTop() > 0 || this.mouseMovedRecently)) { + return { height: this.getScrollHeight(), top: this.getScrollTop() }; } else { return null; } } + getScrollTop = () => { + return this.props.bindToDocument ? document.scrollingElement.scrollTop : this.node.scrollTop; + } + + getScrollHeight = () => { + return this.props.bindToDocument ? document.scrollingElement.scrollHeight : this.node.scrollHeight; + } + + getClientHeight = () => { + return this.props.bindToDocument ? document.scrollingElement.clientHeight : this.node.clientHeight; + } + updateScrollBottom = (snapshot) => { - const newScrollTop = this.node.scrollHeight - snapshot; + const newScrollTop = this.getScrollHeight() - snapshot; this.setScrollTop(newScrollTop); } @@ -155,8 +175,8 @@ export default class ScrollableList extends PureComponent { this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0); - if (pendingChanged || someItemInserted && (this.node.scrollTop > 0 || this.mouseMovedRecently)) { - return this.node.scrollHeight - this.node.scrollTop; + if (pendingChanged || someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently)) { + return this.getScrollHeight() - this.getScrollTop(); } else { return null; } @@ -165,7 +185,9 @@ export default class ScrollableList extends PureComponent { componentDidUpdate (prevProps, prevState, snapshot) { // Reset the scroll position when a new child comes in in order not to // jerk the scrollbar around if you're already scrolled down the page. - if (snapshot !== null) this.updateScrollBottom(snapshot); + if (snapshot !== null) { + this.updateScrollBottom(snapshot); + } } componentWillUnmount () { @@ -191,13 +213,23 @@ export default class ScrollableList extends PureComponent { } attachScrollListener () { - this.node.addEventListener('scroll', this.handleScroll); - this.node.addEventListener('wheel', this.handleWheel); + if (this.props.bindToDocument) { + document.addEventListener('scroll', this.handleScroll); + document.addEventListener('wheel', this.handleWheel); + } else { + this.node.addEventListener('scroll', this.handleScroll); + this.node.addEventListener('wheel', this.handleWheel); + } } detachScrollListener () { - this.node.removeEventListener('scroll', this.handleScroll); - this.node.removeEventListener('wheel', this.handleWheel); + if (this.props.bindToDocument) { + document.removeEventListener('scroll', this.handleScroll); + document.removeEventListener('wheel', this.handleWheel); + } else { + this.node.removeEventListener('scroll', this.handleScroll); + this.node.removeEventListener('wheel', this.handleWheel); + } } getFirstChildKey (props) { diff --git a/app/javascript/flavours/glitch/containers/mastodon.js b/app/javascript/flavours/glitch/containers/mastodon.js index 59eef6636..8101be87e 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.js +++ b/app/javascript/flavours/glitch/containers/mastodon.js @@ -49,6 +49,10 @@ export default class Mastodon extends React.PureComponent { } } + shouldUpdateScroll (_, { location }) { + return !(location.state && location.state.mastodonModalOpen); + } + render () { const { locale } = this.props; @@ -57,7 +61,7 @@ export default class Mastodon extends React.PureComponent { <Provider store={store}> <ErrorBoundary> <BrowserRouter basename='/web'> - <ScrollContext> + <ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}> <Route path='/' component={UI} /> </ScrollContext> </BrowserRouter> diff --git a/app/javascript/flavours/glitch/containers/media_container.js b/app/javascript/flavours/glitch/containers/media_container.js index 41547412e..a483510b0 100644 --- a/app/javascript/flavours/glitch/containers/media_container.js +++ b/app/javascript/flavours/glitch/containers/media_container.js @@ -2,16 +2,17 @@ import React, { PureComponent, Fragment } from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import { IntlProvider, addLocaleData } from 'react-intl'; +import { List as ImmutableList, fromJS } from 'immutable'; import { getLocale } from 'mastodon/locales'; +import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar'; import MediaGallery from 'flavours/glitch/components/media_gallery'; -import Video from 'flavours/glitch/features/video'; -import Card from 'flavours/glitch/features/status/components/card'; import Poll from 'flavours/glitch/components/poll'; import Hashtag from 'flavours/glitch/components/hashtag'; -import Audio from 'flavours/glitch/features/audio'; import ModalRoot from 'flavours/glitch/components/modal_root'; import MediaModal from 'flavours/glitch/features/ui/components/media_modal'; -import { List as ImmutableList, fromJS } from 'immutable'; +import Video from 'flavours/glitch/features/video'; +import Card from 'flavours/glitch/features/status/components/card'; +import Audio from 'flavours/glitch/features/audio'; const { localeData, messages } = getLocale(); addLocaleData(localeData); @@ -33,6 +34,8 @@ export default class MediaContainer extends PureComponent { handleOpenMedia = (media, index) => { document.body.classList.add('with-modals--active'); + document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; + this.setState({ media, index }); } @@ -40,11 +43,15 @@ export default class MediaContainer extends PureComponent { const media = ImmutableList([video]); document.body.classList.add('with-modals--active'); + document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; + this.setState({ media, time }); } handleCloseMedia = () => { document.body.classList.remove('with-modals--active'); + document.documentElement.style.marginRight = 0; + this.setState({ media: null, index: null, time: null }); } diff --git a/app/javascript/flavours/glitch/features/account/components/profile_column_header.js b/app/javascript/flavours/glitch/features/account/components/profile_column_header.js index b6d373a2c..17c08e375 100644 --- a/app/javascript/flavours/glitch/features/account/components/profile_column_header.js +++ b/app/javascript/flavours/glitch/features/account/components/profile_column_header.js @@ -12,11 +12,12 @@ class ProfileColumnHeader extends React.PureComponent { static propTypes = { onClick: PropTypes.func, + multiColumn: PropTypes.bool, intl: PropTypes.object.isRequired, }; render() { - const { onClick, intl } = this.props; + const { onClick, intl, multiColumn } = this.props; return ( <ColumnHeader @@ -24,6 +25,7 @@ class ProfileColumnHeader extends React.PureComponent { title={intl.formatMessage(messages.profile)} onClick={onClick} showBackButton + multiColumn={multiColumn} /> ); } diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js index ff39764bb..f5fe6c930 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.js +++ b/app/javascript/flavours/glitch/features/account_gallery/index.js @@ -55,6 +55,7 @@ class AccountGallery extends ImmutablePureComponent { isLoading: PropTypes.bool, hasMore: PropTypes.bool, isAccount: PropTypes.bool, + multiColumn: PropTypes.bool, }; state = { @@ -130,7 +131,7 @@ class AccountGallery extends ImmutablePureComponent { } render () { - const { attachments, isLoading, hasMore, isAccount } = this.props; + const { attachments, isLoading, hasMore, isAccount, multiColumn } = this.props; const { width } = this.state; if (!isAccount) { @@ -157,7 +158,7 @@ class AccountGallery extends ImmutablePureComponent { return ( <Column ref={this.setColumnRef}> - <ProfileColumnHeader onClick={this.handleHeaderClick} /> + <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} /> <ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={this.shouldUpdateScroll}> <div className='scrollable scrollable--flex' onScroll={this.handleScroll}> diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index 2f0859341..1f02c1be5 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/features/account_timeline/index.js @@ -39,6 +39,7 @@ class AccountTimeline extends ImmutablePureComponent { hasMore: PropTypes.bool, withReplies: PropTypes.bool, isAccount: PropTypes.bool, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -76,7 +77,7 @@ class AccountTimeline extends ImmutablePureComponent { } render () { - const { statusIds, featuredStatusIds, isLoading, hasMore, isAccount } = this.props; + const { statusIds, featuredStatusIds, isLoading, hasMore, isAccount, multiColumn } = this.props; if (!isAccount) { return ( @@ -96,7 +97,7 @@ class AccountTimeline extends ImmutablePureComponent { return ( <Column ref={this.setRef} name='account'> - <ProfileColumnHeader onClick={this.handleHeaderClick} /> + <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} /> <StatusList prepend={<HeaderContainer accountId={this.props.params.accountId} />} @@ -108,6 +109,7 @@ class AccountTimeline extends ImmutablePureComponent { hasMore={hasMore} onLoadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/blocks/index.js b/app/javascript/flavours/glitch/features/blocks/index.js index 8a84f5a55..9eb6fe02e 100644 --- a/app/javascript/flavours/glitch/features/blocks/index.js +++ b/app/javascript/flavours/glitch/features/blocks/index.js @@ -31,6 +31,7 @@ class Blocks extends ImmutablePureComponent { accountIds: ImmutablePropTypes.list, hasMore: PropTypes.bool, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -42,7 +43,7 @@ class Blocks extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, accountIds, hasMore } = this.props; + const { intl, accountIds, hasMore, multiColumn } = this.props; if (!accountIds) { return ( @@ -55,13 +56,14 @@ class Blocks extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.blocks' defaultMessage="You haven't blocked any users yet." />; return ( - <Column name='blocks' icon='ban' heading={intl.formatMessage(messages.heading)}> + <Column name='blocks' bindToDocument={!multiColumn} icon='ban' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <ScrollableList scrollKey='blocks' onLoadMore={this.handleLoadMore} hasMore={hasMore} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => <AccountContainer key={id} id={id} /> diff --git a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js index 7d550e362..58b9e6396 100644 --- a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js +++ b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js @@ -73,7 +73,7 @@ class Bookmarks extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses' defaultMessage="You don't have any bookmarked toots yet. When you bookmark one, it will show up here." />; return ( - <Column ref={this.setRef} name='bookmarks'> + <Column bindToDocument={!multiColumn} ref={this.setRef} name='bookmarks'> <ColumnHeader icon='bookmark' title={intl.formatMessage(messages.heading)} @@ -93,6 +93,7 @@ class Bookmarks extends ImmutablePureComponent { isLoading={isLoading} onLoadMore={this.handleLoadMore} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.js b/app/javascript/flavours/glitch/features/community_timeline/index.js index 5585edc9c..c548824ce 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/index.js +++ b/app/javascript/flavours/glitch/features/community_timeline/index.js @@ -105,7 +105,7 @@ class CommunityTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef} name='local' label={intl.formatMessage(messages.title)}> + <Column ref={this.setRef} name='local' bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}> <ColumnHeader icon='users' active={hasUnread} @@ -125,6 +125,7 @@ class CommunityTimeline extends React.PureComponent { timelineId={`community${onlyMedia ? ':media' : ''}`} onLoadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/direct_timeline/index.js b/app/javascript/flavours/glitch/features/direct_timeline/index.js index e2c0a439a..7741c6922 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/index.js +++ b/app/javascript/flavours/glitch/features/direct_timeline/index.js @@ -135,7 +135,7 @@ class DirectTimeline extends React.PureComponent { } return ( - <Column ref={this.setRef} label={intl.formatMessage(messages.title)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> <ColumnHeader icon='envelope' active={hasUnread} diff --git a/app/javascript/flavours/glitch/features/domain_blocks/index.js b/app/javascript/flavours/glitch/features/domain_blocks/index.js index 49e0368d7..cd105a49b 100644 --- a/app/javascript/flavours/glitch/features/domain_blocks/index.js +++ b/app/javascript/flavours/glitch/features/domain_blocks/index.js @@ -32,6 +32,7 @@ class Blocks extends ImmutablePureComponent { hasMore: PropTypes.bool, domains: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -43,7 +44,7 @@ class Blocks extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, domains, hasMore } = this.props; + const { intl, domains, hasMore, multiColumn } = this.props; if (!domains) { return ( @@ -56,13 +57,14 @@ class Blocks extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no hidden domains yet.' />; return ( - <Column icon='minus-circle' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='minus-circle' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <ScrollableList scrollKey='domain_blocks' onLoadMore={this.handleLoadMore} hasMore={hasMore} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {domains.map(domain => <DomainContainer key={domain} domain={domain} /> diff --git a/app/javascript/flavours/glitch/features/favourited_statuses/index.js b/app/javascript/flavours/glitch/features/favourited_statuses/index.js index 719a31d6e..c6470ba74 100644 --- a/app/javascript/flavours/glitch/features/favourited_statuses/index.js +++ b/app/javascript/flavours/glitch/features/favourited_statuses/index.js @@ -73,7 +73,7 @@ class Favourites extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favourite toots yet. When you favourite one, it will show up here." />; return ( - <Column ref={this.setRef} name='favourites' label={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} name='favourites' label={intl.formatMessage(messages.heading)}> <ColumnHeader icon='star' title={intl.formatMessage(messages.heading)} @@ -93,6 +93,7 @@ class Favourites extends ImmutablePureComponent { isLoading={isLoading} onLoadMore={this.handleLoadMore} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/favourites/index.js b/app/javascript/flavours/glitch/features/favourites/index.js index 7afadf12e..953bf171f 100644 --- a/app/javascript/flavours/glitch/features/favourites/index.js +++ b/app/javascript/flavours/glitch/features/favourites/index.js @@ -6,6 +6,7 @@ import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import { fetchFavourites } from 'flavours/glitch/actions/interactions'; import AccountContainer from 'flavours/glitch/containers/account_container'; import Column from 'flavours/glitch/features/ui/components/column'; +import Icon from 'flavours/glitch/components/icon'; import ColumnHeader from 'flavours/glitch/components/column_header'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -13,6 +14,7 @@ import ScrollableList from '../../components/scrollable_list'; const messages = defineMessages({ heading: { id: 'column.favourited_by', defaultMessage: 'Favourited by' }, + refresh: { id: 'refresh', defaultMessage: 'Refresh' }, }); const mapStateToProps = (state, props) => ({ @@ -27,6 +29,7 @@ class Favourites extends ImmutablePureComponent { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, accountIds: ImmutablePropTypes.list, + multiColumn: PropTypes.bool, intl: PropTypes.object.isRequired, }; @@ -50,8 +53,12 @@ class Favourites extends ImmutablePureComponent { this.column = c; } + handleRefresh = () => { + this.props.dispatch(fetchFavourites(this.props.params.statusId)); + } + render () { - const { intl, accountIds } = this.props; + const { intl, accountIds, multiColumn } = this.props; if (!accountIds) { return ( @@ -70,10 +77,15 @@ class Favourites extends ImmutablePureComponent { title={intl.formatMessage(messages.heading)} onClick={this.handleHeaderClick} showBackButton + multiColumn={multiColumn} + extraButton={( + <button className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button> + )} /> <ScrollableList scrollKey='favourites' emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} /> diff --git a/app/javascript/flavours/glitch/features/follow_requests/index.js b/app/javascript/flavours/glitch/features/follow_requests/index.js index a7e8f4b61..36770aace 100644 --- a/app/javascript/flavours/glitch/features/follow_requests/index.js +++ b/app/javascript/flavours/glitch/features/follow_requests/index.js @@ -31,6 +31,7 @@ class FollowRequests extends ImmutablePureComponent { hasMore: PropTypes.bool, accountIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -42,7 +43,7 @@ class FollowRequests extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, accountIds, hasMore } = this.props; + const { intl, accountIds, hasMore, multiColumn } = this.props; if (!accountIds) { return ( @@ -55,7 +56,7 @@ class FollowRequests extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />; return ( - <Column name='follow-requests' icon='user-plus' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} name='follow-requests' icon='user-plus' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <ScrollableList @@ -63,6 +64,7 @@ class FollowRequests extends ImmutablePureComponent { onLoadMore={this.handleLoadMore} hasMore={hasMore} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => <AccountAuthorizeContainer key={id} id={id} /> diff --git a/app/javascript/flavours/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js index 2bd0e6e2f..c78dcc8e4 100644 --- a/app/javascript/flavours/glitch/features/followers/index.js +++ b/app/javascript/flavours/glitch/features/followers/index.js @@ -33,6 +33,7 @@ class Followers extends ImmutablePureComponent { accountIds: ImmutablePropTypes.list, hasMore: PropTypes.bool, isAccount: PropTypes.bool, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -70,7 +71,7 @@ class Followers extends ImmutablePureComponent { } render () { - const { accountIds, hasMore, isAccount } = this.props; + const { accountIds, hasMore, isAccount, multiColumn } = this.props; if (!isAccount) { return ( @@ -92,7 +93,7 @@ class Followers extends ImmutablePureComponent { return ( <Column ref={this.setRef}> - <ProfileColumnHeader onClick={this.handleHeaderClick} /> + <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} /> <ScrollableList scrollKey='followers' @@ -101,6 +102,7 @@ class Followers extends ImmutablePureComponent { prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />} alwaysPrepend emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} /> diff --git a/app/javascript/flavours/glitch/features/following/index.js b/app/javascript/flavours/glitch/features/following/index.js index f03da0c94..df7c19c22 100644 --- a/app/javascript/flavours/glitch/features/following/index.js +++ b/app/javascript/flavours/glitch/features/following/index.js @@ -33,6 +33,7 @@ class Following extends ImmutablePureComponent { accountIds: ImmutablePropTypes.list, hasMore: PropTypes.bool, isAccount: PropTypes.bool, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -70,7 +71,7 @@ class Following extends ImmutablePureComponent { } render () { - const { accountIds, hasMore, isAccount } = this.props; + const { accountIds, hasMore, isAccount, multiColumn } = this.props; if (!isAccount) { return ( @@ -92,7 +93,7 @@ class Following extends ImmutablePureComponent { return ( <Column ref={this.setRef}> - <ProfileColumnHeader onClick={this.handleHeaderClick} /> + <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} /> <ScrollableList scrollKey='following' @@ -101,6 +102,7 @@ class Following extends ImmutablePureComponent { prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />} alwaysPrepend emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} /> diff --git a/app/javascript/flavours/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js index a5095fbd9..d8a51c689 100644 --- a/app/javascript/flavours/glitch/features/getting_started/index.js +++ b/app/javascript/flavours/glitch/features/getting_started/index.js @@ -166,7 +166,7 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2); ]); return ( - <Column name='getting-started' icon='asterisk' heading={intl.formatMessage(messages.heading)} label={intl.formatMessage(messages.menu)} hideHeadingOnMobile> + <Column bindToDocument={!multiColumn} name='getting-started' icon='asterisk' heading={intl.formatMessage(messages.heading)} label={intl.formatMessage(messages.menu)} hideHeadingOnMobile> <div className='scrollable optionally-scrollable'> <div className='getting-started__wrapper'> {!multiColumn && <NavigationBar account={myAccount} />} diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js index d39505f46..16dd80c4f 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js @@ -145,6 +145,7 @@ class HashtagTimeline extends React.PureComponent { pinned={pinned} multiColumn={multiColumn} showBackButton + bindToDocument={!multiColumn} > {columnId && <ColumnSettingsContainer columnId={columnId} />} </ColumnHeader> @@ -155,6 +156,7 @@ class HashtagTimeline extends React.PureComponent { timelineId={`hashtag:${id}`} onLoadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.js b/app/javascript/flavours/glitch/features/home_timeline/index.js index defb1dcc1..9b71a4404 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/index.js +++ b/app/javascript/flavours/glitch/features/home_timeline/index.js @@ -97,7 +97,7 @@ class HomeTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef} name='home' label={intl.formatMessage(messages.title)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} name='home' label={intl.formatMessage(messages.title)}> <ColumnHeader icon='home' active={hasUnread} @@ -117,6 +117,7 @@ class HomeTimeline extends React.PureComponent { onLoadMore={this.handleLoadMore} timelineId='home' emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Visit {public} or use search to get started and meet other users.' values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js index 95daa6907..e40dbf44e 100644 --- a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js +++ b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js @@ -25,10 +25,10 @@ class KeyboardShortcuts extends ImmutablePureComponent { }; render () { - const { intl, collapseEnabled } = this.props; + const { intl, collapseEnabled, multiColumn } = this.props; return ( - <Column icon='question' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='question' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <div className='keyboard-shortcuts scrollable optionally-scrollable'> <table> diff --git a/app/javascript/flavours/glitch/features/list_timeline/index.js b/app/javascript/flavours/glitch/features/list_timeline/index.js index 8c3d0af51..908a65597 100644 --- a/app/javascript/flavours/glitch/features/list_timeline/index.js +++ b/app/javascript/flavours/glitch/features/list_timeline/index.js @@ -174,6 +174,7 @@ class ListTimeline extends React.PureComponent { onClick={this.handleHeaderClick} pinned={pinned} multiColumn={multiColumn} + bindToDocument={!multiColumn} > <div className='column-header__links'> <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}> @@ -212,6 +213,7 @@ class ListTimeline extends React.PureComponent { timelineId={`list:${id}`} onLoadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet.' />} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/lists/index.js b/app/javascript/flavours/glitch/features/lists/index.js index 79bf2e601..adde3dd5c 100644 --- a/app/javascript/flavours/glitch/features/lists/index.js +++ b/app/javascript/flavours/glitch/features/lists/index.js @@ -40,6 +40,7 @@ class Lists extends ImmutablePureComponent { dispatch: PropTypes.func.isRequired, lists: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -47,7 +48,7 @@ class Lists extends ImmutablePureComponent { } render () { - const { intl, lists } = this.props; + const { intl, lists, multiColumn } = this.props; if (!lists) { return ( @@ -60,7 +61,7 @@ class Lists extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.lists' defaultMessage="You don't have any lists yet. When you create one, it will show up here." />; return ( - <Column icon='bars' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='bars' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <NewListForm /> @@ -69,6 +70,7 @@ class Lists extends ImmutablePureComponent { <ScrollableList scrollKey='lists' emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {lists.map(list => <ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} /> diff --git a/app/javascript/flavours/glitch/features/mutes/index.js b/app/javascript/flavours/glitch/features/mutes/index.js index 7c20ca9b9..c27a530d5 100644 --- a/app/javascript/flavours/glitch/features/mutes/index.js +++ b/app/javascript/flavours/glitch/features/mutes/index.js @@ -31,6 +31,7 @@ class Mutes extends ImmutablePureComponent { hasMore: PropTypes.bool, accountIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -42,7 +43,7 @@ class Mutes extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, accountIds, hasMore } = this.props; + const { intl, accountIds, hasMore, multiColumn } = this.props; if (!accountIds) { return ( @@ -55,13 +56,14 @@ class Mutes extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />; return ( - <Column name='mutes' icon='volume-off' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} name='mutes' icon='volume-off' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <ScrollableList scrollKey='mutes' onLoadMore={this.handleLoadMore} hasMore={hasMore} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => <AccountContainer key={id} id={id} /> diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js index 99b2391c7..7f06d70c5 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.js +++ b/app/javascript/flavours/glitch/features/notifications/index.js @@ -226,6 +226,7 @@ class Notifications extends React.PureComponent { onScrollToTop={this.handleScrollToTop} onScroll={this.handleScroll} shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} > {scrollableContent} </ScrollableList> @@ -233,6 +234,7 @@ class Notifications extends React.PureComponent { return ( <Column + bindToDocument={!multiColumn} ref={this.setColumnRef} name='notifications' extraClasses={this.props.notifCleaningActive ? 'notif-cleaning' : null} diff --git a/app/javascript/flavours/glitch/features/pinned_statuses/index.js b/app/javascript/flavours/glitch/features/pinned_statuses/index.js index 8d406ddf4..34d8e465f 100644 --- a/app/javascript/flavours/glitch/features/pinned_statuses/index.js +++ b/app/javascript/flavours/glitch/features/pinned_statuses/index.js @@ -27,6 +27,7 @@ class PinnedStatuses extends ImmutablePureComponent { statusIds: ImmutablePropTypes.list.isRequired, intl: PropTypes.object.isRequired, hasMore: PropTypes.bool.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -42,15 +43,16 @@ class PinnedStatuses extends ImmutablePureComponent { } render () { - const { intl, statusIds, hasMore } = this.props; + const { intl, statusIds, hasMore, multiColumn } = this.props; return ( - <Column icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}> + <Column bindToDocument={!multiColumn} icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}> <ColumnBackButtonSlim /> <StatusList statusIds={statusIds} scrollKey='pinned_statuses' hasMore={hasMore} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/public_timeline/index.js b/app/javascript/flavours/glitch/features/public_timeline/index.js index 4bcf3da9d..49a5011c0 100644 --- a/app/javascript/flavours/glitch/features/public_timeline/index.js +++ b/app/javascript/flavours/glitch/features/public_timeline/index.js @@ -104,7 +104,7 @@ class PublicTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef} name='federated' label={intl.formatMessage(messages.title)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} name='federated' label={intl.formatMessage(messages.title)}> <ColumnHeader icon='globe' active={hasUnread} @@ -124,6 +124,7 @@ class PublicTimeline extends React.PureComponent { trackScroll={!pinned} scrollKey={`public_timeline-${columnId}`} emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/reblogs/index.js b/app/javascript/flavours/glitch/features/reblogs/index.js index a8e9db7f5..258070358 100644 --- a/app/javascript/flavours/glitch/features/reblogs/index.js +++ b/app/javascript/flavours/glitch/features/reblogs/index.js @@ -6,6 +6,7 @@ import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import { fetchReblogs } from 'flavours/glitch/actions/interactions'; import AccountContainer from 'flavours/glitch/containers/account_container'; import Column from 'flavours/glitch/features/ui/components/column'; +import Icon from 'flavours/glitch/components/icon'; import ColumnHeader from 'flavours/glitch/components/column_header'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -13,6 +14,7 @@ import ScrollableList from 'flavours/glitch/components/scrollable_list'; const messages = defineMessages({ heading: { id: 'column.reblogged_by', defaultMessage: 'Boosted by' }, + refresh: { id: 'refresh', defaultMessage: 'Refresh' }, }); const mapStateToProps = (state, props) => ({ @@ -27,6 +29,7 @@ class Reblogs extends ImmutablePureComponent { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, accountIds: ImmutablePropTypes.list, + multiColumn: PropTypes.bool, intl: PropTypes.object.isRequired, }; @@ -50,8 +53,12 @@ class Reblogs extends ImmutablePureComponent { this.column = c; } + handleRefresh = () => { + this.props.dispatch(fetchReblogs(this.props.params.statusId)); + } + render () { - const { intl, accountIds } = this.props; + const { intl, accountIds, multiColumn } = this.props; if (!accountIds) { return ( @@ -70,11 +77,16 @@ class Reblogs extends ImmutablePureComponent { title={intl.formatMessage(messages.heading)} onClick={this.handleHeaderClick} showBackButton + multiColumn={multiColumn} + extraButton={( + <button className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button> + )} /> <ScrollableList scrollKey='reblogs' emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} /> diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index dd17823ad..508510c4e 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -155,6 +155,7 @@ class Status extends ImmutablePureComponent { descendantsIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, askReplyConfirmation: PropTypes.bool, + multiColumn: PropTypes.bool, domain: PropTypes.string.isRequired, }; @@ -497,13 +498,13 @@ class Status extends ImmutablePureComponent { render () { let ancestors, descendants; const { setExpansion } = this; - const { status, settings, ancestorsIds, descendantsIds, intl, domain } = this.props; + const { status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn } = this.props; const { fullscreen, isExpanded } = this.state; if (status === null) { return ( <Column> - <ColumnBackButton /> + <ColumnBackButton multiColumn={multiColumn} /> <MissingIndicator /> </Column> ); @@ -531,12 +532,13 @@ class Status extends ImmutablePureComponent { }; return ( - <Column ref={this.setColumnRef} label={intl.formatMessage(messages.detailedStatus)}> + <Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.detailedStatus)}> <ColumnHeader icon='comment' title={intl.formatMessage(messages.tootHeading)} onClick={this.handleHeaderClick} showBackButton + multiColumn={multiColumn} extraButton={( <button className='column-header__button' title={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={!isExpanded ? 'false' : 'true'}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button> )} diff --git a/app/javascript/flavours/glitch/features/ui/components/column_loading.js b/app/javascript/flavours/glitch/features/ui/components/column_loading.js index ba2d0824e..22c00c915 100644 --- a/app/javascript/flavours/glitch/features/ui/components/column_loading.js +++ b/app/javascript/flavours/glitch/features/ui/components/column_loading.js @@ -21,7 +21,7 @@ export default class ColumnLoading extends ImmutablePureComponent { let { title, icon } = this.props; return ( <Column> - <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} /> + <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} placeholder /> <div className='scrollable' /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js index 117ce4c55..488daf0cc 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Base from '../../../components/modal_root'; +import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar'; +import Base from 'flavours/glitch/components/modal_root'; import BundleContainer from '../containers/bundle_container'; import BundleModalError from './bundle_modal_error'; import ModalLoading from './modal_loading'; @@ -61,8 +62,10 @@ export default class ModalRoot extends React.PureComponent { componentDidUpdate (prevProps, prevState, { visible }) { if (visible) { document.body.classList.add('with-modals--active'); + document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; } else { document.body.classList.remove('with-modals--active'); + document.documentElement.style.marginRight = 0; } } diff --git a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js index 90e645a82..a67405215 100644 --- a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js +++ b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js @@ -73,9 +73,13 @@ class TabsBar extends React.PureComponent { const { intl: { formatMessage } } = this.props; return ( - <nav className='tabs-bar' ref={this.setRef}> - {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))} - </nav> + <div className='tabs-bar__wrapper'> + <nav className='tabs-bar' ref={this.setRef}> + {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))} + </nav> + + <div id='tabs-bar__portal' /> + </div> ); } diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index c201cd93d..e5925a484 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -129,12 +129,25 @@ class SwitchingColumnsArea extends React.PureComponent { componentWillMount () { window.addEventListener('resize', this.handleResize, { passive: true }); + + if (this.state.mobile) { + document.body.classList.toggle('layout-single-column', true); + document.body.classList.toggle('layout-multiple-columns', false); + } else { + document.body.classList.toggle('layout-single-column', false); + document.body.classList.toggle('layout-multiple-columns', true); + } } - componentDidUpdate (prevProps) { + componentDidUpdate (prevProps, prevState) { if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) { this.node.handleChildrenContentChange(); } + + if (prevState.mobile !== this.state.mobile) { + document.body.classList.toggle('layout-single-column', this.state.mobile); + document.body.classList.toggle('layout-multiple-columns', !this.state.mobile); + } } componentWillUnmount () { diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js index 5a15830df..767fb023c 100644 --- a/app/javascript/flavours/glitch/packs/public.js +++ b/app/javascript/flavours/glitch/packs/public.js @@ -94,14 +94,6 @@ function main() { new Rellax('.parallax', { speed: -1 }); } - if (document.body.classList.contains('with-modals')) { - const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; - const scrollbarWidthStyle = document.createElement('style'); - scrollbarWidthStyle.id = 'scrollbar-width'; - document.head.appendChild(scrollbarWidthStyle); - scrollbarWidthStyle.sheet.insertRule(`body.with-modals--active { margin-right: ${scrollbarWidth}px; }`, 0); - } - delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original')); delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static')); diff --git a/app/javascript/flavours/glitch/styles/basics.scss b/app/javascript/flavours/glitch/styles/basics.scss index 64e543b78..77631097a 100644 --- a/app/javascript/flavours/glitch/styles/basics.scss +++ b/app/javascript/flavours/glitch/styles/basics.scss @@ -7,7 +7,7 @@ body { font-family: $font-sans-serif, sans-serif; - background: darken($ui-base-color, 8%); + background: darken($ui-base-color, 7%); font-size: 13px; line-height: 18px; font-weight: 400; @@ -34,11 +34,19 @@ body { } &.app-body { - position: absolute; - width: 100%; - height: 100%; padding: 0; - background: $ui-base-color; + + &.layout-single-column { + height: auto; + min-height: 100vh; + overflow-y: scroll; + } + + &.layout-multiple-columns { + position: absolute; + width: 100%; + height: 100%; + } &.with-modals--active { overflow-y: hidden; @@ -55,7 +63,6 @@ body { &--active { overflow-y: hidden; - margin-right: 13px; } } @@ -124,7 +131,6 @@ button { & > div { display: flex; width: 100%; - height: 100%; align-items: center; justify-content: center; outline: 0 !important; diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index 7f3c21163..6ba9698c5 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -17,6 +17,7 @@ justify-content: center; width: 100%; height: 100%; + min-height: 100vh; &__pane { height: 100%; @@ -24,12 +25,14 @@ pointer-events: none; display: flex; justify-content: flex-end; + min-width: 285px; &--start { justify-content: flex-start; } &__inner { + position: fixed; width: 285px; pointer-events: auto; height: 100%; @@ -40,6 +43,7 @@ box-sizing: border-box; width: 100%; max-width: 600px; + flex: 0 0 auto; display: flex; flex-direction: column; @@ -50,6 +54,26 @@ } } +.tabs-bar__wrapper { + background: darken($ui-base-color, 8%); + position: sticky; + top: 0; + z-index: 2; + padding-top: 0; + + @media screen and (min-width: $no-gap-breakpoint) { + padding-top: 10px; + } + + .tabs-bar { + margin-bottom: 0; + + @media screen and (min-width: $no-gap-breakpoint) { + margin-bottom: 10px; + } + } +} + .react-swipeable-view-container { &, .columns-area, @@ -83,7 +107,6 @@ flex-direction: column; width: 100%; height: 100%; - background: darken($ui-base-color, 7%); } .column { @@ -91,6 +114,8 @@ } .column-back-button { + box-sizing: border-box; + width: 100%; background: lighten($ui-base-color, 4%); color: $highlight-text-color; cursor: pointer; diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 6f0d4c0be..055a494e7 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -1415,6 +1415,7 @@ position: absolute; top: 0; left: 0; + z-index: 9999; } .icon-badge-wrapper { diff --git a/app/javascript/flavours/glitch/styles/components/single_column.scss b/app/javascript/flavours/glitch/styles/components/single_column.scss index 1d8055fe5..edf705b5f 100644 --- a/app/javascript/flavours/glitch/styles/components/single_column.scss +++ b/app/javascript/flavours/glitch/styles/components/single_column.scss @@ -127,8 +127,17 @@ top: 15px; } + .scrollable { + overflow: visible; + + @supports(display: grid) { + contain: content; + } + } + @media screen and (min-width: $no-gap-breakpoint) { padding: 10px 0; + padding-top: 0; } @media screen and (min-width: 630px) { @@ -217,13 +226,11 @@ @media screen and (min-width: $no-gap-breakpoint) { .tabs-bar { - margin: 10px auto; - margin-bottom: 0; width: 100%; } .react-swipeable-view-container .columns-area--mobile { - height: calc(100% - 20px) !important; + height: calc(100% - 10px) !important; } .getting-started__wrapper, diff --git a/app/javascript/flavours/glitch/util/scrollbar.js b/app/javascript/flavours/glitch/util/scrollbar.js new file mode 100644 index 000000000..929b036d6 --- /dev/null +++ b/app/javascript/flavours/glitch/util/scrollbar.js @@ -0,0 +1,34 @@ +/** @type {number | null} */ +let cachedScrollbarWidth = null; + +/** + * @return {number} + */ +const getActualScrollbarWidth = () => { + const outer = document.createElement('div'); + outer.style.visibility = 'hidden'; + outer.style.overflow = 'scroll'; + document.body.appendChild(outer); + + const inner = document.createElement('div'); + outer.appendChild(inner); + + const scrollbarWidth = outer.offsetWidth - inner.offsetWidth; + outer.parentNode.removeChild(outer); + + return scrollbarWidth; +}; + +/** + * @return {number} + */ +export const getScrollbarWidth = () => { + if (cachedScrollbarWidth !== null) { + return cachedScrollbarWidth; + } + + const scrollbarWidth = getActualScrollbarWidth(); + cachedScrollbarWidth = scrollbarWidth; + + return scrollbarWidth; +}; |