Skip to content

Commit 5ac4734

Browse files
authored
fix: add connectedMoveCallback to the WC implementation (#170)
1 parent 391a75f commit 5ac4734

2 files changed

Lines changed: 101 additions & 14 deletions

File tree

sdks/typescript/client/src/components/UIResourceRendererWC.tsx

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,40 @@ export const UIResourceRendererWCWrapper: FC<UIResourceRendererWCProps> = (props
6969
);
7070
};
7171

72-
customElements.define(
73-
'ui-resource-renderer',
74-
r2wc(UIResourceRendererWCWrapper, {
75-
props: {
76-
resource: 'json',
77-
supportedContentTypes: 'json',
78-
htmlProps: 'json',
79-
remoteDomProps: 'json',
80-
/* `onUIAction` is intentionally omitted as the WC implements its own event dispatching mechanism for UI actions.
81-
* Consumers should listen for the `onUIAction` CustomEvent on the element instead of passing an `onUIAction` prop.
82-
*/
83-
},
84-
}),
85-
);
72+
// Get the base web component class from r2wc
73+
const BaseUIResourceRendererWC = r2wc(UIResourceRendererWCWrapper, {
74+
props: {
75+
resource: 'json',
76+
supportedContentTypes: 'json',
77+
htmlProps: 'json',
78+
remoteDomProps: 'json',
79+
/* `onUIAction` is intentionally omitted as the WC implements its own event dispatching mechanism for UI actions.
80+
* Consumers should listen for the `onUIAction` CustomEvent on the element instead of passing an `onUIAction` prop.
81+
*/
82+
},
83+
});
84+
85+
/**
86+
* Extended web component class that implements connectedMoveCallback.
87+
*
88+
* When an element is moved in the DOM using moveBefore(), browsers that support
89+
* the "atomic move" feature (https://developer.chrome.com/blog/movebefore-api)
90+
* will call connectedMoveCallback instead of disconnectedCallback/connectedCallback.
91+
*
92+
* By implementing an empty connectedMoveCallback, we signal that the component
93+
* should preserve its internal state (including iframe content) when moved,
94+
* rather than being fully torn down and recreated.
95+
*/
96+
class UIResourceRendererWC extends BaseUIResourceRendererWC {
97+
/**
98+
* Called when the element is moved via moveBefore() in browsers that support atomic moves.
99+
* By implementing this method (even as a no-op), we prevent the element from being
100+
* disconnected and reconnected, which would cause iframes to reload and lose state.
101+
*/
102+
connectedMoveCallback() {
103+
// Intentionally empty - by implementing this callback, we opt into atomic move behavior
104+
// and prevent the iframe from reloading when the element is repositioned in the DOM.
105+
}
106+
}
107+
108+
customElements.define('ui-resource-renderer', UIResourceRendererWC);

sdks/typescript/client/src/components/__tests__/UIResourceRendererWC.test.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,68 @@ describe('UIResourceRendererWC', () => {
8484
const dispatchedEvent = onUIAction.mock.calls[0][0] as CustomEvent;
8585
expect(dispatchedEvent.detail).toEqual(mockEventPayload);
8686
});
87+
88+
describe('connectedMoveCallback (atomic move support)', () => {
89+
it('should implement connectedMoveCallback method', () => {
90+
const el = document.createElement('ui-resource-renderer');
91+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
92+
expect(typeof (el as any).connectedMoveCallback).toBe('function');
93+
});
94+
95+
it('connectedMoveCallback should be callable without throwing', () => {
96+
const el = document.createElement('ui-resource-renderer');
97+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
98+
expect(() => (el as any).connectedMoveCallback()).not.toThrow();
99+
});
100+
101+
it('should allow element to be moved between containers (simulating moveBefore)', async () => {
102+
const el = document.createElement('ui-resource-renderer');
103+
const container1 = document.createElement('div');
104+
const container2 = document.createElement('div');
105+
106+
document.body.appendChild(container1);
107+
document.body.appendChild(container2);
108+
container1.appendChild(el);
109+
110+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
111+
(el as any).resource = resource;
112+
113+
// Wait for the component to render
114+
await waitFor(() => {
115+
expect(UIResourceRenderer).toHaveBeenCalled();
116+
});
117+
118+
// Verify initial position
119+
expect(el.parentElement).toBe(container1);
120+
121+
// In browsers that support moveBefore with atomic moves:
122+
// - moveBefore() is called
123+
// - connectedMoveCallback is invoked instead of disconnectedCallback/connectedCallback
124+
// - The element preserves its state (iframes don't reload)
125+
//
126+
// Here we verify that connectedMoveCallback doesn't throw and the element can be moved.
127+
// Full atomic move behavior requires browser support that jsdom doesn't have.
128+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
129+
expect(() => (el as any).connectedMoveCallback()).not.toThrow();
130+
131+
// Move the element to a new container
132+
container2.appendChild(el);
133+
134+
// Verify element moved successfully
135+
expect(el.parentElement).toBe(container2);
136+
expect(container1.contains(el)).toBe(false);
137+
expect(container2.contains(el)).toBe(true);
138+
});
139+
140+
it('element should be an instance of the extended class with connectedMoveCallback', () => {
141+
const el = document.createElement('ui-resource-renderer');
142+
143+
// Verify the element has the connectedMoveCallback method
144+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
145+
expect('connectedMoveCallback' in el).toBe(true);
146+
147+
// The element should be an HTMLElement
148+
expect(el instanceof HTMLElement).toBe(true);
149+
});
150+
});
87151
});

0 commit comments

Comments
 (0)