Updated useShadowRootElements to use the shadow root first child instead of flipping booleans to help with unneeded updates when the DOM doesn't change

Signed-off-by: Alex Lorenzi <alorenzi@spotify.com>
This commit is contained in:
Alex Lorenzi
2024-08-21 13:57:26 -04:00
parent f54d3a5385
commit e46dfec74d
4 changed files with 110 additions and 31 deletions
+51 -2
View File
@@ -19,7 +19,7 @@ import {
useShadowRootElements,
useShadowRootSelection,
} from './hooks';
import { renderHook } from '@testing-library/react';
import { act, renderHook } from '@testing-library/react';
import { fireEvent, waitFor } from '@testing-library/react';
const fireSelectionChangeEvent = (window: Window) => {
@@ -34,7 +34,7 @@ const getSelection = jest.fn();
const mockShadowRoot = () => {
const div = document.createElement('div');
const shadowRoot = div.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = '<h1>Shadow DOM Mock</h1>';
shadowRoot.innerHTML = '<div><h1>Shadow DOM Mock</h1></div>';
(shadowRoot as ShadowRoot & Pick<Document, 'getSelection'>).getSelection =
getSelection;
return shadowRoot;
@@ -85,6 +85,55 @@ describe('hooks', () => {
expect(result.current).toHaveLength(1);
});
it('should update elements if shadow root changes', async () => {
const { result, rerender } = renderHook(() =>
useShadowRootElements(['h1']),
);
act(() => {
shadowRoot.innerHTML = '<div><h1>Updated Shadow DOM Mock</h1></div>';
rerender();
});
await waitFor(() => {
expect(result.current[0].textContent).toBe('Updated Shadow DOM Mock');
});
});
describe('mutation observer', () => {
const observer: jest.Mocked<MutationObserver> = {
observe: jest.fn(),
disconnect: jest.fn(),
takeRecords: jest.fn(),
};
beforeEach(() => {
jest
.spyOn(window, 'MutationObserver')
.mockImplementation(() => observer);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should observe shadow root changes', async () => {
renderHook(() => useShadowRootElements(['h1']));
expect(observer.observe).toHaveBeenCalledWith(shadowRoot, {
childList: true,
attributes: true,
characterData: true,
subtree: true,
});
});
it('should disconnect observer on unmount', async () => {
const { unmount } = renderHook(() => useShadowRootElements(['h1']));
unmount();
expect(observer.disconnect).toHaveBeenCalled();
});
});
});
describe('useShadowRootSelection', () => {
+5 -5
View File
@@ -39,13 +39,13 @@ export const useShadowRootElements = <
selectors: string[],
): TReturnedElement[] => {
const shadowRoot = useShadowRoot();
const [render, rerender] = useState(false);
const [root, setRootNode] = useState(shadowRoot?.firstChild);
useEffect(() => {
let observer: MutationObserver;
if (shadowRoot) {
observer = new MutationObserver(() => {
rerender(!render);
setRootNode(shadowRoot?.firstChild);
});
observer.observe(shadowRoot, {
attributes: true,
@@ -55,12 +55,12 @@ export const useShadowRootElements = <
});
}
return () => observer?.disconnect();
}, [shadowRoot, render, rerender]);
}, [shadowRoot]);
if (!shadowRoot) return [];
if (!root || !(root instanceof HTMLElement)) return [];
return selectors
.map(selector => shadowRoot.querySelectorAll<TReturnedElement>(selector))
.map(selector => root.querySelectorAll<TReturnedElement>(selector))
.filter(nodeList => nodeList.length)
.map(nodeList => Array.from(nodeList))
.flat();