Commit c65e2b09 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Convert Menu to TS

parent 9c2c147d
import classnames from 'classnames'; import classnames from 'classnames';
import { useElementShouldClose } from '@hypothesis/frontend-shared'; import { useElementShouldClose } from '@hypothesis/frontend-shared';
import { MenuExpandIcon } from '@hypothesis/frontend-shared/lib/next'; import { MenuExpandIcon } from '@hypothesis/frontend-shared/lib/next';
import type { ComponentChildren } from 'preact';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import MenuArrow from './MenuArrow'; import MenuArrow from './MenuArrow';
...@@ -13,37 +14,62 @@ import MenuKeyboardNavigation from './MenuKeyboardNavigation'; ...@@ -13,37 +14,62 @@ import MenuKeyboardNavigation from './MenuKeyboardNavigation';
*/ */
let ignoreNextClick = false; let ignoreNextClick = false;
/** export type MenuProps = {
* @typedef MenuProps /**
* @prop {'left'|'right'} [align] - * Whether the menu content is aligned with the left (default) or right edges
* Whether the menu content is aligned with the left (default) or right edges of the * of the toggle element.
* toggle element. */
* @prop {string} [arrowClass] - align?: '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. * Additional CSS class for the arrow caret at the edge of the menu content
* @prop {object|string} [label] - Label element for the toggle button that hides and shows the menu. * that "points" toward the menu's toggle button. This can be used to adjust
* @prop {object} [children] - * the position of that caret respective to the toggle button.
* 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. arrowClass?: string;
* @prop {boolean} [containerPositioned] -
* Whether the menu elements should be positioned relative to the Menu container. When /**
* `false`, the consumer is responsible for positioning. * Label element or string for the toggle button that hides and shows the menu
* @prop {string} [contentClass] - Additional CSS classes to apply to the menu.
* @prop {boolean} [defaultOpen] - Whether the menu is open or closed when initially rendered.
* Ignored if `open` is present.
* @prop {(open: boolean) => void} [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 {boolean} [open] - Whether the menu is currently open; overrides internal state
* management for openness. External components managing state in this way should
* also pass an `onOpenChanged` handler to respond when the user closes the menu.
* @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.
*/ */
label: ComponentChildren;
/** Menu content, typically `MenuSection` and `MenuItem` components */
children: ComponentChildren;
/**
* Whether the menu elements should be positioned relative to the Menu
* container. When `false`, the consumer is responsible for positioning.
*/
containerPositioned?: boolean;
/** Additional CSS classes to apply to the Menu */
contentClass?: string;
/**
* Whether the menu is open when initially rendered. Ignored if `open` is
* present.
*/
defaultOpen?: boolean;
/** Whether to render an (arrow) indicator next to the Menu label */
menuIndicator?: boolean;
/** Callback when the Menu is opened or closed. */
onOpenChanged?: (open: boolean) => void;
/**
* Whether the Menu is currently open, when the Menu is being used as a
* controlled component. In these cases, an `onOpenChanged` handler should
* be provided to respond to the user opening or closing the menu.
*/
open?: boolean;
/**
* A title for the menu. This is important for accessibility if the menu's
* toggle button has only an icon as a label.
*/
title: string;
};
const noop = () => {}; const noop = () => {};
...@@ -64,8 +90,6 @@ const noop = () => {}; ...@@ -64,8 +90,6 @@ const noop = () => {};
* <MenuItem label="Log out"/> * <MenuItem label="Log out"/>
* </MenuSection> * </MenuSection>
* </Menu> * </Menu>
*
* @param {MenuProps} props
*/ */
export default function Menu({ export default function Menu({
align = 'left', align = 'left',
...@@ -79,9 +103,9 @@ export default function Menu({ ...@@ -79,9 +103,9 @@ export default function Menu({
onOpenChanged, onOpenChanged,
menuIndicator = true, menuIndicator = true,
title, title,
}) { }: MenuProps) {
/** @type {[boolean, (open: boolean) => void]} */ let [isOpen, setOpen]: [boolean, (open: boolean) => void] =
let [isOpen, setOpen] = useState(defaultOpen); useState(defaultOpen);
if (typeof open === 'boolean') { if (typeof open === 'boolean') {
isOpen = open; isOpen = open;
setOpen = onOpenChanged || noop; setOpen = onOpenChanged || noop;
...@@ -96,11 +120,12 @@ export default function Menu({ ...@@ -96,11 +120,12 @@ export default function Menu({
} }
}, [isOpen, onOpenChanged]); }, [isOpen, onOpenChanged]);
// Toggle menu when user presses toggle button. The menu is shown on mouse /**
// press for a more responsive/native feel but also handles a click event for * Toggle menu when user presses toggle button. The menu is shown on mouse
// activation via other input methods. * press for a more responsive/native feel but also handles a click event for
/** @param {Event} event */ * activation via other input methods.
const toggleMenu = event => { */
const toggleMenu = (event: Event) => {
// If the menu was opened on press, don't close it again on the subsequent // If the menu was opened on press, don't close it again on the subsequent
// mouse up ("click") event. // mouse up ("click") event.
if (event.type === 'mousedown') { if (event.type === 'mousedown') {
...@@ -122,18 +147,16 @@ export default function Menu({ ...@@ -122,18 +147,16 @@ export default function Menu({
// //
// These handlers close the menu when the user taps or clicks outside the // These handlers close the menu when the user taps or clicks outside the
// menu or presses Escape. // menu or presses Escape.
const menuRef = /** @type {{ current: HTMLDivElement }} */ (useRef()); const menuRef = useRef<HTMLDivElement | null>(null);
// Menu element should close via `closeMenu` whenever it's open and there // Menu element should close via `closeMenu` whenever it's open and there
// are user interactions outside of it (e.g. clicks) in the document // are user interactions outside of it (e.g. clicks) in the document
useElementShouldClose(menuRef, isOpen, closeMenu); useElementShouldClose(menuRef, isOpen, closeMenu);
/** @param {Event} e */ const stopPropagation = (e: Event) => e.stopPropagation();
const stopPropagation = e => e.stopPropagation();
// It should also close if the user presses a key which activates menu items. // It should also close if the user presses a key which activates menu items.
/** @param {KeyboardEvent} event */ const handleMenuKeyDown = (event: KeyboardEvent) => {
const handleMenuKeyDown = event => {
const key = event.key; const key = event.key;
if (key === 'Enter' || key === ' ') { if (key === 'Enter' || key === ' ') {
// The browser will not open the link if the link element is removed // The browser will not open the link if the link element is removed
...@@ -145,7 +168,6 @@ export default function Menu({ ...@@ -145,7 +168,6 @@ export default function Menu({
} }
}; };
/** @type {{ position: 'relative'|'static' }} */
const containerStyle = { const containerStyle = {
position: containerPositioned ? 'relative' : 'static', position: containerPositioned ? 'relative' : 'static',
}; };
......
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