Merge pull request #5659 from matrix-org/t3chguy/a11y/composer-list-autocomplete
This commit is contained in:
commit
df282807b1
15 changed files with 133 additions and 142 deletions
|
@ -161,31 +161,29 @@ const messageComposerBindings = (): KeyBinding<MessageComposerAction>[] => {
|
|||
const autocompleteBindings = (): KeyBinding<AutocompleteAction>[] => {
|
||||
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,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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,23 +93,16 @@ export default class AutocompleteProvider {
|
|||
};
|
||||
}
|
||||
|
||||
async getCompletions(
|
||||
abstract getCompletions(
|
||||
query: string,
|
||||
selection: ISelectionRange,
|
||||
force = false,
|
||||
limit = -1,
|
||||
): Promise<ICompletion[]> {
|
||||
return [];
|
||||
}
|
||||
force: boolean,
|
||||
limit: number,
|
||||
): Promise<ICompletion[]>;
|
||||
|
||||
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 {
|
||||
|
|
|
@ -96,7 +96,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
|||
return (
|
||||
<div
|
||||
className="mx_Autocomplete_Completion_container_block"
|
||||
role="listbox"
|
||||
role="presentation"
|
||||
aria-label={_t("Command Autocomplete")}
|
||||
>
|
||||
{ completions }
|
||||
|
|
|
@ -116,7 +116,7 @@ export default class CommunityProvider extends AutocompleteProvider {
|
|||
return (
|
||||
<div
|
||||
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
||||
role="listbox"
|
||||
role="presentation"
|
||||
aria-label={_t("Community Autocomplete")}
|
||||
>
|
||||
{ completions }
|
||||
|
|
|
@ -105,7 +105,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
|||
return (
|
||||
<div
|
||||
className="mx_Autocomplete_Completion_container_block"
|
||||
role="listbox"
|
||||
role="presentation"
|
||||
aria-label={_t("DuckDuckGo Results")}
|
||||
>
|
||||
{ completions }
|
||||
|
|
|
@ -139,7 +139,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
return (
|
||||
<div
|
||||
className="mx_Autocomplete_Completion_container_pill"
|
||||
role="listbox"
|
||||
role="presentation"
|
||||
aria-label={_t("Emoji Autocomplete")}
|
||||
>
|
||||
{ completions }
|
||||
|
|
|
@ -70,7 +70,7 @@ export default class NotifProvider extends AutocompleteProvider {
|
|||
return (
|
||||
<div
|
||||
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
||||
role="listbox"
|
||||
role="presentation"
|
||||
aria-label={_t("Notification Autocomplete")}
|
||||
>
|
||||
{ completions }
|
||||
|
|
|
@ -134,7 +134,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
|||
return (
|
||||
<div
|
||||
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
||||
role="listbox"
|
||||
role="presentation"
|
||||
aria-label={_t("Room Autocomplete")}
|
||||
>
|
||||
{ completions }
|
||||
|
|
|
@ -181,7 +181,7 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
return (
|
||||
<div
|
||||
className="mx_Autocomplete_Completion_container_pill"
|
||||
role="listbox"
|
||||
role="presentation"
|
||||
aria-label={_t("User Autocomplete")}
|
||||
>
|
||||
{ completions }
|
||||
|
|
|
@ -529,24 +529,24 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
|
||||
if (!isModifier && !ev.altKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
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.FocusSendMessageComposer, 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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -25,7 +25,6 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import Autocompleter from '../../../autocomplete/Autocompleter';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
const COMPOSER_SELECTED = 0;
|
||||
const MAX_PROVIDER_MATCHES = 20;
|
||||
|
||||
export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`;
|
||||
|
@ -34,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;
|
||||
|
@ -71,7 +70,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
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,
|
||||
|
@ -86,7 +85,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
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);
|
||||
|
@ -104,7 +103,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
this.autocompleter.destroy();
|
||||
}
|
||||
|
||||
complete(query: string, selection: ISelectionRange) {
|
||||
private complete(query: string, selection: ISelectionRange): Promise<void> {
|
||||
this.queryRequested = query;
|
||||
if (this.debounceCompletionsRequest) {
|
||||
clearTimeout(this.debounceCompletionsRequest);
|
||||
|
@ -115,7 +114,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
completions: [],
|
||||
completionList: [],
|
||||
// Reset selected completion
|
||||
selectionOffset: COMPOSER_SELECTED,
|
||||
selectionOffset: 1,
|
||||
// Hide the autocomplete box
|
||||
hide: true,
|
||||
});
|
||||
|
@ -135,7 +134,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
processQuery(query: string, selection: ISelectionRange) {
|
||||
private processQuery(query: string, selection: ISelectionRange): Promise<void> {
|
||||
return this.autocompleter.getCompletions(
|
||||
query, selection, this.state.forceComplete, MAX_PROVIDER_MATCHES,
|
||||
).then((completions) => {
|
||||
|
@ -147,30 +146,35 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
processCompletions(completions: IProviderCompletions[]) {
|
||||
private processCompletions(completions: IProviderCompletions[]): void {
|
||||
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(selectionOffset - 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
completions,
|
||||
|
@ -182,25 +186,25 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// 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 {
|
||||
public onEscape(e: KeyboardEvent): boolean {
|
||||
const completionCount = this.countCompletions();
|
||||
if (completionCount === 0) {
|
||||
// autocomplete is already empty, so don't preventDefault
|
||||
|
@ -213,16 +217,16 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
this.hide();
|
||||
}
|
||||
|
||||
hide = () => {
|
||||
private hide = (): void => {
|
||||
this.setState({
|
||||
hide: true,
|
||||
selectionOffset: 0,
|
||||
selectionOffset: 1,
|
||||
completions: [],
|
||||
completionList: [],
|
||||
});
|
||||
};
|
||||
|
||||
forceComplete() {
|
||||
public forceComplete(): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
this.setState({
|
||||
forceComplete: true,
|
||||
|
@ -235,8 +239,13 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
onCompletionClicked = (selectionOffset: number): boolean => {
|
||||
if (this.countCompletions() === 0 || selectionOffset === COMPOSER_SELECTED) {
|
||||
public onConfirmCompletion = (): void => {
|
||||
this.onCompletionClicked(this.state.selectionOffset);
|
||||
};
|
||||
|
||||
private onCompletionClicked = (selectionOffset: number): boolean => {
|
||||
const count = this.countCompletions();
|
||||
if (count === 0 || selectionOffset < 1 || selectionOffset > count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -246,10 +255,10 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
return true;
|
||||
};
|
||||
|
||||
setSelection(selectionOffset: number) {
|
||||
private setSelection(selectionOffset: number): void {
|
||||
this.setState({ selectionOffset, hide: false });
|
||||
if (this.props.onSelectionChange) {
|
||||
this.props.onSelectionChange(this.state.completionList[selectionOffset - 1], selectionOffset - 1);
|
||||
this.props.onSelectionChange(selectionOffset - 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -292,7 +301,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
|
||||
return completions.length > 0 ? (
|
||||
<div key={i} className="mx_Autocomplete_ProviderSection">
|
||||
<div key={i} className="mx_Autocomplete_ProviderSection" role="presentation">
|
||||
<div className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</div>
|
||||
{ completionResult.provider.renderCompletions(completions) }
|
||||
</div>
|
||||
|
@ -300,7 +309,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
}).filter((completion) => !!completion);
|
||||
|
||||
return !this.state.hide && renderedCompletions.length > 0 ? (
|
||||
<div className="mx_Autocomplete" ref={this.containerRef}>
|
||||
<div id="mx_Autocomplete" className="mx_Autocomplete" ref={this.containerRef} role="listbox">
|
||||
{ renderedCompletions }
|
||||
</div>
|
||||
) : null;
|
||||
|
|
|
@ -133,6 +133,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
this.state = {
|
||||
showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"),
|
||||
surroundWith: SettingsStore.getValue("MessageComposerInput.surroundWith"),
|
||||
showVisualBell: false,
|
||||
};
|
||||
|
||||
this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
|
||||
|
@ -215,7 +216,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
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;
|
||||
|
@ -435,7 +440,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
const model = this.props.model;
|
||||
let handled = false;
|
||||
|
||||
if (this.state.surroundWith && document.getSelection().type != "Caret") {
|
||||
if (this.state.surroundWith && document.getSelection().type !== "Caret") {
|
||||
// This surrounds the selected text with a character. This is
|
||||
// intentionally left out of the keybinding manager as the keybinds
|
||||
// here shouldn't be changeable
|
||||
|
@ -456,6 +461,44 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
}
|
||||
}
|
||||
|
||||
const autocompleteAction = getKeyBindingsManager().getAutocompleteAction(event);
|
||||
if (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 && !this.state.showVisualBell) {
|
||||
// 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:
|
||||
|
@ -507,42 +550,6 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
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();
|
||||
|
@ -577,6 +584,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
this.setState({ showVisualBell: true });
|
||||
model.autoComplete.close();
|
||||
}
|
||||
} else {
|
||||
this.setState({ showVisualBell: true });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
@ -592,9 +601,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
this.props.model.autoComplete.onComponentConfirm(completion);
|
||||
};
|
||||
|
||||
private onAutoCompleteSelectionChange = (completion: ICompletion, completionIndex: number): void => {
|
||||
private onAutoCompleteSelectionChange = (completionIndex: number): void => {
|
||||
this.modifiedFlag = true;
|
||||
this.props.model.autoComplete.onComponentSelectionChange(completion);
|
||||
this.setState({ completionIndex });
|
||||
};
|
||||
|
||||
|
@ -718,6 +726,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
};
|
||||
|
||||
const { completionIndex } = this.state;
|
||||
const hasAutocomplete = Boolean(this.state.autoComplete);
|
||||
let activeDescendant;
|
||||
if (hasAutocomplete && completionIndex >= 0) {
|
||||
activeDescendant = generateCompletionDomId(completionIndex);
|
||||
}
|
||||
|
||||
return (<div className={wrapperClasses}>
|
||||
{ autoComplete }
|
||||
|
@ -736,10 +749,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
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"
|
||||
aria-disabled={this.props.disabled}
|
||||
/>
|
||||
|
|
|
@ -32,7 +32,6 @@ export type GetAutocompleterComponent = () => Autocomplete;
|
|||
export type UpdateQuery = (test: string) => Promise<void>;
|
||||
|
||||
export default class AutocompleteWrapperModel {
|
||||
private queryPart: Part;
|
||||
private partIndex: number;
|
||||
|
||||
constructor(
|
||||
|
@ -45,10 +44,6 @@ export default class AutocompleteWrapperModel {
|
|||
|
||||
public onEscape(e: KeyboardEvent): void {
|
||||
this.getAutocompleterComponent().onEscape(e);
|
||||
this.updateCallback({
|
||||
replaceParts: [this.partCreator.plain(this.queryPart.text)],
|
||||
close: true,
|
||||
});
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
|
@ -64,7 +59,8 @@ export default class AutocompleteWrapperModel {
|
|||
return ac && ac.countCompletions() > 0;
|
||||
}
|
||||
|
||||
public onEnter(): void {
|
||||
public async confirmCompletion(): Promise<void> {
|
||||
await this.getAutocompleterComponent().onConfirmCompletion();
|
||||
this.updateCallback({ close: true });
|
||||
}
|
||||
|
||||
|
@ -76,8 +72,6 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,25 +84,10 @@ export default class AutocompleteWrapperModel {
|
|||
}
|
||||
|
||||
public onPartUpdate(part: Part, pos: DocumentPosition): Promise<void> {
|
||||
// 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): void {
|
||||
if (!completion) {
|
||||
this.updateCallback({
|
||||
replaceParts: [this.queryPart],
|
||||
});
|
||||
} else {
|
||||
this.updateCallback({
|
||||
replaceParts: this.partForCompletion(completion),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onComponentConfirm(completion: ICompletion): void {
|
||||
this.updateCallback({
|
||||
replaceParts: this.partForCompletion(completion),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue