Repository: zeit-ui/react (ts)

Issue:
Select inside Modal with overflow
<!-- Please do NOT DELETE the template. -->
<!-- No template issues will be closed. -->

# Bug report 🐞
Select inside Modal component with overflow is getting stick to one place and when backdrop id scrolled. it's just staying at old place .

## Version & Environment

  - Version of browser
     chrome 83.0.4103.116  (64-bit)
  - Version of `zeit-ui/react`
     "@zeit-ui/react": "1.8.0-canary.6",
## Expection
with native select primitive , moda's overflow will be held and until select options are closed. 
![ezgif com-video-to-gif (1)](https://user-images.githubusercontent.com/28837336/87972053-e9f8b900-cae3-11ea-9be4-60aa258aa25e.gif)
![ezgif com-video-to-gif](https://user-images.githubusercontent.com/28837336/87972105-fb41c580-cae3-11ea-83dd-d9386a63cb93.gif)

## Actual results (or Errors)

```

<Select placeholder="Choose one" onChange={handler}>
      <Select.Option value="1">Option 1</Select.Option>
       <Select.Option value="2">Option 2</Select.Option>
 </Select>

```

codesandbox [link](https://codesandbox.io/s/zeit-ui-playground-ntu7z?file=/src/App.js)


Pull request:
feat(dropdown): allow dropdown to set the specified container
## Checklist

- [x] Fix linting errors
- [x] Tests have been added / updated (or snapshots)

## Change information

resolve #342

Patch:
diff --git a/components/modal/modal-content.tsx b/components/modal/modal-content.tsx
index ac7c45b..43561bf 100644
--- a/components/modal/modal-content.tsx
+++ b/components/modal/modal-content.tsx
@@ -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) {
diff --git a/components/select/select-dropdown.tsx b/components/select/select-dropdown.tsx
index 25358e0..356451c 100644
--- a/components/select/select-dropdown.tsx
+++ b/components/select/select-dropdown.tsx
@@ -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>{`
diff --git a/components/select/select.tsx b/components/select/select.tsx
index b9e6306..2b54fd0 100644
--- a/components/select/select.tsx
+++ b/components/select/select.tsx
@@ -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 && (
diff --git a/components/shared/dropdown.tsx b/components/shared/dropdown.tsx
index 6b6ecb0..50b60c5 100644
--- a/components/shared/dropdown.tsx
+++ b/components/shared/dropdown.tsx
@@ -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()
diff --git a/components/utils/use-portal.ts b/components/utils/use-portal.ts
index 8ea76dc..4361bfe 100644
--- a/components/utils/use-portal.ts
+++ b/components/utils/use-portal.ts
@@ -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)
   }, [])
diff --git a/pages/en-us/components/select.mdx b/pages/en-us/components/select.mdx
index cefe6cd..029b475 100644
--- a/pages/en-us/components/select.mdx
+++ b/pages/en-us/components/select.mdx
@@ -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 &#124; 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 &#124; 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>
 
diff --git a/pages/zh-cn/components/select.mdx b/pages/zh-cn/components/select.mdx
index a385d85..ee24879 100644
--- a/pages/zh-cn/components/select.mdx
+++ b/pages/zh-cn/components/select.mdx
@@ -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 &#124; 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 &#124; 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>
 


Test patch:
diff --git a/components/modal/__tests__/__snapshots__/index.test.tsx.snap b/components/modal/__tests__/__snapshots__/index.test.tsx.snap
index 928d38d..382ac85 100644
--- a/components/modal/__tests__/__snapshots__/index.test.tsx.snap
+++ b/components/modal/__tests__/__snapshots__/index.test.tsx.snap
@@ -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) {
diff --git a/components/shared/__tests__/dropdown.test.tsx b/components/shared/__tests__/dropdown.test.tsx
index 95f2507..49b39b5 100644
--- a/components/shared/__tests__/dropdown.test.tsx
+++ b/components/shared/__tests__/dropdown.test.tsx
@@ -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')
+  })
 })