Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MenuUnstyled] Create MenuUnstyled and useMenu #30961

Merged
merged 35 commits into from
Mar 7, 2022
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
70bbeb9
Create MenuUnstyled and MenuItemUnstyled
michaldudak Jan 4, 2022
5355eed
Focus management
michaldudak Feb 1, 2022
906c89e
Include disabled items in navigation
michaldudak Feb 2, 2022
eebfe60
Prevent stealing focus on page load
michaldudak Feb 2, 2022
b0fd03d
Use useButton in MenuItem
michaldudak Feb 2, 2022
2c67501
Docs and demos
michaldudak Feb 3, 2022
243b4d5
Refactor demo to avoid proptypes generation
michaldudak Feb 10, 2022
29522aa
Move the MenuUnstyled docs to the Base subsite
michaldudak Feb 10, 2022
70bffdb
Lint
michaldudak Feb 10, 2022
53303c0
docs:api
michaldudak Feb 10, 2022
48f6c06
Style the demos
michaldudak Feb 16, 2022
2426b43
Merge branch 'master' into unstyled-menu
michaldudak Feb 16, 2022
51b26d1
Update demos
michaldudak Feb 16, 2022
9461f49
Correct types in demo
michaldudak Feb 16, 2022
c9cd01c
small tweaks to the demos
danilo-leal Feb 18, 2022
d7bd3a7
PR fixes
michaldudak Feb 22, 2022
02ce2ea
Support focusVisible
michaldudak Feb 22, 2022
662ceed
Rename files with types
michaldudak Feb 23, 2022
87f0911
Include Popper
michaldudak Feb 23, 2022
096e112
Proptypes and API docs
michaldudak Feb 24, 2022
878d407
Make Popper a root slot
michaldudak Feb 24, 2022
2c2e5d6
Merge remote-tracking branch 'upstream/master' into unstyled-menu
michaldudak Feb 24, 2022
d784001
proptypes
michaldudak Feb 24, 2022
7c4dcf5
Restart CI
michaldudak Feb 24, 2022
31c5c8c
Merge remote-tracking branch 'upstream/master' into unstyled-menu
michaldudak Feb 25, 2022
d72c49b
Correct demos styling
michaldudak Feb 25, 2022
d61b3f2
Merge remote-tracking branch 'upstream/master' into unstyled-menu
michaldudak Feb 28, 2022
beb6315
API docs
michaldudak Feb 28, 2022
3b950cd
A11y improvements
michaldudak Mar 1, 2022
2171704
Add slot props' types
michaldudak Mar 1, 2022
fb91225
Select last item when opening with up arrow
michaldudak Mar 3, 2022
06e1c9c
Type imperative actions properly
michaldudak Mar 3, 2022
f0b6e00
Proptypes
michaldudak Mar 3, 2022
0abace3
Improve demos
michaldudak Mar 3, 2022
ad71ebf
Merge remote-tracking branch 'upstream/master' into unstyled-menu
michaldudak Mar 4, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
181 changes: 181 additions & 0 deletions docs/data/base/components/menu/MenuSimple.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import * as React from 'react';
import MenuUnstyled from '@mui/base/MenuUnstyled';
import MenuItemUnstyled, {
menuItemUnstyledClasses,
} from '@mui/base/MenuItemUnstyled';
import { buttonUnstyledClasses } from '@mui/base/ButtonUnstyled';
import PopperUnstyled from '@mui/base/PopperUnstyled';
import { styled } from '@mui/system';

const blue = {
100: '#DAECFF',
200: '#99CCF3',
400: '#3399FF',
500: '#007FFF',
600: '#0072E5',
900: '#003A75',
};

const grey = {
100: '#E7EBF0',
200: '#E0E3E7',
300: '#CDD2D7',
400: '#B2BAC2',
500: '#A0AAB4',
600: '#6F7E8C',
700: '#3E5060',
800: '#2D3843',
900: '#1A2027',
};

const StyledListbox = styled('ul')(
({ theme }) => `
font-family: IBM Plex Sans, sans-serif;
font-size: 0.875rem;
box-sizing: border-box;
padding: 5px;
margin: 10px 0;
min-width: 200px;
background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'};
border: 1px solid ${theme.palette.mode === 'dark' ? grey[800] : grey[300]};
border-radius: 0.75em;
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
overflow: auto;
outline: 0px;
`,
);

const StyledMenuItem = styled(MenuItemUnstyled)(
({ theme }) => `
list-style: none;
padding: 8px;
border-radius: 0.45em;
cursor: default;

&:last-of-type {
border-bottom: none;
}

&.${menuItemUnstyledClasses.focusVisible} {
outline: 3px solid ${theme.palette.mode === 'dark' ? blue[600] : blue[200]};
background-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
}

&.${menuItemUnstyledClasses.disabled} {
color: ${theme.palette.mode === 'dark' ? grey[700] : grey[400]};
}

&:hover:not(.${menuItemUnstyledClasses.disabled}) {
background-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
}
`,
);

const TriggerButton = styled('button')(
({ theme }) => `
font-family: IBM Plex Sans, sans-serif;
font-size: 0.875rem;
box-sizing: border-box;
min-height: calc(1.5em + 22px);
min-width: 200px;
background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'};
border: 1px solid ${theme.palette.mode === 'dark' ? grey[800] : grey[300]};
border-radius: 0.75em;
margin: 0.5em;
padding: 10px;
text-align: left;
line-height: 1.5;
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};

&:hover {
background: ${theme.palette.mode === 'dark' ? '' : grey[100]};
border-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[400]};
}

&.${buttonUnstyledClasses.focusVisible} {
outline: 3px solid ${theme.palette.mode === 'dark' ? blue[600] : blue[100]};
}

&::after {
content: '▾';
float: right;
}
`,
);

const Popper = styled(PopperUnstyled)`
z-index: 1;
`;

export default function UnstyledMenuSimple() {
const [anchorEl, setAnchorEl] = React.useState(null);
const isOpen = Boolean(anchorEl);
const buttonRef = React.useRef(null);
const menuActions = React.useRef(null);

const handleButtonClick = (event) => {
if (isOpen) {
setAnchorEl(null);
} else {
setAnchorEl(event.currentTarget);
}
};

const handleButtonKeyDown = (event) => {
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the ArrowUp is pressed, the last element should be foced when the menu opens.

event.preventDefault();
setAnchorEl(event.currentTarget);
if (event.key === 'ArrowUp') {
menuActions.current?.highlightLastItem();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that we need to use useImperativeHandle here. Is there a different option? One additional reason to have the MenuButton as a standalone component.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to this:

      <TriggerButton
        type="button"
        onClick={handleButtonClick}
        onKeyDown={handleButtonKeyDown}
        ref={buttonRef}
        aria-controls={isOpen ? 'simple-menu' : undefined}
        aria-expanded={isOpen || undefined}
        aria-haspopup="menu"
      >
        Language
      </TriggerButton>

Has too much boilerplate which is important to not be missed. I would propose to tackle this in the next PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I absolutely agree it's too much code. I will create the MenuButton for sure.
As for useImperativeHandle - I realize it's not quite in the spirit of React, but I couldn't think of a better option that would work with the current MenuUnstyled design. I'm happy to discuss alternatives.

}
}
};

const close = () => {
setAnchorEl(null);
buttonRef.current.focus();
};

const createHandleMenuClick = (menuItem) => {
return () => {
// eslint-disable-next-line no-console
console.log(`Clicked on ${menuItem}`);
close();
};
};

return (
<div>
<TriggerButton
type="button"
onClick={handleButtonClick}
onKeyDown={handleButtonKeyDown}
ref={buttonRef}
aria-controls={isOpen ? 'simple-menu' : undefined}
aria-expanded={isOpen || undefined}
aria-haspopup="menu"
>
Language
</TriggerButton>

<MenuUnstyled
actions={menuActions}
open={isOpen}
onClose={close}
anchorEl={anchorEl}
components={{ Root: Popper, Listbox: StyledListbox }}
componentsProps={{ listbox: { id: 'simple-menu' } }}
>
<StyledMenuItem onClick={createHandleMenuClick('English')}>
English
</StyledMenuItem>
<StyledMenuItem onClick={createHandleMenuClick('中文')}>中文</StyledMenuItem>
<StyledMenuItem onClick={createHandleMenuClick('Português')}>
Português
</StyledMenuItem>
</MenuUnstyled>
</div>
);
}
181 changes: 181 additions & 0 deletions docs/data/base/components/menu/MenuSimple.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import * as React from 'react';
import MenuUnstyled, { MenuUnstyledActions } from '@mui/base/MenuUnstyled';
import MenuItemUnstyled, {
menuItemUnstyledClasses,
} from '@mui/base/MenuItemUnstyled';
import { buttonUnstyledClasses } from '@mui/base/ButtonUnstyled';
import PopperUnstyled from '@mui/base/PopperUnstyled';
import { styled } from '@mui/system';

const blue = {
100: '#DAECFF',
200: '#99CCF3',
400: '#3399FF',
500: '#007FFF',
600: '#0072E5',
900: '#003A75',
};

const grey = {
100: '#E7EBF0',
200: '#E0E3E7',
300: '#CDD2D7',
400: '#B2BAC2',
500: '#A0AAB4',
600: '#6F7E8C',
700: '#3E5060',
800: '#2D3843',
900: '#1A2027',
};

const StyledListbox = styled('ul')(
({ theme }) => `
font-family: IBM Plex Sans, sans-serif;
font-size: 0.875rem;
box-sizing: border-box;
padding: 5px;
margin: 10px 0;
min-width: 200px;
background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'};
border: 1px solid ${theme.palette.mode === 'dark' ? grey[800] : grey[300]};
border-radius: 0.75em;
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
overflow: auto;
outline: 0px;
`,
);

const StyledMenuItem = styled(MenuItemUnstyled)(
({ theme }) => `
list-style: none;
padding: 8px;
border-radius: 0.45em;
cursor: default;

&:last-of-type {
border-bottom: none;
}

&.${menuItemUnstyledClasses.focusVisible} {
outline: 3px solid ${theme.palette.mode === 'dark' ? blue[600] : blue[200]};
background-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
}

&.${menuItemUnstyledClasses.disabled} {
color: ${theme.palette.mode === 'dark' ? grey[700] : grey[400]};
}

&:hover:not(.${menuItemUnstyledClasses.disabled}) {
background-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
}
`,
);

const TriggerButton = styled('button')(
({ theme }) => `
font-family: IBM Plex Sans, sans-serif;
font-size: 0.875rem;
box-sizing: border-box;
min-height: calc(1.5em + 22px);
min-width: 200px;
background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'};
border: 1px solid ${theme.palette.mode === 'dark' ? grey[800] : grey[300]};
border-radius: 0.75em;
margin: 0.5em;
padding: 10px;
text-align: left;
line-height: 1.5;
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};

&:hover {
background: ${theme.palette.mode === 'dark' ? '' : grey[100]};
border-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[400]};
}

&.${buttonUnstyledClasses.focusVisible} {
outline: 3px solid ${theme.palette.mode === 'dark' ? blue[600] : blue[100]};
}

&::after {
content: '▾';
float: right;
}
`,
);

const Popper = styled(PopperUnstyled)`
z-index: 1;
`;

export default function UnstyledMenuSimple() {
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
const isOpen = Boolean(anchorEl);
const buttonRef = React.useRef<HTMLButtonElement>(null);
const menuActions = React.useRef<MenuUnstyledActions>(null);

const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
if (isOpen) {
setAnchorEl(null);
} else {
setAnchorEl(event.currentTarget);
}
};

const handleButtonKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault();
setAnchorEl(event.currentTarget);
if (event.key === 'ArrowUp') {
menuActions.current?.highlightLastItem();
}
}
};

const close = () => {
setAnchorEl(null);
buttonRef.current!.focus();
};

const createHandleMenuClick = (menuItem: string) => {
return () => {
// eslint-disable-next-line no-console
console.log(`Clicked on ${menuItem}`);
close();
};
};

return (
<div>
<TriggerButton
type="button"
onClick={handleButtonClick}
onKeyDown={handleButtonKeyDown}
ref={buttonRef}
aria-controls={isOpen ? 'simple-menu' : undefined}
aria-expanded={isOpen || undefined}
aria-haspopup="menu"
>
Language
</TriggerButton>

<MenuUnstyled
actions={menuActions}
open={isOpen}
onClose={close}
anchorEl={anchorEl}
components={{ Root: Popper, Listbox: StyledListbox }}
componentsProps={{ listbox: { id: 'simple-menu' } }}
>
<StyledMenuItem onClick={createHandleMenuClick('English')}>
English
</StyledMenuItem>
<StyledMenuItem onClick={createHandleMenuClick('中文')}>中文</StyledMenuItem>
<StyledMenuItem onClick={createHandleMenuClick('Português')}>
Português
</StyledMenuItem>
</MenuUnstyled>
</div>
);
}