Skip to content

Commit 0f1189e

Browse files
ming680anlyyao
andauthored
feat(dropdownmenu): 新增DropdownMenu组件 (#503)
* feat(dropdownmenu): 新增DropdownMenu组件 * chore: use relative paths * fix(Checkbox): extend the CheckBoxProps interface type * docs(DropdownMenu): sync api docs * fix(DropdownMenu): fixed multiple problem * fix(DropdownMenu): fix demo * fix: fix spelling errors * fix: fix cr --------- Co-authored-by: anlyyao <[email protected]>
1 parent 7cb2a8a commit 0f1189e

24 files changed

+1601
-7
lines changed

site/mobile/mobile.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ export default {
147147
name: 'dialog',
148148
component: () => import('tdesign-mobile-react/dialog/_example/index.tsx'),
149149
},
150+
{
151+
title: 'DropdownMenu 下拉菜单',
152+
name: 'dropdown-menu',
153+
component: () => import('tdesign-mobile-react/dropdown-menu/_example/index.tsx'),
154+
},
150155
{
151156
title: 'Loading 加载中',
152157
name: 'loading',

site/web/site.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,12 @@ export const docs = [
471471
path: '/mobile-react/components/dialog',
472472
component: () => import('tdesign-mobile-react/dialog/dialog.md'),
473473
},
474+
{
475+
title: 'DropdownMenu 下拉菜单',
476+
name: 'dropdown-menu',
477+
path: '/mobile-react/components/dropdown-menu',
478+
component: () => import('tdesign-mobile-react/dropdown-menu/dropdown-menu.md'),
479+
},
474480
// {
475481
// title: 'DropdownMenu 下拉菜单',
476482
// name: 'dropdown-menu',

src/checkbox/Checkbox.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,15 @@ import {
1212
import { TdCheckboxProps } from './type';
1313
import forwardRefWithStatics from '../_util/forwardRefWithStatics';
1414
import CheckboxGroup from './CheckboxGroup';
15+
import { StyledProps } from '../common';
1516
import useConfig from '../hooks/useConfig';
1617
import useDefault from '../_util/useDefault';
1718
import { parseContentTNode } from '../_util/parseTNode';
1819
import { usePrefixClass } from '../hooks/useClass';
1920
import useDefaultProps from '../hooks/useDefaultProps';
2021
import { checkboxDefaultProps } from './defaultProps';
2122

22-
export interface CheckBoxProps extends TdCheckboxProps {
23-
ref: Ref<HTMLLabelElement>;
24-
}
23+
export interface CheckBoxProps extends TdCheckboxProps, StyledProps {}
2524

2625
export interface CheckContextValue {
2726
inject: (props: CheckBoxProps) => CheckBoxProps;
@@ -35,6 +34,7 @@ const Checkbox = forwardRef<HTMLDivElement, CheckBoxProps>((_props, ref) => {
3534
const { classPrefix } = useConfig();
3635
const classPrefixCheckBox = usePrefixClass('checkbox');
3736
const {
37+
className,
3838
placement,
3939
content,
4040
indeterminate,
@@ -167,7 +167,7 @@ const Checkbox = forwardRef<HTMLDivElement, CheckBoxProps>((_props, ref) => {
167167
);
168168

169169
return (
170-
<div ref={ref} className={checkboxClassName} onClick={handleClick}>
170+
<div ref={ref} className={classNames(checkboxClassName, className)} onClick={handleClick}>
171171
{icon && renderIconNode()}
172172
{renderCheckBoxContent()}
173173
{/* 下边框 */}
@@ -181,6 +181,6 @@ const Checkbox = forwardRef<HTMLDivElement, CheckBoxProps>((_props, ref) => {
181181
Checkbox.displayName = 'Checkbox';
182182

183183
export default forwardRefWithStatics(
184-
(props: TdCheckboxProps, ref: Ref<HTMLDivElement>) => <Checkbox ref={ref} {...props} />,
184+
(props: CheckBoxProps, ref: Ref<HTMLDivElement>) => <Checkbox ref={ref} {...props} />,
185185
{ Group: CheckboxGroup },
186186
);

src/checkbox/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import _Checkbox from './Checkbox';
2+
import _CheckboxGroup from './CheckboxGroup';
23

3-
import './style/index.js';
4+
import './style';
45

56
export type { CheckBoxProps } from './Checkbox';
7+
export type { CheckboxGroupProps } from './CheckboxGroup';
8+
69
export * from './type';
710

811
export const Checkbox = _Checkbox;
9-
// export default Checkbox;
12+
export const CheckboxGroup = _CheckboxGroup;
13+
14+
export default Checkbox;

src/dropdown-menu/DropdownItem.tsx

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
import { useClickAway } from 'ahooks';
2+
import cx from 'classnames';
3+
import uniqueId from 'lodash/uniqueId';
4+
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
5+
import { CaretDownSmallIcon, CaretUpSmallIcon } from 'tdesign-icons-react';
6+
import { Button, Checkbox, Popup, RadioGroup } from 'tdesign-mobile-react';
7+
import useDefault from '../_util/useDefault';
8+
import parseTNode from '../_util/parseTNode';
9+
import CheckboxGroup from '../checkbox/CheckboxGroup';
10+
import { StyledProps } from '../common';
11+
import useDefaultProps from '../hooks/useDefaultProps';
12+
import useConfig from '../hooks/useConfig';
13+
import { usePrefixClass } from '../hooks/useClass';
14+
import { dropdownItemDefaultProps } from './defaultProps';
15+
import DropdownMenuContext from './DropdownMenuContext';
16+
import type { TdDropdownItemProps } from './type';
17+
18+
export interface DropdownItemProps extends TdDropdownItemProps, StyledProps {
19+
children?: React.ReactNode;
20+
}
21+
22+
const DropdownItem: React.FC<DropdownItemProps> = (props) => {
23+
const {
24+
className,
25+
children,
26+
style,
27+
disabled,
28+
options: inputOptions,
29+
optionsColumns,
30+
placement,
31+
label,
32+
value,
33+
defaultValue,
34+
onChange,
35+
multiple,
36+
onConfirm,
37+
onReset,
38+
footer,
39+
keys,
40+
} = useDefaultProps<DropdownItemProps>(props, dropdownItemDefaultProps);
41+
const { classPrefix } = useConfig();
42+
const dropdownMenuClass = usePrefixClass('dropdown-menu');
43+
const dropdownItemClass = usePrefixClass('dropdown-item');
44+
45+
const [innerValue, setInnerValue] = useDefault(value, defaultValue, onChange);
46+
const [modalValue, setModalValue] = useState(innerValue);
47+
48+
const options = useMemo(
49+
() =>
50+
inputOptions.map((item) => ({
51+
value: item[keys?.value ?? 'value'],
52+
label: item[keys?.label ?? 'label'],
53+
disabled: item[keys?.disabled ?? 'disabled'],
54+
})),
55+
[keys, inputOptions],
56+
);
57+
58+
const [id] = useState(() => uniqueId());
59+
60+
const { direction, activedId, onChangeActivedId, showOverlay, zIndex, closeOnClickOverlay } =
61+
useContext(DropdownMenuContext);
62+
63+
const labelText = useMemo(
64+
() => label || options.find((item) => item.value === innerValue)?.label || '',
65+
[options, label, innerValue],
66+
);
67+
68+
const isActived = id === activedId;
69+
70+
const menuItemRef = useRef<HTMLDivElement>();
71+
const itemRef = useRef<HTMLDivElement>();
72+
73+
const getDropdownItemStyle = () => {
74+
const ele = menuItemRef.current;
75+
if (!ele) {
76+
return {};
77+
}
78+
79+
const { top, bottom } = ele.getBoundingClientRect();
80+
81+
if (direction === 'up') {
82+
return {
83+
zIndex,
84+
bottom: `calc(100vh - ${top}px)`,
85+
};
86+
}
87+
88+
return {
89+
zIndex,
90+
top: `${bottom}px`,
91+
};
92+
};
93+
94+
useClickAway(() => {
95+
if (!isActived || !closeOnClickOverlay) {
96+
return;
97+
}
98+
onChangeActivedId('');
99+
}, itemRef);
100+
101+
useEffect(() => {
102+
if (isActived) {
103+
setModalValue(innerValue);
104+
}
105+
}, [isActived, innerValue]);
106+
107+
const attach = useCallback(() => itemRef.current || document.body, []);
108+
109+
return (
110+
<>
111+
<div
112+
ref={menuItemRef}
113+
className={cx(`${dropdownMenuClass}__item`, {
114+
[`${dropdownMenuClass}__item--active`]: isActived,
115+
[`${dropdownMenuClass}__item--disabled`]: disabled,
116+
})}
117+
onClick={(e) => {
118+
if (disabled) {
119+
return;
120+
}
121+
onChangeActivedId(isActived ? '' : id);
122+
if (!isActived) {
123+
e.stopPropagation();
124+
}
125+
}}
126+
>
127+
<div className={`${dropdownMenuClass}__title`}>{labelText}</div>
128+
{direction === 'down' ? (
129+
<CaretDownSmallIcon
130+
className={cx(`${dropdownMenuClass}__icon`, {
131+
[`${dropdownMenuClass}__icon--active`]: isActived,
132+
})}
133+
/>
134+
) : (
135+
<CaretUpSmallIcon
136+
className={cx(`${dropdownMenuClass}__icon`, {
137+
[`${dropdownMenuClass}__icon--active`]: isActived,
138+
})}
139+
/>
140+
)}
141+
</div>
142+
{isActived ? (
143+
<div
144+
key={id}
145+
className={cx(dropdownItemClass, className)}
146+
style={{
147+
...style,
148+
...getDropdownItemStyle(),
149+
}}
150+
ref={itemRef}
151+
>
152+
<Popup
153+
attach={attach}
154+
visible={isActived}
155+
placement={direction === 'up' ? 'bottom' : 'top'}
156+
closeOnOverlayClick={closeOnClickOverlay}
157+
showOverlay={showOverlay}
158+
zIndex={zIndex}
159+
style={{
160+
position: 'absolute',
161+
overflow: 'hidden',
162+
}}
163+
overlayProps={{
164+
style: {
165+
position: 'absolute',
166+
},
167+
}}
168+
onVisibleChange={(visible) => {
169+
if (!visible) {
170+
onChangeActivedId('');
171+
}
172+
}}
173+
>
174+
<div className={cx(`${dropdownItemClass}__content`, `${classPrefix}-popup__content`)}>
175+
<div className={cx(`${dropdownItemClass}__body`)}>
176+
{parseTNode(children) || (
177+
<>
178+
{multiple ? (
179+
<CheckboxGroup
180+
value={modalValue as (string | number)[]}
181+
onChange={(value) => {
182+
setModalValue(value as (string | number)[]);
183+
}}
184+
className={`${dropdownItemClass}__checkbox-group`}
185+
style={{
186+
gridTemplateColumns: `repeat(${optionsColumns}, 1fr)`,
187+
}}
188+
>
189+
{options.map((item, index) => (
190+
<Checkbox
191+
key={`${item.value}-${index}`}
192+
className={`${dropdownItemClass}__checkbox-item t-checkbox--tag`}
193+
icon={false}
194+
borderless
195+
value={item.value as string | number}
196+
label={item.label}
197+
disabled={item.disabled}
198+
/>
199+
))}
200+
</CheckboxGroup>
201+
) : (
202+
<RadioGroup
203+
className={`${dropdownItemClass}__radio-group`}
204+
icon="line"
205+
options={options}
206+
placement={placement}
207+
value={modalValue as string | number}
208+
onChange={(value: string | number) => {
209+
setModalValue(value);
210+
setInnerValue(value);
211+
onChangeActivedId('');
212+
}}
213+
/>
214+
)}
215+
</>
216+
)}
217+
</div>
218+
{parseTNode(footer) ||
219+
(multiple && (
220+
<div className={`${dropdownItemClass}__footer`}>
221+
<Button
222+
disabled={Array.isArray(modalValue) && modalValue.length === 0}
223+
theme="light"
224+
className={`${dropdownItemClass}__footer-btn ${dropdownItemClass}__reset-btn`}
225+
onClick={() => {
226+
if (typeof onReset === 'function') {
227+
onReset(modalValue);
228+
} else {
229+
setModalValue(innerValue);
230+
}
231+
}}
232+
>
233+
重置
234+
</Button>
235+
<Button
236+
disabled={Array.isArray(modalValue) && modalValue.length === 0}
237+
theme="primary"
238+
className={`${dropdownItemClass}__footer-btn ${dropdownItemClass}__confirm-btn`}
239+
onClick={() => {
240+
if (typeof onConfirm === 'function') {
241+
onConfirm(modalValue);
242+
} else {
243+
setInnerValue(modalValue);
244+
}
245+
onChangeActivedId('');
246+
}}
247+
>
248+
确定
249+
</Button>
250+
</div>
251+
))}
252+
</div>
253+
</Popup>
254+
</div>
255+
) : null}
256+
</>
257+
);
258+
};
259+
260+
DropdownItem.displayName = 'DropdownItem';
261+
262+
export default DropdownItem;

0 commit comments

Comments
 (0)