mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-17 14:27:02 +08:00
fix(core): multi sub menu layer handling (#8916)
fix AF-1800, AF-1801
<div class='graphite__hidden'>
<div>🎥 Video uploaded on Graphite:</div>
<a href="https://app.graphite.dev/media/video/T2klNLEk0wxLh4NRDzhk/7523df2b-2326-4878-b37a-d16e4275858d.mp4">
<img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/T2klNLEk0wxLh4NRDzhk/7523df2b-2326-4878-b37a-d16e4275858d.mp4">
</a>
</div>
<video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/7523df2b-2326-4878-b37a-d16e4275858d.mp4">20241126-0806-30.0904958.mp4</video>
This commit is contained in:
@@ -3,6 +3,8 @@ import {
|
||||
type Dispatch,
|
||||
type ReactNode,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useContext,
|
||||
} from 'react';
|
||||
|
||||
import type { MenuSubProps } from '../menu.types';
|
||||
@@ -14,14 +16,61 @@ export type SubMenuContent = {
|
||||
*/
|
||||
title?: string;
|
||||
items: ReactNode;
|
||||
options?: MenuSubProps['subOptions'];
|
||||
contentOptions?: MenuSubProps['subContentOptions'];
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const MobileMenuContext = createContext<{
|
||||
export type MobileMenuContextValue = {
|
||||
subMenus: Array<SubMenuContent>;
|
||||
setSubMenus: Dispatch<SetStateAction<Array<SubMenuContent>>>;
|
||||
setOpen?: (v: boolean) => void;
|
||||
}>({
|
||||
};
|
||||
|
||||
export const MobileMenuContext = createContext<MobileMenuContextValue>({
|
||||
subMenus: [],
|
||||
setSubMenus: () => {},
|
||||
});
|
||||
|
||||
export const useMobileSubMenuHelper = (
|
||||
contextValue?: MobileMenuContextValue
|
||||
) => {
|
||||
const _context = useContext(MobileMenuContext);
|
||||
const { subMenus, setSubMenus } = contextValue ?? _context;
|
||||
|
||||
const addSubMenu = useCallback(
|
||||
(subMenu: SubMenuContent) => {
|
||||
const id = subMenu.id;
|
||||
// if the submenu already exists, do nothing
|
||||
if (subMenus.some(sub => sub.id === id)) {
|
||||
return;
|
||||
}
|
||||
subMenu.options?.onOpenChange?.(true);
|
||||
setSubMenus(prev => {
|
||||
return [...prev, subMenu];
|
||||
});
|
||||
},
|
||||
[setSubMenus, subMenus]
|
||||
);
|
||||
|
||||
const removeSubMenu = useCallback(
|
||||
(id: string) => {
|
||||
setSubMenus(prev => {
|
||||
const index = prev.findIndex(sub => sub.id === id);
|
||||
prev[index]?.options?.onOpenChange?.(false);
|
||||
return prev.filter(sub => sub.id !== id);
|
||||
});
|
||||
},
|
||||
[setSubMenus]
|
||||
);
|
||||
|
||||
const removeAllSubMenus = useCallback(() => {
|
||||
setSubMenus([]);
|
||||
}, [setSubMenus]);
|
||||
|
||||
return {
|
||||
addSubMenu,
|
||||
removeSubMenu,
|
||||
removeAllSubMenus,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ const preventDefault = () => {
|
||||
};
|
||||
|
||||
export const MobileMenuItem = (props: MenuItemProps) => {
|
||||
const { setOpen } = useContext(MobileMenuContext);
|
||||
const { setOpen, subMenus, setSubMenus } = useContext(MobileMenuContext);
|
||||
const { className, children, otherProps } = useMenuItem(props);
|
||||
const { onSelect, onClick, divide, ...restProps } = otherProps;
|
||||
|
||||
@@ -21,10 +21,16 @@ export const MobileMenuItem = (props: MenuItemProps) => {
|
||||
if (preventDefaultFlag) {
|
||||
preventDefaultFlag = false;
|
||||
} else {
|
||||
setOpen?.(false);
|
||||
if (subMenus.length > 1) {
|
||||
// assume the user can only click the last menu
|
||||
// (mimic the back button)
|
||||
setSubMenus(subMenus.slice(0, -1));
|
||||
} else {
|
||||
setOpen?.(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[onClick, onSelect, setOpen]
|
||||
[onClick, onSelect, setOpen, setSubMenus, subMenus]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,8 +9,11 @@ import { Button } from '../../button';
|
||||
import { Modal } from '../../modal';
|
||||
import { Scrollable } from '../../scrollbar';
|
||||
import type { MenuProps } from '../menu.types';
|
||||
import type { SubMenuContent } from './context';
|
||||
import { MobileMenuContext } from './context';
|
||||
import {
|
||||
MobileMenuContext,
|
||||
type SubMenuContent,
|
||||
useMobileSubMenuHelper,
|
||||
} from './context';
|
||||
import * as styles from './styles.css';
|
||||
import { MobileMenuSubRaw } from './sub';
|
||||
|
||||
@@ -32,12 +35,23 @@ export const MobileMenu = ({
|
||||
}: MenuProps) => {
|
||||
const [subMenus, setSubMenus] = useState<SubMenuContent[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const mobileContextValue = {
|
||||
subMenus,
|
||||
setSubMenus,
|
||||
setOpen,
|
||||
};
|
||||
|
||||
const { removeSubMenu, removeAllSubMenus } =
|
||||
useMobileSubMenuHelper(mobileContextValue);
|
||||
|
||||
const [sliderHeight, setSliderHeight] = useState(0);
|
||||
const [sliderElement, setSliderElement] = useState<HTMLDivElement | null>(
|
||||
null
|
||||
);
|
||||
const { setOpen: pSetOpen } = useContext(MobileMenuContext);
|
||||
const finalOpen = rootOptions?.open ?? open;
|
||||
|
||||
// always show the last submenu, if any
|
||||
const activeIndex = subMenus.length;
|
||||
|
||||
// dynamic height for slider
|
||||
@@ -62,12 +76,12 @@ export const MobileMenu = ({
|
||||
// a workaround to hack the onPointerDownOutside event
|
||||
onPointerDownOutside?.({} as any);
|
||||
onInteractOutside?.({} as any);
|
||||
setSubMenus([]);
|
||||
removeAllSubMenus();
|
||||
}
|
||||
setOpen(open);
|
||||
rootOptions?.onOpenChange?.(open);
|
||||
},
|
||||
[onInteractOutside, onPointerDownOutside, rootOptions]
|
||||
[onInteractOutside, onPointerDownOutside, removeAllSubMenus, rootOptions]
|
||||
);
|
||||
|
||||
const onItemClick = useCallback(
|
||||
@@ -93,7 +107,11 @@ export const MobileMenu = ({
|
||||
* ```
|
||||
*/
|
||||
if (pSetOpen) {
|
||||
return <MobileMenuSubRaw items={items}>{children}</MobileMenuSubRaw>;
|
||||
return (
|
||||
<MobileMenuSubRaw items={items} subOptions={rootOptions}>
|
||||
{children}
|
||||
</MobileMenuSubRaw>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -126,7 +144,7 @@ export const MobileMenu = ({
|
||||
</div>
|
||||
{subMenus.map((sub, index) => (
|
||||
<div
|
||||
key={index}
|
||||
key={sub.id}
|
||||
data-index={index + 1}
|
||||
className={styles.menuContent}
|
||||
>
|
||||
@@ -135,7 +153,9 @@ export const MobileMenu = ({
|
||||
variant="plain"
|
||||
className={styles.backButton}
|
||||
prefix={<ArrowLeftSmallIcon />}
|
||||
onClick={() => setSubMenus(prev => prev.slice(0, index))}
|
||||
onClick={() => {
|
||||
removeSubMenu(sub.id);
|
||||
}}
|
||||
prefixStyle={{ width: 24, height: 24 }}
|
||||
>
|
||||
{sub.title || t['com.affine.backButton']()}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ArrowRightSmallPlusIcon } from '@blocksuite/icons/rc';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { type MouseEvent, useCallback, useContext } from 'react';
|
||||
import { type MouseEvent, useCallback, useEffect, useId, useMemo } from 'react';
|
||||
|
||||
import type { MenuSubProps } from '../menu.types';
|
||||
import { useMenuItem } from '../use-menu-item';
|
||||
import { MobileMenuContext } from './context';
|
||||
import { useMobileSubMenuHelper } from './context';
|
||||
|
||||
export const MobileMenuSub = ({
|
||||
title,
|
||||
@@ -42,20 +42,36 @@ export const MobileMenuSubRaw = ({
|
||||
onClick,
|
||||
children,
|
||||
items,
|
||||
subOptions,
|
||||
subContentOptions: contentOptions = {},
|
||||
}: MenuSubProps & {
|
||||
onClick?: (e: MouseEvent<HTMLDivElement>) => void;
|
||||
title?: string;
|
||||
}) => {
|
||||
const { setSubMenus } = useContext(MobileMenuContext);
|
||||
const id = useId();
|
||||
const { addSubMenu } = useMobileSubMenuHelper();
|
||||
|
||||
const subMenuContent = useMemo(
|
||||
() => ({ items, contentOptions, options: subOptions, title, id }),
|
||||
[items, contentOptions, subOptions, title, id]
|
||||
);
|
||||
|
||||
const doAddSubMenu = useCallback(() => {
|
||||
addSubMenu(subMenuContent);
|
||||
}, [addSubMenu, subMenuContent]);
|
||||
|
||||
const onItemClick = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
onClick?.(e);
|
||||
setSubMenus(prev => [...prev, { items, contentOptions, title }]);
|
||||
doAddSubMenu();
|
||||
},
|
||||
[contentOptions, items, onClick, setSubMenus, title]
|
||||
[doAddSubMenu, onClick]
|
||||
);
|
||||
useEffect(() => {
|
||||
if (subOptions?.open) {
|
||||
doAddSubMenu();
|
||||
}
|
||||
}, [doAddSubMenu, subOptions]);
|
||||
|
||||
return <Slot onClick={onItemClick}>{children}</Slot>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user