diff --git a/site/mobile/mobile.config.js b/site/mobile/mobile.config.js index d5967f20..141a1d93 100644 --- a/site/mobile/mobile.config.js +++ b/site/mobile/mobile.config.js @@ -237,5 +237,10 @@ export default { name: 'table', component: () => import('tdesign-mobile-react/table/_example/index.jsx'), }, + { + title: 'ActionSheet 动作面板', + name: 'action-sheet', + component: () => import('tdesign-mobile-react/action-sheet/_example/index.tsx'), + }, ], }; diff --git a/site/web/site.config.js b/site/web/site.config.js index f709bd4a..e44b1b8f 100644 --- a/site/web/site.config.js +++ b/site/web/site.config.js @@ -304,12 +304,12 @@ export default { title: '消息提醒', type: 'component', // 组件文档 children: [ - // { - // title: 'ActionSheet 动作面板', - // name: 'action-sheet', - // path: '/mobile-react/components/actionsheet', - // component: () => import('tdesign-mobile-react/action-sheet/action-sheet.md'), - // }, + { + title: 'ActionSheet 动作面板', + name: 'action-sheet', + path: '/mobile-react/components/actionsheet', + component: () => import('tdesign-mobile-react/action-sheet/action-sheet.md'), + }, { title: 'BackTop 返回顶部', name: 'back-top', diff --git a/src/_common b/src/_common index 074d44f1..a3c3faca 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit 074d44f1a1b06c4ee97759e3edf6cb3bbef2f61a +Subproject commit a3c3facadafa5df66bf78de35d76858adecb3931 diff --git a/src/action-sheet/ActionSheet.tsx b/src/action-sheet/ActionSheet.tsx new file mode 100644 index 00000000..269694d8 --- /dev/null +++ b/src/action-sheet/ActionSheet.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import cx from 'classnames'; + +import type { TdActionSheetProps } from './type'; + +import { Button } from '../button'; +import { Popup } from '../popup'; +import useConfig from '../_util/useConfig'; +import useDefault from '../_util/useDefault'; +import useDefaultProps from '../hooks/useDefaultProps'; +import { actionSheetDefaultProps } from './defaultProps'; +import { ActionSheetList } from './ActionSheetList'; +import { ActionSheetGrid } from './ActionSheetGrid'; + +export type ActionSheetProps = TdActionSheetProps & { + showOverlay?: boolean; + onVisibleChange?: (value: boolean) => void; + gridHeight?: number; +}; + +export const ActionSheet: React.FC = (props) => { + const { + defaultVisible, + items, + visible: visibleFromProps, + theme, + align, + showOverlay, + showCancel, + cancelText, + description, + onClose, + onSelected, + onCancel, + onVisibleChange, + count, + gridHeight, + } = useDefaultProps(props, actionSheetDefaultProps); + + const { classPrefix } = useConfig(); + + const cls = `${classPrefix}-action-sheet`; + + const [visible, onChange] = useDefault(visibleFromProps, defaultVisible, onVisibleChange); + + const handleCancel = (ev) => { + onCancel?.(ev); + }; + + const handleSelected = (idx) => { + const found = items?.[idx]; + + onSelected?.(found, idx); + + onClose?.('select'); + + onChange(false); + }; + + return ( + { + onChange(value); + + if (!value) onClose?.('overlay'); + }} + showOverlay={showOverlay} + > +
+ {description ? ( +

+ {description} +

+ ) : null} + {theme === 'list' ? : null} + {theme === 'grid' && visible ? ( + + ) : null} + {showCancel ? ( +
+
+ +
+ ) : null} +
+
+ ); +}; + +export default ActionSheet; diff --git a/src/action-sheet/ActionSheetGrid.tsx b/src/action-sheet/ActionSheetGrid.tsx new file mode 100644 index 00000000..36c2400c --- /dev/null +++ b/src/action-sheet/ActionSheetGrid.tsx @@ -0,0 +1,89 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import cx from 'classnames'; + +import type { ActionSheetProps } from './ActionSheet'; +import type { ActionSheetItem } from './type'; + +import { Grid, GridItem } from '../grid'; +import { Swiper, SwiperProps } from '../swiper'; +import useConfig from '../_util/useConfig'; + +type ActionSheetGridProps = Pick & { + onSelected?: (idx: number) => void; + count?: number; + gridHeight?: number; +}; + +export function ActionSheetGrid(props: ActionSheetGridProps) { + const { items = [], count = 8, onSelected, gridHeight } = props; + const { classPrefix } = useConfig(); + const cls = `${classPrefix}-action-sheet`; + + const [direction, setDirection] = useState('vertical'); + + const gridColumn = Math.ceil(count / 2); + const pageNum = Math.ceil(items.length / count); + + const actionItems = useMemo(() => { + const res: ActionSheetProps['items'][] = []; + for (let i = 0; i < pageNum; i++) { + const temp = items.slice(i * count, (i + 1) * count); + res.push(temp); + } + return res; + }, [items, count, pageNum]); + + useEffect(() => { + setDirection('horizontal'); + }, []); + + return ( +
1, + [`${cls}__dots`]: pageNum > 1, + })} + > + 1 && `${cls}__swiper-wrap`)} + loop={false} + navigation={pageNum > 1 ? { type: 'dots' } : undefined} + direction={direction} + height={gridHeight || (pageNum > 1 ? 208 : 196)} + > + {actionItems.map((item, idx1) => ( + + + {item.map((it, idx2) => { + let label: string; + let image: React.ReactNode; + let badge: ActionSheetItem['badge']; + if (typeof it === 'string') { + label = it; + } else { + label = it.label; + image = it.icon; + badge = it.badge; + } + return ( + { + onSelected?.(idx1 * count + idx2); + }} + /> + ); + })} + + + ))} + +
+ ); +} diff --git a/src/action-sheet/ActionSheetList.tsx b/src/action-sheet/ActionSheetList.tsx new file mode 100644 index 00000000..afe3e33f --- /dev/null +++ b/src/action-sheet/ActionSheetList.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import cx from 'classnames'; + +import type { TElement } from 'tdesign-mobile-react/common'; +import type { ActionSheetProps } from './ActionSheet'; +import type { ActionSheetItem } from './type'; + +import { Button } from '../button'; +import { Badge } from '../badge'; +import useConfig from '../_util/useConfig'; + +type ActionSheetListProps = Pick & { + onSelected?: (idx: number) => void; +}; + +export function ActionSheetList(props: ActionSheetListProps) { + const { items = [], align, onSelected } = props; + const { classPrefix } = useConfig(); + const cls = `${classPrefix}-action-sheet`; + + return ( +
+ {items?.map((item, idx) => { + let label: React.ReactNode; + let disabled: ActionSheetItem['disabled']; + let icon: ActionSheetItem['icon']; + let color: ActionSheetItem['color']; + + if (typeof item === 'string') { + label = {item}; + } else { + if (item?.badge) { + label = ( + + {item?.label} + + ); + } else { + label = {item?.label}; + } + disabled = item?.disabled; + icon = item?.icon; + color = item?.color; + } + + return ( + + ); + })} +
+ ); +} diff --git a/src/action-sheet/ActionSheetMethod.tsx b/src/action-sheet/ActionSheetMethod.tsx new file mode 100644 index 00000000..d12d1d8c --- /dev/null +++ b/src/action-sheet/ActionSheetMethod.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import renderToBody from '../_util/renderToBody'; +import ActionSheet from './ActionSheet'; +import type { ActionSheetProps } from './ActionSheet'; +import { actionSheetDefaultProps } from './defaultProps'; + +let destroyRef: () => void; + +export function show(config: Partial) { + destroyRef?.(); + + const app = document.createElement('div'); + + document.body.appendChild(app); + + destroyRef = renderToBody(); +} + +export function close() { + destroyRef?.(); +} diff --git a/src/action-sheet/_example/align.tsx b/src/action-sheet/_example/align.tsx new file mode 100644 index 00000000..ddb5a97a --- /dev/null +++ b/src/action-sheet/_example/align.tsx @@ -0,0 +1,44 @@ +import React, { useState } from 'react'; +import { Button, ActionSheet } from 'tdesign-mobile-react'; + +export default function ListExample() { + const [alignCenterVisible, setAlignCenterVisible] = useState(false); + const [alignLeftVisible, setAlignLeftVisible] = useState(false); + + return ( +
+
+ + +
+ { + setAlignCenterVisible(false); + }} + onCancel={() => { + setAlignCenterVisible(false); + }} + /> + { + setAlignLeftVisible(false); + }} + onCancel={() => { + setAlignLeftVisible(false); + }} + /> +
+ ); +} diff --git a/src/action-sheet/_example/grid-multiple.tsx b/src/action-sheet/_example/grid-multiple.tsx new file mode 100644 index 00000000..7743049c --- /dev/null +++ b/src/action-sheet/_example/grid-multiple.tsx @@ -0,0 +1,73 @@ +import React, { useState } from 'react'; +import { Button, ActionSheet } from 'tdesign-mobile-react'; +import { ShareIcon, StarIcon, DownloadIcon, Edit1Icon, ImageIcon } from 'tdesign-icons-react'; + +export default function GridMultipleExample() { + const [multiPageVisible, setMultiPageVisible] = useState(false); + + return ( +
+
+ +
+ + , + }, + { + label: '刷新', + icon: , + }, + { + label: '下载', + icon: , + }, + { + label: '复制', + icon: , + }, + { + label: '文字', + icon: , + }, + { + label: '文字', + icon: , + }, + ]} + onClose={() => { + setMultiPageVisible(false); + }} + onCancel={() => { + setMultiPageVisible(false); + }} + onSelected={(it) => { + console.log(it); + }} + /> +
+ ); +} diff --git a/src/action-sheet/_example/grid.tsx b/src/action-sheet/_example/grid.tsx new file mode 100644 index 00000000..48953d94 --- /dev/null +++ b/src/action-sheet/_example/grid.tsx @@ -0,0 +1,175 @@ +import React, { useState } from 'react'; +import { Button, ActionSheet } from 'tdesign-mobile-react'; +import { ShareIcon, StarIcon, DownloadIcon, Edit1Icon } from 'tdesign-icons-react'; + +export default function GridExample() { + const [normalVisible, setNormalVisible] = useState(false); + const [descVisible, setDescVisible] = useState(false); + const [badgeVisible, setBadgeVisible] = useState(false); + + return ( +
+
+ + + +
+ + , + }, + { + label: '刷新', + icon: , + }, + { + label: '下载', + icon: , + }, + { + label: '复制', + icon: , + }, + ]} + onClose={() => { + setNormalVisible(false); + }} + onCancel={() => { + setNormalVisible(false); + }} + onSelected={(it) => { + console.log(it); + }} + /> + + , + }, + { + label: '刷新', + icon: , + }, + { + label: '下载', + icon: , + }, + { + label: '复制', + icon: , + }, + ]} + onClose={() => { + setDescVisible(false); + }} + onCancel={() => { + setDescVisible(false); + }} + onSelected={(it) => { + console.log(it); + }} + /> + + , + }, + { + label: '刷新', + icon: , + }, + { + label: '下载', + icon: , + }, + { + label: '复制', + icon: , + }, + ]} + onClose={() => { + setBadgeVisible(false); + }} + onCancel={() => { + setBadgeVisible(false); + }} + onSelected={(it) => { + console.log(it); + }} + /> +
+ ); +} diff --git a/src/action-sheet/_example/index.tsx b/src/action-sheet/_example/index.tsx new file mode 100644 index 00000000..a467ffdc --- /dev/null +++ b/src/action-sheet/_example/index.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import TDemoBlock from '../../../site/mobile/components/DemoBlock'; +import TDemoHeader from '../../../site/mobile/components/DemoHeader'; +import ListExample from './list'; +import GridExample from './grid'; +import GridMultipleExample from './grid-multiple'; +import StatusExample from './status'; +import AlignExample from './align'; + +import './style/index.less'; + +export default function Base() { + return ( +
+ + + + + + + + + + + + + + +
+ ); +} diff --git a/src/action-sheet/_example/list.tsx b/src/action-sheet/_example/list.tsx new file mode 100644 index 00000000..381b7e77 --- /dev/null +++ b/src/action-sheet/_example/list.tsx @@ -0,0 +1,125 @@ +import React, { useState } from 'react'; +import { Button, ActionSheet } from 'tdesign-mobile-react'; +import { AppIcon } from 'tdesign-icons-react'; + +export default function ListExample() { + const [normalVisible, setNormalVisible] = useState(false); + const [descVisible, setDescVisible] = useState(false); + const [iconVisible, setIconVisible] = useState(false); + const [badgeVisible, setBadgeVisible] = useState(false); + + const openByMethod = () => { + ActionSheet.show({ + items: ['选项一', '选项二', '选项三', '选项四'], + onClose() { + ActionSheet.close(); + }, + onCancel() { + ActionSheet.close(); + }, + onSelected() { + ActionSheet.close(); + }, + }); + }; + + return ( +
+
+ + + + + +
+ + { + setNormalVisible(false); + }} + onCancel={() => { + setNormalVisible(false); + }} + /> + { + setDescVisible(false); + }} + onCancel={() => { + setDescVisible(false); + }} + /> + , + }, + { + label: '选项二', + icon: , + }, + { + label: '选项三', + icon: , + }, + { + label: '选项四', + icon: , + }, + ]} + onClose={() => { + setIconVisible(false); + }} + onCancel={() => { + setIconVisible(false); + }} + /> + { + setBadgeVisible(false); + }} + onCancel={() => { + setBadgeVisible(false); + }} + /> +
+ ); +} diff --git a/src/action-sheet/_example/status.tsx b/src/action-sheet/_example/status.tsx new file mode 100644 index 00000000..3858b460 --- /dev/null +++ b/src/action-sheet/_example/status.tsx @@ -0,0 +1,48 @@ +import React, { useState } from 'react'; +import { Button, ActionSheet } from 'tdesign-mobile-react'; +import { AppIcon } from 'tdesign-icons-react'; + +export default function ListExample() { + const [statusVisible, setStatusVisible] = useState(false); + + return ( +
+
+ +
+ , + }, + { + label: '选项二', + icon: , + color: '#0052D9', + }, + { + label: '选项三', + icon: , + disabled: true, + }, + { + label: '选项四', + icon: , + color: '#E34D59', + }, + ]} + onClose={() => { + setStatusVisible(false); + }} + onCancel={() => { + setStatusVisible(false); + }} + /> +
+ ); +} diff --git a/src/action-sheet/_example/style/index.less b/src/action-sheet/_example/style/index.less new file mode 100644 index 00000000..a131328b --- /dev/null +++ b/src/action-sheet/_example/style/index.less @@ -0,0 +1,14 @@ +.action-sheet-demo { + padding: 0 16px; + margin-bottom: 16px; + + &-btns { + .t-button { + margin-top: 16px; + + &:first-child { + margin-top: 0; + } + } + } +} \ No newline at end of file diff --git a/src/action-sheet/action-sheet.en-US.md b/src/action-sheet/action-sheet.en-US.md new file mode 100644 index 00000000..cf27d01a --- /dev/null +++ b/src/action-sheet/action-sheet.en-US.md @@ -0,0 +1,23 @@ +:: BASE_DOC :: + +## API + +### ActionSheet Props + +name | type | default | description | required +-- | -- | -- | -- | -- +className | String | - | className of component | N +style | Object | - | CSS(Cascading Style Sheets),Typescript:`React.CSSProperties` | N +align | String | center | options: center/left | N +cancelText | String | - | \- | N +count | Number | 8 | \- | N +description | String | - | \- | N +items | Array | - | required。Typescript:`Array` `interface ActionSheetItem {label: string; color?: string; disabled?: boolean;icon?: string;suffixIcon?: string; }`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/action-sheet/type.ts) | Y +showCancel | Boolean | true | \- | N +theme | String | list | options: list/grid | N +visible | Boolean | false | required | Y +defaultVisible | Boolean | false | required。uncontrolled property | Y +onCancel | Function | | Typescript:`(context: { e: MouseEvent }) => void`
| N +onClose | Function | | Typescript:`(context: { e: MouseEvent }) => void`
| N +onClose | Function | | Typescript:`(trigger: TriggerSource) => void`
[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/action-sheet/type.ts)。
`type TriggerSource = 'overlay' \| 'command' \| 'select' `
| N +onSelected | Function | | Typescript:`(selected: ActionSheetItem \| string, index: number) => void`
| N diff --git a/src/action-sheet/action-sheet.md b/src/action-sheet/action-sheet.md new file mode 100644 index 00000000..ee9b95f1 --- /dev/null +++ b/src/action-sheet/action-sheet.md @@ -0,0 +1,23 @@ +:: BASE_DOC :: + +## API + +### ActionSheet Props + +名称 | 类型 | 默认值 | 描述 | 必传 +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,TS 类型:`React.CSSProperties` | N +align | String | center | 水平对齐方式。可选项:center/left | N +cancelText | String | - | 设置取消按钮的文本 | N +count | Number | 8 | 设置每页展示菜单的数量,仅当 type=grid 时有效 | N +description | String | - | 动作面板描述文字 | N +items | Array | - | 必需。菜单项。TS 类型:`Array` `interface ActionSheetItem {label: string; color?: string; disabled?: boolean;icon?: string;suffixIcon?: string; }`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/action-sheet/type.ts) | Y +showCancel | Boolean | true | 是否显示取消按钮 | N +theme | String | list | 展示类型,列表和表格形式展示。可选项:list/grid | N +visible | Boolean | false | 必需。显示与隐藏 | Y +defaultVisible | Boolean | false | 必需。显示与隐藏。非受控属性 | Y +onCancel | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
点击取消按钮时触发 | N +onClose | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
关闭时触发 | N +onClose | Function | | TS 类型:`(trigger: TriggerSource) => void`
关闭时触发。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/action-sheet/type.ts)。
`type TriggerSource = 'overlay' \| 'command' \| 'select' `
| N +onSelected | Function | | TS 类型:`(selected: ActionSheetItem \| string, index: number) => void`
选择菜单项时触发 | N diff --git a/src/action-sheet/defaultProps.ts b/src/action-sheet/defaultProps.ts new file mode 100644 index 00000000..c4471c5c --- /dev/null +++ b/src/action-sheet/defaultProps.ts @@ -0,0 +1,16 @@ +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdActionSheetProps } from './type'; + +export const actionSheetDefaultProps: TdActionSheetProps = { + align: 'center', + count: 8, + items: [], + showCancel: true, + theme: 'list', + defaultVisible: false, + visible: false, + cancelText: '取消', +}; diff --git a/src/action-sheet/index.tsx b/src/action-sheet/index.tsx new file mode 100644 index 00000000..df3c704c --- /dev/null +++ b/src/action-sheet/index.tsx @@ -0,0 +1,18 @@ +import './style'; + +import _ActionSheet from './ActionSheet'; +import { show, close } from './ActionSheetMethod'; +import { attachMethodsToComponent } from '../_util/attachMethodsToComponent'; +import type { ActionSheetProps } from './ActionSheet'; + +type ActionSheetWithMethods = React.FC & { + show: typeof show; + close: typeof close; +}; + +export const ActionSheet: ActionSheetWithMethods = attachMethodsToComponent(_ActionSheet, { + show, + close, +}); + +export default ActionSheet; diff --git a/src/action-sheet/style/css.js b/src/action-sheet/style/css.js new file mode 100644 index 00000000..6a9a4b13 --- /dev/null +++ b/src/action-sheet/style/css.js @@ -0,0 +1 @@ +import './index.css'; diff --git a/src/action-sheet/style/index.js b/src/action-sheet/style/index.js new file mode 100644 index 00000000..42dd935b --- /dev/null +++ b/src/action-sheet/style/index.js @@ -0,0 +1,2 @@ +import '../../_common/style/mobile/components/action-sheet/v2/_index.less'; +import './index.less'; diff --git a/src/action-sheet/style/index.less b/src/action-sheet/style/index.less new file mode 100644 index 00000000..5c2528ab --- /dev/null +++ b/src/action-sheet/style/index.less @@ -0,0 +1,16 @@ +.t-action-sheet__content { + button.t-button.t-action-sheet__list-item { + // 按钮无圆角 + border-radius: 0; + } + + .t-action-sheet__swiper-wrap--base { + // swiper分页器 底部居中 + .t-swiper__pagination { + position: absolute; + left: 50%; + bottom: 12px; + transform: translate(-50%); + } + } +} diff --git a/src/action-sheet/type.ts b/src/action-sheet/type.ts new file mode 100644 index 00000000..6b804911 --- /dev/null +++ b/src/action-sheet/type.ts @@ -0,0 +1,79 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import type { BadgeProps } from '../badge'; +import type { TElement } from 'tdesign-mobile-react/common'; +import { MouseEvent } from 'react'; + +export interface TdActionSheetProps { + /** + * 水平对齐方式 + * @default center + */ + align?: 'center' | 'left'; + /** + * 设置取消按钮的文本 + * @default '' + */ + cancelText?: string; + /** + * 设置每页展示菜单的数量,仅当 type=grid 时有效 + * @default 8 + */ + count?: number; + /** + * 动作面板描述文字 + * @default '' + */ + description?: string; + /** + * 菜单项 + * @default [] + */ + items: Array; + /** + * 是否显示取消按钮 + * @default true + */ + showCancel?: boolean; + /** + * 展示类型,列表和表格形式展示 + * @default list + */ + theme?: 'list' | 'grid'; + /** + * 显示与隐藏 + * @default false + */ + visible: boolean; + /** + * 显示与隐藏,非受控属性 + * @default false + */ + defaultVisible?: boolean; + /** + * 点击取消按钮时触发 + */ + onCancel?: (context: { e: MouseEvent }) => void; + /** + * 关闭时触发 + */ + onClose?: (trigger: TriggerSource) => void; + /** + * 选择菜单项时触发 + */ + onSelected?: (selected: ActionSheetItem | string, index: number) => void; +} + +export interface ActionSheetItem { + label: string; + color?: string; + disabled?: boolean; + icon?: string | TElement; + badge?: BadgeProps; +} + +export type TriggerSource = 'overlay' | 'command' | 'select'; diff --git a/src/grid/Grid.tsx b/src/grid/Grid.tsx index 7842ff35..342496f7 100644 --- a/src/grid/Grid.tsx +++ b/src/grid/Grid.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, useMemo } from 'react'; +import React, { forwardRef, useMemo, PropsWithChildren } from 'react'; import cls from 'classnames'; import useConfig from '../_util/useConfig'; import { StyledProps } from '../common'; @@ -10,7 +10,7 @@ import { gridDefaultProps } from './defaultProps'; export interface GridProps extends TdGridProps, StyledProps {} -const Grid = forwardRef((props, ref) => { +const Grid = forwardRef>((props, ref) => { const { children, align, border, column, gutter, theme, className, style } = useDefaultProps(props, gridDefaultProps); const { classPrefix } = useConfig(); const name = `${classPrefix}-grid`; diff --git a/src/index.ts b/src/index.ts index 91b4e09e..617cb70f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -60,6 +60,7 @@ export * from './popup'; export * from './pull-down-refresh'; export * from './toast'; export * from './drawer'; +export * from './action-sheet'; /** * 二期组件