fix(chat): maintain bottom scroll on input resize

This commit is contained in:
Leonard Kim 2019-05-08 10:45:32 -07:00 committed by virtuacoplenny
parent dfe5fbb702
commit d86b60ea72
3 changed files with 74 additions and 2 deletions

View File

@ -47,6 +47,9 @@ class Chat extends AbstractChat<Props> {
// Bind event handlers so they are only bound once for every instance. // Bind event handlers so they are only bound once for every instance.
this._renderPanelContent = this._renderPanelContent.bind(this); this._renderPanelContent = this._renderPanelContent.bind(this);
// Bind event handlers so they are only bound once for every instance.
this._onChatInputResize = this._onChatInputResize.bind(this);
} }
/** /**
@ -87,6 +90,19 @@ class Chat extends AbstractChat<Props> {
); );
} }
_onChatInputResize: () => void;
/**
* Callback invoked when {@code ChatInput} changes height. Preserves
* displaying the latest message if it is scrolled to.
*
* @private
* @returns {void}
*/
_onChatInputResize() {
this._messageContainerRef.current.maybeUpdateBottomScroll();
}
/** /**
* Returns a React Element for showing chat messages and a form to send new * Returns a React Element for showing chat messages and a form to send new
* chat messages. * chat messages.
@ -100,7 +116,7 @@ class Chat extends AbstractChat<Props> {
<MessageContainer <MessageContainer
messages = { this.props._messages } messages = { this.props._messages }
ref = { this._messageContainerRef } /> ref = { this._messageContainerRef } />
<ChatInput /> <ChatInput onResize = { this._onChatInputResize } />
</> </>
); );
} }

View File

@ -22,6 +22,12 @@ type Props = {
*/ */
dispatch: Dispatch<any>, dispatch: Dispatch<any>,
/**
* Optional callback to invoke when the chat textarea has auto-resized to
* fit overflowing text.
*/
onResize: ?Function,
/** /**
* Invoked to obtain translated strings. * Invoked to obtain translated strings.
*/ */
@ -120,6 +126,7 @@ class ChatInput extends Component<Props, State> {
inputRef = { this._setTextAreaRef } inputRef = { this._setTextAreaRef }
maxRows = { 5 } maxRows = { 5 }
onChange = { this._onMessageChange } onChange = { this._onMessageChange }
onHeightChange = { this.props.onResize }
onKeyDown = { this._onDetectSubmit } onKeyDown = { this._onDetectSubmit }
placeholder = { this.props.t('chat.messagebox') } placeholder = { this.props.t('chat.messagebox') }
value = { this.state.message } /> value = { this.state.message } />

View File

@ -13,12 +13,25 @@ import ChatMessageGroup from './ChatMessageGroup';
* @extends AbstractMessageContainer * @extends AbstractMessageContainer
*/ */
export default class MessageContainer extends AbstractMessageContainer { export default class MessageContainer extends AbstractMessageContainer {
/**
* Whether or not chat has been scrolled to the bottom of the screen. Used
* to determine if chat should be scrolled automatically to the bottom when
* the {@code ChatInput} resizes.
*/
_isScrolledToBottom: boolean;
/** /**
* Reference to the HTML element at the end of the list of displayed chat * Reference to the HTML element at the end of the list of displayed chat
* messages. Used for scrolling to the end of the chat messages. * messages. Used for scrolling to the end of the chat messages.
*/ */
_messagesListEndRef: Object; _messagesListEndRef: Object;
/**
* A React ref to the HTML element containing all {@code ChatMessageGroup}
* instances.
*/
_messageListRef: Object;
/** /**
* Initializes a new {@code MessageContainer} instance. * Initializes a new {@code MessageContainer} instance.
* *
@ -28,7 +41,12 @@ export default class MessageContainer extends AbstractMessageContainer {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this._isScrolledToBottom = true;
this._messageListRef = React.createRef();
this._messagesListEndRef = React.createRef(); this._messagesListEndRef = React.createRef();
this._onChatScroll = this._onChatScroll.bind(this);
} }
/** /**
@ -50,13 +68,29 @@ export default class MessageContainer extends AbstractMessageContainer {
}); });
return ( return (
<div id = 'chatconversation'> <div
id = 'chatconversation'
onScroll = { this._onChatScroll }
ref = { this._messageListRef }>
{ messages } { messages }
<div ref = { this._messagesListEndRef } /> <div ref = { this._messagesListEndRef } />
</div> </div>
); );
} }
/**
* Scrolls to the bottom again if the instance had previously been scrolled
* to the bottom. This method is used when a resize has occurred below the
* instance and bottom scroll needs to be maintained.
*
* @returns {void}
*/
maybeUpdateBottomScroll() {
if (this._isScrolledToBottom) {
this.scrollToBottom(false);
}
}
/** /**
* Automatically scrolls the displayed chat messages down to the latest. * Automatically scrolls the displayed chat messages down to the latest.
* *
@ -71,4 +105,19 @@ export default class MessageContainer extends AbstractMessageContainer {
} }
_getMessagesGroupedBySender: () => Array<Array<Object>>; _getMessagesGroupedBySender: () => Array<Array<Object>>;
_onChatScroll: () => void;
/**
* Callback invoked to listen to the current scroll location.
*
* @private
* @returns {void}
*/
_onChatScroll() {
const element = this._messageListRef.current;
this._isScrolledToBottom
= element.scrollHeight - element.scrollTop === element.clientHeight;
}
} }