diff --git a/packages/discuz-design/components/menu/MenuContext.ts b/packages/discuz-design/components/menu/MenuContext.ts index 996b08e95892f5c5b73267c3df84bc054e871413..1b5cc318f5970cd806cbd05d263b4c85c40ea8fa 100644 --- a/packages/discuz-design/components/menu/MenuContext.ts +++ b/packages/discuz-design/components/menu/MenuContext.ts @@ -76,6 +76,69 @@ interface MenuContextValue { * @default '' */ defaultOpeneds?: string[]; + + /** + * 当前选中的菜单项 submenu 数组 + * @default [''] + */ + selectedSubmenus?: string[]; + + /** + * 当前选中的菜单项 index 数组 + * @default [''] + */ + selectedIndexs?: string[]; + + /** + * 点击了菜单的回调 + * @param {index} string 菜单的index + * @param {isOpen} boolean 是否展开 + * @param currentActiveItemIndexSet 当前已选中的index Set集合 + */ + onClick?: ( + { index, submenu, selectedIndexs, selectedSubmenus }: + { index: string, submenu: string, selectedIndexs: string[], selectedSubmenus: string[] } + ) => void; + + /** + * 点击 SubMenu 调用此函数 + * @param {selectedIndex} string 菜单的index + * @param {open} boolean 是否展开 + * @param currentActiveItemIndexSet 当前已选中的index Set集合 + */ + onTitleClick?: (selectedIndex: string, open: boolean) => void; + + /** + * SubMenu 展开/关闭的回调 + * @param {selectedIndex} string 菜单的index + * @param {open} boolean 是否展开 + * @param currentActiveItemIndexSet 当前已选中的index Set集合 + */ + onOpenChange?: (selectedIndex: string, open: boolean) => void; + + /** + * 取消选中时调用 + * @param {index} string 菜单的index + * @param {submenu} string 菜单项的submenu + * @param {selectedIndexs} string[] 当前选中的菜单项 index 数组 + * @param {selectedSubmenus} string[] 当前选中的菜单项 submenu 数组 + */ + onDeselect?: ( + { index, submenu, selectedIndexs, selectedSubmenus }: + { index: string, submenu: string, selectedIndexs: string[], selectedSubmenus: string[] } + ) => void; + + /** + * 被选中时调用 + * @param {index} string 菜单的index + * @param {submenu} string 菜单项的submenu + * @param {selectedIndexs} string[] 当前选中的菜单项 index 数组 + * @param {selectedSubmenus} string[] 当前选中的菜单项 submenu 数组 + */ + onSelect?: ( + { index, submenu, selectedIndexs, selectedSubmenus }: + { index: string, submenu: string, selectedIndexs: string[], selectedSubmenus: string[] } + ) => void; } export const INITIAL_VALUE = '-1'; @@ -91,6 +154,12 @@ export const MenuContext = createContext({ mode: 'vertical', menuTrigger: 'click', multiple: false, + selectedIndexs: [], + onClick: () => {}, + onTitleClick: () => {}, + onOpenChange: () => {}, + onSelect: () => {}, + onDeselect: () => {}, currentActiveItemIndexSet: new Set(), setCurrentActiveItemIndexSet: () => {}, currentActiveSubMenuIndexSet: new Set(), diff --git a/packages/discuz-design/components/menu/README.md b/packages/discuz-design/components/menu/README.md index c86882513a310fa9e92c66e333f4b281c8e860a4..117f0c7cb8f07cfda6ee5025e8d8754228f49616 100644 --- a/packages/discuz-design/components/menu/README.md +++ b/packages/discuz-design/components/menu/README.md @@ -14,6 +14,11 @@ [Example: 基础用法](./__examples__/web/base.tsx) +### 可控模式 + +> 设置 index、submenu 数组控制展示 + +[Example: 可控模式](./__examples__/web/controlled.tsx) ### 横向模式 > mode=horizontal diff --git a/packages/discuz-design/components/menu/__examples__/web/base.tsx b/packages/discuz-design/components/menu/__examples__/web/base.tsx index 3fda29d35543f3936b02223e137d9e6b48957b28..75fc60f6d03f5daa58caac4f43f00252de844eeb 100644 --- a/packages/discuz-design/components/menu/__examples__/web/base.tsx +++ b/packages/discuz-design/components/menu/__examples__/web/base.tsx @@ -1,5 +1,6 @@ import { Menu, Card } from '@discuzq/design'; import React from 'react'; +import './index.scss'; function title(name = '导航') { return
@@ -10,12 +11,12 @@ function title(name = '导航') { export default function Base() { return ( - + {title('全部')} 选项1 - 选项2 + 选项2 选项3 选项4 @@ -24,7 +25,7 @@ export default function Base() { 选项1 选项2 - 选项3 + 选项3 diff --git a/packages/discuz-design/components/menu/__examples__/web/controlled.tsx b/packages/discuz-design/components/menu/__examples__/web/controlled.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c63d0b0bf418b75b940706ec24e963084eef6053 --- /dev/null +++ b/packages/discuz-design/components/menu/__examples__/web/controlled.tsx @@ -0,0 +1,52 @@ +import { Menu, Card, Button } from '@discuzq/design'; +import React, { useState } from 'react'; +import './index.scss'; + +function title(name = '导航') { + return
+ {name} + {Math.ceil(Math.random() * 1000)} +
; +} + +export default function Base() { + const [indexs, setIndex] = useState([]); + const [submenus, setSubmenus] = useState([]); + + const onClick1 = () => { + setIndex(['1-2']); + setSubmenus(['1']); + }; + const onDelete = () => { + setIndex([]); + setSubmenus([]); + }; + const onClick2 = () => { + setIndex(['2-1']); + setSubmenus(['2']); + }; + + return ( + + + + + + {title('全部')} + + 选项1 + 选项2 + + 选项3 + 选项4 + + + + 选项1 + 选项2 + 选项3 + + + + ); +} diff --git a/packages/discuz-design/components/menu/__examples__/web/defaultOptions.tsx b/packages/discuz-design/components/menu/__examples__/web/defaultOptions.tsx index 00d85efaa2c0385bbfc2277d15d9e6f2b4f0a053..98da1a60b85908138e192cbd31b37bf74aae639e 100644 --- a/packages/discuz-design/components/menu/__examples__/web/defaultOptions.tsx +++ b/packages/discuz-design/components/menu/__examples__/web/defaultOptions.tsx @@ -1,5 +1,6 @@ import { Menu, Card } from '@discuzq/design'; import React from 'react'; +import './index.scss'; function title(name = '导航') { return
@@ -10,8 +11,8 @@ function title(name = '导航') { export default function Base() { return ( -
- +
+ {title('全部')} @@ -29,7 +30,7 @@ export default function Base() { - + {title('全部')} diff --git a/packages/discuz-design/components/menu/__examples__/web/event.tsx b/packages/discuz-design/components/menu/__examples__/web/event.tsx index f3338493e1d6971cabae2c1e554e80213fc86f7c..3d3054a8aa7967cc8022a8d4aeda5440a626eb89 100644 --- a/packages/discuz-design/components/menu/__examples__/web/event.tsx +++ b/packages/discuz-design/components/menu/__examples__/web/event.tsx @@ -1,5 +1,6 @@ import { Menu, Card } from '@discuzq/design'; import React, { useState } from 'react'; +import './index.scss'; function title(name = '导航') { return
@@ -20,7 +21,7 @@ export default function Base() { } return ( - + {title('全部')} diff --git a/packages/discuz-design/components/menu/__examples__/web/index.scss b/packages/discuz-design/components/menu/__examples__/web/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..6ff509f169599672a90da0b69ad760e0f0a9c3eb --- /dev/null +++ b/packages/discuz-design/components/menu/__examples__/web/index.scss @@ -0,0 +1,13 @@ +@mixin reste { + margin: 0 !important; + padding: 0 !important; +} + +.warp { + .dzq-menu { + @include reste; + } + .dzq-submenu { + margin: 0 !important; + } +} \ No newline at end of file diff --git a/packages/discuz-design/components/menu/__examples__/web/mode.tsx b/packages/discuz-design/components/menu/__examples__/web/mode.tsx index 1691e0dde0d1f5f233d93a4c618e18686c096088..29b5db4e8253b3f856f539683037c505de257fe6 100644 --- a/packages/discuz-design/components/menu/__examples__/web/mode.tsx +++ b/packages/discuz-design/components/menu/__examples__/web/mode.tsx @@ -1,5 +1,6 @@ import { Menu, Card } from '@discuzq/design'; import React from 'react'; +import './index.scss'; function title(name = '导航') { return {name}; @@ -7,7 +8,7 @@ function title(name = '导航') { export default function Base() { return ( - + 全部 diff --git a/packages/discuz-design/components/menu/__examples__/web/multiple.tsx b/packages/discuz-design/components/menu/__examples__/web/multiple.tsx index 6cda40eb3b6f178c2aee23b12cbe85d8703500b0..996becd1b0616364e2454fc3037449a4ec086243 100644 --- a/packages/discuz-design/components/menu/__examples__/web/multiple.tsx +++ b/packages/discuz-design/components/menu/__examples__/web/multiple.tsx @@ -1,5 +1,6 @@ import { Menu, Card } from '@discuzq/design'; import React from 'react'; +import './index.scss'; function title(name = '导航') { return {name}; @@ -11,12 +12,11 @@ function onClick(index, subMenuIndex, list) { export default function Base() { return ( -
+
{title('全部')} @@ -40,17 +40,14 @@ export default function Base() { @@ -10,7 +11,7 @@ function title(name = '导航') { export default function Base() { return ( - + {title('全部')} diff --git a/packages/discuz-design/components/menu/interface.ts b/packages/discuz-design/components/menu/interface.ts index 394a769701a1b361e6ce218dde483435ae5ddbbb..bff0d27bd561db288df5871ba8b0bf264d548cb0 100644 --- a/packages/discuz-design/components/menu/interface.ts +++ b/packages/discuz-design/components/menu/interface.ts @@ -53,4 +53,67 @@ export interface MenuProps extends StyledProps { * @default '' */ defaultSubmenuActives?: string[]; + + /** + * 当前选中的菜单项 submenu 数组 + * @default [''] + */ + selectedSubmenus?: string[]; + + /** + * 当前选中的菜单项 index 数组 + * @default [''] + */ + selectedIndexs?: string[]; + + /** + * 点击 MenuItem 调用此函数 + * @param {index} string 菜单的index + * @param {isOpen} boolean 是否展开 + * @param currentActiveItemIndexSet 当前已选中的index Set集合 + */ + onClick?: ( + { index, submenu, selectedIndexs, selectedSubmenus }: + { index: string, submenu: string, selectedIndexs: string[], selectedSubmenus: string[] } + ) => void; + + /** + * 点击 SubMenu 调用此函数 + * @param {selectedIndex} string 菜单的index + * @param {open} boolean 是否展开 + * @param currentActiveItemIndexSet 当前已选中的index Set集合 + */ + onTitleClick?: (selectedIndex: string, open: boolean) => void; + + /** + * SubMenu 展开/关闭的回调 + * @param {selectedIndex} string 菜单的index + * @param {open} boolean 是否展开 + * @param currentActiveItemIndexSet 当前已选中的index Set集合 + */ + onOpenChange?: (selectedIndex: string, open: boolean) => void; + + /** + * 取消选中时调用 + * @param {index} string 菜单的index + * @param {submenu} string 菜单项的submenu + * @param {selectedIndexs} string[] 当前选中的菜单项 index 数组 + * @param {selectedSubmenus} string[] 当前选中的菜单项 submenu 数组 + */ + onDeselect?: ( + { index, submenu, selectedIndexs, selectedSubmenus }: + { index: string, submenu: string, selectedIndexs: string[], selectedSubmenus: string[] } + ) => void; + + /** + * 被选中时调用 + * @param {index} string 菜单的index + * @param {submenu} string 菜单项的submenu + * @param {selectedIndexs} string[] 当前选中的菜单项 index 数组 + * @param {selectedSubmenus} string[] 当前选中的菜单项 submenu 数组 + */ + onSelect?: ( + { index, submenu, selectedIndexs, selectedSubmenus }: + { index: string, submenu: string, selectedIndexs: string[], selectedSubmenus: string[] } + ) => void; } diff --git a/packages/discuz-design/components/menu/layouts/web.tsx b/packages/discuz-design/components/menu/layouts/web.tsx index 95563f8dd9f70bda0aadd1c5b6028f023ec96447..6ed475c1d3e041491fa6b133fca7b4dc42dacf96 100644 --- a/packages/discuz-design/components/menu/layouts/web.tsx +++ b/packages/discuz-design/components/menu/layouts/web.tsx @@ -35,6 +35,11 @@ export class MenuWebLayout extends Component { currentActiveItemIndex: initCurrentActiveItemIndex, setCurrentActiveItemIndex: this.setCurrentActiveItemIndex.bind(this), uniqueOpened: props.uniqueOpened, + onClick: props.onClick, + onTitleClick: props.onTitleClick, + onOpenChange: props.onOpenChange, + onDeselect: props.onDeselect, + onSelect: props.onSelect, mode: props.mode, menuTrigger: props.menuTrigger, @@ -81,7 +86,7 @@ export class MenuWebLayout extends Component { } public render() { - const { style, className, mode }: MenuProps = this.props; + const { style, className, mode, selectedIndexs, selectedSubmenus }: MenuProps = this.props; const { clsPrefix } = this.context; @@ -97,7 +102,7 @@ export class MenuWebLayout extends Component { return (
    - + {this.props.children}
diff --git a/packages/discuz-design/components/menu/menu-item/layouts/web.tsx b/packages/discuz-design/components/menu/menu-item/layouts/web.tsx index e7687aec7f628a7e6fac012c959616ab0c2fc22d..aec13c088979fe4adcd6c27435852881254a8e0e 100644 --- a/packages/discuz-design/components/menu/menu-item/layouts/web.tsx +++ b/packages/discuz-design/components/menu/menu-item/layouts/web.tsx @@ -1,14 +1,21 @@ -import React, { useContext } from 'react'; +import React, { useContext, useMemo, useEffect, useRef } from 'react'; import { ConfigContext } from '../../../../extends/configContext'; import { MenuContext, SubMenuContext } from '../../MenuContext'; import classNames from 'classnames'; import { MenuItemProps } from '../interface'; export function MenuItemWebLayout(props) { - const { style, className, index, disabled, divided, onClick }: MenuItemProps = props; + const { style, className, index, disabled, divided, onClick: onItemClick }: MenuItemProps = props; const { currentActiveItemIndex, + currentActiveSubMenuIndex, + selectedIndexs, + selectedSubmenus, + menuTrigger, + onClick, + onDeselect, + onSelect, setCurrentActiveItemIndex, setCurrentActiveSubMenuIndex, // 多选相关 @@ -21,18 +28,80 @@ export function MenuItemWebLayout(props) { const { subMenuIndex } = useContext(SubMenuContext); + const initialOpen = useRef(false); + const { clsPrefix } = useContext(ConfigContext); const componentPrefix = `${clsPrefix}-menu-item`; + const isSelect = useMemo(() => { + const select = selectedIndexs?.includes(index); + select && setTimeout(() => { + setCurrentActiveItemIndex(index); + }, 100); + return select; + }, [selectedIndexs]); + + const active = (multiple ? currentActiveItemIndexSet.has(index) : currentActiveItemIndex === index); + + const open = selectedIndexs ? isSelect : active; const classNameStr: string = classNames(componentPrefix, className, { 'is-disabled': disabled, - 'is-active': multiple - ? currentActiveItemIndexSet.has(index) - : currentActiveItemIndex === index, + 'is-active': open, 'is-divided': divided, }); + let indexs = [currentActiveItemIndex]; + let submenus = [currentActiveSubMenuIndex]; + + if (isSelect) { + indexs = selectedIndexs; + submenus = selectedSubmenus; + } + if (multiple) { + indexs = Array.from(currentActiveItemIndexSet); + submenus = Array.from(currentActiveSubMenuIndexSet); + } + + const fnDeselect = async (params?: any) => { + typeof onDeselect === 'function' && await onDeselect({ + index: params?.index || index, + submenu: params?.subMenuIndex || subMenuIndex, + selectedIndexs: params?.indexs || indexs, + selectedSubmenus: params?.submenus || submenus, + }); + }; + + const fnSelect = async () => { + typeof onSelect === 'function' && onSelect({ + index, + submenu: subMenuIndex, + selectedIndexs: indexs, + selectedSubmenus: submenus, + }); + }; + + useEffect(() => { + if (!initialOpen.current) { + initialOpen.current = true; + return; + } + if (open) { + fnSelect(); + return isSelect && fnDeselect; + }; + + !selectedIndexs && fnDeselect(); + }, [active]); + + + const hoverDeselect = () => { + if (menuTrigger !== 'hover' || multiple || submenus.includes('-1') || indexs.includes('-1')) { + return false; + } + return (!submenus.includes(subMenuIndex) && !indexs.includes(index)) || false; + }; + const onMenuItemClick = (e) => { if (!disabled) { e.nativeEvent.stopImmediatePropagation(); @@ -50,11 +119,24 @@ export function MenuItemWebLayout(props) { } setCurrentActiveItemIndexSet(currentActiveItemIndexSet); } else { + hoverDeselect() && fnDeselect({ + index: indexs[0], + indexs: [index], + subMenuIndex: submenus[0], + submenus: [subMenuIndex], + }); setCurrentActiveItemIndex(index); setCurrentActiveSubMenuIndex(subMenuIndex); } - typeof onClick === 'function' && onClick(index, subMenuIndex, currentActiveItemIndexSet); + typeof onClick === 'function' && onClick({ + index, + submenu: subMenuIndex, + selectedIndexs: indexs, + selectedSubmenus: submenus, + }); + + typeof onItemClick === 'function' && onItemClick(index, subMenuIndex, currentActiveItemIndexSet); } }; diff --git a/packages/discuz-design/components/menu/sub-menu/layouts/web.tsx b/packages/discuz-design/components/menu/sub-menu/layouts/web.tsx index c582b40ff2add4de440fe3ce55efbb26db5e2308..4dc996d7de3c9672a8ee53e46f5e137588568644 100644 --- a/packages/discuz-design/components/menu/sub-menu/layouts/web.tsx +++ b/packages/discuz-design/components/menu/sub-menu/layouts/web.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState, useEffect } from 'react'; +import React, { useContext, useState, useEffect, useMemo, useRef } from 'react'; import { ConfigContext } from '../../../../extends/configContext'; import { MenuContext, INITIAL_VALUE, SubMenuContext } from '../../MenuContext'; @@ -18,6 +18,9 @@ export function SubMenuWebLayout(props) { currentActiveSubMenuIndex, setCurrentActiveItemIndex, setCurrentActiveSubMenuIndex, + selectedSubmenus, + onTitleClick, + onOpenChange, // 多选相关 multiple, currentActiveSubMenuIndexSet, @@ -28,6 +31,8 @@ export function SubMenuWebLayout(props) { } = useContext(MenuContext); const { clsPrefix } = useContext(ConfigContext); + const initialOpen = useRef(false); + const initialSelect = useRef(false); const componentPrefix = `${clsPrefix}-submenu`; const menuClassName = `${clsPrefix}-menu`; @@ -36,17 +41,25 @@ export function SubMenuWebLayout(props) { if (defaultOpeneds && defaultOpeneds.length) { defaultOpened = defaultOpeneds.some(item => item === index); } - const [open, setOpen] = useState(index === currentActiveSubMenuIndex || defaultOpened); + + const isSelect = useMemo(() => { + const select = selectedSubmenus?.includes(index); + select && setTimeout(() => { + setCurrentActiveSubMenuIndex(index); + }, 100); + return select; + }, [selectedSubmenus]); + const classNameStr: string = classNames(componentPrefix, className, { - 'is-open': open, + 'is-open': (selectedSubmenus ? isSelect : open), }); const active = (multiple ? currentActiveSubMenuIndexSet.has(index) : index === currentActiveSubMenuIndex); const titleClassNameStr: string = classNames(`${componentPrefix}__title`, { - 'is-active': active, + 'is-active': (selectedSubmenus ? isSelect : active), }); // 开启手风琴模式 @@ -57,7 +70,7 @@ export function SubMenuWebLayout(props) { } // menu菜单子元素 - let childrenElemet = open ?
    + let childrenElemet = (selectedSubmenus ? isSelect : open) ?
      {props.children} @@ -87,6 +100,14 @@ export function SubMenuWebLayout(props) { }; } + useEffect(() => { + if (initialOpen.current) { + typeof onOpenChange === 'function' && onOpenChange(index, open); + return; + } + initialOpen.current = true; + }, [open]); + const onSubMenuClick = (e) => { e.nativeEvent.stopImmediatePropagation(); @@ -96,13 +117,14 @@ export function SubMenuWebLayout(props) { ? currentActiveSubMenuIndexSet.delete(index) : currentActiveSubMenuIndexSet.add(index); setCurrentActiveSubMenuIndexSet(currentActiveSubMenuIndexSet); - setOpen(!open); + !selectedSubmenus && setOpen(!open); } else { setCurrentActiveSubMenuIndex(index); setCurrentActiveItemIndex(INITIAL_VALUE); - setOpen(!open); + !selectedSubmenus && setOpen(!open); } + typeof onTitleClick === 'function' && onTitleClick(index, !open, Array.from(currentActiveSubMenuIndexSet)); typeof onClick === 'function' && onClick(index, !open, currentActiveSubMenuIndexSet); }; diff --git a/packages/discuz-design/site/web/pages/menu.js b/packages/discuz-design/site/web/pages/menu.js index 17aee34e5dd8306026aba4cb21b292def6ff9533..52bf1559f3d7774dec07649d8d3a4657296b52b8 100644 --- a/packages/discuz-design/site/web/pages/menu.js +++ b/packages/discuz-design/site/web/pages/menu.js @@ -4,7 +4,7 @@ import UniqueOpened from '../../../components/menu/__examples__/web/uniqueOpened import Mode from '../../../components/menu/__examples__/web/mode'; import Event from '../../../components/menu/__examples__/web/event'; import Multiple from '../../../components/menu/__examples__/web/multiple'; -import DefaultOpeneds from '../../../components/menu/__examples__/web/default-openeds'; +import DefaultOpeneds from '../../../components/menu/__examples__/web/defaultOptions'; function Title(props) { return

      {props.children}

      ;