import React, { forwardRef, useEffect, useState, useRef } from "react"; import classNames from "classnames"; import { StyledProps } from "../_type"; import { createRocket } from "../_util/create-rocket"; import { Tooltip } from "../tooltip"; import { DropdownBox } from "../dropdown"; import { forwardRefWithStatics } from "../_util/forward-ref-with-statics"; import { useConfig } from "../_util/config-context"; import { isChildOfType } from "../_util/is-child-of-type"; import { noop } from "../_util/noop"; import { isMobile } from "../_util/is-mobile"; import { useOutsideClick } from "../_util/use-outside-click"; import { mergeRefs } from "../util"; export interface ListItemProps extends StyledProps { /** * 是否处于激活态 */ current?: boolean; /** * 是否处于选中态 */ selected?: boolean; /** * 是否处于禁用态 */ disabled?: boolean; /** * 点击时回调 */ onClick?: (evt: React.MouseEvent) => void; /** * Tooltip 说明文本 */ tooltip?: React.ReactNode; /** * 菜单项 */ children?: React.ReactNode; } export const ListItem = forwardRef(function ListItem( { children, className, current, disabled, selected, onClick, tooltip, ...props }: ListItemProps, ref: React.Ref ) { let child = (
  • {children}
  • ); if (tooltip) { child = {child}; } return child; }); ListItem.displayName = "ListItem"; export interface ListSubMenuProps extends StyledProps, React.LiHTMLAttributes { /** * 子菜单名 */ label?: React.ReactNode; /** * 菜单项 */ children?: React.ReactNode; /** * 弹出方向 * @default "right" */ placement?: "right" | "left" | "right-end" | "left-end"; /** * 展开子菜单触发方式 * @default "hover" * @since 2.7.0 */ trigger?: "hover" | "click"; /** * 是否处于禁用态 * @since 2.5.0 */ disabled?: boolean; /** * 菜单项内容是否展示 * * 默认为鼠标 Hover 触发,可通过该属性实现受控 * @since 2.5.0 */ visible?: boolean; /** * 菜单项内容展示状态变化回调 * @since 2.5.0 */ onVisibleChange?: (visible: boolean) => void; /** * 是否处于激活态 * @since 2.6.0 */ current?: boolean; /** * 是否处于选中态 * @since 2.6.0 */ selected?: boolean; } export const ListSubMenu = React.forwardRef(function ListSubMenu( { label, children, placement = "right", trigger = "hover", className, disabled, visible, current, selected, onVisibleChange = noop, onMouseEnter = noop, onMouseLeave = noop, onClick = noop, ...props }: ListSubMenuProps, ref: React.Ref ) { const { classPrefix } = useConfig(); const liRef = useRef(null); const enterTimerRef = useRef(null); const leaveTimerRef = useRef(null); const [hover, setHover] = useState(false); const { listen } = useOutsideClick(liRef); useEffect(() => { return () => { clearTimeout(enterTimerRef.current); clearTimeout(leaveTimerRef.current); }; }, []); const dropdownStyle = { position: undefined }; const [basePlacement, placementModifier] = placement.split("-"); if (basePlacement === "left") { Object.assign(dropdownStyle, { left: "auto", right: "100%", }); } if (placementModifier === "end") { Object.assign(dropdownStyle, { top: undefined, transform: "translateY(-100%)", marginTop: 6, }); } if (disabled) { return (
  • {label}
  • ); } const triggerProps: React.DetailedHTMLProps< React.LiHTMLAttributes, HTMLLIElement > = isMobile || trigger === "click" ? { onClick: event => { onClick(event); onVisibleChange(true); setHover(true); }, onMouseEnter, onMouseLeave, } : { onClick, onMouseEnter: event => { onMouseEnter(event); clearTimeout(enterTimerRef.current); leaveTimerRef.current = setTimeout(() => { onVisibleChange(true); setHover(true); }, 150); }, onMouseLeave: event => { onMouseLeave(event); clearTimeout(leaveTimerRef.current); enterTimerRef.current = setTimeout(() => { onVisibleChange(false); setHover(false); }, 150); }, }; if (isMobile || trigger === "click") { listen(() => { onVisibleChange(false); setHover(false); }); } return (
  • {label} {children}
  • ); }); ListSubMenu.displayName = "ListSubMenu"; export interface ListProps extends StyledProps { /** * 列表内容。使用 `` 来表示列表项 */ children?: React.ReactNode; /** * 列表类型 * * - 可以不传,表示简单平铺的列表 * - `bullet` 列表项以点号开头 * - `number` 列表项以列表序号开头 * - `option` 列表以菜单的形式渲染 * - `option-group` 列表以分组菜单的形式渲染 */ type?: "bullet" | "number" | "option" | "option-group"; /** * 列表项之间的分割方式 * * - `divide` 表示使用分割线分割 * - `stripe` 表示使用条纹背景色分割 */ split?: "divide" | "stripe"; /** * 列表项滚动至底部的回调 */ onScrollBottom?: (event: React.UIEvent) => void; } export const List = forwardRefWithStatics( function List( { className, style, children, type, split, onScrollBottom = () => null, }: ListProps, ref: React.Ref ) { const { classPrefix } = useConfig(); const [hasSubMenu, setHasSubMenu] = useState(false); useEffect(() => { React.Children.forEach(children, child => { if (isChildOfType(child, ListSubMenu)) { setHasSubMenu(true); } }); }, []); // eslint-disable-line react-hooks/exhaustive-deps function handleBodyScroll(event: React.UIEvent) { const list = event.target as HTMLElement; const { scrollHeight, scrollTop, clientHeight } = list; if (scrollHeight <= Math.ceil(clientHeight + scrollTop + 1)) { onScrollBottom(event); } } const digits = String(React.Children.count(children)).length; const listClassName = classNames({ [`${classPrefix}-list`]: true, [`${classPrefix}-list--bullet`]: type === "bullet", [`${classPrefix}-list--number`]: type === "number", [`${classPrefix}-list--option`]: type === "option" || type === "option-group", [`${classPrefix}-list--group`]: type === "option-group", [`${classPrefix}-list--divider`]: split === "divide", [`${classPrefix}-list--striped`]: split === "stripe", "is-2digits": type === "number" && digits === 2, "is-3digits": type === "number" && digits === 3, "is-4digits": type === "number" && digits > 3, [className]: className, }); const Parent = type === "number" ? "ol" : "ul"; return ( {children} ); }, { GroupLabel: createRocket("GroupLabel", "li.@{prefix}-list__label"), Divider: createRocket("Divider", "li.@{prefix}-list__separate"), StatusTip: createRocket("TipItem", "li.@{prefix}-list__status"), SubMenu: ListSubMenu, Item: ListItem, } ); List.displayName = "List";