Skip to content

Commit

Permalink
feat: refactor dropdown(ZhongAnTech#986)
Browse files Browse the repository at this point in the history
  • Loading branch information
JaysonZou committed Jan 16, 2023
1 parent 9c62812 commit cc8e597
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 87 deletions.
2 changes: 1 addition & 1 deletion packages/site/site.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ module.exports = {
general: [
{
key: 'dropdown',
name: 'xiala',
name: '下拉菜单',
module: () => import('zarm/dropdown/demo.md'),
source: 'zarm/dropdown/demo.md',
style: false,
Expand Down
23 changes: 18 additions & 5 deletions packages/zarm/src/dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ import {Popup} from "../index";

export type DropdownProps = React.PropsWithChildren<BaseDropdownProps & HTMLProps>;

export interface DropdownRef {
close: () => void
}

interface CompoundedComponent
extends React.ForwardRefExoticComponent<DropdownProps & React.RefAttributes<HTMLDivElement>> {
extends React.ForwardRefExoticComponent<DropdownProps & React.RefAttributes<DropdownRef>> {
Item: typeof DropdownItem;
}

const Dropdown = React.forwardRef<HTMLDivElement, DropdownProps>((props, ref) => {
const Dropdown = React.forwardRef<DropdownRef, DropdownProps>((props, ref) => {
const containerRef = React.createRef<HTMLDivElement>();
const popupRef = React.useRef();

Expand All @@ -26,6 +30,17 @@ const Dropdown = React.forwardRef<HTMLDivElement, DropdownProps>((props, ref) =>
const navRef = useRef<HTMLDivElement>(null)
const contentRef = useRef<HTMLDivElement>(null)


React.useImperativeHandle(
ref,
() => ({
close: () => {
setSelectedKey(null)
},
}),
[setSelectedKey]
)

// 计算 navs 的 top 值
const [top, setTop] = React.useState<number>()
useEffect(() => {
Expand Down Expand Up @@ -79,11 +94,9 @@ const Dropdown = React.forwardRef<HTMLDivElement, DropdownProps>((props, ref) =>
style={{ top }}
maskStyle={{ top }}
direction="top"
className={bem('popup')}
forceRender
ref={popupRef}
>
<div ref={contentRef}>
<div ref={contentRef} className={bem('content-wrapper')}>
{items.map(item => {
const isActive = item.props.itemKey === selectedKey
return (
Expand Down
15 changes: 6 additions & 9 deletions packages/zarm/src/dropdown/DropdownItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as React from 'react';
import { createBEM } from '@zarm-design/bem';
import {ArrowDown} from "@zarm-design/icons";
import classNames from "classnames";
import { ConfigContext } from '../config-provider';
import type { BaseDropdownItemProps } from './interface';

Expand All @@ -12,10 +11,8 @@ export interface DropdownItemProps extends BaseDropdownItemProps {
const DropdownItem = React.forwardRef<HTMLLIElement, DropdownItemProps>((props, ref) => {
const dropdownItemRef = (ref as any) || React.createRef<HTMLDivElement>();
const {
children,
active,
onClick,
...restProps
} = props;
const { prefixCls } = React.useContext(ConfigContext);
const bem = createBEM('dropdown', { prefixCls });
Expand All @@ -28,14 +25,14 @@ const DropdownItem = React.forwardRef<HTMLLIElement, DropdownItemProps>((props,

return (
<div className={cls} onClick={onClick} ref={dropdownItemRef}>
<div className={`${cls}-title`}>
<span className={`${cls}-title-text`}>{props.title}</span>
<div className={bem('title')}>
<span className={bem('title-text')}>{props.title}</span>
<span
className={classNames(`${cls}-title-arrow`, {
[`${cls}-title-arrow-active`]: props.active,
})}
className={bem('arrow', [{
active: props.active
}])}
>
{props.arrow === undefined ? <ArrowDown /> : props.arrow}
{props.arrow === undefined ? <ArrowDown size='sm' /> : props.arrow}
</span>
</div>
</div>
Expand Down
86 changes: 50 additions & 36 deletions packages/zarm/src/dropdown/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,57 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { render, fireEvent, screen, waitFor } from '@testing-library/react';
import Dropdown from '../index';
import Tabs from "../../tabs";

describe('Tab', () => {
it('renders correctly', () => {
const { container } = render(
<Dropdown>
<Dropdown.Item title="菜单一">
<div>菜单一</div>
</Dropdown.Item>
<Dropdown.Item title="菜单二">
<div>菜单二</div>
</Dropdown.Item>
</Dropdown>,
);
expect(container).toMatchSnapshot();
});
const classPrefix = `adm-dropdown`

it('click trigger', () => {
const onChange = jest.fn();
const { container } = render(
<Dropdown onChange={onChange}>
<Dropdown.Item title="菜单一">
<div>菜单一</div>
describe('Dropdown', () => {
test('basic usage', async () => {
render(
<Dropdown data-testid='dropdown'>
<Dropdown.Item title='sorter' key='sorter' data-testid='item'>
content
</Dropdown.Item>
<Dropdown.Item title="菜单二">
<div>菜单二</div>
</Dropdown>
)

fireEvent.click(screen.getByText('sorter'))
const content = screen.getByText('content')
expect(content).toBeInTheDocument()
expect(screen.getByTestId('dropdown')).toHaveClass(`${classPrefix}-open`)
expect(screen.getByTestId('item')).toHaveClass(
`${classPrefix}-item-active ${classPrefix}-item-highlight`
)

fireEvent.click(document.body)
waitFor(() => expect(content).not.toBeVisible())
})

test('multi item', () => {
render(
<Dropdown data-testid='dropdown'>
<Dropdown.Item title='item1' itemKey='item1' data-testid='item1'>
content1
</Dropdown.Item>
<Dropdown.Item title="菜单三">
<div>菜单三</div>
<Dropdown.Item title='item2' itemKey='item2' data-testid='item2'>
content2
</Dropdown.Item>
</Dropdown>,
);
const el = container.querySelectorAll('.za-dropdown__trigger');
fireEvent.click(el[1]);
expect(onChange).toBeCalledWith(1);
const last = el[el.length - 1];
fireEvent.click(last);
expect(onChange).toBeCalledWith(2);
});
});
</Dropdown>
)

fireEvent.click(screen.getByText('item1'))
expect(screen.getByText('content1')).toBeVisible()
expect(screen.getByTestId('item1')).toHaveClass(
`${classPrefix}-item-active ${classPrefix}-item-highlight`
)
fireEvent.click(screen.getByText('item2'))
expect(screen.getByText('content2')).toBeVisible()
expect(screen.getByTestId('item2')).toHaveClass(
`${classPrefix}-item-active ${classPrefix}-item-highlight`
)
})

test('renders with invalid react element', () => {
render(<Dropdown>{1}</Dropdown>)
expect(screen.getByText(1)).toBeInTheDocument()
})
})
31 changes: 30 additions & 1 deletion packages/zarm/src/dropdown/demo.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
# Dropdown 下拉菜单

## 基本用法
## 一列

```jsx
import { useState } from 'react';
import { Dropdown, List, Button } from 'zarm';

const Demo = () => {
const [activeKey, setActiveKey] = useState('home');

return (
<>
<Dropdown>
<Dropdown.Item title='一列' itemKey={'key1'}>
content
</Dropdown.Item>
</Dropdown>
</>
);
};

ReactDOM.render(<Demo />, mountNode);
```

## 两列

```jsx
import { useState } from 'react';
Expand Down Expand Up @@ -53,6 +76,12 @@ ReactDOM.render(<Demo />, mountNode);
| itemKey | number \| string | - | 唯一标识,对应`activeKey`,不设置则默认取 index 索引 |
| title | ReactNode | - | 标题文字 |
| icon | ReactNode | - | 图标 |
| arrow | 自定义 arrow | `React.ReactNode` | - |
| destroyOnClose | 不可见时卸载内容 | `boolean` | `false` |
| forceRender | 被隐藏时是否渲染 `DOM` 结构 | `boolean` | `false` |
| highlight | 高亮 | `boolean` | `false` |
| key | 唯一值 | `string` | - |
| title | 标题 | `ReactNode` | - |

## CSS 变量

Expand Down
2 changes: 1 addition & 1 deletion packages/zarm/src/dropdown/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ export interface BaseDropdownItemProps {
arrow?: React.ReactNode;
children?: React.ReactNode;
active?: boolean;
onClick: any
onClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
}
56 changes: 22 additions & 34 deletions packages/zarm/src/dropdown/style/component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,15 @@
@include define(color, var(--za-color-text));
@include define(active-color, var(--za-theme-primary));

@include define(arrow-space, 5px);
@include define(arrow-color, var(--za-arrow-color));
@include define(arrow-size, 6px);
@include define(arrow-width, var(--za-arrow-width));

display: flex;
flex-direction: column;
width: 100%;
background: var(--background);
justify-content: center;
align-content: space-evenly;
align-items: center;
transition-duration: 0.2s;
overflow: hidden;
cursor: pointer;
position: relative;

@include e(trigger-list) {
width: 100%;
Expand All @@ -36,47 +29,42 @@
display: flex;
flex: 1;
justify-content: center;
align-items: center;
cursor: pointer;

@include m(active) {
color: var(--active-color);
@include e(title) {
display: flex;
align-items: center;
position: relative;
max-width: 100%;
padding: 12px;
}

@include e(title-text) {
margin-right: 5px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

@include e(arrow) {
margin-left: var(--arrow-space);
margin-top: calc(var(--arrow-size) / -2);
display: inline-block;
content: '';
border-width: var(--arrow-size) var(--arrow-size) 0;
border-style: solid;
border-color: var(--arrow-color) transparent transparent;
width: 0;
height: 0;
transition: all 0.15s ease-out;
transform: rotate(0deg) translateY(1px);
transition: all ease 0.2s;

@include m(active) {
transform: rotate(180deg);
transform: rotate(-180deg) translateY(-1px);
}
}
}
}

@include e(popup) {
//position: fixed;
//overflow: hidden;
//width: 100%;
//right: 0;
//bottom: 0;
//left: 0;
@include m(active) {
color: var(--active-color);
}
}
}

@include e(content) {
flex: 1;
display: none;
width: 100%;
min-height: var(--height);
transition-duration: 0.2s;
transition-property: height bottom;
background: #ffffff;

@include m(active) {
display: block;
Expand Down

0 comments on commit cc8e597

Please sign in to comment.