Skip to content

Commit

Permalink
fix: convert menu, menubutton, and menuitem to typescript
Browse files Browse the repository at this point in the history
Menu, MenuButton and MenuItem were left out of the list on #12513 but I assume
they should be updated too.  Actually, I need MenuButton and MenuItem to be
updated before I can upgrade to Carbon 11.

Other notes:

There's some complication with MouseEventHandler vs. (evt: MouseEvent) => void
being subtly different, which prevented me from adding types to internal methods.
I don't understand the details. MouseEventHandler<HTMLLElement> is also subtly
different from (evt: MouseEvent<HTMLLElement>, MouseEvent>) => void.

I had to add @ts-ignore to the PropTypes declarations for reasons I don't
understand.  I'm not too worried about it since PropTypes declarations are
essentially deprecated.

Also, I know the doc said not to convert internal files to Typescript but it didn’t
seem feasible to do this conversion without useAttachedMenu.js and
MenuContext.ts having the right types, and it didn’t seem feasible for them
to have the right types without converting them to Typescript.

Speaking of which, this conversion required some guessing about types, in particular
the types in MenuContext.ts.
  • Loading branch information
wkeese committed Dec 26, 2023
1 parent 86f16eb commit 1623130
Show file tree
Hide file tree
Showing 6 changed files with 394 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import cx from 'classnames';
import PropTypes from 'prop-types';
import React, {
ComponentProps,
ForwardedRef,
ReactNode,
useContext,
useEffect,
useMemo,
Expand All @@ -27,6 +30,67 @@ import { useLayoutDirection } from '../LayoutDirection';

const spacing = 8; // distance to keep to window edges, in px

export interface MenuProps extends ComponentProps<'ul'> {
/**
* A collection of MenuItems to be rendered within this Menu.
*/
children?: ReactNode;

/**
* Additional CSS class names.
*/
className?: string;

/**
* A label describing the Menu.
*/
label: string;

/**
* The mode of this menu. Defaults to full.
* `full` supports nesting and selectable menu items, but no icons.
* `basic` supports icons but no nesting or selectable menu items.
*
* **This prop is not intended for use and will be set by the respective implementation (like useContextMenu, MenuButton, and ComboButton).**
*/
mode?: 'full' | 'basic';

/**
* Provide an optional function to be called when the Menu should be closed.
*/
onClose?: () => void;

/**
* Provide an optional function to be called when the Menu is opened.
*/
onOpen?: () => void;

/**
* Whether the Menu is open or not.
*/
open?: boolean;

/**
* Specify the size of the Menu.
*/
size?: 'xs' | 'sm' | 'md' | 'lg';

/**
* Specify a DOM node where the Menu should be rendered in. Defaults to document.body.
*/
target?: HTMLElement;

/**
* Specify the x position of the Menu. Either pass a single number or an array with two numbers describing your activator's boundaries ([x1, x2])
*/
x?: number | [number, number];

/**
* Specify the y position of the Menu. Either pass a single number or an array with two numbers describing your activator's boundaries ([y1, y2])
*/
y?: number | [number, number];
}

const Menu = React.forwardRef(function Menu(
{
children,
Expand All @@ -41,12 +105,12 @@ const Menu = React.forwardRef(function Menu(
x = 0,
y = 0,
...rest
},
forwardRef
}: MenuProps,
forwardRef: ForwardedRef<HTMLUListElement>
) {
const prefix = usePrefix();

const focusReturn = useRef(null);
const focusReturn = useRef<Element>();

const context = useContext(MenuContext);

Expand Down Expand Up @@ -75,26 +139,26 @@ const Menu = React.forwardRef(function Menu(
};
}, [childState, childDispatch]);

const menu = useRef();
const ref = useMergedRefs([forwardRef, menu]);
const menu = useRef<HTMLUListElement | null>(null);
const ref = useMergedRefs<HTMLUListElement>([forwardRef, menu]);

const [position, setPosition] = useState([-1, -1]);
const focusableItems = childContext.state.items.filter(
(item) => !item.disabled && item.ref.current
);

// Set RTL based on document direction or `LayoutDirection`
// Set RTL based on the document direction or `LayoutDirection`
const { direction } = useLayoutDirection();

function returnFocus() {
if (focusReturn.current) {
if (focusReturn.current instanceof HTMLElement) {
focusReturn.current.focus();
}
}

function handleOpen() {
if (menu.current) {
focusReturn.current = document.activeElement;
focusReturn.current = document.activeElement ?? undefined;

const pos = calculatePosition();
if (
Expand Down Expand Up @@ -148,7 +212,7 @@ const Menu = React.forwardRef(function Menu(
}
}

function focusItem(e) {
function focusItem(e?) {
const currentItem = focusableItems.findIndex((item) =>
item.ref.current.contains(document.activeElement)
);
Expand Down Expand Up @@ -181,13 +245,14 @@ const Menu = React.forwardRef(function Menu(
}

function handleBlur(e) {
if (open && onClose && isRoot && !menu.current.contains(e.relatedTarget)) {
if (open && onClose && isRoot && !menu.current?.contains(e.relatedTarget)) {
handleClose(e);
}
}

function fitValue(range, axis) {
const { width, height } = menu.current.getBoundingClientRect();
// Note: This method is only called when menu.current is defined.
const { width, height } = menu.current!.getBoundingClientRect();

Check warning on line 255 in packages/react/src/components/Menu/Menu.tsx

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion
const alignment = isRoot ? 'vertical' : 'horizontal';

const axes = {
Expand Down Expand Up @@ -226,7 +291,7 @@ const Menu = React.forwardRef(function Menu(
return bestOption >= spacing ? bestOption : spacing;
}

function calculatePosition() {
function calculatePosition(): [number, number] {
if (menu.current) {
const ranges = {
x: typeof x === 'object' && x.length === 2 ? x : [x, x],
Expand All @@ -252,7 +317,7 @@ const Menu = React.forwardRef(function Menu(
} else {
// reset position when menu is closed in order for the --shown
// modifier to be applied correctly
setPosition(-1, -1);
setPosition([-1, -1]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
Expand Down Expand Up @@ -305,6 +370,7 @@ Menu.propTypes = {
/**
* A label describing the Menu.
*/
// @ts-ignore-next-line -- avoid spurious (?) TS2322 error
label: PropTypes.string,

/**
Expand Down Expand Up @@ -334,16 +400,19 @@ Menu.propTypes = {
/**
* Specify the size of the Menu.
*/
// @ts-ignore-next-line -- avoid spurious (?) TS2322 error
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),

/**
* Specify a DOM node where the Menu should be rendered in. Defaults to document.body.
*/
// @ts-ignore-next-line -- avoid spurious (?) TS2322 error
target: PropTypes.object,

/**
* Specify the x position of the Menu. Either pass a single number or an array with two numbers describing your activator's boundaries ([x1, x2])
*/
// @ts-ignore-next-line -- avoid spurious (?) TS2322 error
x: PropTypes.oneOfType([
PropTypes.number,
PropTypes.arrayOf(PropTypes.number),
Expand All @@ -352,6 +421,7 @@ Menu.propTypes = {
/**
* Specify the y position of the Menu. Either pass a single number or an array with two numbers describing your activator's boundaries ([y1, y2])
*/
// @ts-ignore-next-line -- avoid spurious (?) TS2322 error
y: PropTypes.oneOfType([
PropTypes.number,
PropTypes.arrayOf(PropTypes.number),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@
* LICENSE file in the root directory of this source tree.
*/

import React from 'react';
import React, { MouseEventHandler, RefObject } from 'react';

const menuDefaultState = {
type MenuState = {
isRoot: boolean;
mode: 'full' | 'basic';
hasIcons: boolean;
size: null;
items: [];
requestCloseRoot: MouseEventHandler;
};

const menuDefaultState: MenuState = {
isRoot: true,
mode: 'full',
hasIcons: false,
Expand All @@ -33,11 +42,24 @@ function menuReducer(state, action) {
}
}

const MenuContext = React.createContext({
type DispatchFuncProps = {
type?: 'registerItem' | 'enableIcons';
payload?: {
ref: RefObject<HTMLLIElement>;
disabled: boolean;
};
};

type MenuContextProps = {
state: MenuState;
dispatch: (props: DispatchFuncProps) => void;
};

const MenuContext = React.createContext<MenuContextProps>({
state: menuDefaultState,

// 'dispatch' is populated by the root menu
dispatch: () => {},
dispatch: (_: DispatchFuncProps) => {},
});

export { MenuContext, menuReducer };
Loading

0 comments on commit 1623130

Please sign in to comment.