diff --git a/docs/data/base/components/menu/MenuSimple.js b/docs/data/base/components/menu/MenuSimple.js index e30162faa5bac8..12a46816737323 100644 --- a/docs/data/base/components/menu/MenuSimple.js +++ b/docs/data/base/components/menu/MenuSimple.js @@ -5,14 +5,19 @@ import MenuItemUnstyled, { } from '@mui/base/MenuItemUnstyled'; import PopperUnstyled from '@mui/base/PopperUnstyled'; import { styled } from '@mui/system'; +import { ListActionTypes } from '@mui/base/useList'; export default function UnstyledMenuSimple() { - const [anchorEl, setAnchorEl] = React.useState(null); - const isOpen = Boolean(anchorEl); - const buttonRef = React.useRef(null); + const [buttonElement, setButtonElement] = React.useState(null); + + const [isOpen, setOpen] = React.useState(false); const menuActions = React.useRef(null); const preventReopen = React.useRef(false); + const updateAnchor = React.useCallback((node) => { + setButtonElement(node); + }, []); + const handleButtonClick = (event) => { if (preventReopen.current) { event.preventDefault(); @@ -20,11 +25,7 @@ export default function UnstyledMenuSimple() { return; } - if (isOpen) { - setAnchorEl(null); - } else { - setAnchorEl(event.currentTarget); - } + setOpen((open) => !open); }; const handleButtonMouseDown = () => { @@ -38,22 +39,23 @@ export default function UnstyledMenuSimple() { const handleButtonKeyDown = (event) => { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); - setAnchorEl(event.currentTarget); + setOpen(true); if (event.key === 'ArrowUp') { - menuActions.current?.highlightLastItem(); + // Focus the last item when pressing ArrowUp. + menuActions.current?.dispatch({ + type: ListActionTypes.keyDown, + key: event.key, + event, + }); } } }; - const close = () => { - setAnchorEl(null); - buttonRef.current.focus(); - }; - const createHandleMenuClick = (menuItem) => { return () => { console.log(`Clicked on ${menuItem}`); - close(); + setOpen(false); + buttonElement?.focus(); }; }; @@ -64,7 +66,7 @@ export default function UnstyledMenuSimple() { onClick={handleButtonClick} onKeyDown={handleButtonKeyDown} onMouseDown={handleButtonMouseDown} - ref={buttonRef} + ref={updateAnchor} aria-controls={isOpen ? 'simple-menu' : undefined} aria-expanded={isOpen || undefined} aria-haspopup="menu" @@ -75,8 +77,10 @@ export default function UnstyledMenuSimple() { { + setOpen(open); + }} + anchorEl={buttonElement} slots={{ root: Popper, listbox: StyledListbox }} slotProps={{ listbox: { id: 'simple-menu' } }} > diff --git a/docs/data/base/components/menu/MenuSimple.tsx b/docs/data/base/components/menu/MenuSimple.tsx index 7a789d99f19566..dacfe9d167edab 100644 --- a/docs/data/base/components/menu/MenuSimple.tsx +++ b/docs/data/base/components/menu/MenuSimple.tsx @@ -5,14 +5,20 @@ import MenuItemUnstyled, { } from '@mui/base/MenuItemUnstyled'; import PopperUnstyled from '@mui/base/PopperUnstyled'; import { styled } from '@mui/system'; +import { ListActionTypes } from '@mui/base/useList'; export default function UnstyledMenuSimple() { - const [anchorEl, setAnchorEl] = React.useState(null); - const isOpen = Boolean(anchorEl); - const buttonRef = React.useRef(null); + const [buttonElement, setButtonElement] = React.useState( + null, + ); + const [isOpen, setOpen] = React.useState(false); const menuActions = React.useRef(null); const preventReopen = React.useRef(false); + const updateAnchor = React.useCallback((node: HTMLButtonElement | null) => { + setButtonElement(node); + }, []); + const handleButtonClick = (event: React.MouseEvent) => { if (preventReopen.current) { event.preventDefault(); @@ -20,11 +26,7 @@ export default function UnstyledMenuSimple() { return; } - if (isOpen) { - setAnchorEl(null); - } else { - setAnchorEl(event.currentTarget); - } + setOpen((open) => !open); }; const handleButtonMouseDown = () => { @@ -38,22 +40,23 @@ export default function UnstyledMenuSimple() { const handleButtonKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); - setAnchorEl(event.currentTarget); + setOpen(true); if (event.key === 'ArrowUp') { - menuActions.current?.highlightLastItem(); + // Focus the last item when pressing ArrowUp. + menuActions.current?.dispatch({ + type: ListActionTypes.keyDown, + key: event.key, + event, + }); } } }; - const close = () => { - setAnchorEl(null); - buttonRef.current!.focus(); - }; - const createHandleMenuClick = (menuItem: string) => { return () => { console.log(`Clicked on ${menuItem}`); - close(); + setOpen(false); + buttonElement?.focus(); }; }; @@ -64,7 +67,7 @@ export default function UnstyledMenuSimple() { onClick={handleButtonClick} onKeyDown={handleButtonKeyDown} onMouseDown={handleButtonMouseDown} - ref={buttonRef} + ref={updateAnchor} aria-controls={isOpen ? 'simple-menu' : undefined} aria-expanded={isOpen || undefined} aria-haspopup="menu" @@ -75,8 +78,10 @@ export default function UnstyledMenuSimple() { { + setOpen(open); + }} + anchorEl={buttonElement} slots={{ root: Popper, listbox: StyledListbox }} slotProps={{ listbox: { id: 'simple-menu' } }} > diff --git a/docs/data/base/components/menu/UnstyledMenuIntroduction.js b/docs/data/base/components/menu/UnstyledMenuIntroduction.js index 043292c0d81257..16c791f7fe572b 100644 --- a/docs/data/base/components/menu/UnstyledMenuIntroduction.js +++ b/docs/data/base/components/menu/UnstyledMenuIntroduction.js @@ -5,50 +5,67 @@ import MenuItemUnstyled, { } from '@mui/base/MenuItemUnstyled'; import PopperUnstyled from '@mui/base/PopperUnstyled'; import { styled } from '@mui/system'; +import { ListActionTypes } from '@mui/base/useList'; export default function UnstyledMenuIntroduction() { - const [anchorEl, setAnchorEl] = React.useState(null); - const isOpen = Boolean(anchorEl); - const buttonRef = React.useRef(null); + const [buttonElement, setButtonElement] = React.useState(null); + + const [isOpen, setOpen] = React.useState(false); const menuActions = React.useRef(null); + const preventReopen = React.useRef(false); + + const updateAnchor = React.useCallback((node) => { + setButtonElement(node); + }, []); const handleButtonClick = (event) => { + if (preventReopen.current) { + event.preventDefault(); + preventReopen.current = false; + return; + } + + setOpen((open) => !open); + }; + + const handleButtonMouseDown = () => { if (isOpen) { - setAnchorEl(null); - } else { - setAnchorEl(event.currentTarget); + // Prevents the menu from reopening right after closing + // when clicking the button. + preventReopen.current = true; } }; const handleButtonKeyDown = (event) => { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); - setAnchorEl(event.currentTarget); + setOpen(true); if (event.key === 'ArrowUp') { - menuActions.current?.highlightLastItem(); + // Focus the last item when pressing ArrowUp. + menuActions.current?.dispatch({ + type: ListActionTypes.keyDown, + key: event.key, + event, + }); } } }; - const close = () => { - setAnchorEl(null); - buttonRef.current.focus(); - }; - const createHandleMenuClick = (menuItem) => { return () => { console.log(`Clicked on ${menuItem}`); - close(); + setOpen(false); + buttonElement?.focus(); }; }; - return (
{ + setOpen(open); + }} + anchorEl={buttonElement} slots={{ root: Popper, listbox: StyledListbox }} slotProps={{ listbox: { id: 'simple-menu' } }} > diff --git a/docs/data/base/components/menu/UnstyledMenuIntroduction.tsx b/docs/data/base/components/menu/UnstyledMenuIntroduction.tsx index 2549b14717ab1a..0304bbc43969f5 100644 --- a/docs/data/base/components/menu/UnstyledMenuIntroduction.tsx +++ b/docs/data/base/components/menu/UnstyledMenuIntroduction.tsx @@ -5,50 +5,68 @@ import MenuItemUnstyled, { } from '@mui/base/MenuItemUnstyled'; import PopperUnstyled from '@mui/base/PopperUnstyled'; import { styled } from '@mui/system'; +import { ListActionTypes } from '@mui/base/useList'; export default function UnstyledMenuIntroduction() { - const [anchorEl, setAnchorEl] = React.useState(null); - const isOpen = Boolean(anchorEl); - const buttonRef = React.useRef(null); + const [buttonElement, setButtonElement] = React.useState( + null, + ); + const [isOpen, setOpen] = React.useState(false); const menuActions = React.useRef(null); + const preventReopen = React.useRef(false); + + const updateAnchor = React.useCallback((node: HTMLButtonElement | null) => { + setButtonElement(node); + }, []); const handleButtonClick = (event: React.MouseEvent) => { + if (preventReopen.current) { + event.preventDefault(); + preventReopen.current = false; + return; + } + + setOpen((open) => !open); + }; + + const handleButtonMouseDown = () => { if (isOpen) { - setAnchorEl(null); - } else { - setAnchorEl(event.currentTarget); + // Prevents the menu from reopening right after closing + // when clicking the button. + preventReopen.current = true; } }; const handleButtonKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); - setAnchorEl(event.currentTarget); + setOpen(true); if (event.key === 'ArrowUp') { - menuActions.current?.highlightLastItem(); + // Focus the last item when pressing ArrowUp. + menuActions.current?.dispatch({ + type: ListActionTypes.keyDown, + key: event.key, + event, + }); } } }; - const close = () => { - setAnchorEl(null); - buttonRef.current!.focus(); - }; - const createHandleMenuClick = (menuItem: string) => { return () => { console.log(`Clicked on ${menuItem}`); - close(); + setOpen(false); + buttonElement?.focus(); }; }; - return (
{ + setOpen(open); + }} + anchorEl={buttonElement} slots={{ root: Popper, listbox: StyledListbox }} slotProps={{ listbox: { id: 'simple-menu' } }} > diff --git a/docs/data/base/components/menu/UseMenu.js b/docs/data/base/components/menu/UseMenu.js index 7ff446f26e3467..32263958a0195a 100644 --- a/docs/data/base/components/menu/UseMenu.js +++ b/docs/data/base/components/menu/UseMenu.js @@ -1,46 +1,35 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { MenuUnstyledContext } from '@mui/base/MenuUnstyled'; -import useMenu from '@mui/base/useMenu'; +import clsx from 'clsx'; +import useMenu, { MenuProvider } from '@mui/base/useMenu'; import useMenuItem from '@mui/base/useMenuItem'; import PopperUnstyled from '@mui/base/PopperUnstyled'; import { GlobalStyles } from '@mui/system'; -import clsx from 'clsx'; const Menu = React.forwardRef(function Menu(props, ref) { - const { children, onClose, open, ...other } = props; + const { children, onOpenChange, open, ...other } = props; const { contextValue, getListboxProps } = useMenu({ listboxRef: ref, - onClose, + onOpenChange, open, }); - const menuContextValue = React.useMemo( - () => ({ - ...contextValue, - open: true, - }), - [contextValue], - ); - return (
    - - {children} - + {children}
); }); Menu.propTypes = { children: PropTypes.node, - onClose: PropTypes.func.isRequired, + onOpenChange: PropTypes.func.isRequired, open: PropTypes.bool.isRequired, }; const MenuItem = React.forwardRef(function MenuItem(props, ref) { - const { children, ...other } = props; + const { children, onClick, ...other } = props; const { getRootProps, disabled, focusVisible } = useMenuItem({ ref }); @@ -51,7 +40,11 @@ const MenuItem = React.forwardRef(function MenuItem(props, ref) { }; return ( -
  • +
  • {}) })} + > {children}
  • ); @@ -59,12 +52,18 @@ const MenuItem = React.forwardRef(function MenuItem(props, ref) { MenuItem.propTypes = { children: PropTypes.node, + onClick: PropTypes.func, }; export default function UseMenu() { - const [anchorEl, setAnchorEl] = React.useState(null); + const [buttonElement, setButtonElement] = React.useState(null); + + const [isOpen, setOpen] = React.useState(false); const preventReopen = React.useRef(false); - const buttonRef = React.useRef(null); + + const updateAnchor = React.useCallback((node) => { + setButtonElement(node); + }, []); const handleOnClick = (event) => { if (preventReopen.current) { @@ -73,24 +72,32 @@ export default function UseMenu() { return; } - setAnchorEl(anchorEl ? null : event.currentTarget); - }; - - const handleOnClose = () => { - setAnchorEl(null); - buttonRef.current.focus(); + setOpen((open) => !open); }; - const open = Boolean(anchorEl); - const handleButtonMouseDown = () => { - if (open) { + if (isOpen) { // Prevents the menu from reopening right after closing // when clicking the button. preventReopen.current = true; } }; + const handleButtonKeyDown = (event) => { + if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { + event.preventDefault(); + setOpen(true); + } + }; + + const createHandleMenuClick = (menuItem) => { + return () => { + console.log(`Clicked on ${menuItem}`); + setOpen(false); + buttonElement?.focus(); + }; + }; + return ( @@ -99,15 +106,25 @@ export default function UseMenu() { className="button" onClick={handleOnClick} onMouseDown={handleButtonMouseDown} - ref={buttonRef} + onKeyDown={handleButtonKeyDown} + ref={updateAnchor} + aria-controls="hooks-menu" + aria-expanded={isOpen || undefined} + aria-haspopup="menu" > Commands - - - Cut - Copy - Paste + + { + setOpen(open); + }} + open={isOpen} + id="hooks-menu" + > + Cut + Copy + Paste diff --git a/docs/data/base/components/menu/UseMenu.tsx b/docs/data/base/components/menu/UseMenu.tsx index 4c3a1b3bbed2c5..cb72abf94df1ff 100644 --- a/docs/data/base/components/menu/UseMenu.tsx +++ b/docs/data/base/components/menu/UseMenu.tsx @@ -1,42 +1,28 @@ import * as React from 'react'; -import { - MenuUnstyledContext, - MenuUnstyledContextType, -} from '@mui/base/MenuUnstyled'; -import useMenu from '@mui/base/useMenu'; +import clsx from 'clsx'; +import useMenu, { MenuProvider } from '@mui/base/useMenu'; import useMenuItem from '@mui/base/useMenuItem'; import PopperUnstyled from '@mui/base/PopperUnstyled'; import { GlobalStyles } from '@mui/system'; -import clsx from 'clsx'; const Menu = React.forwardRef(function Menu( props: React.ComponentPropsWithoutRef<'ul'> & { - onClose: () => void; + onOpenChange: (isOpen: boolean) => void; open: boolean; }, ref: React.Ref, ) { - const { children, onClose, open, ...other } = props; + const { children, onOpenChange, open, ...other } = props; const { contextValue, getListboxProps } = useMenu({ listboxRef: ref, - onClose, + onOpenChange, open, }); - const menuContextValue: MenuUnstyledContextType = React.useMemo( - () => ({ - ...contextValue, - open: true, - }), - [contextValue], - ); - return (
      - - {children} - + {children}
    ); }); @@ -45,7 +31,7 @@ const MenuItem = React.forwardRef(function MenuItem( props: React.ComponentPropsWithoutRef<'li'>, ref: React.Ref, ) { - const { children, ...other } = props; + const { children, onClick, ...other } = props; const { getRootProps, disabled, focusVisible } = useMenuItem({ ref }); @@ -56,16 +42,26 @@ const MenuItem = React.forwardRef(function MenuItem( }; return ( -
  • +
  • {}) })} + > {children}
  • ); }); export default function UseMenu() { - const [anchorEl, setAnchorEl] = React.useState(null); + const [buttonElement, setButtonElement] = React.useState( + null, + ); + const [isOpen, setOpen] = React.useState(false); const preventReopen = React.useRef(false); - const buttonRef = React.useRef(null); + + const updateAnchor = React.useCallback((node: HTMLButtonElement | null) => { + setButtonElement(node); + }, []); const handleOnClick = (event: React.MouseEvent) => { if (preventReopen.current) { @@ -74,24 +70,32 @@ export default function UseMenu() { return; } - setAnchorEl(anchorEl ? null : event.currentTarget); - }; - - const handleOnClose = () => { - setAnchorEl(null); - buttonRef.current!.focus(); + setOpen((open) => !open); }; - const open = Boolean(anchorEl); - const handleButtonMouseDown = () => { - if (open) { + if (isOpen) { // Prevents the menu from reopening right after closing // when clicking the button. preventReopen.current = true; } }; + const handleButtonKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { + event.preventDefault(); + setOpen(true); + } + }; + + const createHandleMenuClick = (menuItem: string) => { + return () => { + console.log(`Clicked on ${menuItem}`); + setOpen(false); + buttonElement?.focus(); + }; + }; + return ( @@ -100,15 +104,25 @@ export default function UseMenu() { className="button" onClick={handleOnClick} onMouseDown={handleButtonMouseDown} - ref={buttonRef} + onKeyDown={handleButtonKeyDown} + ref={updateAnchor} + aria-controls="hooks-menu" + aria-expanded={isOpen || undefined} + aria-haspopup="menu" > Commands - - - Cut - Copy - Paste + + { + setOpen(open); + }} + open={isOpen} + id="hooks-menu" + > + Cut + Copy + Paste diff --git a/docs/data/base/components/menu/WrappedMenuItems.js b/docs/data/base/components/menu/WrappedMenuItems.js index e2084c826d3629..2f88d313f182a5 100644 --- a/docs/data/base/components/menu/WrappedMenuItems.js +++ b/docs/data/base/components/menu/WrappedMenuItems.js @@ -7,6 +7,7 @@ import MenuItemUnstyled, { import { buttonUnstyledClasses } from '@mui/base/ButtonUnstyled'; import PopperUnstyled from '@mui/base/PopperUnstyled'; import { styled } from '@mui/system'; +import { ListActionTypes } from '@mui/base/useList'; function MenuSection({ children, label }) { return ( @@ -23,12 +24,16 @@ MenuSection.propTypes = { }; export default function WrappedMenuItems() { - const [anchorEl, setAnchorEl] = React.useState(null); - const isOpen = Boolean(anchorEl); - const buttonRef = React.useRef(null); + const [buttonElement, setButtonElement] = React.useState(null); + + const [isOpen, setOpen] = React.useState(false); const menuActions = React.useRef(null); const preventReopen = React.useRef(false); + const updateAnchor = React.useCallback((node) => { + setButtonElement(node); + }, []); + const handleButtonClick = (event) => { if (preventReopen.current) { event.preventDefault(); @@ -36,11 +41,7 @@ export default function WrappedMenuItems() { return; } - if (isOpen) { - setAnchorEl(null); - } else { - setAnchorEl(event.currentTarget); - } + setOpen((open) => !open); }; const handleButtonMouseDown = () => { @@ -54,22 +55,23 @@ export default function WrappedMenuItems() { const handleButtonKeyDown = (event) => { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); - setAnchorEl(event.currentTarget); + setOpen(true); if (event.key === 'ArrowUp') { - menuActions.current?.highlightLastItem(); + // Focus the last item when pressing ArrowUp. + menuActions.current?.dispatch({ + type: ListActionTypes.keyDown, + key: event.key, + event, + }); } } }; - const close = () => { - setAnchorEl(null); - buttonRef.current.focus(); - }; - const createHandleMenuClick = (menuItem) => { return () => { console.log(`Clicked on ${menuItem}`); - close(); + setOpen(false); + buttonElement?.focus(); }; }; @@ -80,7 +82,7 @@ export default function WrappedMenuItems() { onClick={handleButtonClick} onKeyDown={handleButtonKeyDown} onMouseDown={handleButtonMouseDown} - ref={buttonRef} + ref={updateAnchor} aria-controls={isOpen ? 'wrapped-menu' : undefined} aria-expanded={isOpen || undefined} aria-haspopup="menu" @@ -90,8 +92,10 @@ export default function WrappedMenuItems() { { + setOpen(open); + }} + anchorEl={buttonElement} slots={{ root: Popper, listbox: StyledListbox }} slotProps={{ listbox: { id: 'simple-menu' } }} > diff --git a/docs/data/base/components/menu/WrappedMenuItems.tsx b/docs/data/base/components/menu/WrappedMenuItems.tsx index 7ad2b1fdde6b88..ccaf4e1d6ed30a 100644 --- a/docs/data/base/components/menu/WrappedMenuItems.tsx +++ b/docs/data/base/components/menu/WrappedMenuItems.tsx @@ -6,6 +6,7 @@ import MenuItemUnstyled, { import { buttonUnstyledClasses } from '@mui/base/ButtonUnstyled'; import PopperUnstyled from '@mui/base/PopperUnstyled'; import { styled } from '@mui/system'; +import { ListActionTypes } from '@mui/base/useList'; function MenuSection({ children, label }: MenuSectionProps) { return ( @@ -17,12 +18,17 @@ function MenuSection({ children, label }: MenuSectionProps) { } export default function WrappedMenuItems() { - const [anchorEl, setAnchorEl] = React.useState(null); - const isOpen = Boolean(anchorEl); - const buttonRef = React.useRef(null); + const [buttonElement, setButtonElement] = React.useState( + null, + ); + const [isOpen, setOpen] = React.useState(false); const menuActions = React.useRef(null); const preventReopen = React.useRef(false); + const updateAnchor = React.useCallback((node: HTMLButtonElement | null) => { + setButtonElement(node); + }, []); + const handleButtonClick = (event: React.MouseEvent) => { if (preventReopen.current) { event.preventDefault(); @@ -30,11 +36,7 @@ export default function WrappedMenuItems() { return; } - if (isOpen) { - setAnchorEl(null); - } else { - setAnchorEl(event.currentTarget); - } + setOpen((open) => !open); }; const handleButtonMouseDown = () => { @@ -48,22 +50,23 @@ export default function WrappedMenuItems() { const handleButtonKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); - setAnchorEl(event.currentTarget); + setOpen(true); if (event.key === 'ArrowUp') { - menuActions.current?.highlightLastItem(); + // Focus the last item when pressing ArrowUp. + menuActions.current?.dispatch({ + type: ListActionTypes.keyDown, + key: event.key, + event, + }); } } }; - const close = () => { - setAnchorEl(null); - buttonRef.current!.focus(); - }; - const createHandleMenuClick = (menuItem: string) => { return () => { console.log(`Clicked on ${menuItem}`); - close(); + setOpen(false); + buttonElement?.focus(); }; }; @@ -74,7 +77,7 @@ export default function WrappedMenuItems() { onClick={handleButtonClick} onKeyDown={handleButtonKeyDown} onMouseDown={handleButtonMouseDown} - ref={buttonRef} + ref={updateAnchor} aria-controls={isOpen ? 'wrapped-menu' : undefined} aria-expanded={isOpen || undefined} aria-haspopup="menu" @@ -84,8 +87,10 @@ export default function WrappedMenuItems() { { + setOpen(open); + }} + anchorEl={buttonElement} slots={{ root: Popper, listbox: StyledListbox }} slotProps={{ listbox: { id: 'simple-menu' } }} > diff --git a/docs/data/base/components/select/UnstyledSelectCustomRenderValue.tsx b/docs/data/base/components/select/UnstyledSelectCustomRenderValue.tsx index fcb0cc3a099d56..744690ccab9cdd 100644 --- a/docs/data/base/components/select/UnstyledSelectCustomRenderValue.tsx +++ b/docs/data/base/components/select/UnstyledSelectCustomRenderValue.tsx @@ -3,7 +3,7 @@ import SelectUnstyled, { SelectUnstyledProps, selectUnstyledClasses, } from '@mui/base/SelectUnstyled'; -import { SelectOption } from '@mui/base/useSelect'; +import { SelectOption } from '@mui/base/useOption'; import OptionUnstyled, { optionUnstyledClasses } from '@mui/base/OptionUnstyled'; import PopperUnstyled from '@mui/base/PopperUnstyled'; import { styled } from '@mui/system'; diff --git a/docs/data/base/components/select/UnstyledSelectObjectValuesForm.tsx b/docs/data/base/components/select/UnstyledSelectObjectValuesForm.tsx index 4b0aafb658d28e..c2e3531a9adb49 100644 --- a/docs/data/base/components/select/UnstyledSelectObjectValuesForm.tsx +++ b/docs/data/base/components/select/UnstyledSelectObjectValuesForm.tsx @@ -3,7 +3,7 @@ import SelectUnstyled, { SelectUnstyledProps, selectUnstyledClasses, } from '@mui/base/SelectUnstyled'; -import { SelectOption } from '@mui/base/useSelect'; +import { SelectOption } from '@mui/base/useOption'; import OptionUnstyled, { optionUnstyledClasses } from '@mui/base/OptionUnstyled'; import PopperUnstyled from '@mui/base/PopperUnstyled'; import { styled } from '@mui/system'; diff --git a/docs/data/base/components/select/UseSelect.js b/docs/data/base/components/select/UseSelect.js index da15812ce309a3..06dee98243853f 100644 --- a/docs/data/base/components/select/UseSelect.js +++ b/docs/data/base/components/select/UseSelect.js @@ -1,9 +1,10 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { useSelect, useOption, SelectUnstyledContext } from '@mui/base'; +import clsx from 'clsx'; +import useSelect, { SelectProvider } from '@mui/base/useSelect'; +import useOption from '@mui/base/useOption'; import { styled } from '@mui/system'; import UnfoldMoreRoundedIcon from '@mui/icons-material/UnfoldMoreRounded'; -import clsx from 'clsx'; const blue = { 100: '#DAECFF', @@ -150,6 +151,7 @@ function CustomOption(props) { const { getRootProps, highlighted } = useOption({ value, disabled, + label: children, }); return ( @@ -178,7 +180,6 @@ function CustomSelect({ options, placeholder }) { listboxRef, onOpenChange: setListboxVisible, open: listboxVisible, - options, }); React.useEffect(() => { @@ -201,7 +202,7 @@ function CustomSelect({ options, placeholder }) { aria-hidden={!listboxVisible} className={listboxVisible ? '' : 'hidden'} > - + {options.map((option) => { return ( @@ -209,7 +210,7 @@ function CustomSelect({ options, placeholder }) { ); })} - + ); @@ -219,7 +220,7 @@ CustomSelect.propTypes = { options: PropTypes.arrayOf( PropTypes.shape({ disabled: PropTypes.bool, - label: PropTypes.node, + label: PropTypes.string.isRequired, value: PropTypes.string.isRequired, }), ).isRequired, diff --git a/docs/data/base/components/select/UseSelect.tsx b/docs/data/base/components/select/UseSelect.tsx index 93e036a2702a5c..d63b5ab70d13ee 100644 --- a/docs/data/base/components/select/UseSelect.tsx +++ b/docs/data/base/components/select/UseSelect.tsx @@ -1,13 +1,12 @@ import * as React from 'react'; -import { - useSelect, - SelectOption, - useOption, - SelectUnstyledContext, -} from '@mui/base'; +import clsx from 'clsx'; +import useSelect, { + SelectOptionDefinition, + SelectProvider, +} from '@mui/base/useSelect'; +import useOption from '@mui/base/useOption'; import { styled } from '@mui/system'; import UnfoldMoreRoundedIcon from '@mui/icons-material/UnfoldMoreRounded'; -import clsx from 'clsx'; const blue = { 100: '#DAECFF', @@ -144,7 +143,7 @@ const Option = styled('li')( ); interface Props { - options: SelectOption[]; + options: SelectOptionDefinition[]; placeholder?: string; } @@ -155,7 +154,10 @@ interface OptionProps { disabled?: boolean; } -function renderSelectedValue(value: string | null, options: SelectOption[]) { +function renderSelectedValue( + value: string | null, + options: SelectOptionDefinition[], +) { const selectedOption = options.find((option) => option.value === value); return selectedOption ? `${selectedOption.label} (${value})` : null; @@ -166,6 +168,7 @@ function CustomOption(props: OptionProps) { const { getRootProps, highlighted } = useOption({ value, disabled, + label: children, }); return ( @@ -183,11 +186,13 @@ function CustomSelect({ options, placeholder }: Props) { const listboxRef = React.useRef(null); const [listboxVisible, setListboxVisible] = React.useState(false); - const { getButtonProps, getListboxProps, contextValue, value } = useSelect({ + const { getButtonProps, getListboxProps, contextValue, value } = useSelect< + string, + false + >({ listboxRef, onOpenChange: setListboxVisible, open: listboxVisible, - options, }); React.useEffect(() => { @@ -210,7 +215,7 @@ function CustomSelect({ options, placeholder }: Props) { aria-hidden={!listboxVisible} className={listboxVisible ? '' : 'hidden'} > - + {options.map((option) => { return ( @@ -218,7 +223,7 @@ function CustomSelect({ options, placeholder }: Props) { ); })} - + ); diff --git a/docs/data/base/components/tabs/KeyboardNavigation.js b/docs/data/base/components/tabs/KeyboardNavigation.js index 8b8bd17146f86c..57b80cd3c336e3 100644 --- a/docs/data/base/components/tabs/KeyboardNavigation.js +++ b/docs/data/base/components/tabs/KeyboardNavigation.js @@ -85,26 +85,26 @@ export default function AccessibleTabs1() {

    Selection following focus:

    - One - Two - Three + One + Two + Three

    Selection independent of focus (default behavior):

    - One - Two - Three + One + Two + Three
    diff --git a/docs/data/base/components/tabs/KeyboardNavigation.tsx b/docs/data/base/components/tabs/KeyboardNavigation.tsx index 8b8bd17146f86c..57b80cd3c336e3 100644 --- a/docs/data/base/components/tabs/KeyboardNavigation.tsx +++ b/docs/data/base/components/tabs/KeyboardNavigation.tsx @@ -85,26 +85,26 @@ export default function AccessibleTabs1() {

    Selection following focus:

    - One - Two - Three + One + Two + Three

    Selection independent of focus (default behavior):

    - One - Two - Three + One + Two + Three
    diff --git a/docs/data/base/components/tabs/UnstyledTabsBasic.js b/docs/data/base/components/tabs/UnstyledTabsBasic.js index c4112192089bb4..1ce19b7e447df7 100644 --- a/docs/data/base/components/tabs/UnstyledTabsBasic.js +++ b/docs/data/base/components/tabs/UnstyledTabsBasic.js @@ -6,15 +6,15 @@ import TabUnstyled from '@mui/base/TabUnstyled'; export default function UnstyledTabsBasic() { return ( - + - One - Two - Three + One + Two + Three - First page - Second page - Third page + First page + Second page + Third page ); } diff --git a/docs/data/base/components/tabs/UnstyledTabsBasic.tsx b/docs/data/base/components/tabs/UnstyledTabsBasic.tsx index c4112192089bb4..1ce19b7e447df7 100644 --- a/docs/data/base/components/tabs/UnstyledTabsBasic.tsx +++ b/docs/data/base/components/tabs/UnstyledTabsBasic.tsx @@ -6,15 +6,15 @@ import TabUnstyled from '@mui/base/TabUnstyled'; export default function UnstyledTabsBasic() { return ( - + - One - Two - Three + One + Two + Three - First page - Second page - Third page + First page + Second page + Third page ); } diff --git a/docs/data/base/components/tabs/UnstyledTabsBasic.tsx.preview b/docs/data/base/components/tabs/UnstyledTabsBasic.tsx.preview index 2669f0b1113b07..077b7ae06663d1 100644 --- a/docs/data/base/components/tabs/UnstyledTabsBasic.tsx.preview +++ b/docs/data/base/components/tabs/UnstyledTabsBasic.tsx.preview @@ -1,10 +1,10 @@ - + - One - Two - Three + One + Two + Three - First page - Second page - Third page + First page + Second page + Third page \ No newline at end of file diff --git a/docs/data/base/components/tabs/UnstyledTabsCustomized.js b/docs/data/base/components/tabs/UnstyledTabsCustomized.js index 1deefa2056b9e9..d7c9220c3b0f43 100644 --- a/docs/data/base/components/tabs/UnstyledTabsCustomized.js +++ b/docs/data/base/components/tabs/UnstyledTabsCustomized.js @@ -89,15 +89,15 @@ const TabsList = styled(TabsListUnstyled)( export default function UnstyledTabsCustomized() { return ( - + - One - Two - Three + One + Two + Three - First page - Second page - Third page + First page + Second page + Third page ); } diff --git a/docs/data/base/components/tabs/UnstyledTabsCustomized.tsx b/docs/data/base/components/tabs/UnstyledTabsCustomized.tsx index 1deefa2056b9e9..d7c9220c3b0f43 100644 --- a/docs/data/base/components/tabs/UnstyledTabsCustomized.tsx +++ b/docs/data/base/components/tabs/UnstyledTabsCustomized.tsx @@ -89,15 +89,15 @@ const TabsList = styled(TabsListUnstyled)( export default function UnstyledTabsCustomized() { return ( - + - One - Two - Three + One + Two + Three - First page - Second page - Third page + First page + Second page + Third page ); } diff --git a/docs/data/base/components/tabs/UnstyledTabsCustomized.tsx.preview b/docs/data/base/components/tabs/UnstyledTabsCustomized.tsx.preview index 481df4cdd1f6de..76364ff1a19211 100644 --- a/docs/data/base/components/tabs/UnstyledTabsCustomized.tsx.preview +++ b/docs/data/base/components/tabs/UnstyledTabsCustomized.tsx.preview @@ -1,10 +1,10 @@ - + - One - Two - Three + One + Two + Three - First page - Second page - Third page + First page + Second page + Third page \ No newline at end of file diff --git a/docs/data/base/components/tabs/UnstyledTabsIntroduction.js b/docs/data/base/components/tabs/UnstyledTabsIntroduction.js index 97b2193bc1816a..6f8c6345249b89 100644 --- a/docs/data/base/components/tabs/UnstyledTabsIntroduction.js +++ b/docs/data/base/components/tabs/UnstyledTabsIntroduction.js @@ -98,9 +98,9 @@ export default function UnstyledTabsIntroduction() { return ( - My account - Profile - Language + My account + Profile + Language My account page Profile page diff --git a/docs/data/base/components/tabs/UnstyledTabsIntroduction.tsx b/docs/data/base/components/tabs/UnstyledTabsIntroduction.tsx index 97b2193bc1816a..6f8c6345249b89 100644 --- a/docs/data/base/components/tabs/UnstyledTabsIntroduction.tsx +++ b/docs/data/base/components/tabs/UnstyledTabsIntroduction.tsx @@ -98,9 +98,9 @@ export default function UnstyledTabsIntroduction() { return ( - My account - Profile - Language + My account + Profile + Language My account page Profile page diff --git a/docs/data/base/components/tabs/UnstyledTabsIntroduction.tsx.preview b/docs/data/base/components/tabs/UnstyledTabsIntroduction.tsx.preview index 480f510d20466d..c0e474e9b741ba 100644 --- a/docs/data/base/components/tabs/UnstyledTabsIntroduction.tsx.preview +++ b/docs/data/base/components/tabs/UnstyledTabsIntroduction.tsx.preview @@ -1,8 +1,8 @@ - My account - Profile - Language + My account + Profile + Language My account page Profile page diff --git a/docs/data/joy/components/select/SelectCustomOption.js b/docs/data/joy/components/select/SelectCustomOption.js index d76aa7a3e3b8b3..2b875476bde0d9 100644 --- a/docs/data/joy/components/select/SelectCustomOption.js +++ b/docs/data/joy/components/select/SelectCustomOption.js @@ -5,6 +5,27 @@ import ListDivider from '@mui/joy/ListDivider'; import Select from '@mui/joy/Select'; import Option from '@mui/joy/Option'; +const options = [ + { value: '1', label: 'Eric', src: '/static/images/avatar/1.jpg' }, + { value: '2', label: 'Smith', src: '/static/images/avatar/2.jpg' }, + { value: '3', label: 'Erika', src: '/static/images/avatar/3.jpg' }, +]; + +function renderValue(option) { + if (!option) { + return null; + } + + return ( + + + o.value === option.value)?.src} /> + + {option.label} + + ); +} + export default function SelectCustomOption() { return ( ); } diff --git a/docs/data/joy/components/select/SelectCustomOption.tsx b/docs/data/joy/components/select/SelectCustomOption.tsx index d76aa7a3e3b8b3..cfef40ce1bfd4f 100644 --- a/docs/data/joy/components/select/SelectCustomOption.tsx +++ b/docs/data/joy/components/select/SelectCustomOption.tsx @@ -2,9 +2,30 @@ import * as React from 'react'; import Avatar from '@mui/joy/Avatar'; import ListItemDecorator from '@mui/joy/ListItemDecorator'; import ListDivider from '@mui/joy/ListDivider'; -import Select from '@mui/joy/Select'; +import Select, { SelectOption } from '@mui/joy/Select'; import Option from '@mui/joy/Option'; +const options = [ + { value: '1', label: 'Eric', src: '/static/images/avatar/1.jpg' }, + { value: '2', label: 'Smith', src: '/static/images/avatar/2.jpg' }, + { value: '3', label: 'Erika', src: '/static/images/avatar/3.jpg' }, +]; + +function renderValue(option: SelectOption | null) { + if (!option) { + return null; + } + + return ( + + + o.value === option.value)?.src} /> + + {option.label} + + ); +} + export default function SelectCustomOption() { return ( ); } diff --git a/docs/data/joy/getting-started/templates/email/components/Menu.tsx b/docs/data/joy/getting-started/templates/email/components/Menu.tsx index 8b0ada4a62577b..3dd506947a3997 100644 --- a/docs/data/joy/getting-started/templates/email/components/Menu.tsx +++ b/docs/data/joy/getting-started/templates/email/components/Menu.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import JoyMenu, { MenuUnstyledActions } from '@mui/joy/Menu'; import MenuItem from '@mui/joy/MenuItem'; +import { ListActionTypes } from '@mui/base/useList'; function Menu({ control, @@ -11,31 +12,44 @@ function Menu({ id: string; menus: Array<{ label: string } & { [k: string]: any }>; }) { - const [anchorEl, setAnchorEl] = React.useState(null); - const isOpen = Boolean(anchorEl); + const [buttonElement, setButtonElement] = React.useState( + null, + ); + const [isOpen, setOpen] = React.useState(false); const buttonRef = React.useRef(null); const menuActions = React.useRef(null); + const preventReopen = React.useRef(false); + + const updateAnchor = React.useCallback((node: HTMLButtonElement | null) => { + setButtonElement(node); + }, []); const handleButtonClick = (event: React.MouseEvent) => { - if (isOpen) { - setAnchorEl(null); - } else { - setAnchorEl(event.currentTarget); + if (preventReopen.current) { + event.preventDefault(); + preventReopen.current = false; + return; } + + setOpen((open) => !open); }; const handleButtonKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); - setAnchorEl(event.currentTarget); + setOpen(true); if (event.key === 'ArrowUp') { - menuActions.current?.highlightLastItem(); + menuActions.current?.dispatch({ + type: ListActionTypes.keyDown, + key: event.key, + event, + }); } } }; const close = () => { - setAnchorEl(null); + setOpen(false); buttonRef.current!.focus(); }; @@ -45,7 +59,7 @@ function Menu({ type: 'button', onClick: handleButtonClick, onKeyDown: handleButtonKeyDown, - ref: buttonRef, + ref: updateAnchor, 'aria-controls': isOpen ? id : undefined, 'aria-expanded': isOpen || undefined, 'aria-haspopup': 'menu', @@ -56,7 +70,7 @@ function Menu({ actions={menuActions} open={isOpen} onClose={close} - anchorEl={anchorEl} + anchorEl={buttonElement} sx={{ minWidth: 120 }} > {menus.map(({ label, active, ...item }) => { diff --git a/docs/data/joy/getting-started/templates/files/components/Menu.tsx b/docs/data/joy/getting-started/templates/files/components/Menu.tsx index 8b0ada4a62577b..3dd506947a3997 100644 --- a/docs/data/joy/getting-started/templates/files/components/Menu.tsx +++ b/docs/data/joy/getting-started/templates/files/components/Menu.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import JoyMenu, { MenuUnstyledActions } from '@mui/joy/Menu'; import MenuItem from '@mui/joy/MenuItem'; +import { ListActionTypes } from '@mui/base/useList'; function Menu({ control, @@ -11,31 +12,44 @@ function Menu({ id: string; menus: Array<{ label: string } & { [k: string]: any }>; }) { - const [anchorEl, setAnchorEl] = React.useState(null); - const isOpen = Boolean(anchorEl); + const [buttonElement, setButtonElement] = React.useState( + null, + ); + const [isOpen, setOpen] = React.useState(false); const buttonRef = React.useRef(null); const menuActions = React.useRef(null); + const preventReopen = React.useRef(false); + + const updateAnchor = React.useCallback((node: HTMLButtonElement | null) => { + setButtonElement(node); + }, []); const handleButtonClick = (event: React.MouseEvent) => { - if (isOpen) { - setAnchorEl(null); - } else { - setAnchorEl(event.currentTarget); + if (preventReopen.current) { + event.preventDefault(); + preventReopen.current = false; + return; } + + setOpen((open) => !open); }; const handleButtonKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); - setAnchorEl(event.currentTarget); + setOpen(true); if (event.key === 'ArrowUp') { - menuActions.current?.highlightLastItem(); + menuActions.current?.dispatch({ + type: ListActionTypes.keyDown, + key: event.key, + event, + }); } } }; const close = () => { - setAnchorEl(null); + setOpen(false); buttonRef.current!.focus(); }; @@ -45,7 +59,7 @@ function Menu({ type: 'button', onClick: handleButtonClick, onKeyDown: handleButtonKeyDown, - ref: buttonRef, + ref: updateAnchor, 'aria-controls': isOpen ? id : undefined, 'aria-expanded': isOpen || undefined, 'aria-haspopup': 'menu', @@ -56,7 +70,7 @@ function Menu({ actions={menuActions} open={isOpen} onClose={close} - anchorEl={anchorEl} + anchorEl={buttonElement} sx={{ minWidth: 120 }} > {menus.map(({ label, active, ...item }) => { diff --git a/docs/data/joy/getting-started/templates/team/components/Menu.tsx b/docs/data/joy/getting-started/templates/team/components/Menu.tsx index 8b0ada4a62577b..3dd506947a3997 100644 --- a/docs/data/joy/getting-started/templates/team/components/Menu.tsx +++ b/docs/data/joy/getting-started/templates/team/components/Menu.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import JoyMenu, { MenuUnstyledActions } from '@mui/joy/Menu'; import MenuItem from '@mui/joy/MenuItem'; +import { ListActionTypes } from '@mui/base/useList'; function Menu({ control, @@ -11,31 +12,44 @@ function Menu({ id: string; menus: Array<{ label: string } & { [k: string]: any }>; }) { - const [anchorEl, setAnchorEl] = React.useState(null); - const isOpen = Boolean(anchorEl); + const [buttonElement, setButtonElement] = React.useState( + null, + ); + const [isOpen, setOpen] = React.useState(false); const buttonRef = React.useRef(null); const menuActions = React.useRef(null); + const preventReopen = React.useRef(false); + + const updateAnchor = React.useCallback((node: HTMLButtonElement | null) => { + setButtonElement(node); + }, []); const handleButtonClick = (event: React.MouseEvent) => { - if (isOpen) { - setAnchorEl(null); - } else { - setAnchorEl(event.currentTarget); + if (preventReopen.current) { + event.preventDefault(); + preventReopen.current = false; + return; } + + setOpen((open) => !open); }; const handleButtonKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); - setAnchorEl(event.currentTarget); + setOpen(true); if (event.key === 'ArrowUp') { - menuActions.current?.highlightLastItem(); + menuActions.current?.dispatch({ + type: ListActionTypes.keyDown, + key: event.key, + event, + }); } } }; const close = () => { - setAnchorEl(null); + setOpen(false); buttonRef.current!.focus(); }; @@ -45,7 +59,7 @@ function Menu({ type: 'button', onClick: handleButtonClick, onKeyDown: handleButtonKeyDown, - ref: buttonRef, + ref: updateAnchor, 'aria-controls': isOpen ? id : undefined, 'aria-expanded': isOpen || undefined, 'aria-haspopup': 'menu', @@ -56,7 +70,7 @@ function Menu({ actions={menuActions} open={isOpen} onClose={close} - anchorEl={anchorEl} + anchorEl={buttonElement} sx={{ minWidth: 120 }} > {menus.map(({ label, active, ...item }) => { diff --git a/docs/pages/base/api/menu-unstyled.json b/docs/pages/base/api/menu-unstyled.json index e28d7fa1e3613f..29ea36f1a2ebf1 100644 --- a/docs/pages/base/api/menu-unstyled.json +++ b/docs/pages/base/api/menu-unstyled.json @@ -8,8 +8,7 @@ } }, "component": { "type": { "name": "elementType" } }, - "keepMounted": { "type": { "name": "bool" }, "default": "false" }, - "onClose": { "type": { "name": "func" } }, + "onOpenChange": { "type": { "name": "func" } }, "open": { "type": { "name": "bool" }, "default": "false" }, "slotProps": { "type": { diff --git a/docs/pages/base/api/tab-panel-unstyled.json b/docs/pages/base/api/tab-panel-unstyled.json index fea85c48e849ab..0bb1d8e5d694aa 100644 --- a/docs/pages/base/api/tab-panel-unstyled.json +++ b/docs/pages/base/api/tab-panel-unstyled.json @@ -1,9 +1,5 @@ { "props": { - "value": { - "type": { "name": "union", "description": "number
    | string" }, - "required": true - }, "children": { "type": { "name": "node" } }, "component": { "type": { "name": "elementType" } }, "slotProps": { @@ -13,7 +9,8 @@ "slots": { "type": { "name": "shape", "description": "{ root?: elementType }" }, "default": "{}" - } + }, + "value": { "type": { "name": "union", "description": "number
    | string" } } }, "name": "TabPanelUnstyled", "styles": { "classes": [], "globalClasses": {}, "name": null }, diff --git a/docs/pages/base/api/tabs-unstyled.json b/docs/pages/base/api/tabs-unstyled.json index 9a28f09a2915b7..228ee20b1778cb 100644 --- a/docs/pages/base/api/tabs-unstyled.json +++ b/docs/pages/base/api/tabs-unstyled.json @@ -2,12 +2,7 @@ "props": { "children": { "type": { "name": "node" } }, "component": { "type": { "name": "elementType" } }, - "defaultValue": { - "type": { - "name": "union", - "description": "false
    | number
    | string" - } - }, + "defaultValue": { "type": { "name": "union", "description": "number
    | string" } }, "direction": { "type": { "name": "enum", "description": "'ltr'
    | 'rtl'" }, "default": "'ltr'" @@ -26,12 +21,7 @@ "type": { "name": "shape", "description": "{ root?: elementType }" }, "default": "{}" }, - "value": { - "type": { - "name": "union", - "description": "false
    | number
    | string" - } - } + "value": { "type": { "name": "union", "description": "number
    | string" } } }, "name": "TabsUnstyled", "styles": { "classes": [], "globalClasses": {}, "name": null }, diff --git a/docs/pages/base/api/use-button.json b/docs/pages/base/api/use-button.json index 00b73df07d8a88..cfd717d3b3e77d 100644 --- a/docs/pages/base/api/use-button.json +++ b/docs/pages/base/api/use-button.json @@ -48,6 +48,13 @@ }, "required": true }, + "ref": { + "type": { + "name": "((instance: unknown) => void) | null", + "description": "((instance: unknown) => void) | null" + }, + "required": true + }, "setFocusVisible": { "type": { "name": "React.Dispatch<React.SetStateAction<boolean>>", diff --git a/docs/pages/base/api/use-menu-item.json b/docs/pages/base/api/use-menu-item.json index 459097b94b117b..3b9bdb346daeca 100644 --- a/docs/pages/base/api/use-menu-item.json +++ b/docs/pages/base/api/use-menu-item.json @@ -5,6 +5,7 @@ "required": true }, "disabled": { "type": { "name": "boolean", "description": "boolean" } }, + "id": { "type": { "name": "string", "description": "string" } }, "label": { "type": { "name": "string", "description": "string" } }, "onClick": { "type": { @@ -14,16 +15,8 @@ } }, "returnValue": { - "disabled": { - "type": { "name": "boolean", "description": "boolean" }, - "default": "false", - "required": true - }, - "focusVisible": { - "type": { "name": "boolean", "description": "boolean" }, - "default": "false", - "required": true - }, + "disabled": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, + "focusVisible": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, "getRootProps": { "type": { "name": "<TOther extends EventHandlers = {}>(otherHandlers?: TOther) => UseMenuItemRootSlotProps<TOther>", @@ -31,11 +24,16 @@ }, "required": true }, - "highlighted": { - "type": { "name": "boolean", "description": "boolean" }, - "default": "false", + "highlighted": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, + "index": { "type": { "name": "number", "description": "number" }, "required": true }, + "ref": { + "type": { + "name": "((instance: unknown) => void) | null", + "description": "((instance: unknown) => void) | null" + }, "required": true - } + }, + "totalItemCount": { "type": { "name": "number", "description": "number" }, "required": true } }, "name": "useMenuItem", "filename": "/packages/mui-base/src/useMenuItem/useMenuItem.ts", diff --git a/docs/pages/base/api/use-menu.json b/docs/pages/base/api/use-menu.json index f67a9cbc9f6a86..4cd0ede5cff5c2 100644 --- a/docs/pages/base/api/use-menu.json +++ b/docs/pages/base/api/use-menu.json @@ -1,15 +1,25 @@ { "parameters": { + "defaultOpen": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" }, "listboxId": { "type": { "name": "string", "description": "string" } }, "listboxRef": { "type": { "name": "React.Ref<any>", "description": "React.Ref<any>" } }, - "onClose": { "type": { "name": "() => void", "description": "() => void" } }, + "onOpenChange": { + "type": { "name": "(open: boolean) => void", "description": "(open: boolean) => void" } + }, "open": { "type": { "name": "boolean", "description": "boolean" } } }, "returnValue": { "contextValue": { - "type": { "name": "MenuUnstyledContextType", "description": "MenuUnstyledContextType" }, + "type": { "name": "MenuProviderValue", "description": "MenuProviderValue" }, + "required": true + }, + "dispatch": { + "type": { + "name": "(action: ListAction<string>) => void", + "description": "(action: ListAction<string>) => void" + }, "required": true }, "getListboxProps": { @@ -19,25 +29,18 @@ }, "required": true }, - "highlightedOption": { + "highlightedValue": { "type": { "name": "string | null", "description": "string | null" }, "required": true }, - "highlightFirstItem": { - "type": { "name": "() => void", "description": "() => void" }, - "required": true - }, - "highlightLastItem": { - "type": { "name": "() => void", "description": "() => void" }, - "required": true - }, "menuItems": { "type": { - "name": "Record<string, MenuItemMetadata>", - "description": "Record<string, MenuItemMetadata>" + "name": "Map<string, MenuItemMetadata>", + "description": "Map<string, MenuItemMetadata>" }, "required": true - } + }, + "open": { "type": { "name": "boolean", "description": "boolean" }, "required": true } }, "name": "useMenu", "filename": "/packages/mui-base/src/useMenu/useMenu.ts", diff --git a/docs/pages/base/api/use-option.json b/docs/pages/base/api/use-option.json index 091898c40a09c7..2c70333285a5e9 100644 --- a/docs/pages/base/api/use-option.json +++ b/docs/pages/base/api/use-option.json @@ -1,7 +1,12 @@ { "parameters": { "disabled": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, + "label": { + "type": { "name": "string | React.ReactNode", "description": "string | React.ReactNode" }, + "required": true + }, "value": { "type": { "name": "Value", "description": "Value" }, "required": true }, + "id": { "type": { "name": "string", "description": "string" } }, "optionRef": { "type": { "name": "React.Ref<HTMLElement>", @@ -19,6 +24,13 @@ }, "highlighted": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, "index": { "type": { "name": "number", "description": "number" }, "required": true }, + "ref": { + "type": { + "name": "React.RefCallback<HTMLElement>", + "description": "React.RefCallback<HTMLElement>" + }, + "required": true + }, "selected": { "type": { "name": "boolean", "description": "boolean" }, "required": true } }, "name": "useOption", diff --git a/docs/pages/base/api/use-select.json b/docs/pages/base/api/use-select.json index 767592822eb19c..6e26cd8fc6d182 100644 --- a/docs/pages/base/api/use-select.json +++ b/docs/pages/base/api/use-select.json @@ -1,6 +1,61 @@ { "parameters": {}, - "returnValue": {}, + "returnValue": { + "buttonActive": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, + "buttonFocusVisible": { + "type": { "name": "boolean", "description": "boolean" }, + "required": true + }, + "contextValue": { + "type": { + "name": "SelectProviderValue<Value>", + "description": "SelectProviderValue<Value>" + }, + "required": true + }, + "disabled": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, + "dispatch": { + "type": { + "name": "(action: ListAction<Value> | SelectAction) => void", + "description": "(action: ListAction<Value> | SelectAction) => void" + }, + "required": true + }, + "getButtonProps": { + "type": { + "name": "<OtherHandlers extends EventHandlers = {}>(otherHandlers?: OtherHandlers) => UseSelectButtonSlotProps<OtherHandlers>", + "description": "<OtherHandlers extends EventHandlers = {}>(otherHandlers?: OtherHandlers) => UseSelectButtonSlotProps<OtherHandlers>" + }, + "required": true + }, + "getListboxProps": { + "type": { + "name": "<OtherHandlers extends EventHandlers = {}>(otherHandlers?: OtherHandlers) => UseSelectListboxSlotProps<OtherHandlers>", + "description": "<OtherHandlers extends EventHandlers = {}>(otherHandlers?: OtherHandlers) => UseSelectListboxSlotProps<OtherHandlers>" + }, + "required": true + }, + "getOptionMetadata": { + "type": { + "name": "(optionValue: Value) => SelectOption<Value> | undefined", + "description": "(optionValue: Value) => SelectOption<Value> | undefined" + }, + "required": true + }, + "highlightedOption": { + "type": { "name": "Value | null", "description": "Value | null" }, + "required": true + }, + "open": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, + "options": { "type": { "name": "Value[]", "description": "Value[]" }, "required": true }, + "value": { + "type": { + "name": "SelectValue<Value, Multiple>", + "description": "SelectValue<Value, Multiple>" + }, + "required": true + } + }, "name": "useSelect", "filename": "/packages/mui-base/src/useSelect/useSelect.ts", "demos": "" diff --git a/docs/pages/base/api/use-tab-panel.json b/docs/pages/base/api/use-tab-panel.json index c26a0cfc8c7dd0..99426daf8a3639 100644 --- a/docs/pages/base/api/use-tab-panel.json +++ b/docs/pages/base/api/use-tab-panel.json @@ -1,9 +1,7 @@ { "parameters": { - "value": { - "type": { "name": "number | string", "description": "number | string" }, - "required": true - } + "id": { "type": { "name": "string", "description": "string" } }, + "value": { "type": { "name": "number | string", "description": "number | string" } } }, "returnValue": { "getRootProps": { diff --git a/docs/pages/base/api/use-tab.json b/docs/pages/base/api/use-tab.json index 3747380915540d..10d3634a479d70 100644 --- a/docs/pages/base/api/use-tab.json +++ b/docs/pages/base/api/use-tab.json @@ -1,10 +1,7 @@ { "parameters": { - "ref": { - "type": { "name": "React.Ref<any>", "description": "React.Ref<any>" }, - "required": true - }, - "disabled": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" }, + "disabled": { "type": { "name": "boolean", "description": "boolean" } }, + "id": { "type": { "name": "string", "description": "string" } }, "onChange": { "type": { "name": "(event: React.SyntheticEvent, value: number | string) => void", @@ -14,22 +11,11 @@ "onClick": { "type": { "name": "React.MouseEventHandler", "description": "React.MouseEventHandler" } }, - "onFocus": { - "type": { "name": "React.FocusEventHandler", "description": "React.FocusEventHandler" } - }, + "ref": { "type": { "name": "React.Ref<any>", "description": "React.Ref<any>" } }, "value": { "type": { "name": "number | string", "description": "number | string" } } }, "returnValue": { - "active": { - "type": { "name": "boolean", "description": "boolean" }, - "default": "false", - "required": true - }, - "disabled": { - "type": { "name": "boolean", "description": "boolean" }, - "default": "false", - "required": true - }, + "active": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, "focusVisible": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, "getRootProps": { "type": { @@ -38,6 +24,8 @@ }, "required": true }, + "highlighted": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, + "index": { "type": { "name": "number", "description": "number" }, "required": true }, "selected": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, "setFocusVisible": { "type": { @@ -45,7 +33,8 @@ "description": "React.Dispatch<React.SetStateAction<boolean>>" }, "required": true - } + }, + "totalTabsCount": { "type": { "name": "number", "description": "number" }, "required": true } }, "name": "useTab", "filename": "/packages/mui-base/src/useTab/useTab.ts", diff --git a/docs/pages/base/api/use-tabs-list.json b/docs/pages/base/api/use-tabs-list.json index 847034026445a5..3c782a740b3fd3 100644 --- a/docs/pages/base/api/use-tabs-list.json +++ b/docs/pages/base/api/use-tabs-list.json @@ -3,12 +3,20 @@ "ref": { "type": { "name": "React.Ref<unknown>", "description": "React.Ref<unknown>" }, "required": true - }, - "aria-label": { "type": { "name": "string", "description": "string" } }, - "aria-labelledby": { "type": { "name": "string", "description": "string" } }, - "children": { "type": { "name": "React.ReactNode", "description": "React.ReactNode" } } + } }, "returnValue": { + "contextValue": { + "type": { "name": "TabsListProviderValue", "description": "TabsListProviderValue" }, + "required": true + }, + "dispatch": { + "type": { + "name": "(action: ListAction<string | number>) => void", + "description": "(action: ListAction<string | number>) => void" + }, + "required": true + }, "getRootProps": { "type": { "name": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseTabsListRootSlotProps<TOther>", @@ -16,6 +24,10 @@ }, "required": true }, + "highlightedValue": { + "type": { "name": "string | number | null", "description": "string | number | null" }, + "required": true + }, "isRtl": { "type": { "name": "boolean", "description": "boolean" }, "default": "false", @@ -29,15 +41,8 @@ "default": "'horizontal'", "required": true }, - "processChildren": { - "type": { - "name": "() => React.ReactElement<any, string | React.JSXElementConstructor<any>>[] | null | undefined", - "description": "() => React.ReactElement<any, string | React.JSXElementConstructor<any>>[] | null | undefined" - }, - "required": true - }, - "value": { - "type": { "name": "string | number | false", "description": "string | number | false" }, + "selectedValue": { + "type": { "name": "string | number | null", "description": "string | number | null" }, "required": true } }, diff --git a/docs/pages/base/api/use-tabs.json b/docs/pages/base/api/use-tabs.json index 979d10a730c013..9321bb2f5ca546 100644 --- a/docs/pages/base/api/use-tabs.json +++ b/docs/pages/base/api/use-tabs.json @@ -1,7 +1,7 @@ { "parameters": { "defaultValue": { - "type": { "name": "string | number | false", "description": "string | number | false" } + "type": { "name": "string | number | null", "description": "string | number | null" } }, "direction": { "type": { @@ -12,8 +12,8 @@ }, "onChange": { "type": { - "name": "(event: React.SyntheticEvent, value: number | string | boolean) => void", - "description": "(event: React.SyntheticEvent, value: number | string | boolean) => void" + "name": "(event: React.SyntheticEvent | null, value: number | string | null) => void", + "description": "(event: React.SyntheticEvent | null, value: number | string | null) => void" } }, "orientation": { @@ -25,12 +25,12 @@ }, "selectionFollowsFocus": { "type": { "name": "boolean", "description": "boolean" } }, "value": { - "type": { "name": "string | number | false", "description": "string | number | false" } + "type": { "name": "string | number | null", "description": "string | number | null" } } }, "returnValue": { - "tabsContextValue": { - "type": { "name": "TabsContextValue", "description": "TabsContextValue" }, + "contextValue": { + "type": { "name": "TabsProviderValue", "description": "TabsProviderValue" }, "required": true } }, diff --git a/docs/pages/experiments/base/listbox.tsx b/docs/pages/experiments/base/listbox.tsx new file mode 100644 index 00000000000000..3d87ee935df0c9 --- /dev/null +++ b/docs/pages/experiments/base/listbox.tsx @@ -0,0 +1,229 @@ +/* eslint-disable react/no-danger */ +import * as React from 'react'; +import clsx from 'clsx'; +import useList, { ListContext, useListItem } from '@mui/base/useList'; + +const styles = ` + body { + padding: 0; + margin: 0; + font-family: IBM Plex Sans, sans-serif; + } + + .list { + display: flex; + gap: 16px; + align-items: center; + justify-content: flex-start; + max-height: 70vh; + padding: 16px; + width: 100vw; + flex-wrap: wrap; + background: linear-gradient(-30deg, #009245, #FCEE21); + box-sizing: border-box; + margin: 0; + } + + .list:focus-visible { + outline: none; + } + + .item { + width: 50px; + height: 50px; + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 4px; + box-sizing: border-box; + background: linear-gradient(-30deg, #f5f5f5, #fff); + font-size: 18px; + color: #333; + user-select: none; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1), 0 4px 4px -2px rgba(0, 0, 0, 0.15); + } + + .item.highlighted:not([tabindex]), + .item:focus-visible + { + outline: 2px solid #fff; + outline-offset: 4px; + } + + .item.selected { + background: linear-gradient(-30deg, #363636, #666); + color: #fff; + } + + .controls { + padding: 16px; + } + + .controls > div { + margin-bottom: 8px; + } + + .controls button { + border: 1px solid #999; + background: #fff; + padding: 8px 16px; + border-radius: 4px; + font-family: inherit; + margin: 0 4px; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1), 0 4px 4px -2px rgba(0, 0, 0, 0.15); + } + + .controls button.active { + background: #73bc34; + color: #fff; + } + + .state { + padding: 16px; + background: #202020; + color: #fff; + }`; + +const items = Array.from({ length: 200 }, (_, i) => i + 1); + +const Item = React.forwardRef(function Item( + props: React.PropsWithChildren<{ value: number; id?: string }>, + ref: React.Ref, +) { + const { value: item, id } = props; + const { getRootProps, selected, highlighted } = useListItem({ item, ref }); + + const itemProps = getRootProps(); + + const classes = clsx('item', { + highlighted, + selected, + }); + + return ( +
    + {item} +
    + ); +}); + +function List() { + const [orientation, setOrientation] = React.useState< + 'horizontal-ltr' | 'horizontal-rtl' | 'vertical' + >('horizontal-ltr'); + const [focusManagement, setFocusManagement] = React.useState<'activeDescendant' | 'DOM'>( + 'activeDescendant', + ); + + let flexDirection: React.CSSProperties['flexDirection']; + switch (orientation) { + case 'horizontal-ltr': + flexDirection = 'row'; + break; + case 'horizontal-rtl': + flexDirection = 'row-reverse'; + break; + default: + flexDirection = 'column'; + break; + } + + const itemRefs = React.useRef(new Map()); + const listbox = useList({ + items, + getItemDomElement: React.useCallback((item: number) => itemRefs.current.get(item) || null, []), + getItemId: React.useCallback((item: number) => `item-${item}`, []), + orientation, + focusManagement, + selectionMode: 'single', + }); + + const { + getRootProps, + contextValue, + state: { selectedValues, highlightedValue }, + } = listbox; + + const handleItemRef = React.useCallback((item: number, node: HTMLElement | null) => { + if (node) { + itemRefs.current.set(item, node); + } else { + itemRefs.current.delete(item); + } + }, []); + + return ( + +
    +
    + Orientation:  + + + +
    +
    + Focus management:  + + +
    +
    +
    +
    Selected: {selectedValues.join(', ')}
    +
    Highlighted: {highlightedValue ?? '(none)'}
    +
    +
    + + {items.map((item) => ( + handleItemRef(item, node)} + id={`item-${item}`} + /> + ))} + +
    +
    + ); +} + +export default function Demo() { + return ( + +