Commit f6042890 authored by Robert Knight's avatar Robert Knight

Typecheck UI components in src/annotator/components

This follows the same pattern that was introduced for `SvgIcon` in
`propTypes` declarations are kept around for use at runtime by other
code which is not yet typechecked.

An issue that I encountered is that TS doesn't know about the custom
HTML element types we use. For the moment I have ignored these with
`@ts-ignore`, although I believe it should be possible to teach it.
parent d5b8d349
...@@ -5,6 +5,14 @@ import propTypes from 'prop-types'; ...@@ -5,6 +5,14 @@ import propTypes from 'prop-types';
import { useShortcut } from '../../shared/shortcut'; import { useShortcut } from '../../shared/shortcut';
import SvgIcon from '../../shared/components/svg-icon'; import SvgIcon from '../../shared/components/svg-icon';
/**
* @param {Object} props
* @param {number} [props.badgeCount]
* @param {string} [props.icon]
* @param {string} props.label
* @param {() => any} props.onClick
* @param {string|null} props.shortcut
*/
function ToolbarButton({ badgeCount, icon, label, onClick, shortcut }) { function ToolbarButton({ badgeCount, icon, label, onClick, shortcut }) {
useShortcut(shortcut, onClick); useShortcut(shortcut, onClick);
...@@ -36,9 +44,31 @@ ToolbarButton.propTypes = { ...@@ -36,9 +44,31 @@ ToolbarButton.propTypes = {
shortcut: propTypes.string, shortcut: propTypes.string,
}; };
/**
* Union of possible toolbar commands.
*
* @typedef {'annotate'|'highlight'|'show'} Command
*/
/**
* @typedef AdderToolbarProps
* @prop {'up'|'down'} arrowDirection -
* Whether the arrow pointing out of the toolbar towards the selected text
* should appear above the toolbar pointing Up or below the toolbar pointing
* Down.
* @prop {boolean} isVisible - Whether to show the toolbar or not.
* @prop {(c: Command) => any} onCommand - Called when a toolbar button is clicked.
* @prop {number} [annotationCount] -
* Number of annotations associated with the selected text.
* If non-zero, a "Show" button is displayed to allow the user to see the
* annotations that correspond to the selection.
*/
/** /**
* The toolbar that is displayed above selected text in the document providing * The toolbar that is displayed above selected text in the document providing
* options to create annotations or highlights. * options to create annotations or highlights.
*
* @param {AdderToolbarProps} props
*/ */
export default function AdderToolbar({ export default function AdderToolbar({
arrowDirection, arrowDirection,
...@@ -63,6 +93,7 @@ export default function AdderToolbar({ ...@@ -63,6 +93,7 @@ export default function AdderToolbar({
// nb. The adder is hidden using the `visibility` property rather than `display` // nb. The adder is hidden using the `visibility` property rather than `display`
// so that we can compute its size in order to position it before display. // so that we can compute its size in order to position it before display.
return ( return (
// @ts-ignore - TS doesn't know about our custom element types.
<hypothesis-adder-toolbar <hypothesis-adder-toolbar
class={classnames('annotator-adder', { class={classnames('annotator-adder', {
'annotator-adder--arrow-down': arrowDirection === 'down', 'annotator-adder--arrow-down': arrowDirection === 'down',
...@@ -71,6 +102,7 @@ export default function AdderToolbar({ ...@@ -71,6 +102,7 @@ export default function AdderToolbar({
})} })}
style={{ visibility: isVisible ? 'visible' : 'hidden' }} style={{ visibility: isVisible ? 'visible' : 'hidden' }}
> >
{/* @ts-ignore */}
<hypothesis-adder-actions className="annotator-adder-actions"> <hypothesis-adder-actions className="annotator-adder-actions">
<ToolbarButton <ToolbarButton
icon="annotate" icon="annotate"
...@@ -95,35 +127,16 @@ export default function AdderToolbar({ ...@@ -95,35 +127,16 @@ export default function AdderToolbar({
shortcut={showShortcut} shortcut={showShortcut}
/> />
)} )}
{/* @ts-ignore */}
</hypothesis-adder-actions> </hypothesis-adder-actions>
{/* @ts-ignore */}
</hypothesis-adder-toolbar> </hypothesis-adder-toolbar>
); );
} }
AdderToolbar.propTypes = { AdderToolbar.propTypes = {
/**
* Whether the arrow pointing out of the toolbar towards the selected text
* should appear above the toolbar pointing Up or below the toolbar pointing
* Down.
*/
arrowDirection: propTypes.oneOf(['up', 'down']).isRequired, arrowDirection: propTypes.oneOf(['up', 'down']).isRequired,
/**
* Whether to show the toolbar or not.
*/
isVisible: propTypes.bool.isRequired, isVisible: propTypes.bool.isRequired,
/**
* Callback invoked with the name ("annotate", "highlight", "show") of the
* selected command when a toolbar command is clicked.
*/
onCommand: propTypes.func.isRequired, onCommand: propTypes.func.isRequired,
/**
* Number of annotations associated with the selected text.
*
* If non-zero, a "Show" button is displayed to allow the user to see the
* annotations that correspond to the selection.
*/
annotationCount: propTypes.number, annotationCount: propTypes.number,
}; };
...@@ -4,6 +4,16 @@ import { createElement } from 'preact'; ...@@ -4,6 +4,16 @@ import { createElement } from 'preact';
import SvgIcon from '../../shared/components/svg-icon'; import SvgIcon from '../../shared/components/svg-icon';
/**
* @param {Object} props
* @param {import("preact/hooks").Ref<HTMLButtonElement>} [props.buttonRef]
* @param {boolean} [props.expanded]
* @param {string} [props.extraClasses]
* @param {string} props.label
* @param {string} props.icon
* @param {() => any} props.onClick
* @param {boolean} [props.selected]
*/
function ToolbarButton({ function ToolbarButton({
buttonRef, buttonRef,
expanded, expanded,
...@@ -11,7 +21,7 @@ function ToolbarButton({ ...@@ -11,7 +21,7 @@ function ToolbarButton({
label, label,
icon, icon,
onClick, onClick,
selected, selected = false,
}) { }) {
const handleClick = event => { const handleClick = event => {
// Stop event from propagating up to the document and being treated as a // Stop event from propagating up to the document and being treated as a
...@@ -45,9 +55,39 @@ ToolbarButton.propTypes = { ...@@ -45,9 +55,39 @@ ToolbarButton.propTypes = {
selected: propTypes.bool, selected: propTypes.bool,
}; };
/**
* @typedef ToolbarProps
*
* @prop {() => any} closeSidebar -
* Callback for the "Close sidebar" button. This button is only shown when
* `useMinimalControls` is true and the sidebar is open.
* @prop {() => any} createAnnotation -
* Callback for the "Create annotation" / "Create page note" button. The type
* of annotation depends on whether there is a text selection and is decided
* by the caller.
* @prop {boolean} isSidebarOpen - Is the sidebar currently visible?
* @prop {'annotation'|'note'} newAnnotationType -
* Icon to show on the "Create annotation" button indicating what kind of annotation
* will be created.
* @prop {boolean} showHighlights - Are highlights currently visible in the document?
* @prop {() => any} toggleHighlights -
* Callback to toggle visibility of highlights in the document.
* @prop {() => any} toggleSidebar -
* Callback to toggle the visibility of the sidebar.
* @prop {import("preact/hooks").Ref<HTMLButtonElement>} [toggleSidebarRef] -
* Ref that gets set to the toolbar button for toggling the sidebar.
* This is exposed to enable the drag-to-resize functionality of this
* button.
* @prop {boolean} [useMinimalControls] -
* If true, all controls are hidden except for the "Close sidebar" button
* when the sidebar is open.
*/
/** /**
* Controls on the edge of the sidebar for opening/closing the sidebar, * Controls on the edge of the sidebar for opening/closing the sidebar,
* controlling highlight visibility and creating new page notes. * controlling highlight visibility and creating new page notes.
*
* @param {ToolbarProps} props
*/ */
export default function Toolbar({ export default function Toolbar({
closeSidebar, closeSidebar,
...@@ -102,43 +142,13 @@ export default function Toolbar({ ...@@ -102,43 +142,13 @@ export default function Toolbar({
} }
Toolbar.propTypes = { Toolbar.propTypes = {
/**
* Callback for the "Close sidebar" button. This button is only shown when
* `useMinimalControls` is true and the sidebar is open.
*/
closeSidebar: propTypes.func.isRequired, closeSidebar: propTypes.func.isRequired,
/**
* Callback for the "Create annotation" / "Create page note" button. The type
* of annotation depends on whether there is a text selection and is decided
* by the caller.
*/
createAnnotation: propTypes.func.isRequired, createAnnotation: propTypes.func.isRequired,
/** Is the sidebar currently visible? */
isSidebarOpen: propTypes.bool.isRequired, isSidebarOpen: propTypes.bool.isRequired,
newAnnotationType: propTypes.oneOf(['annotation', 'note']).isRequired, newAnnotationType: propTypes.oneOf(['annotation', 'note']).isRequired,
/** Are highlights currently visible in the document? */
showHighlights: propTypes.bool.isRequired, showHighlights: propTypes.bool.isRequired,
/** Callback to toggle visibility of highlights in the document. */
toggleHighlights: propTypes.func.isRequired, toggleHighlights: propTypes.func.isRequired,
/** Callback to toggle the visibility of the sidebar. */
toggleSidebar: propTypes.func.isRequired, toggleSidebar: propTypes.func.isRequired,
/**
* Ref that gets set to the toolbar button for toggling the sidebar.
* This is exposed to enable the drag-to-resize functionality of this
* button.
*/
toggleSidebarRef: propTypes.any, toggleSidebarRef: propTypes.any,
/**
* If true, all controls are hidden except for the "Close sidebar" button
* when the sidebar is open.
*/
useMinimalControls: propTypes.bool, useMinimalControls: propTypes.bool,
}; };
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
}, },
"include": [ "include": [
"annotator/config/*.js", "annotator/config/*.js",
"annotator/components/*.js",
"annotator/util/*.js", "annotator/util/*.js",
"boot/*.js", "boot/*.js",
"shared/*.js", "shared/*.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