Skip to content

Commit bb884ea

Browse files
TianlunXionganlyyaogithub-actions[bot]
authored
feat(action-sheet): 新增 ActionSheet组件 (#471)
* feat(action-sheet): 新增 ActionSheet组件 * fix: 修复comment 问题 * fix: 修复同步develop后的问题 * fix: action-sheet 使用 useDefault * fix: 规范变量命名 * fix(action-sheet): 修复同步develop 后的问题(action-sheet badge 位置) * fix(grid): grid 补充 children 属性 * fix(action-sheet): 规范文档和类型定义 * fix: 优化类型和默认值 * fix: 优化类型和默认值 * fix: actionSheet适配Badge样式 * fix(action-sheet): 优化css结构 * fix: resolve conflicts * fix: fix cr and sync api docs * chore: update snapshot --------- Co-authored-by: anlyyao <[email protected]> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 624a2ad commit bb884ea

25 files changed

+6762
-8
lines changed

site/mobile/mobile.config.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,11 @@ export default {
262262
name: 'result',
263263
component: () => import('tdesign-mobile-react/result/_example/index.tsx'),
264264
},
265+
{
266+
title: 'ActionSheet 动作面板',
267+
name: 'action-sheet',
268+
component: () => import('tdesign-mobile-react/action-sheet/_example/index.tsx'),
269+
},
265270
{
266271
title: 'Link 链接',
267272
name: 'link',
@@ -302,5 +307,10 @@ export default {
302307
name: 'config-provider',
303308
component: () => import('tdesign-mobile-react/config-provider/_example/index.tsx'),
304309
},
310+
{
311+
title: 'ActionSheet 动作面板',
312+
name: 'action-sheet',
313+
component: () => import('tdesign-mobile-react/action-sheet/_example/index.tsx'),
314+
},
305315
],
306316
};

site/web/site.config.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -456,12 +456,12 @@ export const docs = [
456456
titleEn: 'FeedBack',
457457
type: 'component',
458458
children: [
459-
// {
460-
// title: 'ActionSheet 动作面板',
461-
// name: 'action-sheet',
462-
// path: '/mobile-react/components/actionsheet',
463-
// component: () => import('tdesign-mobile-react/action-sheet/action-sheet.md'),
464-
// },
459+
{
460+
title: 'ActionSheet 动作面板',
461+
name: 'action-sheet',
462+
path: '/mobile-react/components/actionsheet',
463+
component: () => import('tdesign-mobile-react/action-sheet/action-sheet.md'),
464+
},
465465
{
466466
title: 'Dialog 对话框',
467467
titleEn: 'Dialog',

src/action-sheet/ActionSheet.tsx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React from 'react';
2+
import cx from 'classnames';
3+
import type { TdActionSheetProps } from './type';
4+
import { Button } from '../button';
5+
import { Popup } from '../popup';
6+
import { StyledProps } from '../common';
7+
import useControlled from '../hooks/useControlled';
8+
import { usePrefixClass } from '../hooks/useClass';
9+
import useDefaultProps from '../hooks/useDefaultProps';
10+
import { actionSheetDefaultProps } from './defaultProps';
11+
import { ActionSheetList } from './ActionSheetList';
12+
import { ActionSheetGrid } from './ActionSheetGrid';
13+
14+
export interface ActionSheetProps extends TdActionSheetProps, StyledProps {}
15+
16+
export const ActionSheet: React.FC<ActionSheetProps> = (props) => {
17+
const {
18+
items,
19+
theme,
20+
align,
21+
showOverlay,
22+
showCancel,
23+
cancelText,
24+
description,
25+
popupProps,
26+
onClose,
27+
onSelected,
28+
onCancel,
29+
count,
30+
} = useDefaultProps<ActionSheetProps>(props, actionSheetDefaultProps);
31+
32+
const actionSheetClass = usePrefixClass('action-sheet');
33+
34+
const [visible, setVisible] = useControlled(props, 'visible', (visible, context) => {
35+
!visible && onClose?.(context);
36+
});
37+
38+
const handleCancel = (ev) => {
39+
onCancel?.(ev);
40+
};
41+
42+
const handleSelected = (idx) => {
43+
const found = items?.[idx];
44+
45+
onSelected?.(found, idx);
46+
47+
setVisible(false, { trigger: 'select' });
48+
};
49+
50+
return (
51+
<Popup
52+
{...popupProps}
53+
visible={visible}
54+
className={actionSheetClass}
55+
placement="bottom"
56+
onVisibleChange={(value) => {
57+
setVisible(value, { trigger: 'overlay' });
58+
}}
59+
showOverlay={showOverlay}
60+
>
61+
<div className={cx(`${actionSheetClass}__content`)}>
62+
{description ? (
63+
<p
64+
className={cx({
65+
[`${actionSheetClass}__description`]: true,
66+
[`${actionSheetClass}__description--left`]: align === 'left',
67+
[`${actionSheetClass}__description--grid`]: theme === 'grid',
68+
})}
69+
>
70+
{description}
71+
</p>
72+
) : null}
73+
{theme === 'list' ? <ActionSheetList items={items} align={align} onSelected={handleSelected} /> : null}
74+
{theme === 'grid' ? (
75+
<ActionSheetGrid items={items} align={align} onSelected={handleSelected} count={count} />
76+
) : null}
77+
{showCancel ? (
78+
<div className={`${actionSheetClass}__footer`}>
79+
<div className={`${actionSheetClass}__gap-${theme}`}></div>
80+
<Button className={`${actionSheetClass}__cancel`} variant="text" block onClick={handleCancel}>
81+
{cancelText || '取消'}
82+
</Button>
83+
</div>
84+
) : null}
85+
</div>
86+
</Popup>
87+
);
88+
};
89+
90+
export default ActionSheet;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React, { useEffect, useMemo, useState } from 'react';
2+
import cx from 'classnames';
3+
4+
import type { ActionSheetProps } from './ActionSheet';
5+
import type { ActionSheetItem } from './type';
6+
7+
import { Grid, GridItem } from '../grid';
8+
import { Swiper, SwiperProps } from '../swiper';
9+
import { usePrefixClass } from '../hooks/useClass';
10+
11+
type ActionSheetGridProps = Pick<ActionSheetProps, 'items' | 'align'> & {
12+
onSelected?: (idx: number) => void;
13+
count?: number;
14+
};
15+
16+
export function ActionSheetGrid(props: ActionSheetGridProps) {
17+
const { items = [], count = 8, onSelected } = props;
18+
19+
const actionSheetClass = usePrefixClass('action-sheet');
20+
21+
const [direction, setDirection] = useState<SwiperProps['direction']>('vertical');
22+
23+
const gridColumn = Math.ceil(count / 2);
24+
const pageNum = Math.ceil(items.length / count);
25+
26+
const actionItems = useMemo(() => {
27+
const res: ActionSheetProps['items'][] = [];
28+
for (let i = 0; i < pageNum; i++) {
29+
const temp = items.slice(i * count, (i + 1) * count);
30+
res.push(temp);
31+
}
32+
return res;
33+
}, [items, count, pageNum]);
34+
35+
useEffect(() => {
36+
setDirection('horizontal');
37+
}, []);
38+
39+
return (
40+
<div
41+
className={cx({
42+
[`${actionSheetClass}__grid`]: true,
43+
[`${actionSheetClass}__grid--swiper`]: pageNum > 1,
44+
[`${actionSheetClass}__dots`]: pageNum > 1,
45+
})}
46+
>
47+
<Swiper
48+
autoplay={false}
49+
className={cx(`${actionSheetClass}__swiper-wrap--base`, pageNum > 1 && `${actionSheetClass}__swiper-wrap`)}
50+
loop={false}
51+
navigation={pageNum > 1 ? { type: 'dots' } : undefined}
52+
direction={direction}
53+
height={pageNum > 1 ? 208 : 196}
54+
>
55+
{actionItems.map((item, idx1) => (
56+
<Swiper.SwiperItem key={idx1}>
57+
<Grid gutter={0} column={gridColumn} style={{ width: '100%' }}>
58+
{item.map((it, idx2) => {
59+
let label: string;
60+
let image: React.ReactNode;
61+
let badge: ActionSheetItem['badge'];
62+
if (typeof it === 'string') {
63+
label = it;
64+
} else {
65+
label = it.label;
66+
image = it.icon;
67+
badge = it.badge;
68+
}
69+
return (
70+
<GridItem
71+
key={`${idx1}-${idx2}`}
72+
image={image}
73+
text={label}
74+
badge={badge}
75+
// @ts-ignore
76+
onClick={() => {
77+
onSelected?.(idx1 * count + idx2);
78+
}}
79+
/>
80+
);
81+
})}
82+
</Grid>
83+
</Swiper.SwiperItem>
84+
))}
85+
</Swiper>
86+
</div>
87+
);
88+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React from 'react';
2+
import cx from 'classnames';
3+
4+
import type { TElement } from '../common';
5+
import type { ActionSheetProps } from './ActionSheet';
6+
import type { ActionSheetItem } from './type';
7+
8+
import { Button } from '../button';
9+
import { Badge } from '../badge';
10+
import { usePrefixClass } from '../hooks/useClass';
11+
12+
type ActionSheetListProps = Pick<ActionSheetProps, 'items' | 'align'> & {
13+
onSelected?: (idx: number) => void;
14+
};
15+
16+
export function ActionSheetList(props: ActionSheetListProps) {
17+
const { items = [], align, onSelected } = props;
18+
19+
const actionSheetClass = usePrefixClass('action-sheet');
20+
21+
return (
22+
<div className={cx(`${actionSheetClass}__list`)}>
23+
{items?.map((item, idx) => {
24+
let label: React.ReactNode;
25+
let disabled: ActionSheetItem['disabled'];
26+
let icon: ActionSheetItem['icon'];
27+
let color: ActionSheetItem['color'];
28+
29+
if (typeof item === 'string') {
30+
label = <span className={cx([`${actionSheetClass}__list-item-text`])}>{item}</span>;
31+
} else {
32+
if (item?.badge) {
33+
label = (
34+
<Badge
35+
count={item?.badge?.count}
36+
max-count={item?.badge?.maxCount}
37+
dot={item?.badge?.dot}
38+
content={item?.badge?.content}
39+
size={item?.badge?.size}
40+
offset={item?.badge?.offset || [-16, 20]}
41+
>
42+
<span className={cx([`${actionSheetClass}__list-item-text`])}>{item?.label}</span>
43+
</Badge>
44+
);
45+
} else {
46+
label = <span className={cx([`${actionSheetClass}__list-item-text`])}>{item?.label}</span>;
47+
}
48+
disabled = item?.disabled;
49+
icon = item?.icon;
50+
color = item?.color;
51+
}
52+
53+
return (
54+
<Button
55+
key={idx}
56+
variant="text"
57+
block
58+
className={cx({
59+
[`${actionSheetClass}__list-item`]: true,
60+
[`${actionSheetClass}__list-item--left`]: align === 'left',
61+
})}
62+
onClick={() => {
63+
onSelected?.(idx);
64+
}}
65+
disabled={disabled}
66+
icon={icon as TElement}
67+
style={{
68+
color,
69+
}}
70+
shape="rectangle"
71+
>
72+
{label}
73+
</Button>
74+
);
75+
})}
76+
</div>
77+
);
78+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react';
2+
import renderToBody from '../_util/renderToBody';
3+
import ActionSheet from './ActionSheet';
4+
import type { ActionSheetProps } from './ActionSheet';
5+
import { actionSheetDefaultProps } from './defaultProps';
6+
7+
let destroyRef: () => void;
8+
9+
export function show(config: Partial<ActionSheetProps>) {
10+
destroyRef?.();
11+
12+
const app = document.createElement('div');
13+
14+
document.body.appendChild(app);
15+
16+
destroyRef = renderToBody(<ActionSheet {...actionSheetDefaultProps} {...config} visible />);
17+
}
18+
19+
export function close() {
20+
destroyRef?.();
21+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React, { useState } from 'react';
2+
import { Button, ActionSheet } from 'tdesign-mobile-react';
3+
4+
export default function ListExample() {
5+
const [alignCenterVisible, setAlignCenterVisible] = useState(false);
6+
const [alignLeftVisible, setAlignLeftVisible] = useState(false);
7+
8+
return (
9+
<div className="action-sheet-demo">
10+
<div className="action-sheet-demo-btns">
11+
<Button block variant="outline" theme="primary" onClick={() => setAlignCenterVisible(true)}>
12+
居中列表型
13+
</Button>
14+
<Button block variant="outline" theme="primary" onClick={() => setAlignLeftVisible(true)}>
15+
左对齐列表型
16+
</Button>
17+
</div>
18+
<ActionSheet
19+
align="center"
20+
visible={alignCenterVisible}
21+
description="动作面板描述文字"
22+
items={['选项一', '选项二', '选项三', '选项四']}
23+
onClose={() => {
24+
setAlignCenterVisible(false);
25+
}}
26+
onCancel={() => {
27+
setAlignCenterVisible(false);
28+
}}
29+
/>
30+
<ActionSheet
31+
align="left"
32+
visible={alignLeftVisible}
33+
description="动作面板描述文字"
34+
items={['选项一', '选项二', '选项三', '选项四']}
35+
onClose={() => {
36+
setAlignLeftVisible(false);
37+
}}
38+
onCancel={() => {
39+
setAlignLeftVisible(false);
40+
}}
41+
/>
42+
</div>
43+
);
44+
}

0 commit comments

Comments
 (0)