Commit 8493d07d authored by Kyle Keating's avatar Kyle Keating Committed by Kyle Keating

Improve typecheck (menu, menu-item, slider)

parent a5698b9c
......@@ -14,7 +14,7 @@ import { listen } from '../../util/dom';
*/
/**
* @typedef {Ref<HTMLButtonElement> & Ref<PreactElement>} PreactRef
* @typedef {Ref<HTMLElement> | Ref<PreactElement>} PreactRef
*/
/**
......@@ -52,13 +52,13 @@ export default function useElementShouldClose(
const getCurrentNode = closeableEl => {
// if base is present, assume its a preact component
const node = closeableEl.current.base
? closeableEl.current.base
const node = /** @type {PreactElement} */ (closeableEl.current).base
? /** @type {PreactElement} */ (closeableEl.current).base
: closeableEl.current;
if (typeof node !== 'object') {
throw new Error('useElementShouldClose can not find a node reference');
}
return node;
return /** @type {Node} */ (node);
};
useEffect(() => {
......
......@@ -9,6 +9,44 @@ import SvgIcon from '../../shared/components/svg-icon';
import MenuKeyboardNavigation from './menu-keyboard-navigation';
import Slider from './slider';
/**
* @typedef MenuItemProps
* @prop {string} [href] -
* URL of the external link to open when this item is clicked. Either the `href` or an
* `onClick` callback should be supplied.
* @prop {string} [iconAlt] - Alt text for icon.
* @prop {string} [icon] -
* Name or URL of icon to display. If the value is a URL it is displayed using an `<img>`,
* if it is a name it is displayed using `SvgIcon`. If the property is `"blank"` a blank
* placeholder is displayed in place of an icon. If the property is falsey, no placeholder
* is displayed. The placeholder is useful to keep menu item labels aligned in a list if
* some items have icons and others do not.
* @prop {boolean} [isDisabled] -
* Dim the label to indicate that this item is not currently available. The `onClick`
* callback will still be invoked when this item is clicked and the submenu, if any,
* can still be toggled.
* @prop {boolean} [isExpanded] -
* Indicates that the submenu associated with this item is currently open.
* @prop {boolean} [isSelected] -
* Display an indicator to show that this menu item represents something which is currently
* selected/active/focused.
* @prop {boolean} [isSubmenuItem] -
* True if this item is part of a submenu, in which case it is rendered with a different
* style (shaded background)
* @prop {boolean} [isSubmenuVisible] -
* If present, display a button to toggle the sub-menu associated with this item and
* indicate the current state; `true` if the submenu is visible. Note. Omit this prop,
* or set it to null, if there is no `submenu`.
* @prop {string} label - Label of the menu item.
* @prop {(e: Event) => any} [onClick] - Callback to invoke when the menu item is clicked.
* @prop {(e: Event) => any} [onToggleSubmenu] -
* Callback when the user clicks on the toggle to change the expanded state of the menu.
* @prop {Object} [submenu] -
* Contents of the submenu for this item. This is typically a list of `MenuItem` components
* with the `isSubmenuItem` prop set to `true`, but can include other content as well.
* The submenu is only rendered if `isSubmenuVisible` is `true`.
*/
/**
* An item in a dropdown menu.
*
......@@ -24,6 +62,8 @@ import Slider from './slider';
* For items that have submenus, the `MenuItem` will call the `renderSubmenu`
* prop to render the content of the submenu, when the submenu is visible.
* Note that the `submenu` is not supported for link (`href`) items.
*
* @param {MenuItemProps} props
*/
export default function MenuItem({
href,
......@@ -45,7 +85,9 @@ export default function MenuItem({
const hasLeftIcon = icon || isSubmenuItem;
const hasRightIcon = icon && isSubmenuItem;
const menuItemRef = useRef(null);
const menuItemRef = useRef(
/** @type {(HTMLAnchorElement & HTMLDivElement)|null} */ (null)
);
let focusTimer = null;
let renderedIcon = null;
......@@ -110,7 +152,7 @@ export default function MenuItem({
})}
href={href}
target="_blank"
tabIndex="-1"
tabIndex={-1}
rel="noopener noreferrer"
role="menuitem"
onKeyDown={onKeyDown}
......@@ -137,7 +179,7 @@ export default function MenuItem({
'is-expanded': isExpanded,
'is-selected': isSelected,
})}
tabIndex="-1"
tabIndex={-1}
onKeyDown={onKeyDown}
onClick={onClick}
role={isRadioButtonType ? 'menuitemradio' : 'menuitem'}
......@@ -193,76 +235,16 @@ export default function MenuItem({
}
MenuItem.propTypes = {
/**
* URL of the external link to open when this item is clicked.
* Either the `href` or an `onClick` callback should be supplied.
*/
href: propTypes.string,
/** Alt text for icon. */
iconAlt: propTypes.string,
/**
* Name or URL of icon to display. If the value is a URL it is displayed
* using an `<img>`, if it is a name it is displayed using `SvgIcon`.
*
* If the property is `"blank"` a blank placeholder is displayed in place of an
* icon. If the property is falsey, no placeholder is displayed.
* The placeholder is useful to keep menu item labels aligned in a list if
* some items have icons and others do not.
*/
icon: propTypes.string,
/**
* Dim the label to indicate that this item is not currently available.
*
* The `onClick` callback will still be invoked when this item is clicked and
* the submenu, if any, can still be toggled.
*/
isDisabled: propTypes.bool,
/**
* Indicates that the submenu associated with this item is currently open.
*/
isExpanded: propTypes.bool,
/**
* Display an indicator to show that this menu item represents something
* which is currently selected/active/focused.
*/
isSelected: propTypes.bool,
/**
* True if this item is part of a submenu, in which case it is rendered
* with a different style (shaded background)
*/
isSubmenuItem: propTypes.bool,
/**
* If present, display a button to toggle the sub-menu associated with this
* item and indicate the current state; `true` if the submenu is visible.
* Note. Omit this prop, or set it to null, if there is no `submenu`.
*/
isSubmenuVisible: propTypes.bool,
/** Label of the menu item. */
label: propTypes.string.isRequired,
/** Callback to invoke when the menu item is clicked. */
onClick: propTypes.func,
/**
* Callback when the user clicks on the toggle to change the expanded
* state of the menu.
*/
onToggleSubmenu: propTypes.func,
/**
* Contents of the submenu for this item.
*
* This is typically a list of `MenuItem` components with the `isSubmenuItem`
* prop set to `true`, but can include other content as well.
* The submenu is only rendered if `isSubmenuVisible` is `true`.
*/
submenu: propTypes.any,
};
......@@ -24,6 +24,34 @@ const menuArrow = className => (
*/
let ignoreNextClick = false;
/**
* @typedef MenuProps
* @prop {'left'|'right'} [align] -
* Whether the menu content is aligned with the left (default) or right edges of the
* toggle element.
* @prop {string} [arrowClass] -
* Additional CSS class for the arrow caret at the edge of the menu content that "points"
* toward the menu's toggle button. This can be used to adjust the position of that caret
* respective to the toggle button.
* @prop {Object|string} [label] - Label element for the toggle button that hides and shows the menu.
* @prop {Object} [children] -
* Menu items and sections to display in the content area of the menu. These are typically
* `MenuSection` and `MenuItem` components, but other custom content is also allowed.
* @prop {boolean} [containerPositioned] -
* Whether the menu elements should be positioned relative to the Menu container. When
* `false`, the consumer is responsible for positioning.
* @prop {string} [contentClass] - Additional CSS classes to apply to the menu.
* @prop {boolean} [defaultOpen] - Whether the menu is open or closed when initially rendered.
* @prop {(open: boolean) => any} [onOpenChanged] - Callback invoked when the menu is
* opened or closed. This can be used, for example, to reset any ephemeral state that the
* menu content may have.
* @prop {string} title -
* A title for the menu. This is important for accessibility if the menu's toggle button
* has only an icon as a label.
* @prop {boolean} [menuIndicator] -
* Whether to display an indicator next to the label that there is a dropdown menu.
*/
/**
* A drop-down menu.
*
......@@ -41,6 +69,8 @@ let ignoreNextClick = false;
* <MenuItem label="Log out"/>
* </MenuSection>
* </Menu>
*
* @param {MenuProps} props
*/
export default function Menu({
align = 'left',
......@@ -89,7 +119,7 @@ export default function Menu({
//
// These handlers close the menu when the user taps or clicks outside the
// menu or presses Escape.
const menuRef = useRef();
const menuRef = useRef(/** @type {HTMLDivElement|null} */ (null));
// Menu element should close via `closeMenu` whenever it's open and there
// are user interactions outside of it (e.g. clicks) in the document
......@@ -163,7 +193,7 @@ export default function Menu({
contentClass
)}
role="menu"
tabIndex="-1"
tabIndex={-1}
onClick={closeMenu}
onKeyDown={handleMenuKeyDown}
>
......@@ -178,68 +208,17 @@ export default function Menu({
}
Menu.propTypes = {
/**
* Whether the menu content is aligned with the left (default) or right edges
* of the toggle element.
*/
align: propTypes.oneOf(['left', 'right']),
/**
* Additional CSS class for the arrow caret at the edge of the menu
* content that "points" toward the menu's toggle button. This can be used
* to adjust the position of that caret respective to the toggle button.
*/
arrowClass: propTypes.string,
/**
* Label element for the toggle button that hides and shows the menu.
*/
label: propTypes.oneOfType([
propTypes.object.isRequired,
propTypes.string.isRequired,
]),
/**
* Menu items and sections to display in the content area of the menu.
*
* These are typically `MenuSection` and `MenuItem` components, but other
* custom content is also allowed.
*/
children: propTypes.any,
/**
* Whether the menu elements should be positioned relative to the Menu
* container. When `false`, the consumer is responsible for positioning.
*/
containerPositioned: propTypes.bool,
/**
* Additional CSS classes to apply to the menu.
*/
contentClass: propTypes.string,
/**
* Whether the menu is open or closed when initially rendered.
*/
defaultOpen: propTypes.bool,
/**
* Callback invoked when the menu is opened or closed.
*
* This can be used, for example, to reset any ephemeral state that the
* menu content may have.
*/
onOpenChanged: propTypes.func,
/**
* A title for the menu. This is important for accessibility if the menu's
* toggle button has only an icon as a label.
*/
title: propTypes.string.isRequired,
/**
* Whether to display an indicator next to the label that there is a
* dropdown menu.
*/
menuIndicator: propTypes.bool,
};
......@@ -2,6 +2,12 @@ import { createElement } from 'preact';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import propTypes from 'prop-types';
/**
* @typedef SliderProps
* @prop {Object} [children] - The slideable content to hide or reveal.
* @prop {boolean} visible - Whether the content should be visible or not.
*/
/**
* A container which reveals its content when `visible` is `true` using
* a sliding animation.
......@@ -11,6 +17,8 @@ import propTypes from 'prop-types';
* order.
*
* Currently the only reveal/expand direction supported is top-down.
*
* @param {SliderProps} props
*/
export default function Slider({ children, visible }) {
const containerRef = useRef(null);
......@@ -76,6 +84,7 @@ export default function Slider({ children, visible }) {
// nb. Preact uses "ontransitionend" rather than "onTransitionEnd".
// See https://bugs.chromium.org/p/chromium/issues/detail?id=961193
//
// @ts-ignore
// eslint-disable-next-line react/no-unknown-property
ontransitionend={handleTransitionEnd}
ref={containerRef}
......@@ -97,9 +106,5 @@ export default function Slider({ children, visible }) {
Slider.propTypes = {
children: propTypes.any,
/**
* Whether the content should be visible or not.
*/
visible: propTypes.bool,
};
......@@ -206,7 +206,12 @@ const getCurrentlyViewingGroups = createSelector(
*
* // Selectors
* @prop {() => Group[]} allGroups
* // TODO: add rest ...
* @prop {() => Group[]} allGroups
* @prop {() => Group|undefined|null} focusedGroup
* @prop {() => string|null} focusedGroupId
* @prop {() => Group[]} getFeaturedGroups
* @prop {(id: string) => Group|undefined} getGroup
* @prop {() => Group[]} getInScopeGroups
*/
export default {
......
......@@ -48,8 +48,6 @@
"sidebar/components/hypothesis-app.js",
"sidebar/components/logged-out-message.js",
"sidebar/components/login-prompt-panel.js",
"sidebar/components/menu-item.js",
"sidebar/components/menu.js",
"sidebar/components/moderation-banner.js",
"sidebar/components/new-note-btn.js",
"sidebar/components/search-input.js",
......@@ -59,7 +57,6 @@
"sidebar/components/sidebar-content-error.js",
"sidebar/components/sidebar-content.js",
"sidebar/components/sidebar-panel.js",
"sidebar/components/slider.js",
"sidebar/components/sort-menu.js",
"sidebar/components/stream-content.js",
"sidebar/components/stream-search-input.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