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_OPEN = 'DROPDOWN_MENU_OPEN'; | ||||||
| export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE'; | export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE'; | ||||||
| 
 | 
 | ||||||
| export function openDropdownMenu(id, placement, keyboard) { | export function openDropdownMenu(id, placement, keyboard, scroll_key) { | ||||||
|   return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard }; |   return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard, scroll_key }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function closeDropdownMenu(id) { | export function closeDropdownMenu(id) { | ||||||
|  | |||||||
| @ -10,10 +10,18 @@ import { List as ImmutableList } from 'immutable'; | |||||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||||
| import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen'; | import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen'; | ||||||
| import LoadingIndicator from './loading_indicator'; | import LoadingIndicator from './loading_indicator'; | ||||||
|  | import { connect } from 'react-redux'; | ||||||
| 
 | 
 | ||||||
| const MOUSE_IDLE_DELAY = 300; | 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 = { |   static contextTypes = { | ||||||
|     router: PropTypes.object, |     router: PropTypes.object, | ||||||
| @ -37,6 +45,7 @@ export default class ScrollableList extends PureComponent { | |||||||
|     emptyMessage: PropTypes.node, |     emptyMessage: PropTypes.node, | ||||||
|     children: PropTypes.node, |     children: PropTypes.node, | ||||||
|     bindToDocument: PropTypes.bool, |     bindToDocument: PropTypes.bool, | ||||||
|  |     preventScroll: PropTypes.bool, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   static defaultProps = { |   static defaultProps = { | ||||||
| @ -129,7 +138,7 @@ export default class ScrollableList extends PureComponent { | |||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   handleMouseIdle = () => { |   handleMouseIdle = () => { | ||||||
|     if (this.scrollToTopOnMouseIdle) { |     if (this.scrollToTopOnMouseIdle && !this.props.preventScroll) { | ||||||
|       this.setScrollTop(0); |       this.setScrollTop(0); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -179,7 +188,7 @@ export default class ScrollableList extends PureComponent { | |||||||
|       this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); |       this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); | ||||||
|     const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0); |     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(); |       return this.getScrollHeight() - this.getScrollTop(); | ||||||
|     } else { |     } else { | ||||||
|       return null; |       return null; | ||||||
|  | |||||||
| @ -94,6 +94,7 @@ class Status extends ImmutablePureComponent { | |||||||
|     updateScrollBottom: PropTypes.func, |     updateScrollBottom: PropTypes.func, | ||||||
|     cacheMediaWidth: PropTypes.func, |     cacheMediaWidth: PropTypes.func, | ||||||
|     cachedMediaWidth: PropTypes.number, |     cachedMediaWidth: PropTypes.number, | ||||||
|  |     scrollKey: PropTypes.string, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   // Avoid checking props that are functions (and whose equality will always
 |   // Avoid checking props that are functions (and whose equality will always
 | ||||||
| @ -264,7 +265,7 @@ class Status extends ImmutablePureComponent { | |||||||
|     let media = null; |     let media = null; | ||||||
|     let statusAvatar, prepend, rebloggedByText; |     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; |     let { status, account, ...other } = this.props; | ||||||
| 
 | 
 | ||||||
| @ -459,7 +460,7 @@ class Status extends ImmutablePureComponent { | |||||||
| 
 | 
 | ||||||
|             {media} |             {media} | ||||||
| 
 | 
 | ||||||
|             <StatusActionBar status={status} account={account} {...other} /> |             <StatusActionBar scrollKey={scrollKey} status={status} account={account} {...other} /> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </HotKeys> |       </HotKeys> | ||||||
|  | |||||||
| @ -85,6 +85,7 @@ class StatusActionBar extends ImmutablePureComponent { | |||||||
|     onPin: PropTypes.func, |     onPin: PropTypes.func, | ||||||
|     onBookmark: PropTypes.func, |     onBookmark: PropTypes.func, | ||||||
|     withDismiss: PropTypes.bool, |     withDismiss: PropTypes.bool, | ||||||
|  |     scrollKey: PropTypes.string, | ||||||
|     intl: PropTypes.object.isRequired, |     intl: PropTypes.object.isRequired, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
| @ -229,7 +230,7 @@ class StatusActionBar extends ImmutablePureComponent { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { status, relationship, intl, withDismiss } = this.props; |     const { status, relationship, intl, withDismiss, scrollKey } = this.props; | ||||||
| 
 | 
 | ||||||
|     const mutingConversation = status.get('muted'); |     const mutingConversation = status.get('muted'); | ||||||
|     const anonymousAccess    = !me; |     const anonymousAccess    = !me; | ||||||
| @ -333,7 +334,16 @@ class StatusActionBar extends ImmutablePureComponent { | |||||||
|         {shareButton} |         {shareButton} | ||||||
| 
 | 
 | ||||||
|         <div className='status__action-bar-dropdown'> |         <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> | ||||||
|       </div> |       </div> | ||||||
|     ); |     ); | ||||||
|  | |||||||
| @ -99,6 +99,7 @@ export default class StatusList extends ImmutablePureComponent { | |||||||
|           onMoveUp={this.handleMoveUp} |           onMoveUp={this.handleMoveUp} | ||||||
|           onMoveDown={this.handleMoveDown} |           onMoveDown={this.handleMoveDown} | ||||||
|           contextType={timelineId} |           contextType={timelineId} | ||||||
|  |           scrollKey={this.props.scrollKey} | ||||||
|           showThread |           showThread | ||||||
|         /> |         /> | ||||||
|       )) |       )) | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ const mapStateToProps = state => ({ | |||||||
|   openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']), |   openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']), | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const mapDispatchToProps = (dispatch, { status, items }) => ({ | const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({ | ||||||
|   onOpen(id, onItemClick, dropdownPlacement, keyboard) { |   onOpen(id, onItemClick, dropdownPlacement, keyboard) { | ||||||
|     if (status) { |     if (status) { | ||||||
|       dispatch(fetchRelationships([status.getIn(['account', 'id'])])); |       dispatch(fetchRelationships([status.getIn(['account', 'id'])])); | ||||||
| @ -22,7 +22,7 @@ const mapDispatchToProps = (dispatch, { status, items }) => ({ | |||||||
|       status, |       status, | ||||||
|       actions: items, |       actions: items, | ||||||
|       onClick: onItemClick, |       onClick: onItemClick, | ||||||
|     }) : openDropdownMenu(id, dropdownPlacement, keyboard)); |     }) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey)); | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   onClose(id) { |   onClose(id) { | ||||||
|  | |||||||
| @ -36,6 +36,7 @@ class Conversation extends ImmutablePureComponent { | |||||||
|     accounts: ImmutablePropTypes.list.isRequired, |     accounts: ImmutablePropTypes.list.isRequired, | ||||||
|     lastStatus: ImmutablePropTypes.map, |     lastStatus: ImmutablePropTypes.map, | ||||||
|     unread:PropTypes.bool.isRequired, |     unread:PropTypes.bool.isRequired, | ||||||
|  |     scrollKey: PropTypes.string, | ||||||
|     onMoveUp: PropTypes.func, |     onMoveUp: PropTypes.func, | ||||||
|     onMoveDown: PropTypes.func, |     onMoveDown: PropTypes.func, | ||||||
|     markRead: PropTypes.func.isRequired, |     markRead: PropTypes.func.isRequired, | ||||||
| @ -127,7 +128,7 @@ class Conversation extends ImmutablePureComponent { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { accounts, lastStatus, unread, intl } = this.props; |     const { accounts, lastStatus, unread, scrollKey, intl } = this.props; | ||||||
| 
 | 
 | ||||||
|     if (lastStatus === null) { |     if (lastStatus === null) { | ||||||
|       return 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} /> |               <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReply} /> | ||||||
| 
 | 
 | ||||||
|               <div className='status__action-bar-dropdown'> |               <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> |             </div> | ||||||
|           </div> |           </div> | ||||||
|  | |||||||
| @ -10,6 +10,7 @@ export default class ConversationsList extends ImmutablePureComponent { | |||||||
| 
 | 
 | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     conversations: ImmutablePropTypes.list.isRequired, |     conversations: ImmutablePropTypes.list.isRequired, | ||||||
|  |     scrollKey: PropTypes.string.isRequired, | ||||||
|     hasMore: PropTypes.bool, |     hasMore: PropTypes.bool, | ||||||
|     isLoading: PropTypes.bool, |     isLoading: PropTypes.bool, | ||||||
|     onLoadMore: PropTypes.func, |     onLoadMore: PropTypes.func, | ||||||
| @ -58,13 +59,14 @@ export default class ConversationsList extends ImmutablePureComponent { | |||||||
|     const { conversations, onLoadMore, ...other } = this.props; |     const { conversations, onLoadMore, ...other } = this.props; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|       <ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} scrollKey='direct' ref={this.setRef}> |       <ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}> | ||||||
|         {conversations.map(item => ( |         {conversations.map(item => ( | ||||||
|           <ConversationContainer |           <ConversationContainer | ||||||
|             key={item.get('id')} |             key={item.get('id')} | ||||||
|             conversationId={item.get('id')} |             conversationId={item.get('id')} | ||||||
|             onMoveUp={this.handleMoveUp} |             onMoveUp={this.handleMoveUp} | ||||||
|             onMoveDown={this.handleMoveDown} |             onMoveDown={this.handleMoveDown} | ||||||
|  |             scrollKey={this.props.scrollKey} | ||||||
|           /> |           /> | ||||||
|         ))} |         ))} | ||||||
|       </ScrollableList> |       </ScrollableList> | ||||||
|  | |||||||
| @ -4,14 +4,14 @@ import { | |||||||
|   DROPDOWN_MENU_CLOSE, |   DROPDOWN_MENU_CLOSE, | ||||||
| } from '../actions/dropdown_menu'; | } 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) { | export default function dropdownMenu(state = initialState, action) { | ||||||
|   switch (action.type) { |   switch (action.type) { | ||||||
|   case DROPDOWN_MENU_OPEN: |   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: |   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: |   default: | ||||||
|     return state; |     return state; | ||||||
|   } |   } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user