Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Dropdown): Support separated layout & mask visibility control #6306

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
36 changes: 36 additions & 0 deletions src/components/dropdown/demos/demo1.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export default () => {
</Dropdown.Item>
</Dropdown>
</DemoBlock>

<DemoBlock title='自定义arrow' padding={'0'}>
<Dropdown arrow={<DownOutline />}>
<Dropdown.Item key='sorter' title='排序'>
Expand Down Expand Up @@ -127,6 +128,41 @@ export default () => {
</Dropdown.Item>
</Dropdown>
</DemoBlock>

<DemoBlock title='左右布局' padding={'0'}>
<Dropdown>
<Dropdown.Item key='sorter' title='排序'>
<div style={{ padding: 12 }}>
排序内容
<br />
排序内容
<br />
排序内容
<br />
排序内容
<br />
</div>
</Dropdown.Item>
<Dropdown.Item key='bizop' title='商机筛选'>
<div style={{ padding: 12 }}>
商机筛选内容
<br />
商机筛选内容
<br />
商机筛选内容
<br />
</div>
</Dropdown.Item>
<Dropdown.Item key='more' title='更选筛选' align='right'>
<div style={{ padding: 12 }}>
更多筛选内容
<br />
更多筛选内容
<br />
</div>
</Dropdown.Item>
</Dropdown>
</DemoBlock>
</>
)
}
82 changes: 55 additions & 27 deletions src/components/dropdown/demos/demo2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,60 @@ export default () => {
const ref = useRef<DropdownRef>(null)

return (
<DemoBlock title='点击内部按钮隐藏' padding={'0'}>
<Dropdown ref={ref}>
<Dropdown.Item key='sorter' title='排序'>
<div style={{ padding: 12 }}>
排序内容
<br />
排序内容
<br />
排序内容
<br />
排序内容
<br />
</div>
<div style={{ padding: '0 12px 12px' }}>
<Button
color='primary'
block
onClick={() => {
ref.current?.close()
}}
>
确定
</Button>
</div>
</Dropdown.Item>
</Dropdown>
</DemoBlock>
<>
<DemoBlock title='点击内部按钮隐藏' padding={'0'}>
<Dropdown ref={ref}>
<Dropdown.Item key='sorter' title='排序'>
<div style={{ padding: 12 }}>
排序内容
<br />
排序内容
<br />
排序内容
<br />
排序内容
<br />
</div>
<div style={{ padding: '0 12px 12px' }}>
<Button
color='primary'
block
onClick={() => {
ref.current?.close()
}}
>
确定
</Button>
</div>
</Dropdown.Item>
</Dropdown>
</DemoBlock>
<DemoBlock title='不展示遮罩' padding={'0'}>
<Dropdown>
<Dropdown.Item key='sorter' title='排序'>
<div style={{ padding: 12 }}>
排序内容
<br />
排序内容
<br />
排序内容
<br />
排序内容
<br />
</div>
</Dropdown.Item>
<Dropdown.Item key='bizop' title='无遮罩筛选' mask={false}>
<div style={{ padding: 12 }}>
商机筛选内容
<br />
商机筛选内容
<br />
商机筛选内容
<br />
</div>
</Dropdown.Item>
</Dropdown>
</DemoBlock>
</>
)
}
9 changes: 9 additions & 0 deletions src/components/dropdown/dropdown.less
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
border-bottom: 1px solid transparent;
}

.@{class-prefix-dropdown}-separated {
.@{class-prefix-dropdown-item} {
flex: unset;
}
.@{class-prefix-dropdown-item}-gap {
flex: 1;
}
}

&-open {
.@{class-prefix-dropdown}-nav {
border-bottom-color: var(--adm-color-border);
Expand Down
41 changes: 36 additions & 5 deletions src/components/dropdown/dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
import { NativeProps, withNativeProps } from '../../utils/native-props'
import { mergeProps } from '../../utils/with-default-props'
import { usePropsValue } from '../../utils/use-props-value'
import { isPropValueConsecutive } from '../../utils/is-prop-value-consecutive'
import { defaultPopupBaseProps } from '../popup/popup-base-props'
import { devWarning } from '../../utils/dev-log'

const classPrefix = `adm-dropdown`

Expand All @@ -27,10 +29,12 @@
closeOnClickAway?: boolean
onChange?: (key: string | null) => void
arrow?: React.ReactNode
mask?: boolean
getContainer?: PopupProps['getContainer']
} & NativeProps

const defaultProps = {
mask: true,
defaultActiveKey: null,
closeOnMaskClick: true,
closeOnClickAway: false,
Expand All @@ -46,6 +50,7 @@
React.PropsWithChildren<DropdownProps>
>((p, ref) => {
const props = mergeProps(defaultProps, p)
const [showMask, setShowMask] = useState(true)
const [value, setValue] = usePropsValue({
value: props.activeKey,
defaultValue: props.defaultActiveKey,
Expand Down Expand Up @@ -78,30 +83,48 @@
setValue(null)
} else {
setValue(key)
key && setShowMask(maskPopMap[key])
}
}

let popupForceRender = false
let separated = false
const items: ReactElement<ComponentProps<typeof Item>>[] = []
const navs = React.Children.map(props.children, child => {
const leftNavs: React.ReactNode[] = []
const rightNavs: React.ReactNode[] = []
const maskPopMap: Record<string, boolean> = {}
React.Children.forEach(props.children, child => {
if (isValidElement<ComponentProps<typeof Item>>(child)) {
const childProps = {
...child.props,
onClick: () => {
changeActive(child.key as string)
child.props.onClick?.()
},
1587315093 marked this conversation as resolved.
Show resolved Hide resolved
active: child.key === value,
arrow:
child.props.arrow === undefined ? props.arrow : child.props.arrow,
}
items.push(child)
if (child.key) {
maskPopMap[child.key] =
child.props.mask === undefined ? props.mask : child.props.mask
}
if (child.props.forceRender) popupForceRender = true
return cloneElement(child, childProps)
if (child.props.align) separated = true
const cloneChild = cloneElement(child, childProps)
child.props.align === 'right'
? rightNavs.push(cloneChild)
: leftNavs.push(cloneChild)
} else {
return child
leftNavs.push(child)
}
})

if (!isPropValueConsecutive(props.children, 'align')) {
devWarning('Dropdown.Item', `'align' prop should be consecutive.`)

Check warning on line 125 in src/components/dropdown/dropdown.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/dropdown/dropdown.tsx#L125

Added line #L125 was not covered by tests
}

useImperativeHandle(
ref,
() => ({
Expand All @@ -120,8 +143,15 @@
})}
ref={containerRef}
>
<div className={`${classPrefix}-nav`} ref={navRef}>
{navs}
<div
className={classNames(`${classPrefix}-nav`, {
[`${classPrefix}-separated`]: separated,
})}
ref={navRef}
>
{leftNavs}
{separated && <div className={`${classPrefix}-item-gap`} role='grid' />}
{rightNavs}
</div>
<Popup
visible={!!value}
Expand All @@ -131,6 +161,7 @@
maskClassName={`${classPrefix}-popup-mask`}
bodyClassName={`${classPrefix}-popup-body`}
style={{ top }}
mask={showMask}
forceRender={popupForceRender}
onMaskClick={
props.closeOnMaskClick
Expand Down
6 changes: 6 additions & 0 deletions src/components/dropdown/index.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ It is suitable for filtering, sorting and changing the display range or order of
| --- | --- | --- | --- |
| activeKey | Active `Item` key | `string \| null` | - |
| arrow | Custom arrow | `React.ReactNode` | - |
| mask | Whether to show mask when expended | `React.ReactNode` | `true` |
| closeOnClickAway | Whether to automatically hide after clicking outside area | `boolean` | `false` |
| closeOnMaskClick | Whether to automatically close after clicking on the mask | `boolean` | `true` |
| defaultActiveKey | The default active `Item` key | `string \| null` | `null` |
Expand All @@ -36,9 +37,14 @@ It is suitable for filtering, sorting and changing the display range or order of

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| align | The alignment of Item. | `'left' \| 'right' \| undefined` | - |
| arrow | Custom arrow | `React.ReactNode` | - |
| mask | Whether to show mask when expended | `boolean` | 父级 Dropdown 的 `mask` |
| destroyOnClose | Destroy `dom` when not visible | `boolean` | `false` |
| forceRender | Whether to render the content even if it is not active | `boolean` | `false` |
| highlight | Highlight | `boolean` | `false` |
| key | The unique value | `string` | - |
| onClick | Triggered on click | `() => void` | - |
| title | Title | `ReactNode` | - |

If the `align` prop of any `Dropdown.Item` is not `undefined`, all `Dropdown.Item` are displayed separately according to the `align` value. At this time, the `Dropdown.Item` without the `align` prop is on the left by default.
22 changes: 14 additions & 8 deletions src/components/dropdown/index.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
| --- | --- | --- | --- |
| activeKey | 激活的 `Item` `key` | `string \| null` | - |
| arrow | 自定义 arrow | `React.ReactNode` | - |
| mask | 展开时是否展示遮罩 | `boolean` | `true` |
| closeOnClickAway | 是否在点击外部区域后自动隐藏 | `boolean` | `false` |
| closeOnMaskClick | 是否在点击遮罩后自动隐藏 | `boolean` | `true` |
| defaultActiveKey | 默认激活的 `Item` `key` | `string \| null` | `null` |
Expand All @@ -34,11 +35,16 @@

### 属性

| 属性 | 说明 | 类型 | 默认值 |
| -------------- | --------------------------- | ----------------- | ------- |
| arrow | 自定义 arrow | `React.ReactNode` | - |
| destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `false` |
| forceRender | 被隐藏时是否渲染 `DOM` 结构 | `boolean` | `false` |
| highlight | 高亮 | `boolean` | `false` |
| key | 唯一值 | `string` | - |
| title | 标题 | `ReactNode` | - |
| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| align | 对齐方式 | `'left' \| 'right' \| undefined` | - |
| arrow | 自定义 arrow | `React.ReactNode` | - |
| mask | 展开时是否展示遮罩 | `boolean` | 父级 Dropdown 的 `mask` |
| destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `false` |
| forceRender | 被隐藏时是否渲染 `DOM` 结构 | `boolean` | `false` |
| highlight | 高亮 | `boolean` | `false` |
| key | 唯一值 | `string` | - |
| onClick | 点击时触发 | `() => void` | - |
| title | 标题 | `ReactNode` | - |

若有任一`Dropdown.Item`的`align`属性非`undefined`时,所有`Dropdown.Item`按`align`配置左右分开展示,此时未配置`align`属性的`Dropdown.Item`默认居左。
4 changes: 3 additions & 1 deletion src/components/dropdown/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ const classPrefix = `adm-dropdown-item`

export type DropdownItemProps = {
key: string
mask?: boolean
title: React.ReactNode
align?: 'left' | 'right'
active?: boolean
highlight?: boolean
forceRender?: boolean
destroyOnClose?: boolean
onClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
onClick?: () => void
arrow?: React.ReactNode
children?: React.ReactNode
} & NativeProps
Expand Down
23 changes: 23 additions & 0 deletions src/components/dropdown/tests/dropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,27 @@ describe('Dropdown', () => {
)
expect(screen.getByText('content')).toBeInTheDocument()
})

test('item with align', () => {
const { getByTestId, container } = render(
<Dropdown data-testid='dropdown'>
<Dropdown.Item title='item1' key='item1' data-testid='item1'>
content1
</Dropdown.Item>
<Dropdown.Item
title='item2'
key='item2'
data-testid='item2'
align='right'
>
content2
</Dropdown.Item>
</Dropdown>
)

expect(getByTestId('item1')).toHaveStyle('flex: unset')
expect(container.querySelector(`.${classPrefix}-nav`)).toHaveClass(
`${classPrefix}-nav ${classPrefix}-separated`
)
})
})
21 changes: 21 additions & 0 deletions src/utils/is-prop-value-consecutive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { ReactNode, isValidElement } from 'react'

type CheckedValueType = string | number | undefined | null | boolean

export function isPropValueConsecutive(children: ReactNode, propName: string) {
let isConsistant = true
let prevValue: CheckedValueType = undefined
const knownPropValue = new Set<CheckedValueType>()
React.Children.forEach(children, child => {
if (!isValidElement(child)) return

const value = child.props[propName]
if (value !== prevValue && knownPropValue.has(value)) {
isConsistant = false

Check warning on line 14 in src/utils/is-prop-value-consecutive.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/is-prop-value-consecutive.ts#L14

Added line #L14 was not covered by tests
} else {
knownPropValue.add(value)
prevValue = value
}
})
return isConsistant
}
Loading