Skip to content

Commit 2d40eba

Browse files
Update Popover Reference element on rerender (#2833)
* compare to prevReferenceElement * Adds tests for useReferenceElement * Create popover-ref-el.md * rm referenceElement from deps * add lodash
1 parent f99daf8 commit 2d40eba

File tree

7 files changed

+98
-10
lines changed

7 files changed

+98
-10
lines changed

.changeset/popover-ref-el.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@leafygreen-ui/popover': patch
3+
---
4+
5+
Updates reference element positioning logic to ensure the correct element is used as a reference in cases where the element `ref` or DOM node changes in between renders.

packages/popover/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@leafygreen-ui/portal": "workspace:^",
3030
"@leafygreen-ui/tokens": "workspace:^",
3131
"@types/react-transition-group": "^4.4.5",
32+
"lodash": "^4.17.21",
3233
"react-transition-group": "^4.4.5"
3334
},
3435
"devDependencies": {

packages/popover/src/Popover/Popover.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@ import {
1515
getWindowSafePlacementValues,
1616
} from '../utils/positionUtils';
1717

18-
import {
19-
useContentNode,
20-
usePopoverProps,
21-
useReferenceElement,
22-
} from './Popover.hooks';
18+
import { useContentNode, usePopoverProps, useReferenceElement } from './hooks';
2319
import {
2420
contentClassName,
2521
getPopoverStyles,
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { createRef, MutableRefObject } from 'react';
2+
import { act, renderHook } from '@testing-library/react';
3+
4+
import { useReferenceElement } from './Popover.hooks';
5+
6+
describe('packages/popover/hooks', () => {
7+
describe('useReferenceElement', () => {
8+
test('if a `refEl` is provided, the ref value is used as the reference element', () => {
9+
const testRef: MutableRefObject<HTMLDivElement | null> =
10+
createRef<HTMLDivElement>();
11+
testRef.current = document.createElement('div');
12+
13+
const { result } = renderHook(() => useReferenceElement(testRef));
14+
15+
expect(result.current.referenceElement).toBe(testRef.current);
16+
});
17+
18+
test('if the `refEl` changes between renders, the reference element should update', () => {
19+
const testRef: MutableRefObject<HTMLDivElement | null> =
20+
createRef<HTMLDivElement>();
21+
testRef.current = document.createElement('div');
22+
testRef.current.setAttribute('data-testid', 'testRef');
23+
24+
const { result, rerender } = renderHook(
25+
({ refEl }) => useReferenceElement(refEl),
26+
{
27+
initialProps: { refEl: testRef },
28+
},
29+
);
30+
31+
const newRef: MutableRefObject<HTMLDivElement | null> =
32+
createRef<HTMLDivElement>();
33+
newRef.current = document.createElement('div');
34+
newRef.current.setAttribute('data-testid', 'newRef');
35+
36+
rerender({ refEl: newRef });
37+
expect(result.current.referenceElement).toBe(newRef.current);
38+
});
39+
40+
test('if `refEl.current` changes between renders, the reference element should update', () => {
41+
const testRef: MutableRefObject<HTMLDivElement | null> =
42+
createRef<HTMLDivElement>();
43+
testRef.current = document.createElement('div');
44+
testRef.current.setAttribute('data-testid', 'testRef');
45+
46+
const { result, rerender } = renderHook(
47+
({ refEl }) => useReferenceElement(refEl),
48+
{
49+
initialProps: { refEl: testRef },
50+
},
51+
);
52+
testRef.current = document.createElement('div');
53+
testRef.current.setAttribute('data-testid', 'testRef2');
54+
55+
rerender({ refEl: testRef });
56+
expect(result.current.referenceElement).toBe(testRef.current);
57+
});
58+
59+
test('if a `refEl` is not provided, the parent element of the placeholder is used as the reference element', () => {
60+
const { result } = renderHook(() => useReferenceElement());
61+
const placeholderElement = document.createElement('span');
62+
const parentElement = document.createElement('div');
63+
parentElement.appendChild(placeholderElement);
64+
65+
act(() => {
66+
result.current.setPlaceholderElement(placeholderElement);
67+
});
68+
69+
expect(result.current.referenceElement).toBe(parentElement);
70+
});
71+
});
72+
});

packages/popover/src/Popover/Popover.hooks.tsx renamed to packages/popover/src/Popover/hooks/Popover.hooks.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
import React, { useMemo, useRef, useState } from 'react';
2+
import isEqual from 'lodash/isEqual';
23

34
import {
45
useIsomorphicLayoutEffect,
56
useObjectDependency,
7+
usePrevious,
68
} from '@leafygreen-ui/hooks';
79
import {
810
useMigrationContext,
911
usePopoverPortalContainer,
1012
usePopoverPropsContext,
1113
} from '@leafygreen-ui/leafygreen-provider';
1214

13-
import { getElementDocumentPosition } from '../utils/positionUtils';
14-
15+
import { getElementDocumentPosition } from '../../utils/positionUtils';
1516
import {
1617
PopoverProps,
1718
RenderMode,
1819
UseContentNodeReturnObj,
1920
UseReferenceElementReturnObj,
20-
} from './Popover.types';
21+
} from '../Popover.types';
2122

2223
/**
2324
* This hook handles logic for determining what prop values are used for the `Popover`
@@ -129,9 +130,18 @@ export function useReferenceElement(
129130
null,
130131
);
131132

133+
const prevReferenceElement = usePrevious(refEl?.current);
134+
135+
// If the DOM element has changed, we need to update the reference element.
136+
// Store this variable so we can trigger the useLayoutEffect
137+
const didRefElementChange = !isEqual(prevReferenceElement, refEl?.current);
138+
132139
useIsomorphicLayoutEffect(() => {
133140
if (refEl && refEl.current) {
134-
setReferenceElement(refEl.current);
141+
if (didRefElementChange) {
142+
setReferenceElement(refEl.current);
143+
}
144+
135145
return;
136146
}
137147

@@ -142,7 +152,7 @@ export function useReferenceElement(
142152
setReferenceElement(maybeParentEl);
143153
return;
144154
}
145-
}, [placeholderElement, refEl]);
155+
}, [didRefElementChange, placeholderElement, refEl]);
146156

147157
const referenceElDocumentPos = useObjectDependency(
148158
useMemo(
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Popover.hooks';

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)