Skip to content

Commit

Permalink
feat(dropdown): allow dropdown to set the specified container (#344)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
unix authored Jul 21, 2020
1 parent 6e97f89 commit 4125e6f
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions components/modal/modal-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
8 changes: 7 additions & 1 deletion components/select/select-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface Props {
className?: string
dropdownStyle?: object
disableMatchWidth?: boolean
getPopupContainer?: () => HTMLElement | null
}

const defaultProps = {
Expand All @@ -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>{`
Expand Down
5 changes: 4 additions & 1 deletion components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface Props {
dropdownClassName?: string
dropdownStyle?: object
disableMatchWidth?: boolean
getPopupContainer?: () => HTMLElement | null
}

const defaultProps = {
Expand Down Expand Up @@ -60,6 +61,7 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
dropdownClassName,
dropdownStyle,
disableMatchWidth,
getPopupContainer,
...props
}) => {
const theme = useTheme()
Expand Down Expand Up @@ -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 && (
Expand Down
20 changes: 20 additions & 0 deletions components/shared/__tests__/dropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})
32 changes: 25 additions & 7 deletions components/shared/dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface Props {
parent?: MutableRefObject<HTMLElement | null> | undefined
visible: boolean
disableMatchWidth?: boolean
getPopupContainer?: () => HTMLElement | null
}

interface ReactiveDomReact {
Expand All @@ -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()
Expand Down
11 changes: 8 additions & 3 deletions components/utils/use-portal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,24 @@ 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>(
isBrowser ? createElement(id) : 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)
}, [])
Expand Down
65 changes: 48 additions & 17 deletions pages/en-us/components/select.mdx
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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>

Expand Down
Loading

0 comments on commit 4125e6f

Please sign in to comment.