From 22b5669ac3d67943e32969a1110066addaf2eb98 Mon Sep 17 00:00:00 2001 From: Jonathan Fuchs Date: Mon, 23 Aug 2021 14:12:20 -0700 Subject: [PATCH] Overlay and its composing components take a portalContainerName prop --- .changeset/tall-spiders-yawn.md | 5 + docs/content/ActionMenu.mdx | 21 ++-- docs/content/DropdownMenu.mdx | 19 ++-- docs/content/Overlay.mdx | 29 +++--- src/ActionMenu.tsx | 8 +- src/AnchoredOverlay/AnchoredOverlay.tsx | 6 +- src/DropdownMenu/DropdownMenu.tsx | 8 +- src/Overlay.tsx | 6 +- src/SelectPanel/SelectPanel.tsx | 4 +- src/stories/AnchoredOverlay.stories.tsx | 124 ++++++++++++++++++++++++ 10 files changed, 189 insertions(+), 41 deletions(-) create mode 100644 .changeset/tall-spiders-yawn.md create mode 100644 src/stories/AnchoredOverlay.stories.tsx diff --git a/.changeset/tall-spiders-yawn.md b/.changeset/tall-spiders-yawn.md new file mode 100644 index 00000000000..1efea918982 --- /dev/null +++ b/.changeset/tall-spiders-yawn.md @@ -0,0 +1,5 @@ +--- +'@primer/components': minor +--- + +Overlay and its composing components take a portalContainerName prop diff --git a/docs/content/ActionMenu.mdx b/docs/content/ActionMenu.mdx index 85a098b7423..21bd8425a3d 100644 --- a/docs/content/ActionMenu.mdx +++ b/docs/content/ActionMenu.mdx @@ -68,13 +68,14 @@ An `ActionMenu` is an ActionList-based component for creating a menu of actions ## Component props -| Name | Type | Default | Description | -| :------------ | :------------------------------------ | :---------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| items | `ItemProps[]` | `undefined` | Required. A list of item objects conforming to the `ActionList.Item` props interface. | -| renderItem | `(props: ItemProps) => JSX.Element` | `ActionList.Item` | Optional. If defined, each item in `items` will be passed to this function, allowing for `ActionList`-wide custom item rendering. | -| groupMetadata | `GroupProps[]` | `undefined` | Optional. If defined, `ActionList` will group `items` into `ActionList.Group`s separated by `ActionList.Divider` according to their `groupId` property. | -| renderAnchor | `(props: ButtonProps) => JSX.Element` | `Button` | Optional. If defined, provided component will be used to render the menu anchor. Will receive the selected `Item` text as `children` prop when an item is activated. | -| anchorContent | React.ReactNode | `undefined` | Optional. If defined, it will be passed to the trigger as the elements child. | -| onAction | (props: ItemProps) => void | `undefined` | Optional. If defined, this function will be called when a menu item is activated either by a click or a keyboard press. | -| open | boolean | `undefined` | Optional. If defined, ActionMenu will use this to control the open/closed state of the Overlay instead of controlling the state internally. Should be used in conjunction with the `setOpen` prop. | -| setOpen | (state: boolean) => void | `undefined` | Optional. If defined, ActionMenu will use this to control the open/closed state of the Overlay instead of controlling the state internally. Should be used in conjunction with the `open` prop. | +| Name | Type | Default | Description | +| :------------------ | :------------------------------------ | :---------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| items | `ItemProps[]` | `undefined` | Required. A list of item objects conforming to the `ActionList.Item` props interface. | +| renderItem | `(props: ItemProps) => JSX.Element` | `ActionList.Item` | Optional. If defined, each item in `items` will be passed to this function, allowing for `ActionList`-wide custom item rendering. | +| groupMetadata | `GroupProps[]` | `undefined` | Optional. If defined, `ActionList` will group `items` into `ActionList.Group`s separated by `ActionList.Divider` according to their `groupId` property. | +| renderAnchor | `(props: ButtonProps) => JSX.Element` | `Button` | Optional. If defined, provided component will be used to render the menu anchor. Will receive the selected `Item` text as `children` prop when an item is activated. | +| anchorContent | React.ReactNode | `undefined` | Optional. If defined, it will be passed to the trigger as the elements child. | +| onAction | (props: ItemProps) => void | `undefined` | Optional. If defined, this function will be called when a menu item is activated either by a click or a keyboard press. | +| open | boolean | `undefined` | Optional. If defined, ActionMenu will use this to control the open/closed state of the Overlay instead of controlling the state internally. Should be used in conjunction with the `setOpen` prop. | +| setOpen | (state: boolean) => void | `undefined` | Optional. If defined, ActionMenu will use this to control the open/closed state of the Overlay instead of controlling the state internally. Should be used in conjunction with the `open` prop. | +| portalContainerName | boolean | `undefined` | Optional. See the `portalContainerName` prop of `Overlay` | diff --git a/docs/content/DropdownMenu.mdx b/docs/content/DropdownMenu.mdx index 6446a79e82f..e720f0ea445 100644 --- a/docs/content/DropdownMenu.mdx +++ b/docs/content/DropdownMenu.mdx @@ -38,12 +38,13 @@ render() ## Component props -| Name | Type | Default | Description | -| :------------ | :-------------------------------------------- | :---------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| items | `ItemProps[]` | `undefined` | Required. A list of item objects to display in the menu | -| selectedItem | `ItemInput` | `undefined` | An `ItemProps` item from the list of `items` which is currently selected. This item will receive a checkmark next to it in the menu. | -| onChange? | (item?: ItemInput) => unknown | `undefined` | A callback which receives the selected item or `undefined` when an item is activated in the menu. If the activated item is the same as the current `selectedItem`, `undefined` will be passed. | -| placeholder | `string` | `undefined` | Optional. A placeholder value to display when there is no current selection. | -| renderAnchor | `(props: DropdownButtonProps) => JSX.Element` | `DropdownButton` | Optional. If defined, provided component will be used to render the menu anchor. Will receive the selected `Item` text as `children` prop when an item is activated. | -| renderItem | `(props: ItemProps) => JSX.Element` | `ActionList.Item` | Optional. If defined, each item in `items` will be passed to this function, allowing for custom item rendering. | -| groupMetadata | `GroupProps[]` | `undefined` | Optional. If defined, `DropdownMenu` will group `items` into `ActionList.Group`s separated by `ActionList.Divider` according to their `groupId` property. | +| Name | Type | Default | Description | +| :------------------ | :-------------------------------------------- | :---------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| items | `ItemProps[]` | `undefined` | Required. A list of item objects to display in the menu | +| selectedItem | `ItemInput` | `undefined` | An `ItemProps` item from the list of `items` which is currently selected. This item will receive a checkmark next to it in the menu. | +| onChange? | (item?: ItemInput) => unknown | `undefined` | A callback which receives the selected item or `undefined` when an item is activated in the menu. If the activated item is the same as the current `selectedItem`, `undefined` will be passed. | +| placeholder | `string` | `undefined` | Optional. A placeholder value to display when there is no current selection. | +| renderAnchor | `(props: DropdownButtonProps) => JSX.Element` | `DropdownButton` | Optional. If defined, provided component will be used to render the menu anchor. Will receive the selected `Item` text as `children` prop when an item is activated. | +| renderItem | `(props: ItemProps) => JSX.Element` | `ActionList.Item` | Optional. If defined, each item in `items` will be passed to this function, allowing for custom item rendering. | +| groupMetadata | `GroupProps[]` | `undefined` | Optional. If defined, `DropdownMenu` will group `items` into `ActionList.Group`s separated by `ActionList.Divider` according to their `groupId` property. | +| portalContainerName | boolean | `undefined` | Optional. See the `portalContainerName` prop of `Overlay` | diff --git a/docs/content/Overlay.mdx b/docs/content/Overlay.mdx index c4322f8ae43..c0c36a4e904 100644 --- a/docs/content/Overlay.mdx +++ b/docs/content/Overlay.mdx @@ -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 []` | `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` | `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` | `undefined` | Required. Element the `Overlay` should be anchored to. | -| returnFocusRef | `React.RefObject` | `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 []` | `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` | `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` | `undefined` | Required. Element the `Overlay` should be anchored to. | +| returnFocusRef | `React.RefObject` | `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 | boolean | `undefined` | Optional. If defined, Overlays will be rendered in the named portal. See also `Portal`. | diff --git a/src/ActionMenu.tsx b/src/ActionMenu.tsx index dc080001083..045ad689c15 100644 --- a/src/ActionMenu.tsx +++ b/src/ActionMenu.tsx @@ -7,7 +7,7 @@ import {AnchoredOverlay} from './AnchoredOverlay' import {useProvidedStateOrCreate} from './hooks/useProvidedStateOrCreate' import {OverlayProps} from './Overlay' import {useProvidedRefOrCreate} from './hooks' -import {AnchoredOverlayWrapperAnchorProps} from './AnchoredOverlay/AnchoredOverlay' +import {AnchoredOverlayWrapperAnchorProps, AnchoredOverlayProps} from './AnchoredOverlay/AnchoredOverlay' interface ActionMenuBaseProps extends Partial>, ListPropsBase { /** @@ -36,7 +36,9 @@ interface ActionMenuBaseProps extends Partial } -export type ActionMenuProps = ActionMenuBaseProps & AnchoredOverlayWrapperAnchorProps +export type ActionMenuProps = ActionMenuBaseProps & + AnchoredOverlayWrapperAnchorProps & + Pick const ActionMenuItem = (props: ItemProps) => @@ -49,6 +51,7 @@ const ActionMenuBase = ({ onAction, open, setOpen, + portalContainerName, overlayProps, items, ...listProps @@ -94,6 +97,7 @@ const ActionMenuBase = ({ open={combinedOpenState} onOpen={onOpen} onClose={onClose} + portalContainerName={portalContainerName} overlayProps={overlayProps} > diff --git a/src/AnchoredOverlay/AnchoredOverlay.tsx b/src/AnchoredOverlay/AnchoredOverlay.tsx index 974014dd627..ab3814068f8 100644 --- a/src/AnchoredOverlay/AnchoredOverlay.tsx +++ b/src/AnchoredOverlay/AnchoredOverlay.tsx @@ -36,7 +36,7 @@ export type AnchoredOverlayWrapperAnchorProps = | Partial | AnchoredOverlayPropsWithoutAnchor -interface AnchoredOverlayBaseProps extends Pick { +interface AnchoredOverlayBaseProps extends Pick { /** * Determines whether the overlay portion of the component should be shown or not */ @@ -86,7 +86,8 @@ export const AnchoredOverlay: React.FC = ({ width, overlayProps, focusTrapSettings, - focusZoneSettings + focusZoneSettings, + portalContainerName }) => { const anchorRef = useProvidedRefOrCreate(externalAnchorRef) const [overlayRef, updateOverlayRef] = useRenderForcingRef() @@ -168,6 +169,7 @@ export const AnchoredOverlay: React.FC = ({ top={position?.top || 0} left={position?.left || 0} anchorSide={position?.anchorSide} + {...(portalContainerName ? {portalContainerName} : {})} {...overlayProps} > {children} diff --git a/src/DropdownMenu/DropdownMenu.tsx b/src/DropdownMenu/DropdownMenu.tsx index 37b2687449e..e39cc71c2d0 100644 --- a/src/DropdownMenu/DropdownMenu.tsx +++ b/src/DropdownMenu/DropdownMenu.tsx @@ -4,7 +4,7 @@ import {DropdownButton, DropdownButtonProps} from './DropdownButton' import {ItemProps} from '../ActionList/Item' import {AnchoredOverlay} from '../AnchoredOverlay' import {OverlayProps} from '../Overlay' -import {AnchoredOverlayWrapperAnchorProps} from '../AnchoredOverlay/AnchoredOverlay' +import {AnchoredOverlayProps, AnchoredOverlayWrapperAnchorProps} from '../AnchoredOverlay/AnchoredOverlay' import {useProvidedRefOrCreate} from '../hooks/useProvidedRefOrCreate' import {useProvidedStateOrCreate} from '../hooks/useProvidedStateOrCreate' @@ -42,7 +42,9 @@ interface DropdownMenuBaseProps extends Partial void } -export type DropdownMenuProps = DropdownMenuBaseProps & AnchoredOverlayWrapperAnchorProps +export type DropdownMenuProps = DropdownMenuBaseProps & + AnchoredOverlayWrapperAnchorProps & + Pick /** * A `DropdownMenu` provides an anchor (button by default) that will open a floating menu of selectable items. The menu can be @@ -59,6 +61,7 @@ export function DropdownMenu({ items, open, onOpenChange, + portalContainerName, ...listProps }: DropdownMenuProps): JSX.Element { const [combinedOpenState, setCombinedOpenState] = useProvidedStateOrCreate(open, onOpenChange, false) @@ -106,6 +109,7 @@ export function DropdownMenu({ onOpen={onOpen} onClose={onClose} overlayProps={overlayProps} + portalContainerName={portalContainerName} > diff --git a/src/Overlay.tsx b/src/Overlay.tsx index 125bddb01af..fbf9723254c 100644 --- a/src/Overlay.tsx +++ b/src/Overlay.tsx @@ -15,6 +15,7 @@ type StyledOverlayProps = { maxHeight?: keyof Omit visibility?: 'visible' | 'hidden' anchorSide?: AnchorSide + portalContainerName?: string } const heightMap = { @@ -89,6 +90,7 @@ export type OverlayProps = { [additionalKey: string]: unknown top: number left: number + portalContainerName?: string } & Omit, 'visibility' | keyof SystemPositionProps> /** @@ -106,6 +108,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( ( @@ -121,6 +124,7 @@ const Overlay = React.forwardRef( top, left, anchorSide, + portalContainerName, ...rest }, forwardedRef @@ -163,7 +167,7 @@ const Overlay = React.forwardRef( }, [anchorSide, slideAnimationDistance, slideAnimationEasing, visibility]) return ( - + & - Pick & + Pick & AnchoredOverlayWrapperAnchorProps & (SelectPanelSingleSelection | SelectPanelMultiSelection) @@ -61,6 +61,7 @@ export function SelectPanel({ items, textInputProps, overlayProps, + portalContainerName, ...listProps }: SelectPanelProps): JSX.Element { const [filterValue, setInternalFilterValue] = useProvidedStateOrCreate(externalFilterValue, undefined, '') @@ -153,6 +154,7 @@ export function SelectPanel({ overlayProps={overlayProps} focusTrapSettings={focusTrapSettings} focusZoneSettings={focusZoneSettings} + portalContainerName={portalContainerName} > { + return ( + + + + + + ) + } + ] +} as Meta + +const HeaderAndLayout = ({children, includePortal}: {children: JSX.Element; includePortal: boolean}) => { + const scrollingElementRef = useRef(null) + useEffect(() => { + if (scrollingElementRef.current && includePortal) { + registerPortalRoot(scrollingElementRef.current) + } + }, [scrollingElementRef, includePortal]) + return ( + + Header or some such + + {children} + + + ) +} + +const ButtonWithAnchoredOverlay = () => { + const [open, setOpen] = useState(false) + + return ( + setOpen(true)} + onClose={() => setOpen(false)} + width="small" + height="auto" + renderAnchor={props => Kitten, please} + > + + kitten + + + ) +} + +export const DefaultPortal = () => { + const rows = 40 + const columns = 20 + return ( + + + + {Array(rows) + .fill(null) + .map((_, i) => ( + + {Array(columns) + .fill(null) + .map((_1, j) => ( + + ))} + + ))} + +
+ + + +
+
+ ) +} + +export const PortalInsideScrollingElement = () => { + const rows = 40 + const columns = 20 + return ( + + + + {Array(rows) + .fill(null) + .map((_, i) => ( + + {Array(columns) + .fill(null) + .map((_1, j) => ( + + ))} + + ))} + +
+ + + +
+
+ ) +}