From c05eceef7f3560adad8354c0aef0f32d38684718 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 16 Feb 2021 17:56:09 +0000 Subject: [PATCH 1/8] Rework composer autocomplete to be smarter and not trap tab --- src/components/views/rooms/Autocomplete.tsx | 34 ++++++++++++------- .../views/rooms/BasicMessageComposer.tsx | 26 ++++++++++---- src/editor/autocomplete.ts | 5 ++- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 15af75084a..e62a5a9bd6 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -24,8 +24,6 @@ import {Room} from 'matrix-js-sdk/src/models/room'; import SettingsStore from "../../../settings/SettingsStore"; import Autocompleter from '../../../autocomplete/Autocompleter'; -const COMPOSER_SELECTED = 0; - export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`; interface IProps { @@ -68,7 +66,7 @@ export default class Autocomplete extends React.PureComponent { completionList: [], // how far down the completion list we are (THIS IS 1-INDEXED!) - selectionOffset: COMPOSER_SELECTED, + selectionOffset: 1, // whether we should show completions if they're available shouldShowCompletions: true, @@ -112,7 +110,7 @@ export default class Autocomplete extends React.PureComponent { completions: [], completionList: [], // Reset selected completion - selectionOffset: COMPOSER_SELECTED, + selectionOffset: 1, // Hide the autocomplete box hide: true, }); @@ -148,26 +146,31 @@ export default class Autocomplete extends React.PureComponent { const completionList = flatMap(completions, (provider) => provider.completions); // Reset selection when completion list becomes empty. - let selectionOffset = COMPOSER_SELECTED; + let selectionOffset = 1; if (completionList.length > 0) { /* If the currently selected completion is still in the completion list, try to find it and jump to it. If not, select composer. */ - const currentSelection = this.state.selectionOffset === 0 ? null : + const currentSelection = this.state.selectionOffset <= 1 ? null : this.state.completionList[this.state.selectionOffset - 1].completion; selectionOffset = completionList.findIndex( (completion) => completion.completion === currentSelection); if (selectionOffset === -1) { - selectionOffset = COMPOSER_SELECTED; + selectionOffset = 1; } else { selectionOffset++; // selectionOffset is 1-indexed! } } - let hide = this.state.hide; + let hide = true; // If `completion.command.command` is truthy, then a provider has matched with the query const anyMatches = completions.some((completion) => !!completion.command.command); - hide = !anyMatches; + if (anyMatches) { + hide = false; + if (this.props.onSelectionChange) { + this.props.onSelectionChange(this.state.completionList[selectionOffset - 1], selectionOffset - 1); + } + } this.setState({ completions, @@ -193,8 +196,8 @@ export default class Autocomplete extends React.PureComponent { if (completionCount === 0) return; // there are no items to move the selection through // Note: selectionOffset 0 represents the unsubstituted text, while 1 means first pill selected - const index = (this.state.selectionOffset + delta + completionCount + 1) % (completionCount + 1); - this.setSelection(index); + const index = (this.state.selectionOffset + delta + completionCount - 1) % completionCount; + this.setSelection(1 + index); } onEscape(e: KeyboardEvent): boolean { @@ -213,7 +216,7 @@ export default class Autocomplete extends React.PureComponent { hide = () => { this.setState({ hide: true, - selectionOffset: 0, + selectionOffset: 1, completions: [], completionList: [], }); @@ -232,8 +235,13 @@ export default class Autocomplete extends React.PureComponent { }); } + onConfirmCompletion = () => { + this.onCompletionClicked(this.state.selectionOffset); + } + onCompletionClicked = (selectionOffset: number): boolean => { - if (this.countCompletions() === 0 || selectionOffset === COMPOSER_SELECTED) { + const count = this.countCompletions(); + if (count === 0 || selectionOffset < 1 || selectionOffset > count) { return false; } diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 017ce77166..3c82f75b33 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -126,6 +126,7 @@ export default class BasicMessageEditor extends React.Component super(props); this.state = { showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"), + showVisualBell: false, }; this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null, @@ -201,7 +202,11 @@ export default class BasicMessageEditor extends React.Component if (isEmpty) { this.formatBarRef.current.hide(); } - this.setState({autoComplete: this.props.model.autoComplete}); + this.setState({ + autoComplete: this.props.model.autoComplete, + // if a change is happening then clear the showVisualBell + showVisualBell: diff ? false : this.state.showVisualBell, + }); this.historyManager.tryPush(this.props.model, selection, inputType, diff); let isTyping = !this.props.model.isEmpty; @@ -490,6 +495,7 @@ export default class BasicMessageEditor extends React.Component } break; case Key.TAB: + case Key.ENTER: if (!metaOrAltPressed) { autoComplete.onTab(event); handled = true; @@ -504,7 +510,7 @@ export default class BasicMessageEditor extends React.Component default: return; // don't preventDefault on anything else } - } else if (event.key === Key.TAB) { + } else if (!this.props.model.isEmpty && !this.state.showVisualBell && event.key === Key.TAB) { this.tabCompleteName(event); handled = true; } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { @@ -545,6 +551,8 @@ export default class BasicMessageEditor extends React.Component this.setState({showVisualBell: true}); model.autoComplete.close(); } + } else { + this.setState({showVisualBell: true}); } } catch (err) { console.error(err); @@ -562,7 +570,7 @@ export default class BasicMessageEditor extends React.Component private onAutoCompleteSelectionChange = (completion: ICompletion, completionIndex: number) => { this.modifiedFlag = true; - this.props.model.autoComplete.onComponentSelectionChange(completion); + // this.props.model.autoComplete.onComponentSelectionChange(completion); this.setState({completionIndex}); }; @@ -679,6 +687,11 @@ export default class BasicMessageEditor extends React.Component }; const {completionIndex} = this.state; + const hasAutocomplete = Boolean(this.state.autoComplete); + let activeDescendant; + if (hasAutocomplete && completionIndex >= 0) { + activeDescendant = generateCompletionDomId(completionIndex); + } return (
{ autoComplete } @@ -697,10 +710,11 @@ export default class BasicMessageEditor extends React.Component aria-label={this.props.label} role="textbox" aria-multiline="true" - aria-autocomplete="both" + aria-autocomplete="list" aria-haspopup="listbox" - aria-expanded={Boolean(this.state.autoComplete)} - aria-activedescendant={completionIndex >= 0 ? generateCompletionDomId(completionIndex) : undefined} + aria-expanded={hasAutocomplete} + aria-owns="mx_Autocomplete" + aria-activedescendant={activeDescendant} dir="auto" />
); diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts index d8cea961d4..4633cd56c2 100644 --- a/src/editor/autocomplete.ts +++ b/src/editor/autocomplete.ts @@ -74,10 +74,9 @@ export default class AutocompleteWrapperModel { if (acComponent.countCompletions() === 0) { // Force completions to show for the text currently entered await acComponent.forceComplete(); - // Select the first item by moving "down" - await acComponent.moveSelection(+1); } else { - await acComponent.moveSelection(e.shiftKey ? -1 : +1); + await acComponent.onConfirmCompletion(); + this.updateCallback({close: true}); } } From 9e2974d84d70cbdf4e01e8c813b53a3d73fd6133 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Feb 2021 10:25:25 +0000 Subject: [PATCH 2/8] Improve composer keyboard trapping --- src/components/structures/LoggedInView.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index c76cd7cee7..b04d840fb0 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -495,24 +495,24 @@ class LoggedInView extends React.Component { if (handled) { ev.stopPropagation(); ev.preventDefault(); - } else if (!isModifier && !ev.altKey && !ev.ctrlKey && !ev.metaKey) { + } else if (!isModifier && !ev.ctrlKey && !ev.metaKey) { // The above condition is crafted to _allow_ characters with Shift // already pressed (but not the Shift key down itself). - const isClickShortcut = ev.target !== document.body && (ev.key === Key.SPACE || ev.key === Key.ENTER); - // Do not capture the context menu key to improve keyboard accessibility - if (ev.key === Key.CONTEXT_MENU) { - return; - } + // We explicitly allow alt to be held due to it being a common accent modifier. + // XXX: Forwarding Dead keys in this way does not work as intended but better to at least + // move focus to the composer so the user can re-type the dead key correctly. + const isPrintable = ev.key.length === 1 || ev.key === "Dead"; - if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) { + // If the user is entering a printable character outside of an input field + // redirect it to the composer for them. + if (!isClickShortcut && isPrintable && !canElementReceiveInput(ev.target)) { // synchronous dispatch so we focus before key generates input dis.fire(Action.FocusComposer, true); ev.stopPropagation(); - // we should *not* preventDefault() here as - // that would prevent typing in the now-focussed composer + // we should *not* preventDefault() here as that would prevent typing in the now-focused composer } } }; From 9463fda1c10a26542bbfdd6ae0556ed8aa0fbb02 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Feb 2021 10:55:24 +0000 Subject: [PATCH 3/8] Improve VoiceOver & WebKit accessibility support Based on https://bugs.webkit.org/show_bug.cgi?id=167671#c15 (workaround) --- src/autocomplete/AutocompleteProvider.tsx | 17 +++++------------ src/autocomplete/CommandProvider.tsx | 2 +- src/autocomplete/CommunityProvider.tsx | 2 +- src/autocomplete/DuckDuckGoProvider.tsx | 2 +- src/autocomplete/EmojiProvider.tsx | 2 +- src/autocomplete/NotifProvider.tsx | 2 +- src/autocomplete/RoomProvider.tsx | 2 +- src/autocomplete/UserProvider.tsx | 2 +- src/components/views/rooms/Autocomplete.tsx | 4 ++-- 9 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/autocomplete/AutocompleteProvider.tsx b/src/autocomplete/AutocompleteProvider.tsx index a40ce7144d..cc958546e1 100644 --- a/src/autocomplete/AutocompleteProvider.tsx +++ b/src/autocomplete/AutocompleteProvider.tsx @@ -27,11 +27,11 @@ export interface ICommand { }; } -export default class AutocompleteProvider { +export default abstract class AutocompleteProvider { commandRegex: RegExp; forcedCommandRegex: RegExp; - constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) { + protected constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) { if (commandRegex) { if (!commandRegex.global) { throw new Error('commandRegex must have global flag set'); @@ -93,18 +93,11 @@ export default class AutocompleteProvider { }; } - async getCompletions(query: string, selection: ISelectionRange, force = false): Promise { - return []; - } + abstract getCompletions(query: string, selection: ISelectionRange, force: boolean): Promise; - getName(): string { - return 'Default Provider'; - } + abstract getName(): string; - renderCompletions(completions: React.ReactNode[]): React.ReactNode | null { - console.error('stub; should be implemented in subclasses'); - return null; - } + abstract renderCompletions(completions: React.ReactNode[]): React.ReactNode | null; // Whether we should provide completions even if triggered forcefully, without a sigil. shouldForceComplete(): boolean { diff --git a/src/autocomplete/CommandProvider.tsx b/src/autocomplete/CommandProvider.tsx index c2d1290e08..7698dfcd15 100644 --- a/src/autocomplete/CommandProvider.tsx +++ b/src/autocomplete/CommandProvider.tsx @@ -91,7 +91,7 @@ export default class CommandProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index ebf5d536ec..8e2d2789cd 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -112,7 +112,7 @@ export default class CommunityProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/DuckDuckGoProvider.tsx b/src/autocomplete/DuckDuckGoProvider.tsx index e63f7255dc..a16c82aaf9 100644 --- a/src/autocomplete/DuckDuckGoProvider.tsx +++ b/src/autocomplete/DuckDuckGoProvider.tsx @@ -99,7 +99,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 705474f8d0..4a237fe091 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -140,7 +140,7 @@ export default class EmojiProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index ef1823c0ca..e948f8a985 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -66,7 +66,7 @@ export default class NotifProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index 74deacf61f..6614615436 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -123,7 +123,7 @@ export default class RoomProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 32eea55b0b..6d909d38ad 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -178,7 +178,7 @@ export default class UserProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index e62a5a9bd6..cdea607cea 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -298,7 +298,7 @@ export default class Autocomplete extends React.PureComponent { return completions.length > 0 ? ( -
+
{ completionResult.provider.getName() }
{ completionResult.provider.renderCompletions(completions) }
@@ -306,7 +306,7 @@ export default class Autocomplete extends React.PureComponent { }).filter((completion) => !!completion); return !this.state.hide && renderedCompletions.length > 0 ? ( -
+
{ renderedCompletions }
) : null; From 60e7089c770592b90194f478c57a3c587a71bad0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 11 May 2021 11:14:21 +0100 Subject: [PATCH 4/8] post-merge fixes, the new keybindings stuff made it messy --- src/KeyBindingsDefaults.ts | 14 ++-- src/KeyBindingsManager.ts | 12 ++- .../views/rooms/BasicMessageComposer.tsx | 75 ++++++++++--------- src/editor/autocomplete.ts | 6 +- 4 files changed, 52 insertions(+), 55 deletions(-) diff --git a/src/KeyBindingsDefaults.ts b/src/KeyBindingsDefaults.ts index 63c4ac0f86..1270491d08 100644 --- a/src/KeyBindingsDefaults.ts +++ b/src/KeyBindingsDefaults.ts @@ -161,31 +161,29 @@ const messageComposerBindings = (): KeyBinding[] => { const autocompleteBindings = (): KeyBinding[] => { return [ { - action: AutocompleteAction.CompleteOrNextSelection, + action: AutocompleteAction.ForceComplete, keyCombo: { key: Key.TAB, }, }, { - action: AutocompleteAction.CompleteOrNextSelection, + action: AutocompleteAction.ForceComplete, keyCombo: { key: Key.TAB, ctrlKey: true, }, }, { - action: AutocompleteAction.CompleteOrPrevSelection, + action: AutocompleteAction.Complete, keyCombo: { - key: Key.TAB, - shiftKey: true, + key: Key.ENTER, }, }, { - action: AutocompleteAction.CompleteOrPrevSelection, + action: AutocompleteAction.Complete, keyCombo: { - key: Key.TAB, + key: Key.ENTER, ctrlKey: true, - shiftKey: true, }, }, { diff --git a/src/KeyBindingsManager.ts b/src/KeyBindingsManager.ts index aac14bde20..4a84b13257 100644 --- a/src/KeyBindingsManager.ts +++ b/src/KeyBindingsManager.ts @@ -52,13 +52,11 @@ export enum MessageComposerAction { /** Actions for text editing autocompletion */ export enum AutocompleteAction { - /** - * Select previous selection or, if the autocompletion window is not shown, open the window and select the first - * selection. - */ - CompleteOrPrevSelection = 'ApplySelection', - /** Select next selection or, if the autocompletion window is not shown, open it and select the first selection */ - CompleteOrNextSelection = 'CompleteOrNextSelection', + /** Accepts chosen autocomplete selection */ + Complete = 'Complete', + /** Accepts chosen autocomplete selection or, + * if the autocompletion window is not shown, open the window and select the first selection */ + ForceComplete = 'ForceComplete', /** Move to the previous autocomplete selection */ PrevSelection = 'PrevSelection', /** Move to the next autocomplete selection */ diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 5377e08b5e..a2cae654f3 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -434,6 +434,45 @@ export default class BasicMessageEditor extends React.Component private onKeyDown = (event: React.KeyboardEvent) => { const model = this.props.model; let handled = false; + + const autocompleteAction = getKeyBindingsManager().getAutocompleteAction(event); + if (model.autoComplete && model.autoComplete.hasCompletions()) { + const autoComplete = model.autoComplete; + switch (autocompleteAction) { + case AutocompleteAction.ForceComplete: + case AutocompleteAction.Complete: + autoComplete.confirmCompletion(); + handled = true; + break; + case AutocompleteAction.PrevSelection: + autoComplete.selectPreviousSelection(); + handled = true; + break; + case AutocompleteAction.NextSelection: + autoComplete.selectNextSelection(); + handled = true; + break; + case AutocompleteAction.Cancel: + autoComplete.onEscape(event); + handled = true; + break; + default: + return; // don't preventDefault on anything else + } + } else if (autocompleteAction === AutocompleteAction.ForceComplete) { + // there is no current autocomplete window, try to open it + this.tabCompleteName(); + handled = true; + } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { + this.formatBarRef.current.hide(); + } + + if (handled) { + event.preventDefault(); + event.stopPropagation(); + return; + } + const action = getKeyBindingsManager().getMessageComposerAction(event); switch (action) { case MessageComposerAction.FormatBold: @@ -485,42 +524,6 @@ export default class BasicMessageEditor extends React.Component handled = true; break; } - if (handled) { - event.preventDefault(); - event.stopPropagation(); - return; - } - - const autocompleteAction = getKeyBindingsManager().getAutocompleteAction(event); - if (model.autoComplete && model.autoComplete.hasCompletions()) { - const autoComplete = model.autoComplete; - switch (autocompleteAction) { - case AutocompleteAction.CompleteOrPrevSelection: - case AutocompleteAction.PrevSelection: - autoComplete.selectPreviousSelection(); - handled = true; - break; - case AutocompleteAction.CompleteOrNextSelection: - case AutocompleteAction.NextSelection: - autoComplete.selectNextSelection(); - handled = true; - break; - case AutocompleteAction.Cancel: - autoComplete.onEscape(event); - handled = true; - break; - default: - return; // don't preventDefault on anything else - } - } else if (autocompleteAction === AutocompleteAction.CompleteOrPrevSelection - || autocompleteAction === AutocompleteAction.CompleteOrNextSelection) { - // there is no current autocomplete window, try to open it - this.tabCompleteName(); - handled = true; - } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { - this.formatBarRef.current.hide(); - } - if (handled) { event.preventDefault(); event.stopPropagation(); diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts index 86260963e5..fe09406ca1 100644 --- a/src/editor/autocomplete.ts +++ b/src/editor/autocomplete.ts @@ -64,7 +64,8 @@ export default class AutocompleteWrapperModel { return ac && ac.countCompletions() > 0; } - public onEnter() { + public async confirmCompletion() { + await this.getAutocompleterComponent().onConfirmCompletion(); this.updateCallback({close: true}); } @@ -76,9 +77,6 @@ export default class AutocompleteWrapperModel { if (acComponent.countCompletions() === 0) { // Force completions to show for the text currently entered await acComponent.forceComplete(); - } else { - await acComponent.onConfirmCompletion(); - this.updateCallback({close: true}); } } From 78f569de94b623021aed496a6c848b4d6ec6e4bb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 18 May 2021 12:56:23 +0100 Subject: [PATCH 5/8] delint --- src/autocomplete/AutocompleteProvider.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/autocomplete/AutocompleteProvider.tsx b/src/autocomplete/AutocompleteProvider.tsx index 38dd34a047..1924ea48a7 100644 --- a/src/autocomplete/AutocompleteProvider.tsx +++ b/src/autocomplete/AutocompleteProvider.tsx @@ -98,9 +98,7 @@ export default abstract class AutocompleteProvider { selection: ISelectionRange, force: boolean, limit: number, - ): Promise { - return []; - } + ): Promise; abstract getName(): string; From 28eaac0ef808bb5e7604faef3aa240a6c4c2ae54 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 May 2021 22:25:19 +0100 Subject: [PATCH 6/8] remove dead code and fix some types --- src/components/views/rooms/Autocomplete.tsx | 8 ++++---- .../views/rooms/BasicMessageComposer.tsx | 5 ++--- src/editor/autocomplete.ts | 20 ------------------- src/editor/model.ts | 2 +- 4 files changed, 7 insertions(+), 28 deletions(-) diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 1eb2be473b..d9b4268917 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -33,9 +33,9 @@ interface IProps { // the query string for which to show autocomplete suggestions query: string; // method invoked with range and text content when completion is confirmed - onConfirm: (ICompletion) => void; + onConfirm: (completion: ICompletion) => void; // method invoked when selected (if any) completion changes - onSelectionChange?: (ICompletion, number) => void; + onSelectionChange?: (partIndex: number) => void; selection: ISelectionRange; // The room in which we're autocompleting room: Room; @@ -172,7 +172,7 @@ export default class Autocomplete extends React.PureComponent { if (anyMatches) { hide = false; if (this.props.onSelectionChange) { - this.props.onSelectionChange(this.state.completionList[selectionOffset - 1], selectionOffset - 1); + this.props.onSelectionChange(selectionOffset - 1); } } @@ -258,7 +258,7 @@ export default class Autocomplete extends React.PureComponent { setSelection(selectionOffset: number) { this.setState({selectionOffset, hide: false}); if (this.props.onSelectionChange) { - this.props.onSelectionChange(this.state.completionList[selectionOffset - 1], selectionOffset - 1); + this.props.onSelectionChange(selectionOffset - 1); } } diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index a2cae654f3..16332fda8f 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -575,10 +575,9 @@ export default class BasicMessageEditor extends React.Component this.props.model.autoComplete.onComponentConfirm(completion); }; - private onAutoCompleteSelectionChange = (completion: ICompletion, completionIndex: number) => { + private onAutoCompleteSelectionChange = (completionIndex: number) => { this.modifiedFlag = true; - // this.props.model.autoComplete.onComponentSelectionChange(completion); - this.setState({completionIndex}); + this.setState({ completionIndex }); }; private configureEmoticonAutoReplace = () => { diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts index fe09406ca1..ea943c3f2c 100644 --- a/src/editor/autocomplete.ts +++ b/src/editor/autocomplete.ts @@ -32,7 +32,6 @@ export type GetAutocompleterComponent = () => Autocomplete; export type UpdateQuery = (test: string) => Promise; export default class AutocompleteWrapperModel { - private queryPart: Part; private partIndex: number; constructor( @@ -45,10 +44,6 @@ export default class AutocompleteWrapperModel { public onEscape(e: KeyboardEvent) { this.getAutocompleterComponent().onEscape(e); - this.updateCallback({ - replaceParts: [this.partCreator.plain(this.queryPart.text)], - close: true, - }); } public close() { @@ -89,25 +84,10 @@ export default class AutocompleteWrapperModel { } public onPartUpdate(part: Part, pos: DocumentPosition) { - // cache the typed value and caret here - // so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text) - this.queryPart = part; this.partIndex = pos.index; return this.updateQuery(part.text); } - public onComponentSelectionChange(completion: ICompletion) { - if (!completion) { - this.updateCallback({ - replaceParts: [this.queryPart], - }); - } else { - this.updateCallback({ - replaceParts: this.partForCompletion(completion), - }); - } - } - public onComponentConfirm(completion: ICompletion) { this.updateCallback({ replaceParts: this.partForCompletion(completion), diff --git a/src/editor/model.ts b/src/editor/model.ts index f1b6f90957..90765da047 100644 --- a/src/editor/model.ts +++ b/src/editor/model.ts @@ -237,7 +237,7 @@ export default class EditorModel { } } } - // not _autoComplete, only there if active part is autocomplete part + // not autoComplete, only there if active part is autocomplete part if (this.autoComplete) { return this.autoComplete.onPartUpdate(part, pos); } From de88a3960477792048e9d4b96d5dae4c9d61b9af Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jul 2021 10:09:24 +0100 Subject: [PATCH 7/8] delint and improve ts --- src/components/views/rooms/Autocomplete.tsx | 28 +++++++++---------- .../views/rooms/BasicMessageComposer.tsx | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 06386c61b4..34909baef1 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -85,7 +85,7 @@ export default class Autocomplete extends React.PureComponent { this.applyNewProps(); } - private applyNewProps(oldQuery?: string, oldRoom?: Room) { + private applyNewProps(oldQuery?: string, oldRoom?: Room): void { if (oldRoom && this.props.room.roomId !== oldRoom.roomId) { this.autocompleter.destroy(); this.autocompleter = new Autocompleter(this.props.room); @@ -103,7 +103,7 @@ export default class Autocomplete extends React.PureComponent { this.autocompleter.destroy(); } - complete(query: string, selection: ISelectionRange) { + private complete(query: string, selection: ISelectionRange): Promise { this.queryRequested = query; if (this.debounceCompletionsRequest) { clearTimeout(this.debounceCompletionsRequest); @@ -134,7 +134,7 @@ export default class Autocomplete extends React.PureComponent { }); } - processQuery(query: string, selection: ISelectionRange) { + private processQuery(query: string, selection: ISelectionRange): Promise { return this.autocompleter.getCompletions( query, selection, this.state.forceComplete, MAX_PROVIDER_MATCHES, ).then((completions) => { @@ -146,7 +146,7 @@ export default class Autocomplete extends React.PureComponent { }); } - processCompletions(completions: IProviderCompletions[]) { + private processCompletions(completions: IProviderCompletions[]): void { const completionList = flatMap(completions, (provider) => provider.completions); // Reset selection when completion list becomes empty. @@ -186,16 +186,16 @@ export default class Autocomplete extends React.PureComponent { }); } - hasSelection(): boolean { + public hasSelection(): boolean { return this.countCompletions() > 0 && this.state.selectionOffset !== 0; } - countCompletions(): number { + public countCompletions(): number { return this.state.completionList.length; } // called from MessageComposerInput - moveSelection(delta: number) { + public moveSelection(delta: number): void { const completionCount = this.countCompletions(); if (completionCount === 0) return; // there are no items to move the selection through @@ -204,7 +204,7 @@ export default class Autocomplete extends React.PureComponent { this.setSelection(1 + index); } - onEscape(e: KeyboardEvent): boolean { + public onEscape(e: KeyboardEvent): boolean { const completionCount = this.countCompletions(); if (completionCount === 0) { // autocomplete is already empty, so don't preventDefault @@ -217,7 +217,7 @@ export default class Autocomplete extends React.PureComponent { this.hide(); } - hide = () => { + private hide = (): void => { this.setState({ hide: true, selectionOffset: 1, @@ -226,7 +226,7 @@ export default class Autocomplete extends React.PureComponent { }); }; - forceComplete() { + public forceComplete(): Promise { return new Promise((resolve) => { this.setState({ forceComplete: true, @@ -239,11 +239,11 @@ export default class Autocomplete extends React.PureComponent { }); } - onConfirmCompletion = () => { + public onConfirmCompletion = (): void => { this.onCompletionClicked(this.state.selectionOffset); - } + }; - onCompletionClicked = (selectionOffset: number): boolean => { + private onCompletionClicked = (selectionOffset: number): boolean => { const count = this.countCompletions(); if (count === 0 || selectionOffset < 1 || selectionOffset > count) { return false; @@ -255,7 +255,7 @@ export default class Autocomplete extends React.PureComponent { return true; }; - setSelection(selectionOffset: number) { + private setSelection(selectionOffset: number): void { this.setState({ selectionOffset, hide: false }); if (this.props.onSelectionChange) { this.props.onSelectionChange(selectionOffset - 1); diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index fb3df6cd78..f87c735b6f 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -552,7 +552,7 @@ export default class BasicMessageEditor extends React.Component model.autoComplete.close(); } } else { - this.setState({showVisualBell: true}); + this.setState({ showVisualBell: true }); } } catch (err) { console.error(err); From f53eb4eeedc1e7c449d92956a1ecebdbd4d4affa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 12 Aug 2021 11:27:34 +0100 Subject: [PATCH 8/8] Fix tab trapping behaviour --- src/components/views/rooms/BasicMessageComposer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 68dd874037..48f2e2a39b 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -462,7 +462,7 @@ export default class BasicMessageEditor extends React.Component } const autocompleteAction = getKeyBindingsManager().getAutocompleteAction(event); - if (model.autoComplete && model.autoComplete.hasCompletions()) { + if (model.autoComplete?.hasCompletions()) { const autoComplete = model.autoComplete; switch (autocompleteAction) { case AutocompleteAction.ForceComplete: @@ -485,7 +485,7 @@ export default class BasicMessageEditor extends React.Component default: return; // don't preventDefault on anything else } - } else if (autocompleteAction === AutocompleteAction.ForceComplete) { + } else if (autocompleteAction === AutocompleteAction.ForceComplete && !this.state.showVisualBell) { // there is no current autocomplete window, try to open it this.tabCompleteName(); handled = true;