forked from fedi/mastodon
Freeze scroll position when a dropdown menu is open in the TL (#14271)
* Freeze scroll position when a dropdown menu is open in the TL * Apply this to direct TL as well * Fix case when mouse leaves the menu
This commit is contained in:
parent
61c07c3731
commit
6fda3cbbeb
|
@ -1,8 +1,8 @@
|
|||
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
|
||||
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
|
||||
|
||||
export function openDropdownMenu(id, placement, keyboard) {
|
||||
return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard };
|
||||
export function openDropdownMenu(id, placement, keyboard, scroll_key) {
|
||||
return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard, scroll_key };
|
||||
}
|
||||
|
||||
export function closeDropdownMenu(id) {
|
||||
|
|
|
@ -10,10 +10,18 @@ import { List as ImmutableList } from 'immutable';
|
|||
import classNames from 'classnames';
|
||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
|
||||
import LoadingIndicator from './loading_indicator';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const MOUSE_IDLE_DELAY = 300;
|
||||
|
||||
export default class ScrollableList extends PureComponent {
|
||||
const mapStateToProps = (state, { scrollKey }) => {
|
||||
return {
|
||||
preventScroll: scrollKey === state.getIn(['dropdown_menu', 'scroll_key']),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class ScrollableList extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
|
@ -37,6 +45,7 @@ export default class ScrollableList extends PureComponent {
|
|||
emptyMessage: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
bindToDocument: PropTypes.bool,
|
||||
preventScroll: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -129,7 +138,7 @@ export default class ScrollableList extends PureComponent {
|
|||
});
|
||||
|
||||
handleMouseIdle = () => {
|
||||
if (this.scrollToTopOnMouseIdle) {
|
||||
if (this.scrollToTopOnMouseIdle && !this.props.preventScroll) {
|
||||
this.setScrollTop(0);
|
||||
}
|
||||
|
||||
|
@ -179,7 +188,7 @@ 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.getScrollTop() > 0 || this.mouseMovedRecently)) {
|
||||
if (pendingChanged || someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently || this.props.preventScroll)) {
|
||||
return this.getScrollHeight() - this.getScrollTop();
|
||||
} else {
|
||||
return null;
|
||||
|
|
|
@ -94,6 +94,7 @@ class Status extends ImmutablePureComponent {
|
|||
updateScrollBottom: PropTypes.func,
|
||||
cacheMediaWidth: PropTypes.func,
|
||||
cachedMediaWidth: PropTypes.number,
|
||||
scrollKey: PropTypes.string,
|
||||
};
|
||||
|
||||
// Avoid checking props that are functions (and whose equality will always
|
||||
|
@ -264,7 +265,7 @@ class Status extends ImmutablePureComponent {
|
|||
let media = null;
|
||||
let statusAvatar, prepend, rebloggedByText;
|
||||
|
||||
const { intl, hidden, featured, otherAccounts, unread, showThread } = this.props;
|
||||
const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey } = this.props;
|
||||
|
||||
let { status, account, ...other } = this.props;
|
||||
|
||||
|
@ -459,7 +460,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
{media}
|
||||
|
||||
<StatusActionBar status={status} account={account} {...other} />
|
||||
<StatusActionBar scrollKey={scrollKey} status={status} account={account} {...other} />
|
||||
</div>
|
||||
</div>
|
||||
</HotKeys>
|
||||
|
|
|
@ -85,6 +85,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
onPin: PropTypes.func,
|
||||
onBookmark: PropTypes.func,
|
||||
withDismiss: PropTypes.bool,
|
||||
scrollKey: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
@ -229,7 +230,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { status, relationship, intl, withDismiss } = this.props;
|
||||
const { status, relationship, intl, withDismiss, scrollKey } = this.props;
|
||||
|
||||
const mutingConversation = status.get('muted');
|
||||
const anonymousAccess = !me;
|
||||
|
@ -333,7 +334,16 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
{shareButton}
|
||||
|
||||
<div className='status__action-bar-dropdown'>
|
||||
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} />
|
||||
<DropdownMenuContainer
|
||||
scrollKey={scrollKey}
|
||||
disabled={anonymousAccess}
|
||||
status={status}
|
||||
items={menu}
|
||||
icon='ellipsis-h'
|
||||
size={18}
|
||||
direction='right'
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -99,6 +99,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
contextType={timelineId}
|
||||
scrollKey={this.props.scrollKey}
|
||||
showThread
|
||||
/>
|
||||
))
|
||||
|
|
|
@ -12,7 +12,7 @@ const mapStateToProps = state => ({
|
|||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { status, items }) => ({
|
||||
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||
onOpen(id, onItemClick, dropdownPlacement, keyboard) {
|
||||
if (status) {
|
||||
dispatch(fetchRelationships([status.getIn(['account', 'id'])]));
|
||||
|
@ -22,7 +22,7 @@ const mapDispatchToProps = (dispatch, { status, items }) => ({
|
|||
status,
|
||||
actions: items,
|
||||
onClick: onItemClick,
|
||||
}) : openDropdownMenu(id, dropdownPlacement, keyboard));
|
||||
}) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey));
|
||||
},
|
||||
|
||||
onClose(id) {
|
||||
|
|
|
@ -36,6 +36,7 @@ class Conversation extends ImmutablePureComponent {
|
|||
accounts: ImmutablePropTypes.list.isRequired,
|
||||
lastStatus: ImmutablePropTypes.map,
|
||||
unread:PropTypes.bool.isRequired,
|
||||
scrollKey: PropTypes.string,
|
||||
onMoveUp: PropTypes.func,
|
||||
onMoveDown: PropTypes.func,
|
||||
markRead: PropTypes.func.isRequired,
|
||||
|
@ -127,7 +128,7 @@ class Conversation extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { accounts, lastStatus, unread, intl } = this.props;
|
||||
const { accounts, lastStatus, unread, scrollKey, intl } = this.props;
|
||||
|
||||
if (lastStatus === null) {
|
||||
return null;
|
||||
|
@ -194,7 +195,15 @@ class Conversation extends ImmutablePureComponent {
|
|||
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReply} />
|
||||
|
||||
<div className='status__action-bar-dropdown'>
|
||||
<DropdownMenuContainer status={lastStatus} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} />
|
||||
<DropdownMenuContainer
|
||||
scrollKey={scrollKey}
|
||||
status={lastStatus}
|
||||
items={menu}
|
||||
icon='ellipsis-h'
|
||||
size={18}
|
||||
direction='right'
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -10,6 +10,7 @@ export default class ConversationsList extends ImmutablePureComponent {
|
|||
|
||||
static propTypes = {
|
||||
conversations: ImmutablePropTypes.list.isRequired,
|
||||
scrollKey: PropTypes.string.isRequired,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
onLoadMore: PropTypes.func,
|
||||
|
@ -58,13 +59,14 @@ export default class ConversationsList extends ImmutablePureComponent {
|
|||
const { conversations, onLoadMore, ...other } = this.props;
|
||||
|
||||
return (
|
||||
<ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} scrollKey='direct' ref={this.setRef}>
|
||||
<ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
|
||||
{conversations.map(item => (
|
||||
<ConversationContainer
|
||||
key={item.get('id')}
|
||||
conversationId={item.get('id')}
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
scrollKey={this.props.scrollKey}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
|
|
@ -4,14 +4,14 @@ import {
|
|||
DROPDOWN_MENU_CLOSE,
|
||||
} from '../actions/dropdown_menu';
|
||||
|
||||
const initialState = Immutable.Map({ openId: null, placement: null, keyboard: false });
|
||||
const initialState = Immutable.Map({ openId: null, placement: null, keyboard: false, scroll_key: null });
|
||||
|
||||
export default function dropdownMenu(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case DROPDOWN_MENU_OPEN:
|
||||
return state.merge({ openId: action.id, placement: action.placement, keyboard: action.keyboard });
|
||||
return state.merge({ openId: action.id, placement: action.placement, keyboard: action.keyboard, scroll_key: action.scroll_key });
|
||||
case DROPDOWN_MENU_CLOSE:
|
||||
return state.get('openId') === action.id ? state.set('openId', null) : state;
|
||||
return state.get('openId') === action.id ? state.set('openId', null).set('scroll_key', null) : state;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue