Skip to content

Commit 65a6add

Browse files
committed
fix: menu overflow
1 parent b28f612 commit 65a6add

File tree

5 files changed

+94
-5
lines changed

5 files changed

+94
-5
lines changed

.changeset/few-experts-accept.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ultraviolet/ui": patch
3+
---
4+
5+
`Menu`: the popup should shrink when there is not enough size to avoid creating more scroll

packages/ui/src/components/Menu/MenuContent.tsx

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { SIZES } from './constants'
2828
import { getListItem, searchChildren } from './helpers'
2929
import type { MenuProps } from './types'
3030

31+
const SPACE_DISCLOSURE_POPUP = 24 // in px
32+
3133
const StyledPopup = styled(Popup, {
3234
shouldForwardProp: prop => !['searchable'].includes(prop),
3335
})<{ searchable: boolean }>`
@@ -62,12 +64,13 @@ const Footer = styled(Stack)`
6264
`
6365

6466
const MenuList = styled(Stack, {
65-
shouldForwardProp: prop => !['height'].includes(prop),
66-
})<{ height: string }>`
67+
shouldForwardProp: prop => !['height', 'heightAvailableSpace'].includes(prop),
68+
})<{ height: string; heightAvailableSpace: string }>`
6769
overflow-y: auto;
6870
overflow-x: hidden;
69-
max-height: calc(${({ height }) => height} - ${({ theme }) =>
70-
theme.space['0.5']});
71+
max-height: ${({ theme, height, heightAvailableSpace }) =>
72+
`calc(min(${height}, ${heightAvailableSpace}) - ${theme.space['0.5']})`};
73+
7174
&:after,
7275
&:before {
7376
border: solid transparent;
@@ -114,6 +117,7 @@ export const Menu = forwardRef(
114117
align,
115118
searchable = false,
116119
footer,
120+
noShrink = false,
117121
}: MenuProps,
118122
ref: Ref<HTMLButtonElement | null>,
119123
) => {
@@ -128,6 +132,9 @@ export const Menu = forwardRef(
128132
} = useMenu()
129133
const searchInputRef = useRef<HTMLInputElement>(null)
130134
const [localChild, setLocalChild] = useState<ReactNode[] | null>(null)
135+
const [popupMaxHeight, setPopupMaxHeight] = useState<string>(
136+
maxHeight ?? '30rem',
137+
)
131138
const contentRef = useRef<HTMLDivElement>(null)
132139
const tempId = useId()
133140
const finalId = `menu-${id ?? tempId}`
@@ -251,6 +258,24 @@ export const Menu = forwardRef(
251258
}
252259
}
253260

261+
useEffect(() => {
262+
if (disclosureRef.current && placement === 'bottom' && !noShrink) {
263+
const disclosureRect = disclosureRef.current.getBoundingClientRect()
264+
const disclosureBottom = disclosureRect.bottom
265+
const windowSize = window.innerHeight
266+
const availableSpace =
267+
windowSize - disclosureBottom - SPACE_DISCLOSURE_POPUP
268+
269+
setPopupMaxHeight(`${availableSpace}px`)
270+
}
271+
}, [
272+
isVisible,
273+
portalTarget.scrollHeight,
274+
disclosureRef,
275+
placement,
276+
noShrink,
277+
])
278+
254279
return (
255280
<StyledPopup
256281
debounceDelay={triggerMethod === 'hover' ? 250 : 0}
@@ -271,14 +296,15 @@ export const Menu = forwardRef(
271296
setShouldBeVisible(undefined)
272297
}}
273298
tabIndex={-1}
274-
maxHeight={maxHeight ?? '30rem'}
299+
maxHeight={popupMaxHeight}
275300
searchable={searchable}
276301
text={
277302
<MenuList
278303
data-testid={dataTestId}
279304
className={className}
280305
role="menu"
281306
height={maxHeight ?? '30rem'}
307+
heightAvailableSpace={popupMaxHeight}
282308
onKeyDown={handleKeyDown}
283309
>
284310
<Content ref={contentRef}>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { StoryFn } from '@storybook/react-vite'
2+
import { Menu } from '..'
3+
import { Button } from '../../Button'
4+
5+
export const Shrink: StoryFn<typeof Menu> = () => (
6+
<>
7+
<Menu disclosure={<Button>default</Button>}>
8+
<Menu.Item>item</Menu.Item>
9+
<Menu.Item>item</Menu.Item>
10+
<Menu.Item>item</Menu.Item>
11+
<Menu.Item>item</Menu.Item>
12+
<Menu.Item>item</Menu.Item>
13+
14+
<Menu.Item>item</Menu.Item>
15+
</Menu>
16+
17+
<Menu disclosure={<Button>noShrink=true</Button>} noShrink>
18+
<Menu.Item>item</Menu.Item>
19+
<Menu.Item>item</Menu.Item>
20+
<Menu.Item>item</Menu.Item>
21+
<Menu.Item>item</Menu.Item>
22+
<Menu.Item>item</Menu.Item>
23+
24+
<Menu.Item>item</Menu.Item>
25+
</Menu>
26+
</>
27+
)
28+
29+
Shrink.parameters = {
30+
docs: {
31+
description: {
32+
story:
33+
'When the menu is at the bottom of a page (with `placement = "bottom"`), it will shrink so that it does not cause overflow. It is possible to removet this feature using prop `noShrink`',
34+
},
35+
},
36+
}
37+
38+
Shrink.decorators = [
39+
StoryComponent => (
40+
<div
41+
style={{
42+
width: '100%',
43+
display: 'flex',
44+
alignItems: 'flex-end',
45+
position: 'absolute',
46+
justifyContent: 'center',
47+
gap: '16px',
48+
}}
49+
>
50+
<StoryComponent />
51+
</div>
52+
),
53+
]

packages/ui/src/components/Menu/__stories__/index.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ export { FunctionChildrenToggle } from './FunctionChildrenToggle.stories'
3939
export { Overflowing } from './Overflowing.stories'
4040
export { Footer } from './Footer.stories'
4141
export { Nested } from './Nested.stories'
42+
export { Shrink } from './Shrink.stories'

packages/ui/src/components/Menu/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,8 @@ export type MenuProps = {
5252
hideOnClickItem?: boolean
5353
footer?: ReactNode
5454
placement?: Exclude<ComponentProps<typeof Popup>['placement'], 'nested-menu'>
55+
/**
56+
* When set to true, the menu does not shrink (height) to avoid overflow on the page
57+
*/
58+
noShrink?: boolean
5559
} & Pick<ComponentProps<typeof Popup>, 'dynamicDomRendering' | 'align'>

0 commit comments

Comments
 (0)