Unverified Commit e76b5470 authored by Kyle Keating's avatar Kyle Keating Committed by GitHub

Improve typechecking for several components and their dependencies

- TagList
- TagEditor
- MarkdownEditor
- Excerpt
- AutocompleteList
- AnnotationBody
parent 6c76613c
...@@ -11,8 +11,24 @@ import MarkdownView from './markdown-view'; ...@@ -11,8 +11,24 @@ import MarkdownView from './markdown-view';
import TagEditor from './tag-editor'; import TagEditor from './tag-editor';
import TagList from './tag-list'; import TagList from './tag-list';
/** @typedef {import("../../types/api").Annotation} Annotation */
/**
* @typedef AnnotationBodyProps
* @prop {Annotation} annotation - The annotation in question
* @prop {boolean} [isEditing] - Whether to display the body in edit mode (if true) or view mode.
* @prop {(a: Object<'tags', string[]>) => void} [onEditTags] - Callback invoked when the user edits tags.
* @prop {(a?: Object<'text', string>) => void} [onEditText] - Callback invoked when the user edits the content of the annotation body.
* @prop {string[]} tags
* @prop {string} text -
* The markdown annotation body, which is either rendered as HTML (if `isEditing`
* is false) or displayed in a text area otherwise.
*/
/** /**
* Display the rendered content of an annotation. * Display the rendered content of an annotation.
*
* @param {AnnotationBodyProps} props
*/ */
export default function AnnotationBody({ export default function AnnotationBody({
annotation, annotation,
...@@ -63,11 +79,14 @@ export default function AnnotationBody({ ...@@ -63,11 +79,14 @@ export default function AnnotationBody({
)} )}
{isCollapsible && !isEditing && ( {isCollapsible && !isEditing && (
<div className="annotation-body__collapse-toggle"> <div className="annotation-body__collapse-toggle">
{/* @ts-ignore - TODO: Button props need to be fixed */}
<Button <Button
buttonText={toggleText} buttonText={toggleText}
className="annotation-body__collapse-toggle-button" className="annotation-body__collapse-toggle-button"
isExpanded={!isCollapsed} isExpanded={!isCollapsed}
onClick={() => setIsCollapsed(!isCollapsed)} onClick={() => {
setIsCollapsed(!isCollapsed);
}}
aria-label="Toggle visibility of full annotation text" aria-label="Toggle visibility of full annotation text"
title="Toggle visibility of full annotation text" title="Toggle visibility of full annotation text"
/> />
...@@ -80,31 +99,10 @@ export default function AnnotationBody({ ...@@ -80,31 +99,10 @@ export default function AnnotationBody({
} }
AnnotationBody.propTypes = { AnnotationBody.propTypes = {
/**
* The annotation in question
*/
annotation: propTypes.object.isRequired, annotation: propTypes.object.isRequired,
/**
* Whether to display the body in edit mode (if true) or view mode.
*/
isEditing: propTypes.bool, isEditing: propTypes.bool,
/**
* Callback invoked when the user edits tags.
*/
onEditTags: propTypes.func, onEditTags: propTypes.func,
/**
* Callback invoked when the user edits the content of the annotation body.
*/
onEditText: propTypes.func, onEditText: propTypes.func,
tags: propTypes.array.isRequired, tags: propTypes.array.isRequired,
/**
* The markdown annotation body, which is either rendered as HTML (if `isEditing`
* is false) or displayed in a text area otherwise.
*/
text: propTypes.string, text: propTypes.string,
}; };
...@@ -5,6 +5,25 @@ import propTypes from 'prop-types'; ...@@ -5,6 +5,25 @@ import propTypes from 'prop-types';
const defaultListFormatter = item => item; const defaultListFormatter = item => item;
/**
* @template T
* @typedef AutocompleteListProps
* @prop {number} [activeItem] - The index of the highlighted item.
* @prop {string} [id] - Optional unique HTML attribute id. This can be used
* for parent `aria-controls` coupling.
* @prop {string} [itemPrefixId] - Optional unique HTML attribute id prefix
* for each item in the list. The final value of each items' id is
* `{itemPrefixId}{activeItem}`
* @prop {T[]} list - The list of items to render. This can be a simple
* list of strings or a list of objects when used with listFormatter.
* @prop {(item: T, index?: number) => any} [listFormatter] - An optional formatter
* to render each item inside an <li> tag This is useful if the list is an array of
* objects rather than just strings.
* @prop {(item: T) => void} onSelectItem - Callback when an item is clicked with
* the mouse.
* @prop {boolean} [open] - Is the list open or closed?
*/
/** /**
* Custom autocomplete component. Use this in conjunction with an <input> field. * Custom autocomplete component. Use this in conjunction with an <input> field.
* To make this component W3 accessibility compliant, it is is intended to be * To make this component W3 accessibility compliant, it is is intended to be
...@@ -12,8 +31,10 @@ const defaultListFormatter = item => item; ...@@ -12,8 +31,10 @@ const defaultListFormatter = item => item;
* used by itself. * used by itself.
* *
* Modeled after the "ARIA 1.1 Combobox with Listbox Popup" * Modeled after the "ARIA 1.1 Combobox with Listbox Popup"
*
* @template T
* @param {AutocompleteListProps<T>} props
*/ */
export default function AutocompleteList({ export default function AutocompleteList({
activeItem = -1, activeItem = -1,
id, id,
...@@ -65,7 +86,7 @@ export default function AutocompleteList({ ...@@ -65,7 +86,7 @@ export default function AutocompleteList({
> >
<ul <ul
className="autocomplete-list__items" className="autocomplete-list__items"
tabIndex="-1" tabIndex={-1}
aria-label="Suggestions" aria-label="Suggestions"
role="listbox" role="listbox"
{...props} {...props}
...@@ -77,39 +98,11 @@ export default function AutocompleteList({ ...@@ -77,39 +98,11 @@ export default function AutocompleteList({
} }
AutocompleteList.propTypes = { AutocompleteList.propTypes = {
/**
* The index of the highlighted item.
*/
activeItem: propTypes.number, activeItem: propTypes.number,
/**
* Optional unique HTML attribute id. This can be used for
* parent `aria-controls` coupling.
*/
id: propTypes.string, id: propTypes.string,
/**
* Optional unique HTML attribute id prefix for each item in the list.
* The final value of each items' id is `{itemPrefixId}{activeItem}`
*/
itemPrefixId: propTypes.string, itemPrefixId: propTypes.string,
/**
* The list of items to render. This can be a simple list of
* strings or a list of objects when used with listFormatter.
*/
list: propTypes.array.isRequired, list: propTypes.array.isRequired,
/**
* An optional formatter to render each item inside an <li> tag
* This is useful if the list is an array of objects rather than
* just strings.
*/
listFormatter: propTypes.func, listFormatter: propTypes.func,
/**
* Callback when an item is clicked with the mouse.
* (item, index) => {}
*/
onSelectItem: propTypes.func.isRequired, onSelectItem: propTypes.func.isRequired,
/**
* Is the list open or closed?
*/
open: propTypes.bool, open: propTypes.bool,
}; };
...@@ -17,7 +17,7 @@ export default function Button({ ...@@ -17,7 +17,7 @@ export default function Button({
icon = '', icon = '',
isExpanded, isExpanded,
isPressed, isPressed,
onClick = () => null, onClick = () => {},
style = {}, style = {},
title, title,
}) { }) {
......
...@@ -7,9 +7,18 @@ import observeElementSize from '../util/observe-element-size'; ...@@ -7,9 +7,18 @@ import observeElementSize from '../util/observe-element-size';
import { withServices } from '../util/service-context'; import { withServices } from '../util/service-context';
import { applyTheme } from '../util/theme'; import { applyTheme } from '../util/theme';
/**
* @typedef InlineControlsProps
* @prop {boolean} [isCollapsed]
* @prop {(collapsed: boolean) => any} [setCollapsed]
* @prop {Object} [linkStyle]
*/
/** /**
* An optional toggle link at the bottom of an excerpt which controls whether * An optional toggle link at the bottom of an excerpt which controls whether
* it is expanded or collapsed. * it is expanded or collapsed.
*
* @param {InlineControlsProps} props
*/ */
function InlineControls({ isCollapsed, setCollapsed, linkStyle = {} }) { function InlineControls({ isCollapsed, setCollapsed, linkStyle = {} }) {
const toggleLabel = isCollapsed ? 'More' : 'Less'; const toggleLabel = isCollapsed ? 'More' : 'Less';
...@@ -39,12 +48,36 @@ InlineControls.propTypes = { ...@@ -39,12 +48,36 @@ InlineControls.propTypes = {
const noop = () => {}; const noop = () => {};
/**
* @typedef ExcerptProps
* @prop {Object} [children]
* @prop {boolean} [inlineControls] - If `true`, the excerpt provides internal
* controls to expand and collapse the content. If `false`, the caller sets
* the collapsed state via the `collapse` prop. When using inline controls,
* the excerpt is initially collapsed.
* @prop {boolean} [collapse] - If the content should be truncated if its height
* exceeds `collapsedHeight + overflowThreshold`. This prop is only used if
* `inlineControls` is false.
* @prop {number} collapsedHeight - Maximum height of the container, in pixels,
* when it is collapsed.
* @prop {number} [overflowThreshold] - An additional margin of pixels by which
* the content height can exceed `collapsedHeight` before it becomes collapsible.
* @prop {(isCollapsible?: boolean) => any} [onCollapsibleChanged] - Called when the content height
* exceeds or falls below `collapsedHeight + overflowThreshold`.
* @prop {(collapsed?: boolean) => any} [onToggleCollapsed] - When `inlineControls` is `false`, this
* function is called when the user requests to expand the content by clicking a
* zone at the bottom of the container.
* @prop {Object} [settings] - Used for theming.
*/
/** /**
* A container which truncates its content when they exceed a specified height. * A container which truncates its content when they exceed a specified height.
* *
* The collapsed state of the container can be handled either via internal * The collapsed state of the container can be handled either via internal
* controls (if `inlineControls` is `true`) or by the caller using the * controls (if `inlineControls` is `true`) or by the caller using the
* `collapse` prop. * `collapse` prop.
*
* @param {ExcerptProps} props
*/ */
function Excerpt({ function Excerpt({
children, children,
...@@ -61,7 +94,7 @@ function Excerpt({ ...@@ -61,7 +94,7 @@ function Excerpt({
); );
// Container for the excerpt's content. // Container for the excerpt's content.
const contentElement = useRef(null); const contentElement = useRef(/** @type {HTMLDivElement|null} */ (null));
// Measured height of `contentElement` in pixels. // Measured height of `contentElement` in pixels.
const [contentHeight, setContentHeight] = useState(0); const [contentHeight, setContentHeight] = useState(0);
...@@ -94,6 +127,7 @@ function Excerpt({ ...@@ -94,6 +127,7 @@ function Excerpt({
const isCollapsed = inlineControls ? collapsedByInlineControls : collapse; const isCollapsed = inlineControls ? collapsedByInlineControls : collapse;
const isExpandable = isOverflowing && isCollapsed; const isExpandable = isOverflowing && isCollapsed;
/** @type {Object} */
const contentStyle = {}; const contentStyle = {};
if (contentHeight !== 0) { if (contentHeight !== 0) {
contentStyle['max-height'] = isExpandable ? collapsedHeight : contentHeight; contentStyle['max-height'] = isExpandable ? collapsedHeight : contentHeight;
...@@ -132,48 +166,12 @@ function Excerpt({ ...@@ -132,48 +166,12 @@ function Excerpt({
Excerpt.propTypes = { Excerpt.propTypes = {
children: propTypes.object, children: propTypes.object,
/**
* If `true`, the excerpt provides internal controls to expand and collapse
* the content. If `false`, the caller sets the collapsed state via the
* `collapse` prop.
*
* When using inline controls, the excerpt is initially collapsed.
*/
inlineControls: propTypes.bool, inlineControls: propTypes.bool,
/**
* If the content should be truncated if its height exceeds
* `collapsedHeight + overflowThreshold`.
*
* This prop is only used if `inlineControls` is false.
*/
collapse: propTypes.bool, collapse: propTypes.bool,
/**
* Maximum height of the container, in pixels, when it is collapsed.
*/
collapsedHeight: propTypes.number, collapsedHeight: propTypes.number,
/**
* An additional margin of pixels by which the content height can exceed
* `collapsedHeight` before it becomes collapsible.
*/
overflowThreshold: propTypes.number, overflowThreshold: propTypes.number,
/**
* Called when the content height exceeds or falls below `collapsedHeight + overflowThreshold`.
*/
onCollapsibleChanged: propTypes.func, onCollapsibleChanged: propTypes.func,
/**
* When `inlineControls` is `false`, this function is called when the user
* requests to expand the content by clicking a zone at the bottom of the
* container.
*/
onToggleCollapsed: propTypes.func, onToggleCollapsed: propTypes.func,
// Used for theming.
settings: propTypes.object, settings: propTypes.object,
}; };
......
...@@ -4,17 +4,17 @@ import { normalizeKeyName } from '../../../shared/browser-compatibility-utils'; ...@@ -4,17 +4,17 @@ import { normalizeKeyName } from '../../../shared/browser-compatibility-utils';
import { listen } from '../../util/dom'; import { listen } from '../../util/dom';
/** /**
* @typedef Ref * @template T
* @prop current {Node} - HTML node * @typedef {import("preact/hooks").Ref<T>} Ref
*
* A ref object attached to a HTML node.
*/ */
/** /**
* @typedef PreactRef * @typedef PreactElement
* @prop current {Object} - preact component object * @prop {Node} base
* */
* A ref object attached to a custom preact component.
/**
* @typedef {Ref<HTMLButtonElement> & Ref<PreactElement>} PreactRef
*/ */
/** /**
...@@ -28,11 +28,11 @@ import { listen } from '../../util/dom'; ...@@ -28,11 +28,11 @@ import { listen } from '../../util/dom';
* Limitation: This will not work when attached to a custom component that has * Limitation: This will not work when attached to a custom component that has
* more than one element nested under a root <Fragment> * more than one element nested under a root <Fragment>
* *
* @param {Ref|PreactRef} closeableEl - ref object: * @param {PreactRef} closeableEl - ref object:
* Reference to a DOM element or preat component * Reference to a DOM element or preat component
* that should be closed when DOM elements external * that should be closed when DOM elements external
* to it are interacted with or `Esc` is pressed * to it are interacted with or `Esc` is pressed
* @param {bool} isOpen - Whether the element is currently open. This hook does * @param {boolean} isOpen - Whether the element is currently open. This hook does
* not attach event listeners/do anything if it's not. * not attach event listeners/do anything if it's not.
* @param {() => void} handleClose - A function that will do the actual closing * @param {() => void} handleClose - A function that will do the actual closing
* of `closeableEl` * of `closeableEl`
...@@ -46,7 +46,7 @@ export default function useElementShouldClose( ...@@ -46,7 +46,7 @@ export default function useElementShouldClose(
* Helper to return the underlying node object whether * Helper to return the underlying node object whether
* `closeableEl` is attached to an HTMLNode or Preact component. * `closeableEl` is attached to an HTMLNode or Preact component.
* *
* @param {Preact ref} closeableEl * @param {PreactRef} closeableEl
* @returns {Node} * @returns {Node}
*/ */
...@@ -67,7 +67,9 @@ export default function useElementShouldClose( ...@@ -67,7 +67,9 @@ export default function useElementShouldClose(
} }
// Close element when user presses Escape key, regardless of focus. // Close element when user presses Escape key, regardless of focus.
const removeKeyDownListener = listen(document.body, ['keydown'], event => { const removeKeyDownListener = listen(document.body, ['keydown'], (
/** @type {KeyboardEvent}*/ event
) => {
if (normalizeKeyName(event.key) === 'Escape') { if (normalizeKeyName(event.key) === 'Escape') {
handleClose(); handleClose();
} }
...@@ -80,7 +82,7 @@ export default function useElementShouldClose( ...@@ -80,7 +82,7 @@ export default function useElementShouldClose(
'focus', 'focus',
event => { event => {
const current = getCurrentNode(closeableEl); const current = getCurrentNode(closeableEl);
if (!current.contains(event.target)) { if (!current.contains(/** @type {Node} */ (event.target))) {
handleClose(); handleClose();
} }
}, },
...@@ -94,7 +96,7 @@ export default function useElementShouldClose( ...@@ -94,7 +96,7 @@ export default function useElementShouldClose(
['mousedown', 'click'], ['mousedown', 'click'],
event => { event => {
const current = getCurrentNode(closeableEl); const current = getCurrentNode(closeableEl);
if (!current.contains(event.target)) { if (!current.contains(/** @type {Node} */ (event.target))) {
handleClose(); handleClose();
} }
}, },
......
...@@ -31,7 +31,7 @@ const SHORTCUT_KEYS = { ...@@ -31,7 +31,7 @@ const SHORTCUT_KEYS = {
* Apply a toolbar command to an editor input field. * Apply a toolbar command to an editor input field.
* *
* @param {string} command * @param {string} command
* @param {HTMLInputElement} inputEl * @param {HTMLInputElement|HTMLTextAreaElement} inputEl
*/ */
function handleToolbarCommand(command, inputEl) { function handleToolbarCommand(command, inputEl) {
const update = newStateFn => { const update = newStateFn => {
...@@ -94,11 +94,24 @@ function handleToolbarCommand(command, inputEl) { ...@@ -94,11 +94,24 @@ function handleToolbarCommand(command, inputEl) {
} }
} }
/**
* @typedef ToolbarButtonProps
* @prop {Object} buttonRef
* @prop {boolean} [disabled]
* @prop {string} [iconName]
* @prop {string} [label]
* @prop {(e: MouseEvent) => void} onClick
* @prop {string} [shortcutKey]
* @prop {number} tabIndex
* @prop {string} [title]
*/
/** @param {ToolbarButtonProps} props */
function ToolbarButton({ function ToolbarButton({
buttonRef, buttonRef,
disabled = false, disabled = false,
iconName, iconName,
label = null, label,
onClick, onClick,
shortcutKey, shortcutKey,
tabIndex, tabIndex,
...@@ -147,9 +160,14 @@ ToolbarButton.propTypes = { ...@@ -147,9 +160,14 @@ ToolbarButton.propTypes = {
}; };
/** /**
* @typedef {string} ButtonID * @typedef {'bold'|'italic'|'quote'|'link'|'image'|'math'|'numlist'|'list'|'preview'|'help'} ButtonID
* A unique string that can be one of one of: */
* [bold, italic, quote, link, image, math, numlist, list, preview, help]
/**
* @typedef ToolbarProps
* @prop {boolean} [isPreviewing] - `true` if the editor's "Preview" mode is active.
* @prop {(a: ButtonID) => any} [onCommand] - Callback invoked with the selected command when a toolbar button is clicked.
* @prop {() => any} [onTogglePreview] - Callback invoked when the "Preview" toggle button is clicked.
*/ */
/** /**
...@@ -159,6 +177,8 @@ ToolbarButton.propTypes = { ...@@ -159,6 +177,8 @@ ToolbarButton.propTypes = {
* *
* Canonical example * Canonical example
* https://www.w3.org/TR/wai-aria-practices/examples/toolbar/toolbar.html * https://www.w3.org/TR/wai-aria-practices/examples/toolbar/toolbar.html
*
* @param {ToolbarProps} props
*/ */
function Toolbar({ isPreviewing, onCommand, onTogglePreview }) { function Toolbar({ isPreviewing, onCommand, onTogglePreview }) {
const buttonIds = { const buttonIds = {
...@@ -247,8 +267,8 @@ function Toolbar({ isPreviewing, onCommand, onTogglePreview }) { ...@@ -247,8 +267,8 @@ function Toolbar({ isPreviewing, onCommand, onTogglePreview }) {
* Each element should be set to -1 unless its the * Each element should be set to -1 unless its the
* active roving index, in which case it will be 0. * active roving index, in which case it will be 0.
* *
* @param {ButtonID} id * @param {number} index - An index from `buttonIds`
* @return {number} index - An index from `buttonIds` * @return {number}
*/ */
const getTabIndex = index => { const getTabIndex = index => {
if (rovingElement === index) { if (rovingElement === index) {
...@@ -364,18 +384,25 @@ function Toolbar({ isPreviewing, onCommand, onTogglePreview }) { ...@@ -364,18 +384,25 @@ function Toolbar({ isPreviewing, onCommand, onTogglePreview }) {
} }
Toolbar.propTypes = { Toolbar.propTypes = {
/** `true` if the editor's "Preview" mode is active. */
isPreviewing: propTypes.bool, isPreviewing: propTypes.bool,
/** Callback invoked with the selected command when a toolbar button is clicked. */
onCommand: propTypes.func, onCommand: propTypes.func,
/** Callback invoked when the "Preview" toggle button is clicked. */
onTogglePreview: propTypes.func, onTogglePreview: propTypes.func,
}; };
/**
* @typedef MarkdownEditorProps
* @prop {string} label - An accessible label for the input field.
* @prop {string} [text] - The markdown text to edit.
* @prop {(a?: Object<'text', string>) => void} [onEditText]
* - Callback invoked with `{ text }` object when user edits text.
* TODO: Simplify this callback to take just a string rather than an object once the
* parent component is converted to Preact.
*/
/** /**
* Viewer/editor for the body of an annotation in markdown format. * Viewer/editor for the body of an annotation in markdown format.
*
* @param {MarkdownEditorProps} props
*/ */
export default function MarkdownEditor({ export default function MarkdownEditor({
label = '', label = '',
...@@ -386,7 +413,7 @@ export default function MarkdownEditor({ ...@@ -386,7 +413,7 @@ export default function MarkdownEditor({
const [preview, setPreview] = useState(false); const [preview, setPreview] = useState(false);
// The input element where the user inputs their comment. // The input element where the user inputs their comment.
const input = useRef(null); const input = useRef(/** @type {HTMLTextAreaElement|null} */ (null));
useEffect(() => { useEffect(() => {
if (!preview) { if (!preview) {
...@@ -435,7 +462,11 @@ export default function MarkdownEditor({ ...@@ -435,7 +462,11 @@ export default function MarkdownEditor({
ref={input} ref={input}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onInput={e => onEditText({ text: e.target.value })} onInput={e => {
onEditText({
text: /** @type {HTMLTextAreaElement} */ (e.target).value,
});
}}
value={text} value={text}
/> />
)} )}
...@@ -444,19 +475,7 @@ export default function MarkdownEditor({ ...@@ -444,19 +475,7 @@ export default function MarkdownEditor({
} }
MarkdownEditor.propTypes = { MarkdownEditor.propTypes = {
/**
* An accessible label for the input field.
*/
label: propTypes.string.isRequired, label: propTypes.string.isRequired,
/** The markdown text to edit. */
text: propTypes.string, text: propTypes.string,
/**
* Callback invoked with `{ text }` object when user edits text.
*
* TODO: Simplify this callback to take just a string rather than an object
* once the parent component is converted to Preact.
*/
onEditText: propTypes.func, onEditText: propTypes.func,
}; };
...@@ -13,16 +13,24 @@ import useElementShouldClose from './hooks/use-element-should-close'; ...@@ -13,16 +13,24 @@ import useElementShouldClose from './hooks/use-element-should-close';
// 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;
/**
* @typedef TagEditorProps
* @prop {(a: Object<'tags', string[]>) => any} onEditTags - Callback that saves the tag list.
* @prop {string[]} tagList - The list of editable tags as strings.
* @prop {Object} tags - Services
*/
/** /**
* 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({ onEditTags, tags: tagsService, tagList }) { function TagEditor({ onEditTags, tags: tagsService, tagList }) {
const inputEl = useRef(null); const inputEl = useRef(/** @type {HTMLInputElement|null} */ (null));
const [suggestions, setSuggestions] = useState([]); const [suggestions, setSuggestions] = useState(/** @type {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(() => {
...@@ -31,7 +39,8 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) { ...@@ -31,7 +39,8 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) {
}); });
// 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 = useRef(); const closeWrapperRef = useRef(/** @type {HTMLElement|null} */ (null));
/** @ts-ignore - TODO: fix useElementShouldClose Ref types */
useElementShouldClose(closeWrapperRef, suggestionsListOpen, () => { useElementShouldClose(closeWrapperRef, suggestionsListOpen, () => {
setSuggestionsListOpen(false); setSuggestionsListOpen(false);
}); });
...@@ -42,9 +51,9 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) { ...@@ -42,9 +51,9 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) {
* 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 {Array<string>} suggestions - Original list of suggestions * @param {string[]} suggestions - Original list of suggestions
* @param {Array<string>} duplicates - Items to be removed from the result * @param {string[]} duplicates - Items to be removed from the result
* @return {Array<string>} * @return {string[]}
*/ */
const removeDuplicates = (suggestions, duplicates) => { const removeDuplicates = (suggestions, duplicates) => {
const suggestionsSet = []; const suggestionsSet = [];
...@@ -73,11 +82,14 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) { ...@@ -73,11 +82,14 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) {
setSuggestions(removeDuplicates(suggestions, tagList)); setSuggestions(removeDuplicates(suggestions, tagList));
setSuggestionsListOpen(suggestions.length > 0); setSuggestionsListOpen(suggestions.length > 0);
} }
setActiveItem(-1); setActiveItem(-1);
}; };
/** /**
* Handle changes to this annotation's tags * Handle changes to this annotation's tags
*
* @param {string[]} tagList
*/ */
const updateTags = tagList => { const updateTags = tagList => {
// update suggested tags list via service // update suggested tags list via service
...@@ -100,6 +112,8 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) { ...@@ -100,6 +112,8 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) {
/** /**
* Adds a tag to the annotation equal to the value of the input field * Adds a tag to the annotation equal to the value of the input field
* and then clears out the suggestions list and the input field. * and then clears out the suggestions list and the input field.
*
* @param {string} newTag
*/ */
const addTag = newTag => { const addTag = newTag => {
const value = newTag.trim(); const value = newTag.trim();
...@@ -115,12 +129,15 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) { ...@@ -115,12 +129,15 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) {
setSuggestionsListOpen(false); setSuggestionsListOpen(false);
setActiveItem(-1); setActiveItem(-1);
inputEl.current.value = ''; const input = inputEl.current;
inputEl.current.focus(); input.value = '';
input.focus();
}; };
/** /**
* Update the suggestions if the user changes the value of the input * Update the suggestions if the user changes the value of the input
*
* @param {import("preact").JSX.TargetedEvent<HTMLInputElement, InputEvent>} e
*/ */
const handleOnInput = e => { const handleOnInput = e => {
if ( if (
...@@ -135,6 +152,8 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) { ...@@ -135,6 +152,8 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) {
/** /**
* 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 => {
if (item) { if (item) {
...@@ -176,6 +195,8 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) { ...@@ -176,6 +195,8 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) {
* Keydown handler for keyboard navigation of the suggestions list * Keydown handler for keyboard navigation of the suggestions list
* and when the user presses "Enter" or ","" to add a new typed item not * and when the user presses "Enter" or ","" to add a new typed item not
* found in the suggestions list * found in the suggestions list
*
* @param {KeyboardEvent} e
*/ */
const handleKeyDown = e => { const handleKeyDown = e => {
switch (normalizeKeyName(e.key)) { switch (normalizeKeyName(e.key)) {
...@@ -191,7 +212,7 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) { ...@@ -191,7 +212,7 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) {
case ',': case ',':
if (activeItem === -1) { if (activeItem === -1) {
// nothing selected, just add the typed text // nothing selected, just add the typed text
addTag(inputEl.current.value); addTag(/** @type {HTMLInputElement} */ (inputEl.current).value);
} else { } else {
addTag(suggestions[activeItem]); addTag(suggestions[activeItem]);
} }
...@@ -288,27 +309,12 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) { ...@@ -288,27 +309,12 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) {
); );
} }
/**
* @typedef Tag
* @param tag {string} - The tag text
*/
TagEditor.propTypes = { TagEditor.propTypes = {
/**
* Callback that saves the tag list.
*
* @param {Array<Tag>} - Array of tags to save
*/
onEditTags: propTypes.func.isRequired, onEditTags: propTypes.func.isRequired,
/* The list of editable tags as strings. */
tagList: propTypes.array.isRequired, tagList: propTypes.array.isRequired,
/** Services */
tags: propTypes.object.isRequired, tags: propTypes.object.isRequired,
serviceUrl: propTypes.func.isRequired,
}; };
TagEditor.injectedProps = ['serviceUrl', 'tags']; TagEditor.injectedProps = ['tags'];
export default withServices(TagEditor); export default withServices(TagEditor);
...@@ -5,8 +5,19 @@ import propTypes from 'prop-types'; ...@@ -5,8 +5,19 @@ import propTypes from 'prop-types';
import { isThirdPartyUser } from '../util/account-id'; import { isThirdPartyUser } from '../util/account-id';
import { withServices } from '../util/service-context'; import { withServices } from '../util/service-context';
/** @typedef {import('../../types/api').Annotation} Annotation */
/**
* @typedef TagListProps
* @prop {Annotation} annotation - Annotation that owns the tags.
* @prop {string[]} tags - List of tags as strings.
* @prop {(a: string, b: Object<'tag', string>) => any} [serviceUrl] - Services
* @prop {Object} [settings]
*/
/** /**
* Component to render an annotation's tags. * Component to render an annotation's tags.
* @param {TagListProps} props
*/ */
function TagList({ annotation, serviceUrl, settings, tags }) { function TagList({ annotation, serviceUrl, settings, tags }) {
const renderLink = useMemo( const renderLink = useMemo(
...@@ -52,19 +63,9 @@ function TagList({ annotation, serviceUrl, settings, tags }) { ...@@ -52,19 +63,9 @@ function TagList({ annotation, serviceUrl, settings, tags }) {
); );
} }
/**
* @typedef Tag
* @param tag {string} - The tag text
*/
TagList.propTypes = { TagList.propTypes = {
/* Annotation that owns the tags. */
annotation: propTypes.object.isRequired, annotation: propTypes.object.isRequired,
/* List of tags as strings. */
tags: propTypes.array.isRequired, tags: propTypes.array.isRequired,
/** Services */
serviceUrl: propTypes.func, serviceUrl: propTypes.func,
settings: propTypes.object, settings: propTypes.object,
}; };
......
...@@ -83,14 +83,10 @@ function replaceText(state, pos, length, text) { ...@@ -83,14 +83,10 @@ function replaceText(state, pos, length, text) {
* Convert the selected text into a Markdown link. * Convert the selected text into a Markdown link.
* *
* @param {EditorState} state - The current state of the input field. * @param {EditorState} state - The current state of the input field.
* @param {LinkType} linkType - The type of link to insert. * @param {LinkType} [linkType] - The type of link to insert.
* @return {EditorState} - The new state of the input field. * @return {EditorState} - The new state of the input field.
*/ */
export function convertSelectionToLink(state, linkType) { export function convertSelectionToLink(state, linkType = LinkType.ANCHOR_LINK) {
if (typeof linkType === 'undefined') {
linkType = LinkType.ANCHOR_LINK;
}
const selection = state.text.slice(state.selectionStart, state.selectionEnd); const selection = state.text.slice(state.selectionStart, state.selectionEnd);
let linkPrefix = ''; let linkPrefix = '';
......
...@@ -28,7 +28,7 @@ export function getElementHeightWithMargins(element) { ...@@ -28,7 +28,7 @@ export function getElementHeightWithMargins(element) {
* function that removes the listeners. * function that removes the listeners.
* *
* @param {HTMLElement} element * @param {HTMLElement} element
* @param {string[]} events * @param {string[]|string} events
* @param {EventListener} listener * @param {EventListener} listener
* @param {Object} options * @param {Object} options
* @param {boolean} [options.useCapture] * @param {boolean} [options.useCapture]
...@@ -42,7 +42,7 @@ export function listen(element, events, listener, { useCapture = false } = {}) { ...@@ -42,7 +42,7 @@ export function listen(element, events, listener, { useCapture = false } = {}) {
element.addEventListener(event, listener, useCapture) element.addEventListener(event, listener, useCapture)
); );
return () => { return () => {
events.forEach(event => /** @type {string[]} */ (events).forEach(event =>
element.removeEventListener(event, listener, useCapture) element.removeEventListener(event, listener, useCapture)
); );
}; };
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
"shared/components/*.js", "shared/components/*.js",
"sidebar/*.js", "sidebar/*.js",
"sidebar/components/*.js", "sidebar/components/*.js",
"sidebar/components/hooks/*.js",
"sidebar/store/modules/*.js", "sidebar/store/modules/*.js",
"sidebar/services/*.js", "sidebar/services/*.js",
"sidebar/util/*.js", "sidebar/util/*.js",
...@@ -31,7 +32,6 @@ ...@@ -31,7 +32,6 @@
// Files in `src/sidebar/components` that may still have errors. // Files in `src/sidebar/components` that may still have errors.
// Remove them from this list as they are resolved. // Remove them from this list as they are resolved.
"sidebar/components/annotation-action-bar.js", "sidebar/components/annotation-action-bar.js",
"sidebar/components/annotation-body.js",
"sidebar/components/annotation-header.js", "sidebar/components/annotation-header.js",
"sidebar/components/annotation-publish-control.js", "sidebar/components/annotation-publish-control.js",
"sidebar/components/annotation-quote.js", "sidebar/components/annotation-quote.js",
...@@ -39,8 +39,6 @@ ...@@ -39,8 +39,6 @@
"sidebar/components/annotation-share-info.js", "sidebar/components/annotation-share-info.js",
"sidebar/components/annotation-viewer-content.js", "sidebar/components/annotation-viewer-content.js",
"sidebar/components/annotation.js", "sidebar/components/annotation.js",
"sidebar/components/autocomplete-list.js",
"sidebar/components/excerpt.js",
"sidebar/components/focused-mode-header.js", "sidebar/components/focused-mode-header.js",
"sidebar/components/group-list-item.js", "sidebar/components/group-list-item.js",
"sidebar/components/group-list-section.js", "sidebar/components/group-list-section.js",
...@@ -49,7 +47,6 @@ ...@@ -49,7 +47,6 @@
"sidebar/components/hypothesis-app.js", "sidebar/components/hypothesis-app.js",
"sidebar/components/logged-out-message.js", "sidebar/components/logged-out-message.js",
"sidebar/components/login-prompt-panel.js", "sidebar/components/login-prompt-panel.js",
"sidebar/components/markdown-editor.js",
"sidebar/components/menu-item.js", "sidebar/components/menu-item.js",
"sidebar/components/menu.js", "sidebar/components/menu.js",
"sidebar/components/moderation-banner.js", "sidebar/components/moderation-banner.js",
...@@ -65,8 +62,6 @@ ...@@ -65,8 +62,6 @@
"sidebar/components/sort-menu.js", "sidebar/components/sort-menu.js",
"sidebar/components/stream-content.js", "sidebar/components/stream-content.js",
"sidebar/components/stream-search-input.js", "sidebar/components/stream-search-input.js",
"sidebar/components/tag-editor.js",
"sidebar/components/tag-list.js",
"sidebar/components/thread-card.js", "sidebar/components/thread-card.js",
"sidebar/components/thread-list.js", "sidebar/components/thread-list.js",
"sidebar/components/thread.js", "sidebar/components/thread.js",
......
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