Skip to content

Commit

Permalink
feat(dropdownmenu): 新增DropdownMenu组件
Browse files Browse the repository at this point in the history
  • Loading branch information
ming680 committed Aug 28, 2024
1 parent 96dcd6a commit f03c248
Show file tree
Hide file tree
Showing 20 changed files with 990 additions and 0 deletions.
5 changes: 5 additions & 0 deletions site/mobile/mobile.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ export default {
name: 'dialog',
component: () => import('tdesign-mobile-react/dialog/_example/index.jsx'),
},
{
title: 'DropdownMenu 下拉菜单',
name: 'dropdown-menu',
component: () => import('tdesign-mobile-react/dropdown-menu/_example/index.tsx'),
},
{
title: 'Loading 加载中',
name: 'loading',
Expand Down
6 changes: 6 additions & 0 deletions site/web/site.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,12 @@ export default {
path: '/mobile-react/components/dialog',
component: () => import('tdesign-mobile-react/dialog/dialog.md'),
},
{
title: 'DropdownMenu 下拉菜单',
name: 'dropdown-menu',
path: '/mobile-react/components/dropdown-menu',
component: () => import('tdesign-mobile-react/dropdown-menu/dropdown-menu.md'),
},
{
title: 'Drawer 抽屉',
name: 'drawer',
Expand Down
273 changes: 273 additions & 0 deletions src/dropdown-menu/DropdownItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
import { useClickAway } from 'ahooks';
import cx from 'classnames';
import uniqueId from 'lodash/uniqueId';
import React, { FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { CaretDownSmallIcon, CaretUpSmallIcon } from 'tdesign-icons-react';
import { Button, Checkbox, Popup, Radio, RadioGroup } from 'tdesign-mobile-react';
import useDefault from 'tdesign-mobile-react/_util/useDefault';
import CheckboxGroup from 'tdesign-mobile-react/checkbox/CheckboxGroup';
import { StyledProps } from 'tdesign-mobile-react/common';
import useDefaultProps from 'tdesign-mobile-react/hooks/useDefaultProps';
import useConfig from '../_util/useConfig';
import { dropdownItemDefaultProps } from './defaultProps';
import DropdownMenuContext from './DropdownMenuContext';
import type { TdDropdownItemProps } from './type';

export interface DropdownItemProps extends TdDropdownItemProps, StyledProps {}

const DropdownItem: FC<DropdownItemProps> = (props) => {
const {
className,
style,
disabled,
options: inputOptions,
optionsColumns,
label,
value,
defaultValue,
onChange,
multiple,
onConfirm,
onReset,
footer,
keys,
} = useDefaultProps<DropdownItemProps>(props, dropdownItemDefaultProps);
const { classPrefix } = useConfig();
const itemClass = `${classPrefix}-dropdown-item`;
const [innerValue, setInnerValue] = useDefault(value, defaultValue, onChange);
const [modalValue, setModalValue] = useState(innerValue);

const options = useMemo(
() =>
inputOptions.map((item) => ({
value: item[keys?.value ?? 'value'],
label: item[keys?.label ?? 'label'],
disabled: item[keys?.disabled ?? 'disabled'],
})),
[keys, inputOptions],
);

const [id] = useState(uniqueId());

const { direction, activedId, onChangeActivedId, showOverlay, zIndex, closeOnClickOverlay } =
useContext(DropdownMenuContext);

const labelText = useMemo(
() => label || options.find((item) => item.value === innerValue)?.label || '',
[options, label, innerValue],
);

const isActived = id === activedId;

const menuItemRef = useRef<HTMLDivElement>();
const itemRef = useRef<HTMLDivElement>();

const getDropdownItemStyle = () => {
const ele = menuItemRef.current;
if (!ele) {
return {};
}

const { top, bottom } = ele.getBoundingClientRect();

if (direction === 'up') {
return {
transform: 'rotateX(180deg) rotateY(180deg)',
zIndex,
bottom: `calc(100vh - ${top}px)`,
};
}

return {
zIndex,
top: `${bottom}px`,
};
};

useClickAway(() => {
if (!isActived || !closeOnClickOverlay) {
return;
}
onChangeActivedId('');
}, itemRef);

useEffect(() => {
if (isActived) {
setModalValue(innerValue);
}
}, [isActived, innerValue]);

const attach = useCallback(() => itemRef.current || document.body, []);

return (
<>
<div
ref={menuItemRef}
className={cx(`${classPrefix}-dropdown-menu__item`, {
[`${classPrefix}-dropdown-menu__item--active`]: isActived,
[`${classPrefix}-dropdown-menu__item--disabled`]: disabled,
})}
onClick={(e) => {
if (disabled) {
return;
}
onChangeActivedId(isActived ? '' : id);
if (!isActived) {
e.stopPropagation();
}
}}
>
<div className={`${classPrefix}-dropdown-menu__title`}>{labelText}</div>
{direction === 'down' ? (
<CaretDownSmallIcon
className={cx(`${classPrefix}-dropdown-menu__icon`, {
[`${classPrefix}-dropdown-menu__icon--active`]: isActived,
})}
/>
) : (
<CaretUpSmallIcon
className={cx(`${classPrefix}-dropdown-menu__icon`, {
[`${classPrefix}-dropdown-menu__icon--active`]: isActived,
})}
/>
)}
</div>
{isActived ? (
<div
key={id}
className={cx(itemClass, className)}
style={{
...style,
...getDropdownItemStyle(),
}}
ref={itemRef}
>
{/* TODO Popup 暂不支持 duration */}
<Popup
attach={attach}
visible={isActived}
onVisibleChange={(visible) => {
if (!visible) {
onChangeActivedId('');
}
}}
closeOnOverlayClick={closeOnClickOverlay}
showOverlay={showOverlay}
zIndex={zIndex}
style={{
position: 'absolute',
borderRadius: 0,
}}
overlayProps={{
style: {
position: 'absolute',
},
}}
className={`${itemClass}__popup-host`}
>
<div className={cx(`${itemClass}__content`, `${classPrefix}-popup__content`)}>
<div
className={cx(`${itemClass}__body`)}
style={
direction === 'up'
? {
transform: 'rotateX(180deg) rotateY(180deg)',
}
: {}
}
>
{props.children ? (
props.children
) : (
<>
{/* TODO checkbox 组件未升级 样式不对 */}
{multiple ? (
<CheckboxGroup
value={modalValue as (string | number)[]}
onChange={(value) => {
setModalValue(value);
}}
// className={`itemClass__checkbox-group`}
style={{
gridTemplateColumns: `repeat(${optionsColumns}, 1fr)`,
}}
>
{options.map((item, index) => (
<Checkbox
key={index}
value={item.value as string | number}
label={item.label}
disabled={item.disabled}
/>
))}
</CheckboxGroup>
) : (
<RadioGroup
value={modalValue as string | number}
onChange={(value: string | number) => {
setModalValue(value);
setInnerValue(value);
onChangeActivedId('');
}}
>
{/* TODO radio 暂不支持 icon line */}
{options.map((item, index) => (
<Radio
key={index}
value={item.value as string | number}
label={item.label}
disabled={item.disabled}
/>
))}
</RadioGroup>
)}
</>
)}
</div>

{footer ? footer : null}

{multiple && !footer ? (
<div className={`${itemClass}__footer`}>
<Button
disabled={Array.isArray(modalValue) && modalValue.length === 0}
theme="light"
className={`${itemClass}__footer-btn ${itemClass}__reset-btn`}
onClick={() => {
if (typeof onReset === 'function') {
onReset(modalValue);
} else {
setModalValue(innerValue);
}
}}
>
重置
</Button>
<Button
disabled={Array.isArray(modalValue) && modalValue.length === 0}
theme="primary"
className={`${itemClass}__footer-btn ${itemClass}__confirm-btn`}
onClick={() => {
if (typeof onConfirm === 'function') {
onConfirm(modalValue);
} else {
setInnerValue(modalValue);
}
onChangeActivedId('');
}}
>
确定
</Button>
</div>
) : null}
</div>
</Popup>
</div>
) : null}
</>
);
};

DropdownItem.displayName = 'DropdownItem';

export default DropdownItem;
66 changes: 66 additions & 0 deletions src/dropdown-menu/DropdownMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import cx from 'classnames';
import React, { ComponentProps, FC, forwardRef, useImperativeHandle, useState } from 'react';
import { StyledProps } from 'tdesign-mobile-react/common';
import useDefaultProps from 'tdesign-mobile-react/hooks/useDefaultProps';
import useConfig from '../_util/useConfig';
import { dropdownMenuDefaultProps } from './defaultProps';
import DropdownItem from './DropdownItem';
import DropdownMenuContext from './DropdownMenuContext';
import type { TdDropdownMenuProps } from './type';

export interface DropdownMenuProps extends TdDropdownMenuProps, StyledProps {}

type DropdownMenuRef = {
collapseMenu: () => void;
};

const DropdownMenu: FC<DropdownMenuProps & { ref?: React.ForwardedRef<DropdownMenuRef> }> = forwardRef<
DropdownMenuRef,
DropdownMenuProps
>((props, ref) => {
const { className, style, direction, zIndex, closeOnClickOverlay, showOverlay, duration } =
useDefaultProps<DropdownMenuProps>(props, dropdownMenuDefaultProps);

const { classPrefix } = useConfig();
const name = `${classPrefix}-dropdown-menu`;

const items = [];
React.Children.forEach(props.children, (child: typeof DropdownItem) => {
if (
React.isValidElement<ComponentProps<typeof DropdownItem>>(child) &&
(child.type as any)?.displayName === DropdownItem.displayName
) {
items.push(child);
}
});

const [activedId, setActivedId] = useState('');

useImperativeHandle(ref, () => ({
collapseMenu: () => {
setActivedId('');
},
}));

return (
<DropdownMenuContext.Provider
value={{
direction,
zIndex,
closeOnClickOverlay,
showOverlay,
duration,
activedId,
onChangeActivedId: setActivedId,
}}
>
<div className={cx(name, className)} style={style}>
{items}
</div>
</DropdownMenuContext.Provider>
);
});

DropdownMenu.displayName = 'DropdownMenu';

export default DropdownMenu;
11 changes: 11 additions & 0 deletions src/dropdown-menu/DropdownMenuContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import noop from 'lodash/noop';
import React from 'react';
import { dropdownMenuDefaultProps } from './defaultProps';

const DropdownMenuContext = React.createContext({
...dropdownMenuDefaultProps,
activedId: '',
onChangeActivedId: noop,
});

export default DropdownMenuContext;
Loading

0 comments on commit f03c248

Please sign in to comment.