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

feat(dropdownmenu): 新增DropdownMenu组件 #503

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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';
Copy link
Collaborator

Choose a reason for hiding this comment

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

组件内部使用相对路径

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`;
Copy link
Collaborator

Choose a reason for hiding this comment

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

改用 usePrefixClass()

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());
Copy link
Collaborator

@HaixingOoO HaixingOoO Sep 20, 2024

Choose a reason for hiding this comment

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

用ref

const idRef = useRef(null)
if(!idRef.current){
idRef.current = 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 ? (
Copy link
Collaborator

Choose a reason for hiding this comment

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

直接使用children就好,props.这个可以去掉,统一使用解构的值

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}
Copy link
Collaborator

Choose a reason for hiding this comment

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

TNode类型的使用parseTNode来渲染


{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';
Copy link
Collaborator

Choose a reason for hiding this comment

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

import type { FC } from "react"
类型建议这样引入

import { StyledProps } from 'tdesign-mobile-react/common';
Copy link
Collaborator

Choose a reason for hiding this comment

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

组件内部使用相对路径

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`;
Copy link
Collaborator

Choose a reason for hiding this comment

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

usePrefixClass


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
Loading