diff --git a/packages/react/src/components/Menu/Menu.tsx b/packages/react/src/components/Menu/Menu.tsx index aa180f492147..9449099d2262 100644 --- a/packages/react/src/components/Menu/Menu.tsx +++ b/packages/react/src/components/Menu/Menu.tsx @@ -8,6 +8,8 @@ import cx from 'classnames'; import PropTypes from 'prop-types'; import React, { + forwardRef, + ReactNode, useContext, useEffect, useMemo, @@ -36,7 +38,7 @@ interface MenuProps extends React.HTMLAttributes { /** * A collection of MenuItems to be rendered within this Menu. */ - children?: React.ReactNode; + children?: ReactNode; /** * Additional CSS class names. @@ -46,7 +48,7 @@ interface MenuProps extends React.HTMLAttributes { /** * A label describing the Menu. */ - label?: string; + label: string; /** * Specify how the menu should align with the button element @@ -85,20 +87,20 @@ interface MenuProps extends React.HTMLAttributes { /** * Specify a DOM node where the Menu should be rendered in. Defaults to document.body. */ - target?: any; + 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 | null | undefined)[]; + 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 | null | undefined)[]; + y?: number | [number, number]; } -const Menu = React.forwardRef(function Menu( +const Menu = forwardRef(function Menu( { children, className, @@ -149,7 +151,7 @@ const Menu = React.forwardRef(function Menu( }, [childState, childDispatch]); const menu = useRef(null); - const ref = useMergedRefs([forwardRef, menu]); + const ref = useMergedRefs([forwardRef, menu]); const [position, setPosition] = useState([-1, -1]); const focusableItems = childContext.state.items.filter( @@ -163,7 +165,7 @@ const Menu = React.forwardRef(function Menu( actionButtonWidth = w; } - // Set RTL based on document direction or `LayoutDirection` + // Set RTL based on the document direction or `LayoutDirection` const { direction } = useLayoutDirection(); function returnFocus() { @@ -450,6 +452,7 @@ Menu.propTypes = { /** * A label describing the Menu. */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error label: PropTypes.string, /** @@ -489,11 +492,13 @@ Menu.propTypes = { /** * 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), @@ -502,6 +507,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), diff --git a/packages/react/src/components/Menu/MenuContext.ts b/packages/react/src/components/Menu/MenuContext.ts index b23f2d33249c..3731beb1fce2 100644 --- a/packages/react/src/components/Menu/MenuContext.ts +++ b/packages/react/src/components/Menu/MenuContext.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; +import { createContext, KeyboardEvent, RefObject } from 'react'; type ActionType = { type: 'enableIcons' | 'registerItem'; @@ -18,9 +18,7 @@ type StateType = { hasIcons: boolean; size: 'xs' | 'sm' | 'md' | 'lg' | null; items: any[]; - requestCloseRoot: ( - e: Pick, 'type'> - ) => void; + requestCloseRoot: (e: Pick, 'type'>) => void; }; const menuDefaultState: StateType = { @@ -49,13 +47,23 @@ function menuReducer(state: StateType, action: ActionType) { } } -const MenuContext = React.createContext<{ +type DispatchFuncProps = { + type: 'registerItem' | 'enableIcons'; + payload: { + ref: RefObject; + disabled: boolean; + }; +}; + +type MenuContextProps = { state: StateType; - dispatch: React.Dispatch; -}>({ + dispatch: (props: DispatchFuncProps) => void; +}; + +const MenuContext = createContext({ state: menuDefaultState, // 'dispatch' is populated by the root menu - dispatch: () => {}, + dispatch: (_: DispatchFuncProps) => {}, }); export { MenuContext, menuReducer }; diff --git a/packages/react/src/components/Menu/MenuItem.tsx b/packages/react/src/components/Menu/MenuItem.tsx index 0e6a95b8e7bb..295db5b8a5e1 100644 --- a/packages/react/src/components/Menu/MenuItem.tsx +++ b/packages/react/src/components/Menu/MenuItem.tsx @@ -7,7 +7,21 @@ import cx from 'classnames'; import PropTypes from 'prop-types'; -import React, { useContext, useEffect, useRef, useState } from 'react'; +import React, { + ChangeEventHandler, + ComponentProps, + FC, + ForwardedRef, + forwardRef, + KeyboardEvent, + LiHTMLAttributes, + MouseEvent, + ReactNode, + useContext, + useEffect, + useRef, + useState, +} from 'react'; import { CaretRight, CaretLeft, Checkmark } from '@carbon/icons-react'; import { keys, match } from '../../internal/keyboard'; @@ -21,11 +35,11 @@ import { MenuContext } from './MenuContext'; import { useLayoutDirection } from '../LayoutDirection'; import { Text } from '../Text'; -interface MenuItemProps extends React.LiHTMLAttributes { +export interface MenuItemProps extends LiHTMLAttributes { /** * Optionally provide another Menu to create a submenu. props.children can't be used to specify the content of the MenuItem itself. Use props.label instead. */ - children?: React.ReactNode; + children?: ReactNode; /** * Additional CSS class names. @@ -51,13 +65,13 @@ interface MenuItemProps extends React.LiHTMLAttributes { * Provide an optional function to be called when the MenuItem is clicked. */ onClick?: ( - event: React.KeyboardEvent | React.MouseEvent + event: KeyboardEvent | MouseEvent ) => void; /** * Only applicable if the parent menu is in `basic` mode. Sets the menu item's icon. */ - renderIcon?: any; + renderIcon?: FC; /** * Provide a shortcut for the action of this MenuItem. Note that the component will only render it as a hint but not actually register the shortcut. @@ -67,7 +81,7 @@ interface MenuItemProps extends React.LiHTMLAttributes { const hoverIntentDelay = 150; // in ms -const MenuItem = React.forwardRef( +export const MenuItem = forwardRef( function MenuItem( { children, @@ -86,12 +100,12 @@ const MenuItem = React.forwardRef( const context = useContext(MenuContext); const menuItem = useRef(null); - const ref = useMergedRefs([forwardRef, menuItem]); + const ref = useMergedRefs([forwardRef, menuItem]); const [boundaries, setBoundaries] = useState<{ - x: number | number[]; - y: number | number[]; + x: number | [number, number]; + y: number | [number, number]; }>({ x: -1, y: -1 }); - const [isRtl, setRtl] = useState(false); + const [rtl, setRtl] = useState(false); const hasChildren = Boolean(children); const [submenuOpen, setSubmenuOpen] = useState(false); @@ -116,8 +130,9 @@ const MenuItem = React.forwardRef( if (!menuItem.current) { return; } + const { x, y, width, height } = menuItem.current.getBoundingClientRect(); - if (isRtl) { + if (rtl) { setBoundaries({ x: [-x, x - width], y: [y, y + height], @@ -138,7 +153,7 @@ const MenuItem = React.forwardRef( } function handleClick( - e: React.KeyboardEvent | React.MouseEvent + e: KeyboardEvent | MouseEvent ) { if (!isDisabled) { if (hasChildren) { @@ -194,7 +209,7 @@ const MenuItem = React.forwardRef( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Set RTL based on document direction or `LayoutDirection` + // Set RTL based on the document direction or `LayoutDirection` const { direction } = useLayoutDirection(); useEffect(() => { if (document?.dir === 'rtl' || direction === 'rtl') { @@ -211,6 +226,7 @@ const MenuItem = React.forwardRef( useEffect(() => { if (iconsAllowed && IconElement && !context.state.hasIcons) { + // @ts-ignore - TODO: Should we be passing payload? context.dispatch({ type: 'enableIcons' }); } }, [iconsAllowed, IconElement, context.state.hasIcons, context]); @@ -222,8 +238,8 @@ const MenuItem = React.forwardRef( ref={ref} className={classNames} tabIndex={-1} - aria-disabled={isDisabled} - aria-haspopup={hasChildren || undefined} + aria-disabled={isDisabled ?? undefined} + aria-haspopup={hasChildren ?? undefined} aria-expanded={hasChildren ? submenuOpen : undefined} onClick={handleClick} onMouseEnter={hasChildren ? handleMouseEnter : undefined} @@ -241,7 +257,7 @@ const MenuItem = React.forwardRef( {hasChildren && ( <>
- {isRtl ? : } + {rtl ? : }
{ /** * Specify whether the option should be selected by default. */ defaultSelected?: boolean; - /** - * A required label titling this option. - */ - label: string; - /** * Provide an optional function to be called when the selection state changes. */ - onChange?: React.ChangeEventHandler; + onChange?: ChangeEventHandler; /** - * Pass a bool to props.selected to control the state of this option. + * Controls the state of this option. */ selected?: boolean; } -const MenuItemSelectable = React.forwardRef< +export const MenuItemSelectable = forwardRef< HTMLLIElement, MenuItemSelectableProps >(function MenuItemSelectable( @@ -363,6 +373,7 @@ const MenuItemSelectable = React.forwardRef< useEffect(() => { if (!context.state.hasIcons) { + // @ts-ignore - TODO: Should we be passing payload? context.dispatch({ type: 'enableIcons' }); } }, [context.state.hasIcons, context]); @@ -392,6 +403,7 @@ MenuItemSelectable.propTypes = { /** * Specify whether the option should be selected by default. */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error defaultSelected: PropTypes.bool, /** @@ -402,19 +414,21 @@ MenuItemSelectable.propTypes = { /** * Provide an optional function to be called when the selection state changes. */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error onChange: PropTypes.func, /** * Pass a bool to props.selected to control the state of this option. */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error selected: PropTypes.bool, }; -interface MenuItemGroupProps { +export interface MenuItemGroupProps extends ComponentProps<'ul'> { /** * A collection of MenuItems to be rendered within this group. */ - children?: React.ReactNode; + children?: ReactNode; /** * Additional CSS class names. @@ -427,7 +441,7 @@ interface MenuItemGroupProps { label: string; } -const MenuItemGroup = React.forwardRef( +export const MenuItemGroup = forwardRef( function MenuItemGroup({ children, className, label, ...rest }, forwardRef) { const prefix = usePrefix(); @@ -460,9 +474,10 @@ MenuItemGroup.propTypes = { label: PropTypes.string.isRequired, }; -const defaultItemToString = (item: any) => item.toString(); +const defaultItemToString = (item) => item.toString(); -interface MenuItemRadioGroupProps { +export interface MenuItemRadioGroupProps + extends Omit, 'onChange'> { /** * Additional CSS class names. */ @@ -471,17 +486,17 @@ interface MenuItemRadioGroupProps { /** * Specify the default selected item. Must match the type of props.items. */ - defaultSelectedItem?: any; + defaultSelectedItem?: Item; /** * Provide a function to convert an item to the string that will be rendered. Defaults to item.toString(). */ - itemToString?: (item: any) => string; + itemToString?: (item: Item) => string; /** * Provide the options for this radio group. Can be of any type, as long as you provide an appropriate props.itemToString function. */ - items?: any[]; + items: Item[]; /** * A required label titling this radio group. @@ -491,18 +506,15 @@ interface MenuItemRadioGroupProps { /** * Provide an optional function to be called when the selection changes. */ - onChange?: React.ChangeEventHandler; + onChange?: ChangeEventHandler; /** * Provide props.selectedItem to control the state of this radio group. Must match the type of props.items. */ - selectedItem?: any; + selectedItem?: Item; } -const MenuItemRadioGroup = React.forwardRef< - HTMLLIElement, - MenuItemRadioGroupProps ->(function MenuItemRadioGroup( +export const MenuItemRadioGroup = forwardRef(function MenuItemRadioGroup( { className, defaultSelectedItem, @@ -512,8 +524,8 @@ const MenuItemRadioGroup = React.forwardRef< onChange, selectedItem, ...rest - }, - forwardRef + }: MenuItemRadioGroupProps, + forwardRef: ForwardedRef ) { const prefix = usePrefix(); const context = useContext(MenuContext); @@ -541,6 +553,7 @@ const MenuItemRadioGroup = React.forwardRef< useEffect(() => { if (!context.state.hasIcons) { + // @ts-ignore - TODO: Should we be passing payload? context.dispatch({ type: 'enableIcons' }); } }, [context.state.hasIcons, context]); @@ -550,7 +563,7 @@ const MenuItemRadioGroup = React.forwardRef< return (
    • - {items?.map((item, i) => ( + {items.map((item, i) => ( { /** * Additional CSS class names. */ className?: string; } -const MenuItemDivider = React.forwardRef( +export const MenuItemDivider = forwardRef( function MenuItemDivider({ className, ...rest }, forwardRef) { const prefix = usePrefix(); @@ -629,11 +645,3 @@ MenuItemDivider.propTypes = { */ className: PropTypes.string, }; - -export { - MenuItem, - MenuItemSelectable, - MenuItemGroup, - MenuItemRadioGroup, - MenuItemDivider, -}; diff --git a/packages/react/src/components/MenuButton/index.js b/packages/react/src/components/MenuButton/index.js deleted file mode 100644 index e58cfde881b1..000000000000 --- a/packages/react/src/components/MenuButton/index.js +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Copyright IBM Corp. 2023 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import React, { useRef, useState } from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import { ChevronDown } from '@carbon/icons-react'; -import Button from '../Button'; -import { Menu } from '../Menu'; - -import { useAttachedMenu } from '../../internal/useAttachedMenu'; -import { useId } from '../../internal/useId'; -import { useMergedRefs } from '../../internal/useMergedRefs'; -import { usePrefix } from '../../internal/usePrefix'; - -const spacing = 0; // top and bottom spacing between the button and the menu. in px -const validButtonKinds = ['primary', 'tertiary', 'ghost']; -const defaultButtonKind = 'primary'; - -const MenuButton = React.forwardRef(function MenuButton( - { - children, - className, - disabled, - kind = defaultButtonKind, - label, - size = 'lg', - menuAlignment = 'bottom', - tabIndex = 0, - ...rest - }, - forwardRef -) { - const id = useId('MenuButton'); - const prefix = usePrefix(); - - const triggerRef = useRef(null); - const menuRef = useRef(null); - const ref = useMergedRefs([forwardRef, triggerRef]); - const [width, setWidth] = useState(0); - const { - open, - x, - y, - handleClick: hookOnClick, - handleMousedown, - handleClose, - } = useAttachedMenu(triggerRef); - - function handleClick() { - if (triggerRef.current) { - const { width: w } = triggerRef.current.getBoundingClientRect(); - setWidth(w); - hookOnClick(); - } - } - - function handleOpen() { - menuRef.current.style.inlineSize = `${width}px`; - menuRef.current.style.minInlineSize = `${width}px`; - if (menuAlignment !== 'bottom' && menuAlignment !== 'top') { - menuRef.current.style.inlineSize = `fit-content`; - } - } - - const containerClasses = classNames( - `${prefix}--menu-button__container`, - className - ); - - const triggerClasses = classNames(`${prefix}--menu-button__trigger`, { - [`${prefix}--menu-button__trigger--open`]: open, - }); - - const menuClasses = classNames(`${prefix}--menu-button__${menuAlignment}`); - - const buttonKind = validButtonKinds.includes(kind) ? kind : defaultButtonKind; - - return ( -
      - - - {children} - -
      - ); -}); - -MenuButton.propTypes = { - /** - * A collection of MenuItems to be rendered as actions for this MenuButton. - */ - children: PropTypes.node.isRequired, - - /** - * Additional CSS class names. - */ - className: PropTypes.string, - - /** - * Specify whether the MenuButton should be disabled, or not. - */ - disabled: PropTypes.bool, - - /** - * Specify the type of button to be used as the base for the trigger button. - */ - kind: PropTypes.oneOf(validButtonKinds), - - /** - * Provide the label to be renderd on the trigger button. - */ - label: PropTypes.string.isRequired, - - /** - * Experimental property. Specify how the menu should align with the button element - */ - menuAlignment: PropTypes.oneOf([ - 'top', - 'top-start', - 'top-end', - 'bottom', - 'bottom-start', - 'bottom-end', - ]), - - /** - * Specify the size of the button and menu. - */ - size: PropTypes.oneOf(['sm', 'md', 'lg']), - - /** - * Specify the tabIndex of the button. - */ - tabIndex: PropTypes.number, -}; - -export { MenuButton }; diff --git a/packages/react/src/components/MenuButton/index.tsx b/packages/react/src/components/MenuButton/index.tsx new file mode 100644 index 000000000000..cf2ab9e874de --- /dev/null +++ b/packages/react/src/components/MenuButton/index.tsx @@ -0,0 +1,233 @@ +/** + * Copyright IBM Corp. 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { + ComponentProps, + forwardRef, + ReactNode, + useRef, + useState, +} from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { ChevronDown } from '@carbon/icons-react'; +import Button from '../Button'; +import { Menu } from '../Menu'; + +import { useAttachedMenu } from '../../internal/useAttachedMenu'; +import { useId } from '../../internal/useId'; +import { useMergedRefs } from '../../internal/useMergedRefs'; +import { usePrefix } from '../../internal/usePrefix'; + +const spacing = 0; // top and bottom spacing between the button and the menu. in px +const validButtonKinds = ['primary', 'tertiary', 'ghost']; +const defaultButtonKind = 'primary'; + +export interface MenuButtonProps extends ComponentProps<'div'> { + /** + * A collection of MenuItems to be rendered as actions for this MenuButton. + */ + children?: ReactNode; + + /** + * Additional CSS class names. + */ + className?: string; + + /** + * Specify whether the MenuButton should be disabled, or not. + */ + disabled?: boolean; + + /** + * Specify the type of button to be used as the base for the trigger button. + */ + kind?: 'primary' | 'tertiary' | 'ghost'; + + /** + * Provide the label to be rendered on the trigger button. + */ + label: string; + + /** + * Experimental property. Specify how the menu should align with the button element + */ + menuAlignment: + | 'top' + | 'top-start' + | 'top-end' + | 'bottom' + | 'bottom-start' + | 'bottom-end'; + + /** + * Specify the size of the button and menu. + */ + size?: 'sm' | 'md' | 'lg'; + + /** + * Specify the tabIndex of the button. + */ + tabIndex?: number; +} + +const MenuButton = forwardRef( + function MenuButton( + { + children, + className, + disabled, + kind = defaultButtonKind, + label, + size = 'lg', + menuAlignment = 'bottom', + tabIndex = 0, + ...rest + }, + forwardRef + ) { + const id = useId('MenuButton'); + const prefix = usePrefix(); + + const triggerRef = useRef(null); + const menuRef = useRef(null); + const ref = useMergedRefs([forwardRef, triggerRef]); + const [width, setWidth] = useState(0); + const { + open, + x, + y, + handleClick: hookOnClick, + handleMousedown, + handleClose, + } = useAttachedMenu(triggerRef); + + function handleClick() { + if (triggerRef.current) { + const { width: w } = triggerRef.current.getBoundingClientRect(); + setWidth(w); + hookOnClick(); + } + } + + function handleOpen() { + if (menuRef.current) { + menuRef.current.style.inlineSize = `${width}px`; + menuRef.current.style.minInlineSize = `${width}px`; + if (menuAlignment !== 'bottom' && menuAlignment !== 'top') { + menuRef.current.style.inlineSize = `fit-content`; + } + } + } + + const containerClasses = classNames( + `${prefix}--menu-button__container`, + className + ); + + const triggerClasses = classNames(`${prefix}--menu-button__trigger`, { + [`${prefix}--menu-button__trigger--open`]: open, + }); + + const menuClasses = classNames(`${prefix}--menu-button__${menuAlignment}`); + + return ( +
      + + + {children} + +
      + ); + } +); + +MenuButton.propTypes = { + /** + * A collection of MenuItems to be rendered as actions for this MenuButton. + */ + children: PropTypes.node.isRequired, + + /** + * Additional CSS class names. + */ + className: PropTypes.string, + + /** + * Specify whether the MenuButton should be disabled, or not. + */ + disabled: PropTypes.bool, + + /** + * Specify the type of button to be used as the base for the trigger button. + */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error + kind: PropTypes.oneOf(validButtonKinds), + + /** + * Provide the label to be renderd on the trigger button. + */ + label: PropTypes.string.isRequired, + + /** + * Experimental property. Specify how the menu should align with the button element + */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error + menuAlignment: PropTypes.oneOf([ + 'top', + 'top-start', + 'top-end', + 'bottom', + 'bottom-start', + 'bottom-end', + ]), + + /** + * Specify the size of the button and menu. + */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error + size: PropTypes.oneOf(['sm', 'md', 'lg']), + + /** + * Specify the tabIndex of the button. + */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error + tabIndex: PropTypes.number, +}; + +export { MenuButton }; diff --git a/packages/react/src/internal/useAttachedMenu.js b/packages/react/src/internal/useAttachedMenu.ts similarity index 60% rename from packages/react/src/internal/useAttachedMenu.js rename to packages/react/src/internal/useAttachedMenu.ts index d6c4becc5d69..237c15e2cb65 100644 --- a/packages/react/src/internal/useAttachedMenu.js +++ b/packages/react/src/internal/useAttachedMenu.ts @@ -5,17 +5,44 @@ * LICENSE file in the root directory of this source tree. */ -import { useState } from 'react'; +import { MouseEventHandler, useState } from 'react'; /** - * @typedef {object} useAttachedMenuReturn - * @property {boolean} open Whether the menu is open or not - * @property {[number, number]} x The x position of the menu - * @property {[number, number]} y The y position of the menu - * @property {Function} handleClick A function to be called when the trigger element receives a click event - * @property {Function} handleMousedown A function to be called when the trigger element recives a mousedown event - * @property {Function} handleClose A function to be called when the menu emits onClose + * Array of two numbers either representing [left, right] or [top, bottom]. */ +type TwoCoordinates = [number, number]; + +export interface UseAttachedMenuReturn { + /** + * Whether the menu is open or not. + */ + open: boolean; + + /** + * The x position of the menu. + */ + x: TwoCoordinates; + + /** + * The y position of the menu. + */ + y: TwoCoordinates; + + /** + * A function to be called when the trigger element receives a click event. + */ + handleClick: () => void; + + /** + * A function to be called when the trigger element receives a mousedown event. + */ + handleMousedown: MouseEventHandler; + + /** + * A function to be called when the menu emits onClose. + */ + handleClose: () => void; +} /** * This hook contains common code to be used when a menu should be visually attached to an anchor based on a click event. @@ -23,9 +50,9 @@ import { useState } from 'react'; * @param {Element|object} anchor The element or ref the menu should visually be attached to. * @returns {useAttachedMenuReturn} */ -export function useAttachedMenu(anchor) { +export function useAttachedMenu(anchor): UseAttachedMenuReturn { const [open, setOpen] = useState(false); - const [position, setPosition] = useState([ + const [position, setPosition] = useState([ [-1, -1], [-1, -1], ]);