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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user