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

[base] Refactor the compound components building blocks #36400

Merged
merged 51 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
5a5c21a
Few fixes and additions to useListbox
michaldudak Mar 1, 2023
fac8a2b
Redesign unstyled Tabs to work with context
michaldudak Mar 1, 2023
4cf59ea
Extract useListItem to a separate file
michaldudak Mar 15, 2023
ce1aeac
Clean up useListbox
michaldudak Mar 16, 2023
ec17fdd
Rename useListbox to useList
michaldudak Mar 17, 2023
56c6416
Use the new building blocks in MenuUnstyled
michaldudak Mar 17, 2023
db53bd0
Use the new building blocks in SelectUnstyled
michaldudak Mar 21, 2023
319985b
Decouple the controllableReducer from the List
michaldudak Mar 23, 2023
5db7fae
Docs and cleanup of useList
michaldudak Mar 28, 2023
776be3e
Rename defaultListboxReducer to listReducer
michaldudak Mar 28, 2023
2da76ad
API docs
michaldudak Mar 29, 2023
34fa5ac
Do not require compound subcomponents to have a defined value
michaldudak Mar 29, 2023
8967e16
Sync ids between tabs and tab panels
michaldudak Mar 30, 2023
f266cd6
Fix errors in Joy
michaldudak Mar 30, 2023
b30051a
joy-ui: add GroupListContext
siriwatknp Apr 3, 2023
1a31ee2
Fix all the tests
michaldudak Apr 3, 2023
cb3742f
Docs and demos
michaldudak Apr 3, 2023
78be657
Include inaccessible elements in tests
michaldudak Apr 3, 2023
af31a7f
Name the TabsListUnstyled function
michaldudak Apr 4, 2023
35ca76f
Remove controlled open prop from Select implementations
michaldudak Apr 4, 2023
5ddbe6f
Make Joy's Option value mandatory
michaldudak Apr 4, 2023
23fb884
Update Joy Select demo to restore the previous look
michaldudak Apr 4, 2023
54b7875
Update the description of the tabs API
michaldudak Apr 4, 2023
6bcb725
Don't try to focus a closed menu's item
michaldudak Apr 4, 2023
7f5c9c5
Revert unrelated changes
michaldudak Apr 4, 2023
ff4bf70
Remove old code and update demos
michaldudak Apr 4, 2023
b97ed12
Restart CI
michaldudak Apr 4, 2023
c1305f6
Merge remote-tracking branch 'upstream/master' into tabs-with-context
michaldudak Apr 5, 2023
e9a6afe
Docs + small changes
michaldudak Apr 5, 2023
6999645
Rename TValue to something more meaningful
michaldudak Apr 5, 2023
422e0bf
Revert useLatest
michaldudak Apr 5, 2023
15c8ae1
Wrap useStateChangeDetection's parameters in an object
michaldudak Apr 6, 2023
10ace6d
listReducer keyboard navigation tests and fixes
michaldudak Apr 6, 2023
091cd3a
Do not focus a disabled first item after initially populating the opt…
michaldudak Apr 6, 2023
d46eb12
Configurable pageSize in useList
michaldudak Apr 6, 2023
3c7faf4
Small changes
michaldudak Apr 6, 2023
843e882
Highlight selected item in Select
michaldudak Apr 6, 2023
85878a2
Extract reducers and add a few tests
michaldudak Apr 6, 2023
e3528fb
Open the Joy Select when clicked on the root element
michaldudak Apr 6, 2023
8e14f71
Docs and tests
michaldudak Apr 7, 2023
d18aee7
Simplify action addon (do not require a ref)
michaldudak Apr 7, 2023
f8da22a
Remove the unnecessary setState action (and simplify many generics)
michaldudak Apr 7, 2023
1a05c79
Simplify, rename and explain the action addon concept
michaldudak Apr 7, 2023
f7448ad
Remove redundant return values from useList
michaldudak Apr 7, 2023
b9e76dd
Add comments in useList
michaldudak Apr 7, 2023
a4227d4
Fix unnecessary rerendering of options
michaldudak Apr 11, 2023
3af4d0c
Merge branch 'master' into tabs-with-context
michaldudak Apr 11, 2023
b074464
Finx linter violation
michaldudak Apr 11, 2023
430ccad
Change MD3 tabs to use null instead of false
michaldudak Apr 11, 2023
bba8c16
Docs
michaldudak Apr 11, 2023
6070647
Change selectionLimit to selectionMode
michaldudak Apr 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 23 additions & 19 deletions docs/data/base/components/menu/MenuSimple.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,27 @@ 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();
preventReopen.current = false;
return;
}

if (isOpen) {
setAnchorEl(null);
} else {
setAnchorEl(event.currentTarget);
}
setOpen((open) => !open);
};

const handleButtonMouseDown = () => {
Expand All @@ -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();
};
};

Expand All @@ -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"
Expand All @@ -75,8 +77,10 @@ export default function UnstyledMenuSimple() {
<MenuUnstyled
actions={menuActions}
open={isOpen}
onClose={close}
anchorEl={anchorEl}
onOpenChange={(open) => {
setOpen(open);
}}
anchorEl={buttonElement}
slots={{ root: Popper, listbox: StyledListbox }}
slotProps={{ listbox: { id: 'simple-menu' } }}
>
Expand Down
43 changes: 24 additions & 19 deletions docs/data/base/components/menu/MenuSimple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,28 @@ 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<HTMLButtonElement | null>(null);
const isOpen = Boolean(anchorEl);
const buttonRef = React.useRef<HTMLButtonElement>(null);
const [buttonElement, setButtonElement] = React.useState<HTMLButtonElement | null>(
null,
);
const [isOpen, setOpen] = React.useState(false);
Copy link
Member Author

Choose a reason for hiding this comment

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

I think it may be easier to understand if we have an explicit isOpen state instead of setting and resetting the anchorEl.
All the menu demos (and Joy templates) use the same pattern.

const menuActions = React.useRef<MenuUnstyledActions>(null);
const preventReopen = React.useRef(false);

const updateAnchor = React.useCallback((node: HTMLButtonElement | null) => {
setButtonElement(node);
}, []);

const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
if (preventReopen.current) {
event.preventDefault();
preventReopen.current = false;
return;
}

if (isOpen) {
setAnchorEl(null);
} else {
setAnchorEl(event.currentTarget);
}
setOpen((open) => !open);
};

const handleButtonMouseDown = () => {
Expand All @@ -38,22 +40,23 @@ export default function UnstyledMenuSimple() {
const handleButtonKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
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();
};
};

Expand All @@ -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"
Expand All @@ -75,8 +78,10 @@ export default function UnstyledMenuSimple() {
<MenuUnstyled
actions={menuActions}
open={isOpen}
onClose={close}
anchorEl={anchorEl}
onOpenChange={(open) => {
setOpen(open);
}}
anchorEl={buttonElement}
slots={{ root: Popper, listbox: StyledListbox }}
slotProps={{ listbox: { id: 'simple-menu' } }}
>
Expand Down
55 changes: 37 additions & 18 deletions docs/data/base/components/menu/UnstyledMenuIntroduction.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
<TriggerButton
type="button"
onClick={handleButtonClick}
onKeyDown={handleButtonKeyDown}
ref={buttonRef}
onMouseDown={handleButtonMouseDown}
ref={updateAnchor}
aria-controls={isOpen ? 'simple-menu' : undefined}
aria-expanded={isOpen || undefined}
aria-haspopup="menu"
Expand All @@ -58,8 +75,10 @@ export default function UnstyledMenuIntroduction() {
<MenuUnstyled
actions={menuActions}
open={isOpen}
onClose={close}
anchorEl={anchorEl}
onOpenChange={(open) => {
setOpen(open);
}}
anchorEl={buttonElement}
slots={{ root: Popper, listbox: StyledListbox }}
slotProps={{ listbox: { id: 'simple-menu' } }}
>
Expand Down
56 changes: 38 additions & 18 deletions docs/data/base/components/menu/UnstyledMenuIntroduction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLButtonElement | null>(null);
const isOpen = Boolean(anchorEl);
const buttonRef = React.useRef<HTMLButtonElement>(null);
const [buttonElement, setButtonElement] = React.useState<HTMLButtonElement | null>(
null,
);
const [isOpen, setOpen] = React.useState(false);
const menuActions = React.useRef<MenuUnstyledActions>(null);
const preventReopen = React.useRef(false);

const updateAnchor = React.useCallback((node: HTMLButtonElement | null) => {
setButtonElement(node);
}, []);

const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
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<HTMLButtonElement>) => {
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 (
<div>
<TriggerButton
type="button"
onClick={handleButtonClick}
onKeyDown={handleButtonKeyDown}
ref={buttonRef}
onMouseDown={handleButtonMouseDown}
ref={updateAnchor}
aria-controls={isOpen ? 'simple-menu' : undefined}
aria-expanded={isOpen || undefined}
aria-haspopup="menu"
Expand All @@ -58,8 +76,10 @@ export default function UnstyledMenuIntroduction() {
<MenuUnstyled
actions={menuActions}
open={isOpen}
onClose={close}
anchorEl={anchorEl}
onOpenChange={(open) => {
setOpen(open);
}}
anchorEl={buttonElement}
slots={{ root: Popper, listbox: StyledListbox }}
slotProps={{ listbox: { id: 'simple-menu' } }}
>
Expand Down
Loading