Page MenuHomePhorge

D5850.1768786294.diff
No OneTemporary

Size
12 KB
Referenced Files
None
Subscribers
None

D5850.1768786294.diff

diff --git a/web/chat/chat-input-bar.react.js b/web/chat/chat-input-bar.react.js
--- a/web/chat/chat-input-bar.react.js
+++ b/web/chat/chat-input-bar.react.js
@@ -110,6 +110,17 @@
if (inputState.draft !== prevInputState.draft) {
this.updateHeight();
}
+
+ if (
+ inputState.draft !== prevInputState.draft ||
+ inputState.textCursorPosition !== prevInputState.textCursorPosition
+ ) {
+ inputState.setTypeaheadState({
+ isVisible: true,
+ chosenButtonNumber: 0,
+ });
+ }
+
const curUploadIDs = ChatInputBar.unassignedUploadIDs(
inputState.pendingUploads,
);
@@ -330,7 +341,11 @@
}
let typeaheadTooltip;
- if (this.props.typeaheadMatchedStrings && this.textarea) {
+ if (
+ this.props.typeaheadMatchedStrings &&
+ this.textarea &&
+ this.props.inputState.typeaheadState.isVisible
+ ) {
typeaheadTooltip = (
<TypeaheadTooltip
inputState={this.props.inputState}
@@ -404,9 +419,33 @@
};
onKeyDown = (event: SyntheticKeyboardEvent<HTMLTextAreaElement>) => {
- if (event.key === 'Enter' && !event.shiftKey) {
+ const accept = this.props.inputState.typeaheadState.accept;
+ const close = this.props.inputState.typeaheadState.close;
+
+ if (!this.props.inputState.typeaheadState.isVisible || !accept || !close) {
+ if (event.key === 'Enter' && !event.shiftKey) {
+ event.preventDefault();
+ this.send();
+ }
+ } else if (event.key === 'Enter' || event.key === 'Tab') {
+ event.preventDefault();
+ accept();
+ close();
+ } else if (event.key === 'ArrowDown') {
+ event.preventDefault();
+ this.props.inputState.setTypeaheadState({
+ chosenButtonNumber:
+ (this.props.inputState.typeaheadState.chosenButtonNumber ?? 0) + 1,
+ });
+ } else if (event.key === 'ArrowUp') {
+ event.preventDefault();
+ this.props.inputState.setTypeaheadState({
+ chosenButtonNumber:
+ (this.props.inputState.typeaheadState.chosenButtonNumber ?? 0) - 1,
+ });
+ } else if (event.key === 'Escape') {
event.preventDefault();
- this.send();
+ close();
}
};
diff --git a/web/chat/typeahead-tooltip.css b/web/chat/typeahead-tooltip.css
--- a/web/chat/typeahead-tooltip.css
+++ b/web/chat/typeahead-tooltip.css
@@ -53,7 +53,7 @@
white-space: nowrap;
}
-.suggestion:hover {
+.suggestionHover {
background-color: var(--typeahead-overlay-light);
}
diff --git a/web/chat/typeahead-tooltip.react.js b/web/chat/typeahead-tooltip.react.js
--- a/web/chat/typeahead-tooltip.react.js
+++ b/web/chat/typeahead-tooltip.react.js
@@ -7,14 +7,16 @@
import { threadOtherMembers } from 'lib/shared/thread-utils';
import type { RelativeMemberInfo } from 'lib/types/thread-types';
-import Button from '../components/button.react';
import type { InputState } from '../input/input-state';
import { type TypeaheadMatchedStrings } from './chat-input-bar.react';
import css from './typeahead-tooltip.css';
import {
+ getTypeaheadOverlayScroll,
getTypeaheadTooltipActions,
+ getTypeaheadTooltipButtons,
getTypeaheadTooltipPosition,
getTypeaheadUserSuggestions,
+ getTypeaheadChosenActionPosition,
} from './typeahead-utils';
export type TypeaheadTooltipProps = {
@@ -35,13 +37,6 @@
viewerID,
matchedStrings,
} = props;
- const [isVisible, setIsVisible] = React.useState(false);
-
- React.useEffect(() => {
- setIsVisible(true);
-
- return () => setIsVisible(false);
- }, [setIsVisible]);
const {
entireText: matchedText,
@@ -51,28 +46,37 @@
const typedPrefix = matchedUsernamePrefix ?? '';
- const suggestedUsers = React.useMemo(
- () =>
- getTypeaheadUserSuggestions(
- userSearchIndex,
- threadOtherMembers(threadMembers, viewerID),
- typedPrefix,
- ),
- [userSearchIndex, threadMembers, viewerID, typedPrefix],
- );
+ const [animation, setAnimation] = React.useState(false);
+ const overlayRef = React.useRef<?HTMLDivElement>();
+
+ React.useEffect(() => {
+ setAnimation(true);
+
+ return () => setAnimation(false);
+ }, [setAnimation]);
+
+ const suggestedUsers = React.useMemo(() => {
+ return getTypeaheadUserSuggestions(
+ userSearchIndex,
+ threadOtherMembers(threadMembers, viewerID),
+ typedPrefix,
+ );
+ }, [userSearchIndex, threadMembers, viewerID, typedPrefix]);
const actions = React.useMemo(
() =>
- getTypeaheadTooltipActions(
- inputState,
- textarea,
+ getTypeaheadTooltipActions({
+ inputStateDraft: inputState.draft,
+ inputStateSetDraft: inputState.setDraft,
+ inputStateSetTextCursorPosition: inputState.setTextCursorPosition,
suggestedUsers,
matchedTextBeforeAtSymbol,
matchedText,
- ),
+ }),
[
- inputState,
- textarea,
+ inputState.draft,
+ inputState.setDraft,
+ inputState.setTextCursorPosition,
suggestedUsers,
matchedTextBeforeAtSymbol,
matchedText,
@@ -97,25 +101,80 @@
[tooltipPosition],
);
+ const chosenActionPosition = React.useMemo(
+ () =>
+ getTypeaheadChosenActionPosition(
+ inputState.typeaheadState.chosenButtonNumber,
+ actions.length,
+ ),
+ [inputState.typeaheadState.chosenButtonNumber, actions.length],
+ );
+
const tooltipButtons = React.useMemo(() => {
- return actions.map(({ key, onClick, actionButtonContent }) => (
- <Button key={key} onClick={onClick} className={css.suggestion}>
- <span>@{actionButtonContent}</span>
- </Button>
- ));
- }, [actions]);
+ return getTypeaheadTooltipButtons(
+ inputState.setTypeaheadState,
+ actions,
+ chosenActionPosition,
+ );
+ }, [inputState.setTypeaheadState, actions, chosenActionPosition]);
+
+ const close = React.useCallback(() => {
+ const setter = inputState.setTypeaheadState;
+ setter({
+ isVisible: false,
+ chosenButtonNumber: 0,
+ close: null,
+ accept: null,
+ });
+ }, [inputState.setTypeaheadState]);
+
+ const accept = React.useCallback(() => {
+ actions[chosenActionPosition].onClick();
+ }, [actions, chosenActionPosition]);
- if (!actions || actions.length === 0) {
- return null;
- }
+ React.useEffect(() => {
+ const setter = inputState.setTypeaheadState;
+ setter({
+ close: close,
+ accept: accept,
+ });
+ }, [close, accept, inputState.setTypeaheadState]);
+
+ React.useEffect(() => {
+ const current = overlayRef.current;
+ if (current) {
+ current.scrollTop = getTypeaheadOverlayScroll(
+ current.scrollTop,
+ chosenActionPosition,
+ );
+ }
+ }, [chosenActionPosition]);
+
+ React.useEffect(() => {
+ const current = overlayRef.current;
+ if (current) {
+ current.scrollTop = getTypeaheadOverlayScroll(
+ current.scrollTop,
+ chosenActionPosition,
+ );
+ }
+ }, [chosenActionPosition]);
const overlayClasses = classNames(css.suggestionsContainer, {
- [css.notVisible]: !isVisible,
- [css.visible]: isVisible,
+ [css.notVisible]: !animation,
+ [css.visible]: animation,
});
+ if (!actions || actions.length === 0) {
+ return null;
+ }
+
return (
- <div className={overlayClasses} style={tooltipPositionStyle}>
+ <div
+ ref={overlayRef}
+ className={overlayClasses}
+ style={tooltipPositionStyle}
+ >
{tooltipButtons}
</div>
);
diff --git a/web/chat/typeahead-utils.js b/web/chat/typeahead-utils.js
--- a/web/chat/typeahead-utils.js
+++ b/web/chat/typeahead-utils.js
@@ -1,5 +1,6 @@
// @flow
+import classNames from 'classnames';
import * as React from 'react';
import { oldValidUsernameRegexString } from 'lib/shared/account-utils';
@@ -7,16 +8,18 @@
import { stringForUserExplicit } from 'lib/shared/user-utils';
import type { RelativeMemberInfo } from 'lib/types/thread-types';
+import Button from '../components/button.react';
+import type { TypeaheadState } from '../input/input-state';
+import { typeaheadStyle } from './chat-constants';
+import css from './typeahead-tooltip.css';
+
const typeaheadRegex: RegExp = new RegExp(
`(?<textPrefix>(?:^(?:.|\n)*\\s+)|^)@(?<username>${oldValidUsernameRegexString})?$`,
);
-import { type InputState } from '../input/input-state';
-import { typeaheadStyle } from './chat-constants';
-
export type TypeaheadTooltipAction = {
+key: string,
- +onClick: (SyntheticEvent<HTMLButtonElement>) => mixed,
+ +onClick: () => mixed,
+actionButtonContent: React.Node,
};
@@ -86,14 +89,26 @@
caretLeftOffset,
};
}
-
-function getTypeaheadTooltipActions(
- inputState: InputState,
- textarea: HTMLTextAreaElement,
+export type getTypeaheadTooltipActionsParams = {
+ inputStateDraft: string,
+ inputStateSetDraft: (draft: string) => void,
+ inputStateSetTextCursorPosition: (newPosition: number) => void,
suggestedUsers: $ReadOnlyArray<RelativeMemberInfo>,
- matchedTextBefore: string,
+ matchedTextBeforeAtSymbol: string,
matchedText: string,
+};
+
+function getTypeaheadTooltipActions(
+ params: getTypeaheadTooltipActionsParams,
): $ReadOnlyArray<TypeaheadTooltipAction> {
+ const {
+ inputStateDraft,
+ inputStateSetDraft,
+ inputStateSetTextCursorPosition,
+ suggestedUsers,
+ matchedTextBeforeAtSymbol,
+ matchedText,
+ } = params;
return suggestedUsers
.filter(
suggestedUser => stringForUserExplicit(suggestedUser) !== 'anonymous',
@@ -101,9 +116,9 @@
.map(suggestedUser => ({
key: suggestedUser.id,
onClick: () => {
- const newPrefixText = matchedTextBefore;
+ const newPrefixText = matchedTextBeforeAtSymbol;
- let newSuffixText = inputState.draft.slice(matchedText.length);
+ let newSuffixText = inputStateDraft.slice(matchedText.length);
newSuffixText = (newSuffixText[0] !== ' ' ? ' ' : '') + newSuffixText;
const newText =
@@ -112,14 +127,71 @@
stringForUserExplicit(suggestedUser) +
newSuffixText;
- inputState.setDraft(newText);
- inputState.setTextCursorPosition(
+ inputStateSetDraft(newText);
+ inputStateSetTextCursorPosition(
newText.length - newSuffixText.length + 1,
);
},
actionButtonContent: stringForUserExplicit(suggestedUser),
}));
}
+
+function getTypeaheadTooltipButtons(
+ inputStateSetTypeaheadState: ($Shape<TypeaheadState>) => void,
+ actions: $ReadOnlyArray<TypeaheadTooltipAction>,
+ chosenActionPosition: number,
+): $ReadOnlyArray<React.Node> {
+ return actions.map(({ key, onClick, actionButtonContent }, idx) => {
+ const buttonClasses = classNames(css.suggestion, {
+ [css.suggestionHover]: idx === chosenActionPosition,
+ });
+
+ const onMouseMove: (
+ event: SyntheticEvent<HTMLButtonElement>,
+ ) => mixed = () => {
+ inputStateSetTypeaheadState({
+ chosenButtonNumber: idx,
+ });
+ };
+
+ return (
+ <Button
+ key={key}
+ onClick={onClick}
+ onMouseMove={onMouseMove}
+ className={buttonClasses}
+ >
+ <span>@{actionButtonContent}</span>
+ </Button>
+ );
+ });
+}
+
+function getTypeaheadOverlayScroll(
+ currentScrollTop: number,
+ chosenActionPosition: number,
+): number {
+ let newScrollTop = currentScrollTop;
+
+ const upperButtonBoundary = chosenActionPosition * typeaheadStyle.rowHeight;
+ const lowerButtonBoundary =
+ (chosenActionPosition + 1) * typeaheadStyle.rowHeight;
+
+ if (upperButtonBoundary < currentScrollTop) {
+ newScrollTop = upperButtonBoundary;
+ } else if (
+ lowerButtonBoundary - typeaheadStyle.tooltipMaxHeight >
+ currentScrollTop
+ ) {
+ newScrollTop =
+ lowerButtonBoundary +
+ typeaheadStyle.tooltipVerticalPadding -
+ typeaheadStyle.tooltipMaxHeight;
+ }
+
+ return newScrollTop;
+}
+
function getTypeaheadTooltipPosition(
textarea: HTMLTextAreaElement,
actionsLength: number,
@@ -150,10 +222,26 @@
return { top, left };
}
+function getTypeaheadChosenActionPosition(
+ chosenButtonNumber: number,
+ actionsLength: number,
+): number {
+ // Getting positive modulo of chosenButtonNumber
+ return (
+ (chosenButtonNumber +
+ Math.abs(Math.floor(chosenButtonNumber / actionsLength)) *
+ actionsLength) %
+ actionsLength
+ );
+}
+
export {
typeaheadRegex,
getTypeaheadUserSuggestions,
getCaretOffsets,
getTypeaheadTooltipActions,
+ getTypeaheadTooltipButtons,
+ getTypeaheadOverlayScroll,
getTypeaheadTooltipPosition,
+ getTypeaheadChosenActionPosition,
};

File Metadata

Mime Type
text/plain
Expires
Mon, Jan 19, 1:31 AM (11 h, 54 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5953798
Default Alt Text
D5850.1768786294.diff (12 KB)

Event Timeline