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

wip: SelectPanel overflow #5562

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -312,3 +312,99 @@ export const OverlayPropsOverrides = () => {
</AnchoredOverlay>
)
}

export const RepositionAfterContentGrows = () => {
const [open, setOpen] = useState(false)

const [loading, setLoading] = useState(true)

React.useEffect(() => {
window.setTimeout(() => {
if (open) setLoading(false)
}, 2000)
}, [open])

return (
<Stack direction="vertical" justify="space-between" style={{height: 'calc(100vh - 200px)'}}>
<div>
What to expect:
<ul>
<li>The anchored overlay should open below the anchor (default position)</li>
<li>After 2000ms, the amount of content in the overlay grows</li>
<li>the overlay should reposition itself above the anchor so that it stays inside the window</li>
</ul>
</div>
<AnchoredOverlay
renderAnchor={props => (
<Button {...props} sx={{width: 'fit-content'}}>
Button
</Button>
)}
open={open}
onOpen={() => setOpen(true)}
onClose={() => {
setOpen(false)
setLoading(true)
}}
>
{loading ? (
<>
<Spinner />
loading for 2000ms
</>
) : (
<div style={{height: '300px'}}>content with 300px height</div>
)}
</AnchoredOverlay>
</Stack>
)
}

export const RepositionAfterContentGrowsWithinDialog = () => {
const [open, setOpen] = useState(false)

const [loading, setLoading] = useState(true)

React.useEffect(() => {
window.setTimeout(() => {
if (open) setLoading(false)
}, 2000)
}, [open])

return (
<Dialog onClose={() => {}}>
<Stack direction="vertical" justify="space-between" style={{height: 'calc(100vh - 300px)'}}>
<div>
What to expect:
<ul>
<li>The anchored overlay should open below the anchor (default position)</li>
<li>After 2000ms, the amount of content in the overlay grows</li>
<li>the overlay should reposition itself above the anchor so that it stays inside the window</li>
</ul>
</div>
<AnchoredOverlay
renderAnchor={props => (
<Button {...props} sx={{width: 'fit-content'}}>
Button
</Button>
)}
open={open}
onOpen={() => setOpen(true)}
onClose={() => {
setOpen(false)
setLoading(true)
}}
>
{loading ? (
<>
<Spinner />
loading for 2000ms
</>
) : (
<div style={{height: '300px'}}>content with 300px height</div>
)}
</AnchoredOverlay>
</Stack>
</Dialog>
)
}
2 changes: 1 addition & 1 deletion packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
overlayProps,
focusTrapSettings,
focusZoneSettings,
side = 'outside-bottom',
side = overlayProps?.['anchorSide'] || 'outside-bottom',
align = 'start',
alignmentOffset,
anchorOffset,
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/Overlay/Overlay.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

&:where([data-reflow-container='true']) {
max-width: calc(100vw - 2rem);
max-height: 100vh;
}

&:where([data-overflow-auto]) {
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/Overlay/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ const StyledOverlay = toggleStyledComponent(

&[data-reflow-container='true'] {
max-width: calc(100vw - 2rem);
max-height: 100vh;
}

${sx};
Expand Down
71 changes: 71 additions & 0 deletions packages/react/src/SelectPanel/SelectPanel.examples.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type {OverlayProps} from '../Overlay'
import {TriangleDownIcon} from '@primer/octicons-react'
import {ActionList} from '../deprecated/ActionList'
import FormControl from '../FormControl'
import {Stack} from '../Stack'
import {Dialog} from '../experimental'

const meta = {
title: 'Components/SelectPanel/Examples',
Expand Down Expand Up @@ -442,3 +444,72 @@ export const ItemsInScope = () => {
</FormControl>
)
}

export const RepositionAfterLoading = () => {
const [selected, setSelected] = React.useState<ItemInput[]>([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 (
<>
<Stack direction="vertical" justify="space-between" style={{height: 'calc(100vh - 300px)', width: 'fit-content'}}>
<h1>Reposition panel after loading</h1>
<SelectPanel
loading={loading}
title="Select labels"
placeholderText="Filter Labels"
open={open}
onOpenChange={setOpen}
items={filteredItems}
selected={selected}
onSelectedChange={setSelected}
onFilterChange={setFilter}
/>
</Stack>
</>
)
}

export const SelectPanelRepositionInsideDialog = () => {
const [selected, setSelected] = React.useState<ItemInput[]>([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 (
<Dialog title="SelectPanel reposition after loading inside Dialog" onClose={() => {}}>
<Stack direction="vertical" justify="space-between" style={{height: 'calc(100vh - 500px)', width: 'fit-content'}}>
<p>other content</p>
<SelectPanel
loading={loading}
title="Select labels"
placeholderText="Filter Labels"
open={open}
onOpenChange={setOpen}
items={filteredItems}
selected={selected}
onSelectedChange={setSelected}
onFilterChange={setFilter}
overlayProps={{anchorSide: 'outside-top'}}
/>
</Stack>
</Dialog>
)
}
8 changes: 7 additions & 1 deletion packages/react/src/SelectPanel/SelectPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ interface SelectPanelBaseProps {

export type SelectPanelProps = SelectPanelBaseProps &
Omit<FilteredActionListProps, 'selectionVariant'> &
Pick<AnchoredOverlayProps, 'open'> &
Pick<AnchoredOverlayProps, 'open' | 'width' | 'height'> &
AnchoredOverlayWrapperAnchorProps &
(SelectPanelSingleSelection | SelectPanelMultiSelection)

Expand Down Expand Up @@ -102,6 +102,8 @@ export function SelectPanel({
overlayProps,
sx,
className,
height,
width,
...listProps
}: SelectPanelProps): JSX.Element {
const titleId = useId()
Expand Down Expand Up @@ -205,6 +207,8 @@ export function SelectPanel({
open={open}
onOpen={onOpen}
onClose={onClose}
height={height}
width={width}
overlayProps={{
role: 'dialog',
'aria-labelledby': titleId,
Expand All @@ -213,6 +217,8 @@ export function SelectPanel({
}}
focusTrapSettings={focusTrapSettings}
focusZoneSettings={focusZoneSettings}
// TODO: fix
preventOverflow={false}
>
<LiveRegionOutlet />
{usingModernActionList ? null : (
Expand Down
36 changes: 34 additions & 2 deletions packages/react/src/hooks/useAnchoredPosition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,54 @@
const floatingElementRef = useProvidedRefOrCreate(settings?.floatingElementRef)
const anchorElementRef = useProvidedRefOrCreate(settings?.anchorElementRef)
const [position, setPosition] = React.useState<AnchorPosition | undefined>(undefined)
const [_, setPrevHeight] = React.useState<number | undefined>(undefined)

Check failure on line 33 in packages/react/src/hooks/useAnchoredPosition.ts

View workflow job for this annotation

GitHub Actions / lint

'_' is assigned a value but never used

const updatePosition = React.useCallback(
() => {
// TODO: remove
console.log('updatePosition')

Check failure on line 38 in packages/react/src/hooks/useAnchoredPosition.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
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

Check failure on line 41 in packages/react/src/hooks/useAnchoredPosition.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary optional chain on a non-nullish value
setPosition(prev => {
// TODO: remove
console.log({

Check failure on line 44 in packages/react/src/hooks/useAnchoredPosition.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
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) {

Check failure on line 58 in packages/react/src/hooks/useAnchoredPosition.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary optional chain on a non-nullish value
;(floatingElementRef.current as HTMLElement).style.height = `${prevHeight}px`
}
return prevHeight
})
return prev
}
}
return newPosition
})
} else {
setPosition(undefined)
}
setPrevHeight(floatingElementRef?.current?.clientHeight)

Check failure on line 71 in packages/react/src/hooks/useAnchoredPosition.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary optional chain on a non-nullish value
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[floatingElementRef, anchorElementRef, ...dependencies],
)

useLayoutEffect(updatePosition, [updatePosition])

useResizeObserver(updatePosition)
useResizeObserver(updatePosition) // watches for changes in window size
useResizeObserver(updatePosition, floatingElementRef as React.RefObject<HTMLElement>) // watches for changes in floating element size

return {
floatingElementRef,
Expand Down
5 changes: 3 additions & 2 deletions packages/react/src/hooks/useResizeObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ export function useResizeObserver<T extends HTMLElement>(
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
}
Expand All @@ -36,5 +37,5 @@ export function useResizeObserver<T extends HTMLElement>(
observer.disconnect()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [target, ...depsArray])
}, [targetEl, ...depsArray])
}
Loading