Skip to content

Commit

Permalink
wip: SelectPanel overflow
Browse files Browse the repository at this point in the history
  • Loading branch information
francinelucca committed Jan 17, 2025
1 parent 35dec2e commit 80dfbfd
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 7 deletions.
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 @@ export function useAnchoredPosition(
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])
}

0 comments on commit 80dfbfd

Please sign in to comment.