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

Overlay and its composing components take a portalContainerName prop #1388

Merged
merged 3 commits into from
Sep 1, 2021
Merged
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
5 changes: 5 additions & 0 deletions .changeset/tall-spiders-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/components': minor
---

Overlay takes a portalContainerName prop. This allows overlays with an anchor inside a scrolling container to track with their anchor, so long as the specified portal is also inside that scrolling container.
29 changes: 15 additions & 14 deletions docs/content/Overlay.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,18 @@ System props are deprecated in all components except [Box](/Box). Please use the

## Component props

| Name | Type | Default | Description |
| :-------------- | :------------------------------------------------------------- | :---------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ignoreClickRefs | `React.RefObject<HTMLElement> []` | `undefined` | Optional. An array of ref objects to ignore clicks on in the `onOutsideClick` behavior. This is often used to ignore clicking on the element that toggles the open/closed state for the `Overlay` to prevent the `Overlay` from being toggled twice. |
| initialFocusRef | `React.RefObject<HTMLElement>` | `undefined` | Optional. Ref for the element to focus when the `Overlay` is opened. If nothing is provided, the first focusable element in the `Overlay` body is focused. |
| anchorRef | `React.RefObject<HTMLElement>` | `undefined` | Required. Element the `Overlay` should be anchored to. |
| returnFocusRef | `React.RefObject<HTMLElement>` | `undefined` | Required. Ref for the element to focus when the `Overlay` is closed. |
| onClickOutside | `function` | `undefined` | Required. Function to call when clicking outside of the `Overlay`. Typically this function sets the `Overlay` visibility state to `false`. |
| onEscape | `function` | `undefined` | Required. Function to call when user presses `Escape`. Typically this function sets the `Overlay` visibility state to `false`. |
| width | `'small' │ 'medium' │ 'large' │ 'xlarge' │ 'xxlarge' │ 'auto'` | `auto` | Sets the width of the `Overlay`, pick from our set list of widths, or pass `auto` to automatically set the width based on the content of the `Overlay`. `small` corresponds to `256px`, `medium` corresponds to `320px`, `large` corresponds to `480px`, `xlarge` corresponds to `640px`, `xxlarge` corresponds to `960px`. |
| height | `'xsmall', 'small', 'medium', 'large', 'xlarge', 'auto'` | `auto` | Sets the height of the `Overlay`, pick from our set list of heights, or pass `auto` to automatically set the height based on the content of the `Overlay`. `xsmall` corresponds to `192px`, `small` corresponds to `256px`, `medium` corresponds to `320px`, `large` corresponds to `432px`, `xlarge` corresponds to `600px`. |
| visibility | `'visible', 'hidden'` | `visible` | Sets the visibility of the `Overlay`. |
| anchorSide | `AnchorSide` | undefined | Optional. If provided, the Overlay will slide into position from the side of the anchor with a brief animation |
| top | `number` | 0 | Vertical position of the overlay, relative to its closest positioned ancestor (often its `Portal`). |
| left | `number` | 0 | Horizontal position of the overlay, relative to its closest positioned ancestor (often its `Portal`). |
| Name | Type | Default | Description |
| :------------------ | :------------------------------------------------------------- | :---------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ignoreClickRefs | `React.RefObject<HTMLElement> []` | `undefined` | Optional. An array of ref objects to ignore clicks on in the `onOutsideClick` behavior. This is often used to ignore clicking on the element that toggles the open/closed state for the `Overlay` to prevent the `Overlay` from being toggled twice. |
| initialFocusRef | `React.RefObject<HTMLElement>` | `undefined` | Optional. Ref for the element to focus when the `Overlay` is opened. If nothing is provided, the first focusable element in the `Overlay` body is focused. |
| anchorRef | `React.RefObject<HTMLElement>` | `undefined` | Required. Element the `Overlay` should be anchored to. |
| returnFocusRef | `React.RefObject<HTMLElement>` | `undefined` | Required. Ref for the element to focus when the `Overlay` is closed. |
| onClickOutside | `function` | `undefined` | Required. Function to call when clicking outside of the `Overlay`. Typically this function sets the `Overlay` visibility state to `false`. |
| onEscape | `function` | `undefined` | Required. Function to call when user presses `Escape`. Typically this function sets the `Overlay` visibility state to `false`. |
| width | `'small' │ 'medium' │ 'large' │ 'xlarge' │ 'xxlarge' │ 'auto'` | `auto` | Sets the width of the `Overlay`, pick from our set list of widths, or pass `auto` to automatically set the width based on the content of the `Overlay`. `small` corresponds to `256px`, `medium` corresponds to `320px`, `large` corresponds to `480px`, `xlarge` corresponds to `640px`, `xxlarge` corresponds to `960px`. |
| height | `'xsmall', 'small', 'medium', 'large', 'xlarge', 'auto'` | `auto` | Sets the height of the `Overlay`, pick from our set list of heights, or pass `auto` to automatically set the height based on the content of the `Overlay`. `xsmall` corresponds to `192px`, `small` corresponds to `256px`, `medium` corresponds to `320px`, `large` corresponds to `432px`, `xlarge` corresponds to `600px`. |
| visibility | `'visible', 'hidden'` | `visible` | Sets the visibility of the `Overlay`. |
| anchorSide | `AnchorSide` | undefined | Optional. If provided, the Overlay will slide into position from the side of the anchor with a brief animation |
| top | `number` | 0 | Vertical position of the overlay, relative to its closest positioned ancestor (often its `Portal`). |
| left | `number` | 0 | Horizontal position of the overlay, relative to its closest positioned ancestor (often its `Portal`). |
| portalContainerName | `string` | `undefined` | Optional. If defined, Overlays will be rendered in the named portal. See also `Portal`. |
5 changes: 4 additions & 1 deletion src/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export type OverlayProps = {
[additionalKey: string]: unknown
top: number
left: number
portalContainerName?: string
} & Omit<ComponentProps<typeof StyledOverlay>, 'visibility' | keyof SystemPositionProps>

/**
Expand All @@ -106,6 +107,7 @@ export type OverlayProps = {
* @param anchorSide If provided, the Overlay will slide into position from the side of the anchor with a brief animation
* @param top Optional. Vertical position of the overlay, relative to its closest positioned ancestor (often its `Portal`).
* @param left Optional. Horizontal position of the overlay, relative to its closest positioned ancestor (often its `Portal`).
* @param portalContainerName Optional. The name of the portal container to render the Overlay into.
*/
const Overlay = React.forwardRef<HTMLDivElement, OverlayProps>(
(
Expand All @@ -121,6 +123,7 @@ const Overlay = React.forwardRef<HTMLDivElement, OverlayProps>(
top,
left,
anchorSide,
portalContainerName,
...rest
},
forwardedRef
Expand Down Expand Up @@ -163,7 +166,7 @@ const Overlay = React.forwardRef<HTMLDivElement, OverlayProps>(
}, [anchorSide, slideAnimationDistance, slideAnimationEasing, visibility])

return (
<Portal>
<Portal containerName={portalContainerName}>
<StyledOverlay
height={height}
role={role}
Expand Down
117 changes: 117 additions & 0 deletions src/stories/AnchoredOverlay.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React, {useEffect, useRef, useState} from 'react'
import {Meta} from '@storybook/react'

import {BaseStyles, Box, ThemeProvider} from '..'
import Heading from '../Heading'
import DropdownButton from '../Button'
import {AnchoredOverlay} from '../AnchoredOverlay'
import {registerPortalRoot} from '../Portal'

export default {
title: 'Generic behaviors/AnchoredOverlay',
decorators: [
Story => {
return (
<ThemeProvider>
<BaseStyles>
<Story />
</BaseStyles>
</ThemeProvider>
)
}
]
} as Meta

const HeaderAndLayout = ({children}: {children: JSX.Element}) => {
const scrollingElementRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (scrollingElementRef.current) {
registerPortalRoot(scrollingElementRef.current, 'scrollingPortal')
}
}, [scrollingElementRef])
return (
<Box position="absolute" top={0} right={0} bottom={0} left={0} padding={4} backgroundColor="lavenderblush">
<Heading>Header or some such</Heading>
<Box position="absolute" top={10} right={4} bottom={4} left={4} overflow="scroll" backgroundColor="powderblue">
{children}
<Box ref={scrollingElementRef} position="absolute" top={0} left={0} />
</Box>
</Box>
)
}

const ButtonWithAnchoredOverlay = ({portalContainerName}: {portalContainerName?: string}) => {
const [open, setOpen] = useState(false)

return (
<AnchoredOverlay
open={open}
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
width="small"
height="auto"
renderAnchor={props => <DropdownButton {...props}>Kitten, please</DropdownButton>}
overlayProps={{portalContainerName}}
>
<Box width="100%" height="100%" backgroundColor="thistle" display="flex" flexDirection="column">
<img src={`//placekitten.com/200/300`} alt="kitten" />
</Box>
</AnchoredOverlay>
)
}

export const DefaultPortal = () => {
const rows = 40
const columns = 20
return (
<HeaderAndLayout>
<table>
<tbody>
{Array(rows)
.fill(null)
.map((_, i) => (
<tr key={i}>
{Array(columns)
.fill(null)
.map((_1, j) => (
<td key={`${i}${j}`}>
<Box m={2}>
<ButtonWithAnchoredOverlay />
</Box>
</td>
))}
</tr>
))}
</tbody>
</table>
</HeaderAndLayout>
)
}

export const PortalInsideScrollingElement = () => {
const rows = 40
const columns = 20
return (
<HeaderAndLayout>
<table>
<tbody>
{Array(rows)
.fill(null)
.map((_, i) => (
<tr key={i}>
{Array(columns)
.fill(null)
.map((_1, j) => (
<td key={`${i}${j}`}>
<Box m={2}>
<ButtonWithAnchoredOverlay portalContainerName="scrollingPortal" />
</Box>
</td>
))}
</tr>
))}
</tbody>
</table>
</HeaderAndLayout>
)
}