Skip to content

Commit

Permalink
Merge pull request #338 from omnifed/335-feature-create-portal-component
Browse files Browse the repository at this point in the history
335 feature create portal component
  • Loading branch information
caseybaggz authored Aug 2, 2024
2 parents 91b1e85 + 0e15b7f commit 7e17b25
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 13 deletions.
57 changes: 57 additions & 0 deletions docs/app/react/portal/components/portal-preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client'

import { Close } from '@cerberus-design/icons'
import { Button, IconButton, Portal, Show } from '@cerberus-design/react'
import { hstack } from '@cerberus/styled-system/patterns'
import { useState } from 'react'

export default function PortalPreview() {
const [showPortal, setShowPortal] = useState<boolean>(false)

function handleShowPortal() {
setShowPortal(true)
}

function handleClosePortal() {
setShowPortal(false)
}

return (
<>
<Button onClick={handleShowPortal}>Show Portal</Button>
<Show when={showPortal}>
<Portal>
<div
className={hstack({
backgroundColor: 'info.surface.initial',
color: 'info.text.initial',
justify: 'space-between',
left: 0,
mxi: '4',
p: '4',
position: 'absolute',
right: 0,
rounded: 'md',
shadow: 'md',
top: '4',
zIndex: 'toast',
})}
>
<p>
This is a portal element that is outside of the DOM hierarchy of
the parent component.
</p>

<IconButton
ariaLabel="Close Portal"
onClick={handleClosePortal}
tooltipPosition="bottom"
>
<Close />
</IconButton>
</div>
</Portal>
</Show>
</>
)
}
107 changes: 107 additions & 0 deletions docs/app/react/portal/doc.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
heading: 'Portal'
description: 'Portal displays content in outside of the DOM hierarchy.'
a11y: 'utilities'
npm: '@cerberus-design/react'
source: 'components/Portal.tsx'
recipe: ''
---

import {
WhenToUseAdmonition,
WhenNotToUseAdmonition,
} from '@/app/components/Admonition'
import CodePreview from '@/app/components/CodePreview'
import PortalPreview from '@/app/react/portal/components/portal-preview'

```ts
import { Portal } from '@cerberus-design/react'
```

<WhenToUseAdmonition description="When you need display a component outside of the parent it is contained within." />

## Usage

<CodePreview preview={<PortalPreview />}>
```tsx title="nav.tsx" {23}
'use client'

import { Close } from '@cerberus-design/icons'
import { Button, IconButton, Portal, Show } from '@cerberus-design/react'
import { hstack } from '@cerberus/styled-system/patterns'
import { useState } from 'react'

function PortalPreview() {
const [showPortal, setShowPortal] = useState<boolean>(false)

function handleShowPortal() {
setShowPortal(true)
}

function handleClosePortal() {
setShowPortal(false)
}

return (
<>
<Button onClick={handleShowPortal}>Show Portal</Button>
<Show when={showPortal}>
<Portal>
<div
className={hstack({
backgroundColor: 'info.surface.initial',
color: 'info.text.initial',
justify: 'space-between',
left: 0,
mxi: '4',
p: '4',
position: 'absolute',
right: 0,
rounded: 'md',
shadow: 'md',
top: '4',
zIndex: 'toast',
})}
>
<p>
This is a portal element that is outside of the DOM hierarchy of
the parent component.
</p>

<IconButton
ariaLabel="Close Portal"
onClick={handleClosePortal}
tooltipPosition="bottom"
>
<Close />
</IconButton>
</div>
</Portal>
</Show>
</>
)
}
```
</CodePreview>

<WhenNotToUseAdmonition description="Portals should only be used for alert/dialog level components. Anything else should try to achieve this without the use of a portal." />

## API

```ts showLineNumbers=false
export interface PortalProps {
container?: Element | DocumentFragment
key?: null | string
}

define function Portal(props: PropsWithChildren<PortalProps>): ReactPortal | null
```

### Props

The `Portal` component accepts the following props:

| Name | Default | Description |
| -------- | ------- | ------------------------------------------------------------- |
| container | `document.body` | The mounted element to place the children contents within. |
| key | null | A unique key to attached to the Portal instance. |
28 changes: 28 additions & 0 deletions docs/app/react/portal/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import ApiLinks from '@/app/components/ApiLinks'
import OnThisPage from '../../components/OnThisPage'
import { PageMainContent, PageSections } from '../../components/PageLayout'
import Doc, { frontmatter } from './doc.mdx'
import FeatureHeader from '@/app/components/FeatureHeader'
import type { MatchFeatureKind } from '@/app/components/MatchFeatureImg'

export default function PortalPage() {
return (
<>
<PageMainContent>
<FeatureHeader
heading={frontmatter.heading}
description={frontmatter.description}
a11y={frontmatter.a11y as MatchFeatureKind}
/>
<ApiLinks {...frontmatter} />
<main>
<Doc />
</main>
</PageMainContent>

<PageSections>
<OnThisPage />
</PageSections>
</>
)
}
7 changes: 7 additions & 0 deletions docs/app/react/side-nav.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@
"tag": "",
"type": "route"
},
{
"id": "2:n",
"label": "Portal",
"route": "/react/portal",
"tag": "next",
"type": "route"
},
{
"id": "3",
"label": "Hooks",
Expand Down
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@cerberus-design/styled-system": "workspace:*",
"@microsoft/api-extractor": "^7.46.2",
"@types/react": "^18",
"@types/react-dom": "^18",
"react": "^18",
"react-dom": "^18",
"tsup": "^8.1.0"
Expand Down
22 changes: 22 additions & 0 deletions packages/react/src/components/Portal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { PropsWithChildren } from 'react'
import { createPortal } from 'react-dom'

/**
* This module is the Portal component.
* @module
*/

export interface PortalProps {
container?: Element | DocumentFragment
key?: null | string
}

/**
* The Portal component is used to render children into a DOM node that exists outside the DOM hierarchy of the parent component.
* @param container - The props for the Portal component.
* @returns ReactPortal
*/
export function Portal(props: PropsWithChildren<PortalProps>) {
const container = props.container || document.body
return createPortal(props.children, container, props.key)
}
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from './components/Label'
export * from './components/NavMenuTrigger'
export * from './components/NavMenuList'
export * from './components/NavMenuLink'
export * from './components/Portal'
export * from './components/Radio'
export * from './components/Tab'
export * from './components/TabList'
Expand Down
21 changes: 8 additions & 13 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions tests/react/components/portal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, test, expect, afterEach } from 'bun:test'
import { cleanup, render, screen } from '@testing-library/react'
import { Portal } from '@cerberus-design/react'
import { setupStrictMode } from '@/utils'

describe('Portal', () => {
setupStrictMode()
afterEach(cleanup)

test('should render children provided', () => {
render(
<Portal>
<div>children</div>
</Portal>,
)
expect(screen.getByText('children')).toBeTruthy()
})
})

0 comments on commit 7e17b25

Please sign in to comment.