Skip to content

Commit 0e01aa5

Browse files
committed
fix(popover): auto update on anchor and popover resize
1 parent 294cd40 commit 0e01aa5

File tree

4 files changed

+159
-156
lines changed

4 files changed

+159
-156
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- `IconButton`: remove the children prop as it's not actually supported by the component
1313
- `Popover`: fix improper first placement on React 18
14+
- `Popover`: update placement on both anchor and popover resize
1415

1516
## [3.11.0][] - 2025-02-05
1617

packages/lumx-react/src/components/popover/Popover.stories.tsx

Lines changed: 142 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@ import {
1414
Popover,
1515
Size,
1616
Elevation,
17+
Message,
18+
FlexBox,
19+
IconButtonProps,
1720
} from '@lumx/react';
1821
import range from 'lodash/range';
1922
import { withCombinations } from '@lumx/react/stories/decorators/withCombinations';
2023
import { getSelectArgType } from '@lumx/react/stories/controls/selectArgType';
2124
import { withChromaticForceScreenSize } from '@lumx/react/stories/decorators/withChromaticForceScreenSize';
25+
import { FitAnchorWidth } from '@lumx/react/components/popover/constants';
26+
import { withUndefined } from '@lumx/react/stories/controls/withUndefined';
2227

2328
export default {
2429
title: 'LumX components/popover/Popover',
@@ -110,164 +115,150 @@ export const Placements = {
110115
],
111116
};
112117

113-
export const WithUpdatingChildren = () => {
114-
const anchorRef = useRef(null);
115-
const [isOpen, setIsOpen] = useState(false);
116-
117-
const toggleOpen = () => setIsOpen(!isOpen);
118-
119-
const [text, setText] = useState('Long loading text with useless words');
120-
useEffect(() => {
121-
if (isOpen) {
122-
const timer = setTimeout(() => {
123-
setText('Text');
124-
}, 1000);
125-
return () => clearTimeout(timer);
126-
}
127-
setText('Long loading text with useless words');
128-
return undefined;
129-
}, [isOpen]);
130-
131-
return (
132-
<div style={{ float: 'right' }} className="lumx-spacing-margin-right-huge">
133-
<IconButton
134-
label="Notifications"
135-
className="lumx-spacing-margin-right-huge"
136-
ref={anchorRef}
137-
emphasis={Emphasis.low}
138-
icon={mdiBell}
139-
size={Size.m}
140-
onClick={toggleOpen}
141-
/>
142-
<Popover
143-
closeOnClickAway
144-
closeOnEscape
145-
isOpen={isOpen}
146-
anchorRef={anchorRef}
147-
placement={Placement.BOTTOM_END}
148-
onClose={toggleOpen}
149-
fitWithinViewportHeight
150-
>
151-
<List>
152-
<ListItem before={<Icon icon={mdiAccount} />} className="lumx-spacing-margin-right-huge">
153-
<span>{text}</span>
154-
</ListItem>
155-
</List>
156-
</Popover>
157-
</div>
158-
);
118+
/**
119+
* Demo all fitAnchorWidth configurations
120+
*/
121+
export const FitToAnchorWidth = {
122+
render({ anchorText, fitAnchorWidth }: any) {
123+
const anchorRef = useRef(null);
124+
return (
125+
<>
126+
<Chip className="lumx-spacing-margin-huge" ref={anchorRef} size="s">
127+
{anchorText}
128+
</Chip>
129+
<Popover
130+
isOpen
131+
className="lumx-spacing-padding"
132+
placement="top"
133+
anchorRef={anchorRef}
134+
fitToAnchorWidth={fitAnchorWidth}
135+
>
136+
Popover {fitAnchorWidth}
137+
</Popover>
138+
</>
139+
);
140+
},
141+
decorators: [
142+
withCombinations({
143+
combinations: {
144+
cols: {
145+
'Small Anchor': { anchorText: 'Small' },
146+
'Large Anchor': { anchorText: 'Very very very very large anchor' },
147+
},
148+
rows: { key: 'fitAnchorWidth', options: withUndefined(Object.values(FitAnchorWidth)) },
149+
},
150+
cellStyle: { padding: 16 },
151+
}),
152+
],
159153
};
160154

161-
export const WithScrollingPopover = () => {
162-
const anchorRef = useRef(null);
163-
const [isOpen, setIsOpen] = useState(false);
155+
/**
156+
* Testing update of the popover on anchor and popover resize and move
157+
*/
158+
export const TestUpdatingChildrenAndMovingAnchor = {
159+
render() {
160+
const anchorRef = useRef(null);
161+
const [isOpen, setIsOpen] = useState(false);
162+
163+
const toggleOpen = () => setIsOpen(!isOpen);
164164

165-
const toggleOpen = () => setIsOpen(!isOpen);
165+
const [text, setText] = useState('Initial large span of text');
166+
const [anchorSize, setAnchorSize] = useState<IconButtonProps['size']>('m');
167+
useEffect(() => {
168+
if (isOpen) {
169+
const timers = [
170+
// Update popover size
171+
setTimeout(() => setText('Text'), 1000),
172+
// Update anchor size
173+
setTimeout(() => setAnchorSize('s'), 1000),
174+
];
175+
return () => timers.forEach(clearTimeout);
176+
}
177+
setText('Initial large span of text');
178+
setAnchorSize('m');
179+
return undefined;
180+
}, [isOpen]);
166181

167-
return (
168-
<div style={{ float: 'right' }} className="lumx-spacing-margin-right-huge">
169-
<IconButton
170-
label="Notifications"
171-
className="lumx-spacing-margin-right-huge"
172-
ref={anchorRef}
173-
emphasis={Emphasis.low}
174-
icon={mdiBell}
175-
size={Size.m}
176-
onClick={toggleOpen}
177-
/>
178-
<Popover
179-
closeOnClickAway
180-
closeOnEscape
181-
isOpen={isOpen}
182-
anchorRef={anchorRef}
183-
placement={Placement.BOTTOM}
184-
onClose={toggleOpen}
185-
fitWithinViewportHeight
186-
>
187-
<List style={{ overflowY: 'auto' }}>
188-
{range(100).map((n: number) => {
189-
return (
190-
<ListItem
191-
key={`key-${n}`}
192-
before={<Icon icon={mdiAccount} />}
193-
className="lumx-spacing-margin-right-huge"
194-
>
195-
<span>{`List item ${n} and some text`}</span>
196-
</ListItem>
197-
);
198-
})}
199-
</List>
200-
</Popover>
201-
</div>
202-
);
182+
return (
183+
<FlexBox orientation="vertical" gap="huge">
184+
<Message kind="info">Test popover text resize (after 1sec) and anchor resize (after 1.5sec)</Message>
185+
<FlexBox orientation="horizontal" vAlign="center">
186+
<IconButton
187+
label="Notifications"
188+
className="lumx-spacing-margin-right-huge"
189+
ref={anchorRef}
190+
emphasis={Emphasis.low}
191+
icon={mdiBell}
192+
size={anchorSize}
193+
onClick={toggleOpen}
194+
/>
195+
<Popover
196+
closeOnClickAway
197+
closeOnEscape
198+
isOpen={isOpen}
199+
anchorRef={anchorRef}
200+
placement={Placement.BOTTOM_END}
201+
onClose={toggleOpen}
202+
fitWithinViewportHeight
203+
hasArrow
204+
>
205+
<Text as="p" className="lumx-spacing-padding-huge">
206+
{text}
207+
</Text>
208+
</Popover>
209+
</FlexBox>
210+
</FlexBox>
211+
);
212+
},
213+
parameters: { chromatic: { disable: true } },
203214
};
204215

205-
export const FitToAnchorWidth = () => {
206-
const demoPopperStyle = {
207-
alignItems: 'center',
208-
display: 'flex',
209-
height: 100,
210-
justifyContent: 'center',
211-
width: 200,
212-
};
213-
214-
const container = {
215-
alignItems: 'center',
216-
display: 'flex',
217-
justifyContent: 'center',
218-
flexDirection: 'column',
219-
gap: 150,
220-
marginTop: 150,
221-
} as const;
216+
/**
217+
* Testing popover with scroll inside
218+
*/
219+
export const TestScrollingPopover = {
220+
render() {
221+
const anchorRef = useRef(null);
222+
const [isOpen, setIsOpen] = useState(false);
222223

223-
const maxWidthAnchorRef = useRef(null);
224-
const widthSmallAnchorRef = useRef(null);
225-
const widthLargeAnchorRef = useRef(null);
226-
const minWidthAnchorRef = useRef(null);
227-
const defaultWidthAnchorRef = useRef(null);
224+
const toggleOpen = () => setIsOpen(!isOpen);
228225

229-
return (
230-
<div style={container}>
231-
<div>
232-
<Chip ref={maxWidthAnchorRef} size={Size.s}>
233-
Anchor
234-
</Chip>
235-
</div>
236-
<Popover anchorRef={maxWidthAnchorRef} fitToAnchorWidth="maxWidth" isOpen placement="top">
237-
<div style={demoPopperStyle}>Popover maxWidth</div>
238-
</Popover>
239-
<div>
240-
<Chip ref={widthSmallAnchorRef} size={Size.s}>
241-
Anchor
242-
</Chip>
243-
</div>
244-
<Popover anchorRef={widthSmallAnchorRef} fitToAnchorWidth="width" isOpen placement="top">
245-
<div style={demoPopperStyle}>Popover width small anchor</div>
246-
</Popover>
247-
<div>
248-
<Chip ref={widthLargeAnchorRef} size={Size.s}>
249-
VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLargeAnchor
250-
</Chip>
251-
</div>
252-
<Popover anchorRef={widthLargeAnchorRef} fitToAnchorWidth="width" isOpen placement="top">
253-
<div style={demoPopperStyle}>Popover width large anchor</div>
254-
</Popover>
255-
<div>
256-
<Chip ref={minWidthAnchorRef} size={Size.s}>
257-
VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLargeAnchor
258-
</Chip>
259-
</div>
260-
<Popover anchorRef={minWidthAnchorRef} fitToAnchorWidth="minWidth" isOpen placement="top">
261-
<div style={demoPopperStyle}>Popover minWidth</div>
262-
</Popover>
263-
<div>
264-
<Chip ref={defaultWidthAnchorRef} size={Size.s}>
265-
VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLargeAnchor
266-
</Chip>
226+
return (
227+
<div style={{ float: 'right' }} className="lumx-spacing-margin-right-huge">
228+
<IconButton
229+
label="Notifications"
230+
className="lumx-spacing-margin-right-huge"
231+
ref={anchorRef}
232+
emphasis={Emphasis.low}
233+
icon={mdiBell}
234+
size={Size.m}
235+
onClick={toggleOpen}
236+
/>
237+
<Popover
238+
closeOnClickAway
239+
closeOnEscape
240+
isOpen={isOpen}
241+
anchorRef={anchorRef}
242+
placement={Placement.BOTTOM_END}
243+
onClose={toggleOpen}
244+
fitWithinViewportHeight
245+
>
246+
<List style={{ overflowY: 'auto' }}>
247+
{range(100).map((n: number) => {
248+
return (
249+
<ListItem
250+
key={`key-${n}`}
251+
before={<Icon icon={mdiAccount} />}
252+
className="lumx-spacing-margin-right-huge"
253+
>
254+
<span>{`List item ${n} and some text`}</span>
255+
</ListItem>
256+
);
257+
})}
258+
</List>
259+
</Popover>
267260
</div>
268-
<Popover anchorRef={defaultWidthAnchorRef} isOpen placement="top">
269-
<div style={demoPopperStyle}>Popover default</div>
270-
</Popover>
271-
</div>
272-
);
261+
);
262+
},
263+
parameters: { chromatic: { disable: true } },
273264
};

packages/lumx-react/src/components/popover/Popover.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,6 @@ const _InnerPopover = forwardRef<PopoverProps, HTMLDivElement>((props, ref) => {
137137
fitWithinViewportHeight,
138138
boundaryRef,
139139
anchorRef,
140-
children,
141140
placement,
142141
style,
143142
zIndex,

packages/lumx-react/src/components/popover/usePopoverStyle.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ type Options = Pick<
7474
| 'fitWithinViewportHeight'
7575
| 'boundaryRef'
7676
| 'anchorRef'
77-
| 'children'
7877
| 'placement'
7978
| 'style'
8079
| 'zIndex'
@@ -97,7 +96,6 @@ export function usePopoverStyle({
9796
fitWithinViewportHeight,
9897
boundaryRef,
9998
anchorRef,
100-
children,
10199
placement,
102100
style,
103101
zIndex,
@@ -136,9 +134,23 @@ export function usePopoverStyle({
136134
}
137135

138136
const { styles, attributes, state, update } = usePopper(anchorRef.current, popperElement, { placement, modifiers });
137+
138+
// Auto update popover
139139
useEffect(() => {
140-
update?.();
141-
}, [children, update]);
140+
const { current: anchorElement } = anchorRef;
141+
if (!update || !popperElement || !anchorElement || !WINDOW?.ResizeObserver) {
142+
return undefined;
143+
}
144+
update();
145+
146+
// On anchor or popover resize
147+
const resizeObserver = new ResizeObserver(update);
148+
resizeObserver.observe(anchorElement);
149+
resizeObserver.observe(popperElement);
150+
return () => {
151+
resizeObserver.disconnect();
152+
};
153+
}, [anchorRef, popperElement, update]);
142154

143155
const position = state?.placement ?? placement;
144156

0 commit comments

Comments
 (0)