Skip to content
Closed
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 @@ -107,6 +107,11 @@ export default {
name: 'sticky',
component: () => import('tdesign-mobile-react/sticky/_example/index.jsx'),
},
{
title: 'ActionSheet 动作面板',
name: 'action-sheet',
component: () => import('tdesign-mobile-react/action-sheet/_example/mobile.jsx'),
},
{
title: 'BackTop 返回顶部',
name: 'back-top',
Expand Down
12 changes: 6 additions & 6 deletions site/web/site.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,12 +281,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',
Expand Down
82 changes: 82 additions & 0 deletions src/action-sheet/ActionSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, {useState, useEffect, MouseEvent} from 'react';
import useConfig from "tdesign-mobile-react/_util/useConfig";
import {Popup} from "tdesign-mobile-react";
import classNames from "classnames";
import {TdActionSheetProps} from "./type";
import MenuList from "./MenuList";
import MenuGrid from "./MenuGrid";

export interface ActionSheetProps extends TdActionSheetProps {
type?: string;
}

const ActionSheet: React.FC<ActionSheetProps> = (props) => {
const {
visible,
items,
type,
count,
showCancel = true,
cancelText = '取消',
onSelected,
onCancel,
onClose,
} = props;
const [show, setShow] = useState(visible);

const {classPrefix} = useConfig();
const name = `${classPrefix}-action-sheet`;

const rootClasses = classNames(name, {
[`${name}__panel`]: true,
[`${name}__panel-list`]: type === 'list',
[`${name}__panel-grid`]: type === 'grid',
});
const handleCancel = (e: MouseEvent<HTMLButtonElement>) => {
setShow(false);
onCancel?.({e});
onClose?.({e});
};
const handleOverlayClick = (e: MouseEvent<HTMLDivElement>) => {
setShow(false);
onClose?.({e});
};

const handleSelected = (index: number) => {
onSelected?.(items[index], index,);
};
useEffect(() => {
setShow(visible);
}, [visible]);

return (
<Popup
className={name}
visible={show}
placement='bottom'
overlayProps={{onOverlayClick: handleOverlayClick}}
>
<div className={`${rootClasses}`}>
{
type === 'list' ?
<>
<MenuList items={items} onSelected={handleSelected}></MenuList>
</> :
<MenuGrid items={items} count={count} onSelected={handleSelected}></MenuGrid>
}
{
showCancel && <>
{
type === 'list' && <div className={`${name}__separation`}></div>
}
<button className={`${name}__action`} onClick={handleCancel}>{cancelText}</button>
</>
}
</div>
</Popup>
);
}

ActionSheet.displayName = 'ActionSheet';

export default ActionSheet;
128 changes: 128 additions & 0 deletions src/action-sheet/MenuGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import React, {CSSProperties, TouchEvent, useEffect, useState, useRef} from 'react';
import useConfig from "tdesign-mobile-react/_util/useConfig";
import {Grid, GridItem} from "tdesign-mobile-react";
import classNames from "classnames";
import {ActionSheetItem} from "./type";

export interface MenuGridProps{
items: Array<string | ActionSheetItem>,
onSelected?: (index: number) => void,
count: number,
}

let startX = 0;
let startOffset = 0;
let canMove = true;

const MenuGrid: React.FC<MenuGridProps> = (props) => {
const {
items,
onSelected,
count
} = props;
const [offset, setOffset] = useState(0);
const [transition, setTransition] = useState(true);
const [pageNum, setPageNum] = useState(1);
const [currentIndex, setCurrentIndex] = useState(0);
const [actionItems, setactionItems] = useState<Array<Array<ActionSheetItem>>>([])
const {classPrefix} = useConfig();
const name = `${classPrefix}-action-sheet`;
const containerWrapper = useRef(null);

useEffect(() => {
const num = Math.ceil(items && items.length / count);
setPageNum(num);
const res: any = [];
for (let i = 0; i < num; i++) {
const temp = items.slice(i * count, (i + 1) * count);
res.push(temp);
}
setactionItems(res);
}, [items, count])

const wrapperStyle: CSSProperties = {
transform: `translate3d(${offset}px, 0, 0)`,
transition: transition ? 'transform 300ms' : 'all',
};
const gridColumn = Math.ceil(count / 2);
const moveByIndex = (index: number) => {
setTransition(true);
if (containerWrapper) {
const moveOffset = pageNum > 1 ? index * containerWrapper.current.offsetWidth * -1 : 0;
setOffset(moveOffset);
}
};
// 滑动时的最大偏移
const getMaxOffset = () => {
if (!containerWrapper) return 0;

return (pageNum - 1) * containerWrapper.current.offsetWidth;
};
const handleTouchStart = (e: TouchEvent<HTMLDivElement>) => {
canMove = true;
setTransition(false);
startX = e.touches[0].clientX;
startOffset = startX - offset;
};
const handleTouchMove = (e: TouchEvent<HTMLDivElement>) => {
const {clientX} = e.touches[0];
const minOffset = 0;
const maxOffset = getMaxOffset();

if (Math.abs(clientX - startX) < 15) return;
let moveOffset = clientX - startOffset;

// 滑动临界值判单
if (moveOffset > minOffset) {
moveOffset = minOffset;
canMove = false;
}
if (Math.abs(moveOffset) >= maxOffset) {
moveOffset = maxOffset * -1;
canMove = false;
}
setOffset(moveOffset);
};
const handleTouchEnd = (e: TouchEvent<HTMLDivElement>) => {
if (!canMove) return;

const distance = e.changedTouches[0].clientX - startX;
const targetIndex = Math.abs(distance) > 50 ? currentIndex + (distance < 0 ? 1 : -1) : currentIndex;
setCurrentIndex(targetIndex);
moveByIndex(targetIndex);
};
const handleSelected = (index: number) => {
onSelected?.(index);
};
return (
<div ref={containerWrapper} className={`${name}__menu-wrapper`}>
<div
className={`${name}__menu-slider`}
style={wrapperStyle}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{actionItems.map((items, i) => <div key={i} className={`${name}__menu`}>
<Grid column={gridColumn}>
{items.map((item:ActionSheetItem, index) =>
// @ts-ignore
<GridItem text={item.label} key={index} onClick={() => handleSelected(i * count + index)} image={item.icon}></GridItem>
)}
</Grid>
</div>)}

</div>
{pageNum > 1 && <div className={`${name}__indicator`}>
{Array(pageNum).map((e, index) => <div key={index} className={classNames({
[`${name}__indicator-item`]: true,
on: currentIndex === index - 1
})}></div>)}
</div>}
</div>
);
}

MenuGrid.displayName = 'MenuGrid';

export default MenuGrid;
39 changes: 39 additions & 0 deletions src/action-sheet/MenuList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import useConfig from "tdesign-mobile-react/_util/useConfig";
import {ActionSheetItem} from "./type";

export interface MenuListProps {
items: Array<string | ActionSheetItem>,
onSelected?: (index: number) => void;
}

const MenuList: React.FC<MenuListProps> = (props) => {
const {
items,
onSelected
} = props;

const {classPrefix} = useConfig();
const name = `${classPrefix}-action-sheet`;

return (
<div className={`${name}__menu`}>
{items.map((item: ActionSheetItem, index) =>
<button key={index}
className={`${name}__cell`}
disabled={item.disabled}
onClick={() => onSelected(index)}>
{/* {item.icon && <span className={`${name}__cell-icon`} style={{color: item.color}}>{item.icon}</span>} */}
<div className={`${name}__cell-text`} style={{color: item.color}}>
{item.label}
</div>
</button>
)}

</div>
);
}

MenuList.displayName = 'ActionSheet';

export default MenuList;
19 changes: 19 additions & 0 deletions src/action-sheet/__tests__/demo.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import { testExamples, render } from '@test/utils';
// import ActionSheet from '../ActionSheet';
import {ActionSheet} from 'tdesign-mobile-react';

// 测试组件代码 Example 快照
testExamples(__dirname);

describe('ActionSheet 列表测试', () => {
const items = [
{label: '默认按钮'},
{label: '自定义按钮', color: '#0052D9'},
];
const open = true;
test('content', async () => {
const as = render(<ActionSheet visible={open} type='list' items={items}/>);
expect(as).toMatchSnapshot();
});
});
73 changes: 73 additions & 0 deletions src/action-sheet/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react';
import {render, fireEvent, screen, } from '@test/utils';
import ActionSheet from '../ActionSheet';

describe('ActionSheet 组件测试', () => {

describe('props', () => {
it('type', async () =>{
const {container} = render(<ActionSheet visible={true} type='list' items={[]} />);
expect(container.getElementsByClassName('t-action-sheet__panel-list').length).toBe(1)
const {container:recontainer} = render(<ActionSheet visible={true} type='grid' items={[]} />);
expect(recontainer.getElementsByClassName('t-action-sheet__panel-grid').length).toBe(1)
})

it('cancelText', async () =>{
const {container} = render(<ActionSheet visible={true} type='list' items={[]} />);
expect(container.getElementsByClassName('t-action-sheet__action')[0].textContent).toBe('取消')
const {container:recontainer} = render(<ActionSheet visible={true} type='list' items={[]} cancelText={'Cancel'}/>);
expect(recontainer.getElementsByClassName('t-action-sheet__action')[0].textContent).toBe('Cancel')
})

it('showCancel', async () => {
const {container} = render(<ActionSheet visible={true} type='list' items={[]} />);
expect(container.getElementsByClassName('t-action-sheet__action').length).toBe(1)
const {container:recontainer} = render(<ActionSheet visible={true} type='list' items={[]} showCancel={false}/>);
expect(recontainer.getElementsByClassName('t-action-sheet__action').length).toBe(0)
})

it('items, count', async () =>{
const items = [
{label: '默认按钮'},
{label: '自定义按钮', color: '#0052D9'},
{label: '失效按钮', disabled: true},
{label: '告警按钮', color: '#E34D59'},
];
const {container} = render(<ActionSheet visible={true} type='grid' items={items} count={2} />);
expect(container.getElementsByClassName('t-action-sheet__menu').length).toBe(2)
const {container:recontainer} = render(<ActionSheet visible={true} type='grid' items={items} count={4} />);
expect(recontainer.getElementsByClassName('t-action-sheet__menu').length).toBe(1)
})

it('visible', async () => {
const {container} = render(<ActionSheet visible={true} type='list' items={[]} />);
expect(container.getElementsByClassName('t-action-sheet__panel').length).toBe(1)
const {container:recontainer} = render(<ActionSheet visible={false} type='list' items={[]}/>);
expect(recontainer.getElementsByClassName('t-action-sheet__panel').length).toBe(0)
})
})

describe('events', () => {
it('onCancel', async () => {
const onCancel = jest.fn();
render(<ActionSheet visible={true} type='list' items={[]} onCancel={onCancel} />);
fireEvent.click(screen.getByText('取消'))
expect(onCancel).toHaveBeenCalled()
})

it('onClose', async () => {
const onClose = jest.fn();
render(<ActionSheet visible={true} type='list' items={[]} onClose={onClose} />);
fireEvent.click(screen.getByText('取消'))
expect(onClose).toHaveBeenCalled()
})

it('onSelected', async () => {
const items = [{label: '默认按钮'}];
const onSelected = jest.fn();
render(<ActionSheet visible={true} type='list' items={items} onSelected={onSelected} />);
fireEvent.click(screen.getByText('默认按钮'))
expect(onSelected).toHaveBeenCalled()
})
})
});
Loading