mirror of
https://github.com/zhigang1992/react.git
synced 2026-03-26 22:42:51 +08:00
feat(dropdown): allow dropdown to set the specified container (#344)
* feat(dropdown): allow dropdown to set the specified container * test(modal): update snapshots * docs(select): add example for custom popup container * fix(dropdown): fix type of getPopupContainer * test(dropdown): add testcase for specified container rendering
This commit is contained in:
@@ -94,6 +94,7 @@ exports[`Modal should render correctly 1`] = `
|
||||
margin: 0 -16pt;
|
||||
padding: 16pt 16pt 8pt;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content > :global(*:first-child) {
|
||||
|
||||
@@ -26,6 +26,7 @@ const ModalContent: React.FC<ModalContentProps> = ({ className, children, ...pro
|
||||
margin: 0 -${theme.layout.gap};
|
||||
padding: ${theme.layout.gap} ${theme.layout.gap} ${theme.layout.gapHalf};
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content > :global(*:first-child) {
|
||||
|
||||
@@ -9,6 +9,7 @@ interface Props {
|
||||
className?: string
|
||||
dropdownStyle?: object
|
||||
disableMatchWidth?: boolean
|
||||
getPopupContainer?: () => HTMLElement | null
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
@@ -25,12 +26,17 @@ const SelectDropdown: React.FC<React.PropsWithChildren<SelectDropdownProps>> = (
|
||||
className,
|
||||
dropdownStyle,
|
||||
disableMatchWidth,
|
||||
getPopupContainer,
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const { ref } = useSelectContext()
|
||||
|
||||
return (
|
||||
<Dropdown parent={ref} visible={visible} disableMatchWidth={disableMatchWidth}>
|
||||
<Dropdown
|
||||
parent={ref}
|
||||
visible={visible}
|
||||
disableMatchWidth={disableMatchWidth}
|
||||
getPopupContainer={getPopupContainer}>
|
||||
<div className={`select-dropdown ${className}`} style={dropdownStyle}>
|
||||
{children}
|
||||
<style jsx>{`
|
||||
|
||||
@@ -28,6 +28,7 @@ interface Props {
|
||||
dropdownClassName?: string
|
||||
dropdownStyle?: object
|
||||
disableMatchWidth?: boolean
|
||||
getPopupContainer?: () => HTMLElement | null
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
@@ -60,6 +61,7 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
|
||||
dropdownClassName,
|
||||
dropdownStyle,
|
||||
disableMatchWidth,
|
||||
getPopupContainer,
|
||||
...props
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
@@ -148,7 +150,8 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
|
||||
visible={visible}
|
||||
className={dropdownClassName}
|
||||
dropdownStyle={dropdownStyle}
|
||||
disableMatchWidth={disableMatchWidth}>
|
||||
disableMatchWidth={disableMatchWidth}
|
||||
getPopupContainer={getPopupContainer}>
|
||||
{children}
|
||||
</SelectDropdown>
|
||||
{!pure && (
|
||||
|
||||
@@ -164,4 +164,24 @@ describe('Dropdown', () => {
|
||||
|
||||
expect(() => wrapper.unmount()).not.toThrow()
|
||||
})
|
||||
|
||||
it('should render to specified container', () => {
|
||||
const Mock: React.FC<{}> = () => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const customContainer = useRef<HTMLDivElement>(null)
|
||||
return (
|
||||
<div>
|
||||
<div ref={customContainer} id="custom" />
|
||||
<div ref={ref}>
|
||||
<Dropdown parent={ref} visible getPopupContainer={() => customContainer.current}>
|
||||
<span>test-value</span>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const wrapper = mount(<Mock />)
|
||||
const customContainer = wrapper.find('#custom')
|
||||
expect(customContainer.html()).toContain('dropdown')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ interface Props {
|
||||
parent?: MutableRefObject<HTMLElement | null> | undefined
|
||||
visible: boolean
|
||||
disableMatchWidth?: boolean
|
||||
getPopupContainer?: () => HTMLElement | null
|
||||
}
|
||||
|
||||
interface ReactiveDomReact {
|
||||
@@ -26,31 +27,48 @@ const defaultRect: ReactiveDomReact = {
|
||||
width: 0,
|
||||
}
|
||||
|
||||
const getRect = (ref: MutableRefObject<HTMLElement | null>): ReactiveDomReact => {
|
||||
const getOffset = (el?: HTMLElement | null | undefined) => {
|
||||
if (!el)
|
||||
return {
|
||||
top: 0,
|
||||
left: 0,
|
||||
}
|
||||
const { top, left } = el.getBoundingClientRect()
|
||||
return { top, left }
|
||||
}
|
||||
|
||||
const getRect = (
|
||||
ref: MutableRefObject<HTMLElement | null>,
|
||||
getContainer?: () => HTMLElement | null,
|
||||
): ReactiveDomReact => {
|
||||
if (!ref || !ref.current) return defaultRect
|
||||
const rect = ref.current.getBoundingClientRect()
|
||||
const container = getContainer ? getContainer() : null
|
||||
const scrollElement = container || document.documentElement
|
||||
const { top: offsetTop, left: offsetLeft } = getOffset(container)
|
||||
|
||||
return {
|
||||
...rect,
|
||||
width: rect.width || rect.right - rect.left,
|
||||
top: rect.bottom + document.documentElement.scrollTop,
|
||||
left: rect.left + document.documentElement.scrollLeft,
|
||||
top: rect.bottom + scrollElement.scrollTop - offsetTop,
|
||||
left: rect.left + scrollElement.scrollLeft - offsetLeft,
|
||||
}
|
||||
}
|
||||
|
||||
const Dropdown: React.FC<React.PropsWithChildren<Props>> = React.memo(
|
||||
({ children, parent, visible, disableMatchWidth }) => {
|
||||
const el = usePortal('dropdown')
|
||||
({ children, parent, visible, disableMatchWidth, getPopupContainer }) => {
|
||||
const el = usePortal('dropdown', getPopupContainer)
|
||||
const [rect, setRect] = useState<ReactiveDomReact>(defaultRect)
|
||||
if (!parent) return null
|
||||
|
||||
const updateRect = () => {
|
||||
const { top, left, right, width: nativeWidth } = getRect(parent)
|
||||
const { top, left, right, width: nativeWidth } = getRect(parent, getPopupContainer)
|
||||
setRect({ top, left, right, width: nativeWidth })
|
||||
}
|
||||
|
||||
useResize(updateRect)
|
||||
useClickAnyWhere(() => {
|
||||
const { top, left } = getRect(parent)
|
||||
const { top, left } = getRect(parent, getPopupContainer)
|
||||
const shouldUpdatePosition = top !== rect.top || left !== rect.left
|
||||
if (!shouldUpdatePosition) return
|
||||
updateRect()
|
||||
|
||||
@@ -8,7 +8,10 @@ const createElement = (id: string): HTMLElement => {
|
||||
return el
|
||||
}
|
||||
|
||||
const usePortal = (selectId: string = getId()): HTMLElement | null => {
|
||||
const usePortal = (
|
||||
selectId: string = getId(),
|
||||
getContainer?: () => HTMLElement | null,
|
||||
): HTMLElement | null => {
|
||||
const id = `zeit-ui-${selectId}`
|
||||
const { isBrowser } = useSSR()
|
||||
const [elSnapshot, setElSnapshot] = useState<HTMLElement | null>(
|
||||
@@ -16,11 +19,13 @@ const usePortal = (selectId: string = getId()): HTMLElement | null => {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const hasElement = document.querySelector<HTMLElement>(`#${id}`)
|
||||
const customContainer = getContainer ? getContainer() : null
|
||||
const parentElement = customContainer || document.body
|
||||
const hasElement = parentElement.querySelector<HTMLElement>(`#${id}`)
|
||||
const el = hasElement || createElement(id)
|
||||
|
||||
if (!hasElement) {
|
||||
document.body.appendChild(el)
|
||||
parentElement.appendChild(el)
|
||||
}
|
||||
setElSnapshot(el)
|
||||
}, [])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Layout, Playground, Attributes } from 'lib/components'
|
||||
import { Select, Spacer, Code } from 'components'
|
||||
import { Select, Spacer, Code, Modal, useModal, Button } from 'components'
|
||||
|
||||
export const meta = {
|
||||
title: 'select',
|
||||
@@ -142,25 +142,56 @@ Display a dropdown list of items.
|
||||
`}
|
||||
/>
|
||||
|
||||
<Playground
|
||||
title="Set parent element"
|
||||
desc="you can specify the container for drop-down box rendering."
|
||||
scope={{ Select, Spacer, useModal, Modal, Button, Code }}
|
||||
code={`
|
||||
() => {
|
||||
const { visible, setVisible, bindings } = useModal()
|
||||
return (
|
||||
<>
|
||||
<Button auto onClick={() => setVisible(true)}>Show Select</Button>
|
||||
<Modal {...bindings}>
|
||||
<Modal.Title>Modal</Modal.Title>
|
||||
<Modal.Content id="customModalSelect">
|
||||
<Select placeholder="Choose one" initialValue="1"
|
||||
getPopupContainer={() => document.getElementById('customModalSelect')}>
|
||||
<Select.Option value="1"><Code>TypeScript</Code></Select.Option>
|
||||
<Select.Option value="2"><Code>JavaScript</Code></Select.Option>
|
||||
</Select>
|
||||
<p>Scroll through the content to see the changes.</p>
|
||||
<div style={{ height: '1200px' }}></div>
|
||||
<p>Scroll through the content to see the changes.</p>
|
||||
</Modal.Content>
|
||||
<Modal.Action passive onClick={() => setVisible(false)}>Cancel</Modal.Action>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
`}
|
||||
/>
|
||||
|
||||
<Attributes edit="/pages/en-us/components/select.mdx">
|
||||
<Attributes.Title>Select.Props</Attributes.Title>
|
||||
|
||||
| Attribute | Description | Type | Accepted values | Default |
|
||||
| --------------------- | --------------------------------------- | --------------------------------------------------- | --------------------------------- | --------------- |
|
||||
| **value** | selected value | `string`, `string[]` | - | - |
|
||||
| **initialValue** | initial value | `string`, `string[]` | - | - |
|
||||
| **placeholder** | placeholder string | `string` | - | - |
|
||||
| **width** | css width value of select | `string` | - | `initial` |
|
||||
| **size** | select component size | `NormalSizes` | [NormalSizes](#normalsizes) | `medium` |
|
||||
| **icon** | icon component | `ComponentType` | - | `SVG Component` |
|
||||
| **pure** | remove icon component | `boolean` | - | `false` |
|
||||
| **multiple** | support multiple selection | `boolean` | - | `false` |
|
||||
| **disabled** | disable current radio | `boolean` | - | `false` |
|
||||
| **onChange** | selected value | <Code>(val: string | string[]) => void </Code> | - | - |
|
||||
| **dropdownClassName** | className of dropdown menu | `string` | - | - |
|
||||
| **dropdownStyle** | style of dropdown menu | `object` | - | - |
|
||||
| **disableMatchWidth** | disable Option from follow Select width | `boolean` | - | `false` |
|
||||
| ... | native props | `HTMLAttributes` | `'name', 'alt', 'className', ...` | - |
|
||||
| Attribute | Description | Type | Accepted values | Default |
|
||||
| --------------------- | ----------------------------------------------------- | --------------------------------------------------- | --------------------------------- | --------------- |
|
||||
| **value** | selected value | `string`, `string[]` | - | - |
|
||||
| **initialValue** | initial value | `string`, `string[]` | - | - |
|
||||
| **placeholder** | placeholder string | `string` | - | - |
|
||||
| **width** | css width value of select | `string` | - | `initial` |
|
||||
| **size** | select component size | `NormalSizes` | [NormalSizes](#normalsizes) | `medium` |
|
||||
| **icon** | icon component | `ComponentType` | - | `SVG Component` |
|
||||
| **pure** | remove icon component | `boolean` | - | `false` |
|
||||
| **multiple** | support multiple selection | `boolean` | - | `false` |
|
||||
| **disabled** | disable current radio | `boolean` | - | `false` |
|
||||
| **onChange** | selected value | <Code>(val: string | string[]) => void </Code> | - | - |
|
||||
| **dropdownClassName** | className of dropdown menu | `string` | - | - |
|
||||
| **dropdownStyle** | style of dropdown menu | `object` | - | - |
|
||||
| **disableMatchWidth** | disable Option from follow Select width | `boolean` | - | `false` |
|
||||
| **getPopupContainer** | dropdown render parent element, the default is `body` | `() => HTMLElement` | - | - |
|
||||
| ... | native props | `HTMLAttributes` | `'name', 'alt', 'className', ...` | - |
|
||||
|
||||
<Attributes.Title>Select.Option.Props</Attributes.Title>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Layout, Playground, Attributes } from 'lib/components'
|
||||
import { Select, Spacer, Code } from 'components'
|
||||
import { Select, Spacer, Code, Modal, useModal, Button } from 'components'
|
||||
|
||||
export const meta = {
|
||||
title: '选择器 Select',
|
||||
@@ -142,25 +142,56 @@ export const meta = {
|
||||
`}
|
||||
/>
|
||||
|
||||
<Playground
|
||||
title="设置渲染容器"
|
||||
desc="你可以指定下拉框的元素渲染的容器。"
|
||||
scope={{ Select, Spacer, useModal, Modal, Button, Code }}
|
||||
code={`
|
||||
() => {
|
||||
const { visible, setVisible, bindings } = useModal()
|
||||
return (
|
||||
<>
|
||||
<Button auto onClick={() => setVisible(true)}>显示选择器</Button>
|
||||
<Modal {...bindings}>
|
||||
<Modal.Title>Modal</Modal.Title>
|
||||
<Modal.Content id="customModalSelect">
|
||||
<Select placeholder="选择语言" initialValue="1"
|
||||
getPopupContainer={() => document.getElementById('customModalSelect')}>
|
||||
<Select.Option value="1"><Code>TypeScript</Code></Select.Option>
|
||||
<Select.Option value="2"><Code>JavaScript</Code></Select.Option>
|
||||
</Select>
|
||||
<p>滚动以查看下拉框的工作方式。(超出弹出层会被隐藏)</p>
|
||||
<div style={{ height: '1200px' }}></div>
|
||||
<p>滚动以查看下拉框的工作方式。</p>
|
||||
</Modal.Content>
|
||||
<Modal.Action passive onClick={() => setVisible(false)}>取消</Modal.Action>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
`}
|
||||
/>
|
||||
|
||||
<Attributes edit="/pages/zh-cn/components/select.mdx">
|
||||
<Attributes.Title>Select.Props</Attributes.Title>
|
||||
|
||||
| 属性 | 描述 | 类型 | 推荐值 | 默认 |
|
||||
| --------------------- | ---------------------------- | --------------------------------------------------- | --------------------------------- | --------------- |
|
||||
| **value** | 手动设置选择器的值 | `string`, `string[]` | - | - |
|
||||
| **initialValue** | 选择器初始值 | `string`, `string[]` | - | - |
|
||||
| **placeholder** | 占位文本内容 | `string` | - | - |
|
||||
| **width** | 组件的 CSS 宽度值 | `string` | - | `initial` |
|
||||
| **size** | 选择器组件大小 | `NormalSizes` | [NormalSizes](#normalsizes) | `medium` |
|
||||
| **icon** | 右侧图标组件 | `ComponentType` | - | `SVG Component` |
|
||||
| **pure** | 隐藏右侧图标组件 | `boolean` | - | `false` |
|
||||
| **multiple** | 是否支持多选 | `boolean` | - | `false` |
|
||||
| **disabled** | 禁用所有的交互 | `boolean` | - | `false` |
|
||||
| **onChange** | 选项被选中所触发的事件 | <Code>(val: string | string[]) => void </Code> | - | - |
|
||||
| **dropdownClassName** | 下拉框的自定义类名 | `string` | - | - |
|
||||
| **dropdownStyle** | 下拉框的自定义样式 | `object` | - | - |
|
||||
| **disableMatchWidth** | 禁止 Option 跟随单选框的宽度 | `boolean` | - | `false` |
|
||||
| ... | 原生属性 | `HTMLAttributes` | `'name', 'alt', 'className', ...` | - |
|
||||
| 属性 | 描述 | 类型 | 推荐值 | 默认 |
|
||||
| --------------------- | --------------------------------- | --------------------------------------------------- | --------------------------------- | --------------- |
|
||||
| **value** | 手动设置选择器的值 | `string`, `string[]` | - | - |
|
||||
| **initialValue** | 选择器初始值 | `string`, `string[]` | - | - |
|
||||
| **placeholder** | 占位文本内容 | `string` | - | - |
|
||||
| **width** | 组件的 CSS 宽度值 | `string` | - | `initial` |
|
||||
| **size** | 选择器组件大小 | `NormalSizes` | [NormalSizes](#normalsizes) | `medium` |
|
||||
| **icon** | 右侧图标组件 | `ComponentType` | - | `SVG Component` |
|
||||
| **pure** | 隐藏右侧图标组件 | `boolean` | - | `false` |
|
||||
| **multiple** | 是否支持多选 | `boolean` | - | `false` |
|
||||
| **disabled** | 禁用所有的交互 | `boolean` | - | `false` |
|
||||
| **onChange** | 选项被选中所触发的事件 | <Code>(val: string | string[]) => void </Code> | - | - |
|
||||
| **dropdownClassName** | 下拉框的自定义类名 | `string` | - | - |
|
||||
| **dropdownStyle** | 下拉框的自定义样式 | `object` | - | - |
|
||||
| **disableMatchWidth** | 禁止 Option 跟随单选框的宽度 | `boolean` | - | `false` |
|
||||
| **getPopupContainer** | 下拉框渲染的父元素,默认是 `body` | `() => HTMLElement` | - | - |
|
||||
| ... | 原生属性 | `HTMLAttributes` | `'name', 'alt', 'className', ...` | - |
|
||||
|
||||
<Attributes.Title>Select.Option.Props</Attributes.Title>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user