diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.features.stories.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.features.stories.tsx index 0cf7566b9f9..d8c2f613d68 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.features.stories.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.features.stories.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useRef, useState} from 'react' import type {Args, Meta} from '@storybook/react' import {FocusKeys} from '@primer/behaviors' -import {Avatar, Box, Link, Text} from '..' +import {Avatar, Box, Dialog, Link, Spinner, Text} from '..' import {AnchoredOverlay} from '../AnchoredOverlay' import Heading from '../Heading' import Octicon from '../Octicon' @@ -312,3 +312,99 @@ export const OverlayPropsOverrides = () => { ) } + +export const RepositionAfterContentGrows = () => { + const [open, setOpen] = useState(false) + + const [loading, setLoading] = useState(true) + + React.useEffect(() => { + window.setTimeout(() => { + if (open) setLoading(false) + }, 2000) + }, [open]) + + return ( + + + What to expect: + + The anchored overlay should open below the anchor (default position) + After 2000ms, the amount of content in the overlay grows + the overlay should reposition itself above the anchor so that it stays inside the window + + + ( + + Button + + )} + open={open} + onOpen={() => setOpen(true)} + onClose={() => { + setOpen(false) + setLoading(true) + }} + > + {loading ? ( + <> + + loading for 2000ms + > + ) : ( + content with 300px height + )} + + + ) +} + +export const RepositionAfterContentGrowsWithinDialog = () => { + const [open, setOpen] = useState(false) + + const [loading, setLoading] = useState(true) + + React.useEffect(() => { + window.setTimeout(() => { + if (open) setLoading(false) + }, 2000) + }, [open]) + + return ( + {}}> + + + What to expect: + + The anchored overlay should open below the anchor (default position) + After 2000ms, the amount of content in the overlay grows + the overlay should reposition itself above the anchor so that it stays inside the window + + + ( + + Button + + )} + open={open} + onOpen={() => setOpen(true)} + onClose={() => { + setOpen(false) + setLoading(true) + }} + > + {loading ? ( + <> + + loading for 2000ms + > + ) : ( + content with 300px height + )} + + + + ) +} diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx index e046e301938..59ea874a235 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx @@ -112,7 +112,7 @@ export const AnchoredOverlay: React.FC { ) } + +export const RepositionAfterLoading = () => { + const [selected, setSelected] = React.useState([items[0], items[1]]) + const [open, setOpen] = useState(false) + const [filter, setFilter] = React.useState('') + const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase())) + const [loading, setLoading] = useState(true) + + React.useEffect(() => { + if (!open) setLoading(true) + window.setTimeout(() => { + if (open) setLoading(false) + }, 2000) + }, [open]) + + return ( + <> + + Reposition panel after loading + + + > + ) +} + +export const SelectPanelRepositionInsideDialog = () => { + const [selected, setSelected] = React.useState([items[0], items[1]]) + const [open, setOpen] = useState(false) + const [filter, setFilter] = React.useState('') + const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase())) + const [loading, setLoading] = useState(true) + + React.useEffect(() => { + if (!open) setLoading(true) + window.setTimeout(() => { + if (open) setLoading(false) + }, 2000) + }, [open]) + + return ( + {}}> + + other content + + + + ) +} diff --git a/packages/react/src/SelectPanel/SelectPanel.tsx b/packages/react/src/SelectPanel/SelectPanel.tsx index 39d41d26094..4c043ec9a44 100644 --- a/packages/react/src/SelectPanel/SelectPanel.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.tsx @@ -50,7 +50,7 @@ interface SelectPanelBaseProps { export type SelectPanelProps = SelectPanelBaseProps & Omit & - Pick & + Pick & AnchoredOverlayWrapperAnchorProps & (SelectPanelSingleSelection | SelectPanelMultiSelection) @@ -102,6 +102,8 @@ export function SelectPanel({ overlayProps, sx, className, + height, + width, ...listProps }: SelectPanelProps): JSX.Element { const titleId = useId() @@ -205,6 +207,8 @@ export function SelectPanel({ open={open} onOpen={onOpen} onClose={onClose} + height={height} + width={width} overlayProps={{ role: 'dialog', 'aria-labelledby': titleId, @@ -213,6 +217,8 @@ export function SelectPanel({ }} focusTrapSettings={focusTrapSettings} focusZoneSettings={focusZoneSettings} + // TODO: fix + preventOverflow={false} > {usingModernActionList ? null : ( diff --git a/packages/react/src/hooks/useAnchoredPosition.ts b/packages/react/src/hooks/useAnchoredPosition.ts index 6365a23cd27..ea5a8f26ed6 100644 --- a/packages/react/src/hooks/useAnchoredPosition.ts +++ b/packages/react/src/hooks/useAnchoredPosition.ts @@ -30,14 +30,45 @@ export function useAnchoredPosition( const floatingElementRef = useProvidedRefOrCreate(settings?.floatingElementRef) const anchorElementRef = useProvidedRefOrCreate(settings?.anchorElementRef) const [position, setPosition] = React.useState(undefined) + const [_, setPrevHeight] = React.useState(undefined) const updatePosition = React.useCallback( () => { + // TODO: remove + console.log('updatePosition') if (floatingElementRef.current instanceof Element && anchorElementRef.current instanceof Element) { - setPosition(getAnchoredPosition(floatingElementRef.current, anchorElementRef.current, settings)) + const newPosition = getAnchoredPosition(floatingElementRef.current, anchorElementRef.current, settings) + const anchorTop = anchorElementRef.current?.getBoundingClientRect().top + setPosition(prev => { + // TODO: remove + console.log({ + prev, + newPosition, + floatingHeight: floatingElementRef.current?.clientHeight, + floatingBottom: floatingElementRef.current?.getBoundingClientRect().bottom, + anchorTop, + }) + if ( + prev && + prev.anchorSide !== newPosition.anchorSide && + ['outside-top', 'inside-top'].includes(prev.anchorSide) + ) { + if (anchorTop > (floatingElementRef.current?.clientHeight ?? 0)) { + setPrevHeight(prevHeight => { + if (floatingElementRef?.current && prevHeight) { + ;(floatingElementRef.current as HTMLElement).style.height = `${prevHeight}px` + } + return prevHeight + }) + return prev + } + } + return newPosition + }) } else { setPosition(undefined) } + setPrevHeight(floatingElementRef?.current?.clientHeight) }, // eslint-disable-next-line react-hooks/exhaustive-deps [floatingElementRef, anchorElementRef, ...dependencies], @@ -45,7 +76,8 @@ export function useAnchoredPosition( useLayoutEffect(updatePosition, [updatePosition]) - useResizeObserver(updatePosition) + useResizeObserver(updatePosition) // watches for changes in window size + useResizeObserver(updatePosition, floatingElementRef as React.RefObject) // watches for changes in floating element size return { floatingElementRef, diff --git a/packages/react/src/hooks/useResizeObserver.ts b/packages/react/src/hooks/useResizeObserver.ts index 32a0d496e00..7483358d67d 100644 --- a/packages/react/src/hooks/useResizeObserver.ts +++ b/packages/react/src/hooks/useResizeObserver.ts @@ -20,8 +20,9 @@ export function useResizeObserver( savedCallback.current = callback }) + const targetEl = target && 'current' in target ? target.current : document.documentElement + useLayoutEffect(() => { - const targetEl = target && 'current' in target ? target.current : document.documentElement if (!targetEl) { return } @@ -36,5 +37,5 @@ export function useResizeObserver( observer.disconnect() } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [target, ...depsArray]) + }, [targetEl, ...depsArray]) }
other content