Skip to content

Commit

Permalink
feat: add arrow key navigation for results dropdown
Browse files Browse the repository at this point in the history
  • Loading branch information
Antonella Sgarlatta committed Jun 3, 2021
1 parent 386ca34 commit 31d454c
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 29 deletions.
45 changes: 31 additions & 14 deletions app/assets/javascripts/components/AutocompleteTagInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => {
autocompleteSearchQuery,
autocompleteTagHintVisible,
autocompleteTagResults,
autocompleteTagResultElements,
autocompleteInputElement,
tagElements,
tags,
} = appState.noteTags;
Expand All @@ -23,7 +25,6 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => {
const [dropdownMaxHeight, setDropdownMaxHeight] =
useState<number | 'auto'>('auto');

const inputRef = useRef<HTMLInputElement>();
const dropdownRef = useRef<HTMLDivElement>();

const [closeOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) => {
Expand All @@ -32,9 +33,11 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => {
});

const showDropdown = () => {
const { clientHeight } = document.documentElement;
const inputRect = inputRef.current.getBoundingClientRect();
setDropdownMaxHeight(clientHeight - inputRect.bottom - 32 * 2);
if (autocompleteInputElement) {
const { clientHeight } = document.documentElement;
const inputRect = autocompleteInputElement.getBoundingClientRect();
setDropdownMaxHeight(clientHeight - inputRect.bottom - 32 * 2);
}
setDropdownVisible(true);
};

Expand All @@ -49,6 +52,24 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => {
await appState.noteTags.createAndAddNewTag();
};

const onKeyDown = (event: KeyboardEvent) => {
switch (event.key) {
case 'Backspace':
if (autocompleteSearchQuery === '' && tagElements.length > 0) {
tagElements[tagElements.length - 1]?.focus();
}
break;
case 'ArrowDown':
event.preventDefault();
if (autocompleteTagResultElements.length > 0) {
autocompleteTagResultElements[0]?.focus();
}
break;
default:
return;
}
};

useEffect(() => {
appState.noteTags.searchActiveNoteAutocompleteTags();
}, [appState.noteTags]);
Expand All @@ -60,23 +81,19 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => {
>
<Disclosure open={dropdownVisible} onChange={showDropdown}>
<input
ref={inputRef}
ref={(element) => {
if (element) {
appState.noteTags.setAutocompleteInputElement(element);
}
}}
className="w-80 bg-default text-xs color-text no-border h-7 focus:outline-none focus:shadow-none focus:border-bottom"
value={autocompleteSearchQuery}
onChange={onSearchQueryChange}
type="text"
placeholder="Add tag"
onBlur={closeOnBlur}
onFocus={showDropdown}
onKeyUp={(event) => {
if (
event.key === 'Backspace' &&
autocompleteSearchQuery === '' &&
tagElements.length > 0
) {
tagElements[tagElements.length - 1]?.focus();
}
}}
onKeyDown={onKeyDown}
/>
{dropdownVisible && (
<DisclosurePanel
Expand Down
23 changes: 22 additions & 1 deletion app/assets/javascripts/components/AutocompleteTagResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,33 @@ type Props = {

export const AutocompleteTagResult = observer(
({ appState, tagResult, closeOnBlur }: Props) => {
const { autocompleteSearchQuery } = appState.noteTags;
const { autocompleteInputElement, autocompleteSearchQuery, autocompleteTagResults } = appState.noteTags;

const onTagOptionClick = async (tag: SNTag) => {
await appState.noteTags.addTagToActiveNote(tag);
appState.noteTags.clearAutocompleteSearch();
};

const onKeyDown = (event: KeyboardEvent) => {
const tagResultIndex = appState.noteTags.getTagIndex(tagResult, autocompleteTagResults);
switch (event.key) {
case 'ArrowUp':
event.preventDefault();
if (tagResultIndex === 0) {
autocompleteInputElement?.focus();
} else {
appState.noteTags.getPreviousAutocompleteTagResultElement(tagResult)?.focus();
}
break;
case 'ArrowDown':
event.preventDefault();
appState.noteTags.getNextAutocompleteTagResultElement(tagResult)?.focus();
break;
default:
return;
}
};

return (
<button
ref={(element) => {
Expand All @@ -32,6 +52,7 @@ export const AutocompleteTagResult = observer(
className="sn-dropdown-item"
onClick={() => onTagOptionClick(tagResult)}
onBlur={closeOnBlur}
onKeyDown={onKeyDown}
>
<Icon type="hashtag" className="color-neutral mr-2 min-h-5 min-w-5" />
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
Expand Down
13 changes: 4 additions & 9 deletions app/assets/javascripts/components/NoteTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,16 @@ export const NoteTag = observer(({ appState, tag }: Props) => {
}
};

const onKeyUp = (event: KeyboardEvent) => {
let previousTagElement;
let nextTagElement;

const onKeyDown = (event: KeyboardEvent) => {
switch (event.key) {
case 'Backspace':
deleteTag();
break;
case 'ArrowLeft':
previousTagElement = appState.noteTags.getPreviousTagElement(tag);
previousTagElement?.focus();
appState.noteTags.getPreviousTagElement(tag)?.focus();
break;
case 'ArrowRight':
nextTagElement = appState.noteTags.getNextTagElement(tag);
nextTagElement?.focus();
appState.noteTags.getNextTagElement(tag)?.focus();
break;
default:
return;
Expand All @@ -65,7 +60,7 @@ export const NoteTag = observer(({ appState, tag }: Props) => {
className="sn-tag pl-1 pr-2 mr-2"
style={{ maxWidth: tagsContainerMaxWidth }}
onClick={onTagClick}
onKeyUp={onKeyUp}
onKeyDown={onKeyDown}
onFocus={onFocus}
onBlur={onBlur}
>
Expand Down
4 changes: 2 additions & 2 deletions app/assets/javascripts/components/NotesOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export const NotesOptions = observer(
{appState.tags.tagsCount > 0 && (
<Disclosure open={tagsMenuOpen} onChange={openTagsMenu}>
<DisclosureButton
onKeyUp={(event) => {
onKeyDown={(event) => {
if (event.key === 'Escape') {
setTagsMenuOpen(false);
}
Expand All @@ -149,7 +149,7 @@ export const NotesOptions = observer(
<Icon type="chevron-right" className="color-neutral" />
</DisclosureButton>
<DisclosurePanel
onKeyUp={(event) => {
onKeyDown={(event) => {
if (event.key === 'Escape') {
setTagsMenuOpen(false);
tagsButtonRef.current.focus();
Expand Down
4 changes: 2 additions & 2 deletions app/assets/javascripts/components/NotesOptionsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
}}
>
<DisclosureButton
onKeyUp={(event) => {
onKeyDown={(event) => {
if (event.key === 'Escape' && !submenuOpen) {
setOpen(false);
}
Expand All @@ -60,7 +60,7 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
<Icon type="more" className="block" />
</DisclosureButton>
<DisclosurePanel
onKeyUp={(event) => {
onKeyDown={(event) => {
if (event.key === 'Escape' && !submenuOpen) {
setOpen(false);
buttonRef.current.focus();
Expand Down
37 changes: 36 additions & 1 deletion app/assets/javascripts/ui_models/app_state/note_tags_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { WebApplication } from '../application';
import { AppState } from './app_state';

export class NoteTagsState {
autocompleteInputElement: HTMLInputElement | undefined = undefined;
autocompleteSearchQuery = '';
autocompleteTagResultElements: (HTMLButtonElement | undefined)[] = [];
autocompleteTagResults: SNTag[] = [];
Expand All @@ -17,6 +18,7 @@ export class NoteTagsState {
appEventListeners: (() => void)[]
) {
makeObservable(this, {
autocompleteInputElement: observable,
autocompleteSearchQuery: observable,
autocompleteTagResultElements: observable,
autocompleteTagResults: observable,
Expand All @@ -27,6 +29,7 @@ export class NoteTagsState {
autocompleteTagHintVisible: computed,

clearAutocompleteSearch: action,
setAutocompleteInputElement: action,
setAutocompleteSearchQuery: action,
setAutocompleteTagResultElement: action,
setAutocompleteTagResultElements: action,
Expand Down Expand Up @@ -58,6 +61,10 @@ export class NoteTagsState {
);
}

setAutocompleteInputElement(element: HTMLInputElement | undefined): void {
this.autocompleteInputElement = element;
}

setAutocompleteSearchQuery(query: string): void {
this.autocompleteSearchQuery = query;
}
Expand Down Expand Up @@ -107,7 +114,9 @@ export class NoteTagsState {
}

async createAndAddNewTag(): Promise<void> {
const newTag = await this.application.findOrCreateTag(this.autocompleteSearchQuery);
const newTag = await this.application.findOrCreateTag(
this.autocompleteSearchQuery
);
await this.addTagToActiveNote(newTag);
this.clearAutocompleteSearch();
}
Expand Down Expand Up @@ -138,6 +147,32 @@ export class NoteTagsState {
}
}

getPreviousAutocompleteTagResultElement(
tagResult: SNTag
): HTMLButtonElement | undefined {
const previousTagIndex =
this.getTagIndex(tagResult, this.autocompleteTagResults) - 1;
if (
previousTagIndex > -1 &&
this.autocompleteTagResultElements.length > previousTagIndex
) {
return this.autocompleteTagResultElements[previousTagIndex];
}
}

getNextAutocompleteTagResultElement(
tagResult: SNTag
): HTMLButtonElement | undefined {
const nextTagIndex =
this.getTagIndex(tagResult, this.autocompleteTagResults) + 1;
if (
nextTagIndex > -1 &&
this.autocompleteTagResultElements.length > nextTagIndex
) {
return this.autocompleteTagResultElements[nextTagIndex];
}
}

reloadTags(): void {
const { activeNote } = this;
if (activeNote) {
Expand Down

0 comments on commit 31d454c

Please sign in to comment.