Commit 06c4bdec authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Convert TagEditor to TS

parent 87e819a4
...@@ -3,32 +3,29 @@ import { Input } from '@hypothesis/frontend-shared/lib/next'; ...@@ -3,32 +3,29 @@ import { Input } from '@hypothesis/frontend-shared/lib/next';
import { useRef, useState } from 'preact/hooks'; import { useRef, useState } from 'preact/hooks';
import { withServices } from '../service-context'; import { withServices } from '../service-context';
import type { TagsService } from '../services/tags';
import AutocompleteList from './AutocompleteList'; import AutocompleteList from './AutocompleteList';
import TagList from './TagList'; import TagList from './TagList';
import TagListItem from './TagListItem'; import TagListItem from './TagListItem';
/** @typedef {import("preact").JSX.Element} JSXElement */
// Global counter used to create a unique id for each instance of a TagEditor // Global counter used to create a unique id for each instance of a TagEditor
let tagEditorIdCounter = 0; let tagEditorIdCounter = 0;
/** export type TagEditorProps = {
* @typedef TagEditorProps onAddTag: (tag: string) => boolean;
* @prop {(tag: string) => boolean} onAddTag - Callback to add a tag to the annotation onRemoveTag: (tag: string) => boolean;
* @prop {(tag: string) => boolean} onRemoveTag - Callback to remove a tag from the annotation onTagInput: (tag: string) => void;
* @prop {(tag: string) => void} onTagInput - Callback when inputted tag text changes tagList: string[];
* @prop {string[]} tagList - The list of tags for the annotation under edit
* @prop {import('../services/tags').TagsService} tags
*/
// injected
tags: TagsService;
};
/** /**
* Component to edit annotation's tags. * Component to edit annotation's tags.
* *
* Component accessibility is modeled after "Combobox with Listbox Popup Examples" found here: * Component accessibility is modeled after "Combobox with Listbox Popup Examples" found here:
* https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html * https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html
*
* @param {TagEditorProps} props
*/ */
function TagEditor({ function TagEditor({
onAddTag, onAddTag,
...@@ -36,9 +33,9 @@ function TagEditor({ ...@@ -36,9 +33,9 @@ function TagEditor({
onTagInput, onTagInput,
tagList, tagList,
tags: tagsService, tags: tagsService,
}) { }: TagEditorProps) {
const inputEl = /** @type {{ current: HTMLInputElement }} */ (useRef()); const inputEl = useRef<HTMLInputElement>();
const [suggestions, setSuggestions] = useState(/** @type {string[]} */ ([])); const [suggestions, setSuggestions] = useState([] as string[]);
const [activeItem, setActiveItem] = useState(-1); // -1 is unselected const [activeItem, setActiveItem] = useState(-1); // -1 is unselected
const [suggestionsListOpen, setSuggestionsListOpen] = useState(false); const [suggestionsListOpen, setSuggestionsListOpen] = useState(false);
const [tagEditorId] = useState(() => { const [tagEditorId] = useState(() => {
...@@ -47,7 +44,7 @@ function TagEditor({ ...@@ -47,7 +44,7 @@ function TagEditor({
}); });
// Set up callback to monitor outside click events to close the AutocompleteList // Set up callback to monitor outside click events to close the AutocompleteList
const closeWrapperRef = /** @type {{ current: HTMLDivElement }} */ (useRef()); const closeWrapperRef = useRef<HTMLDivElement>(null);
useElementShouldClose(closeWrapperRef, suggestionsListOpen, () => { useElementShouldClose(closeWrapperRef, suggestionsListOpen, () => {
setSuggestionsListOpen(false); setSuggestionsListOpen(false);
}); });
...@@ -55,24 +52,20 @@ function TagEditor({ ...@@ -55,24 +52,20 @@ function TagEditor({
/** /**
* Retrieve the current trimmed text value of the tag <input> * Retrieve the current trimmed text value of the tag <input>
*/ */
const pendingTag = () => inputEl.current.value.trim(); const pendingTag = () => inputEl.current!.value.trim();
const hasPendingTag = () => pendingTag() && pendingTag().length > 0; const hasPendingTag = () => pendingTag() && pendingTag().length > 0;
const clearPendingTag = () => { const clearPendingTag = () => {
inputEl.current.value = ''; inputEl.current!.value = '';
onTagInput?.(''); onTagInput?.('');
}; };
/** /**
* Helper function that returns a list of suggestions less any * Helper function that returns a list of suggestions less any
* results also found from the duplicates list. * results also found from the duplicates list.
*
* @param {string[]} suggestions - Original list of suggestions
* @param {string[]} duplicates - Items to be removed from the result
* @return {string[]}
*/ */
const removeDuplicates = (suggestions, duplicates) => { const removeDuplicates = (suggestions: string[], duplicates: string[]) => {
const suggestionsSet = []; const suggestionsSet = [];
for (let suggestion of suggestions) { for (const suggestion of suggestions) {
if (duplicates.indexOf(suggestion) < 0) { if (duplicates.indexOf(suggestion) < 0) {
suggestionsSet.push(suggestion); suggestionsSet.push(suggestion);
} }
...@@ -102,16 +95,14 @@ function TagEditor({ ...@@ -102,16 +95,14 @@ function TagEditor({
/** /**
* Invokes callback to add tag. If the tag was added, close the suggestions * Invokes callback to add tag. If the tag was added, close the suggestions
* list, clear the field content and maintain focus. * list, clear the field content and maintain focus.
*
* @param {string} newTag
*/ */
const addTag = newTag => { const addTag = (newTag: string) => {
if (onAddTag(newTag)) { if (onAddTag(newTag)) {
setSuggestionsListOpen(false); setSuggestionsListOpen(false);
setActiveItem(-1); setActiveItem(-1);
clearPendingTag(); clearPendingTag();
inputEl.current.focus(); inputEl.current!.focus();
} }
}; };
...@@ -123,10 +114,8 @@ function TagEditor({ ...@@ -123,10 +114,8 @@ function TagEditor({
/** /**
* Callback when the user clicked one of the items in the suggestions list. * Callback when the user clicked one of the items in the suggestions list.
* This will add a new tag. * This will add a new tag.
*
* @param {string} item
*/ */
const handleSelect = item => { const handleSelect = (item: string) => {
if (item) { if (item) {
addTag(item); addTag(item);
} }
...@@ -152,7 +141,7 @@ function TagEditor({ ...@@ -152,7 +141,7 @@ function TagEditor({
* *
* @param {number} direction - Pass 1 for the next item or -1 for the previous * @param {number} direction - Pass 1 for the next item or -1 for the previous
*/ */
const changeSelectedItem = direction => { const changeSelectedItem = (direction: -1 | 1) => {
let nextActiveItem = activeItem + direction; let nextActiveItem = activeItem + direction;
if (nextActiveItem < -1) { if (nextActiveItem < -1) {
nextActiveItem = suggestions.length - 1; nextActiveItem = suggestions.length - 1;
...@@ -165,10 +154,8 @@ function TagEditor({ ...@@ -165,10 +154,8 @@ function TagEditor({
/** /**
* Keydown handler for keyboard navigation of the tag editor field and the * Keydown handler for keyboard navigation of the tag editor field and the
* suggested-tags list. * suggested-tags list.
*
* @param {KeyboardEvent} e
*/ */
const handleKeyDown = e => { const handleKeyDown = (e: KeyboardEvent) => {
switch (e.key) { switch (e.key) {
case 'ArrowUp': case 'ArrowUp':
// Select the previous item in the suggestion list // Select the previous item in the suggestion list
...@@ -227,11 +214,8 @@ function TagEditor({ ...@@ -227,11 +214,8 @@ function TagEditor({
* Callback for formatting a suggested tag item. Use selective bolding * Callback for formatting a suggested tag item. Use selective bolding
* to help delineate which part of the entered tag text matches the * to help delineate which part of the entered tag text matches the
* suggestion. * suggestion.
*
* @param {string} item - Suggested tag
* @return {JSXElement} - Formatted tag for use in list
*/ */
const formatSuggestedItem = item => { const formatSuggestedItem = (item: string) => {
// filtering of tags is case-insensitive // filtering of tags is case-insensitive
const curVal = pendingTag().toLowerCase(); const curVal = pendingTag().toLowerCase();
const suggestedTag = item.toLowerCase(); const suggestedTag = item.toLowerCase();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment