Skip to content

Commit

Permalink
Get modal recording and replay working
Browse files Browse the repository at this point in the history
  • Loading branch information
Juice10 committed Jun 12, 2024
1 parent 29d4827 commit dcc867c
Show file tree
Hide file tree
Showing 14 changed files with 407 additions and 71 deletions.
34 changes: 13 additions & 21 deletions packages/rrdom/src/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,15 +291,20 @@ function diffAfterUpdatingChildren(
const rrDialog = newRRElement as unknown as RRDialogElement;
const wasOpen = dialog.open;
const wasModal = dialog.matches('dialog:modal');
const isOpen = rrDialog.open;
const { isModal } = rrDialog;
const isOpen = typeof rrDialog.getAttribute('open') === 'string';
const isModal = rrDialog.getAttribute('rr_open') === 'modal';

const modeChanged = (wasModal && !isModal) || (!wasModal && isModal);
const modeChanged = wasModal !== isModal;
const openChanged = wasOpen !== isOpen;

if (wasOpen && modeChanged) dialog.close();
if ((wasOpen && modeChanged) || (isOpen && !wasOpen)) {
if (isModal) dialog.showModal();
else dialog.show();
if (modeChanged || wasOpen !== isOpen) dialog.close();
if (isOpen && (openChanged || modeChanged)) {
try {
if (isModal) dialog.showModal();
else dialog.show();
} catch (e) {
console.warn(e);
}
}

break;
Expand Down Expand Up @@ -349,23 +354,10 @@ function diffProps(
}
};
} else if (newTree.tagName === 'IFRAME' && name === 'srcdoc') continue;
else if (
newTree.tagName === 'DIALOG' &&
(name === 'rr_open' || name === 'open')
) {
const rrDialog = newTree as RRDialogElement;
const isModal = newAttributes.rr_open === 'modal';
const isOpen = isModal || newAttributes.open === '';
if (isModal) rrDialog.showModal();
else if (isOpen) rrDialog.show();
else rrDialog.close();
continue;
} else oldTree.setAttribute(name, newValue);
else oldTree.setAttribute(name, newValue);
}

for (const { name } of Array.from(oldAttributes)) {
if (newTree.tagName === 'DIALOG' && (name === 'rr_open' || name === 'open'))
continue; // attributes are handled in diffAfterUpdatingChildren for Dialog elements
if (!(name in newAttributes)) oldTree.removeAttribute(name);
}
newTree.scrollLeft && (oldTree.scrollLeft = newTree.scrollLeft);
Expand Down
2 changes: 1 addition & 1 deletion packages/rrdom/src/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ export class BaseRRElement extends BaseRRNode implements IRRElement {
}

public getAttribute(name: string): string | null {
return this.attributes[name] || null;
return this.attributes[name] ?? null;
}

public setAttribute(name: string, attribute: string) {
Expand Down
6 changes: 3 additions & 3 deletions packages/rrdom/test/diff/dialog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('diff algorithm for rrdom', () => {

const rrDocument = new RRDocument();
const rrNode = rrDocument.createElement(tagName);
rrNode.attributes = { rr_open: 'modal' };
rrNode.attributes = { rr_open: 'modal', open: '' };

mirror.add(node, elementSn);
rrDocument.mirror.add(rrNode, elementSn);
Expand All @@ -75,7 +75,6 @@ describe('diff algorithm for rrdom', () => {
const tagName = 'DIALOG';
const node = document.createElement(tagName) as HTMLDialogElement;
node.showModal();
console.log('node', { x: node.getAttribute('open') });
vi.spyOn(node, 'matches').mockReturnValue(true); // matches is used to check if the dialog was opened with showModal
const closeFn = vi.spyOn(node, 'close');

Expand All @@ -93,13 +92,14 @@ describe('diff algorithm for rrdom', () => {
it('should not trigger `close` on rr_open is kept', () => {
const tagName = 'DIALOG';
const node = document.createElement(tagName) as HTMLDialogElement;
vi.spyOn(node, 'matches').mockReturnValue(true); // matches is used to check if the dialog was opened with showModal
node.setAttribute('rr_open', 'modal');
node.setAttribute('open', '');
const closeFn = vi.spyOn(node, 'close');

const rrDocument = new RRDocument();
const rrNode = rrDocument.createElement(tagName);
rrNode.attributes = { rr_open: 'modal' };
rrNode.attributes = { rr_open: 'modal', open: '' };

mirror.add(node, elementSn);
rrDocument.mirror.add(rrNode, elementSn);
Expand Down
14 changes: 7 additions & 7 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -706,13 +706,13 @@ function serializeElementNode(
}
}

if (
tagName === 'dialog' &&
(n as HTMLDialogElement).open &&
n.matches('dialog:modal')
) {
(attributes as DialogAttributes).rr_open = 'modal';
delete attributes.open; // prevent default `show()` behavior which blocks `showModal()` from working
if (tagName === 'dialog' && (n as HTMLDialogElement).open) {
// register what type of dialog is this
// `modal` or `non-modal`
// this is used to trigger `showModal()` or `show()` on replay (outside of rrweb-snapshot, in rrweb)
(attributes as DialogAttributes).rr_open = n.matches('dialog:modal')
? 'modal'
: 'non-modal';
}

// canvas image data
Expand Down
19 changes: 11 additions & 8 deletions packages/rrweb-snapshot/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,17 @@ export type mediaAttributes = {
rr_mediaVolume?: number;
};

export type DialogAttributes =
// | {
// open: '';
// }
{
rr_open: 'modal';
// rr_open_index?: number;
};
export type DialogAttributes = {
open: string;
/**
* Represents the dialog's open mode.
* `modal` means the dialog is opened with `showModal()`.
* `non-modal` means the dialog is opened with `show()` or
* by adding an `open` attribute.
*/
rr_open: 'modal' | 'non-modal';
// rr_open_index?: number;
};

// @deprecated
export interface INode extends Node {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ exports[`dialog integration tests > should capture open attribute for modal dial
\\"type\\": 2,
\\"tagName\\": \\"dialog\\",
\\"attributes\\": {
\\"open\\": \\"\\",
\\"rr_open\\": \\"modal\\"
},
\\"childNodes\\": [
Expand Down Expand Up @@ -88,7 +89,8 @@ exports[`dialog integration tests > should capture open attribute for non modal
\\"type\\": 2,
\\"tagName\\": \\"dialog\\",
\\"attributes\\": {
\\"open\\": \\"\\"
\\"open\\": \\"\\",
\\"rr_open\\": \\"non-modal\\"
},
\\"childNodes\\": [
{
Expand Down
6 changes: 6 additions & 0 deletions packages/rrweb/src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,12 @@ export default class MutationBuffer {
item.styleDiff[pname] = false; // delete
}
}
} else if (attributeName === 'open' && target.tagName === 'DIALOG') {
if (target.matches('dialog:modal')) {
item.attributes['rr_open'] = 'modal';
} else {
item.attributes['rr_open'] = 'non-modal';
}
}
}
break;
Expand Down
30 changes: 22 additions & 8 deletions packages/rrweb/src/replay/dialog/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,39 @@ import { RRNode } from 'rrdom';

export function triggerShowModalForModals(
node: HTMLDialogElement | Node | RRNode,
attributeMutation?: attributeMutation,
) {
if (node.nodeName !== 'DIALOG' || node instanceof RRNode) return;
const dialog = node as HTMLDialogElement;
if (dialog.getAttribute('rr_open') !== 'modal') return;
const isOpen = dialog.open;
const isModal = isOpen && dialog.matches('dialog:modal');

const shouldBeOpen =
typeof attributeMutation?.attributes.open === 'string' ||
typeof dialog.getAttribute('open') === 'string';
const shouldBeModal = dialog.getAttribute('rr_open') === 'modal';
const shouldBeNonModal = dialog.getAttribute('rr_open') === 'non-modal';
const modeChanged =
(isModal && shouldBeNonModal) || (!isModal && shouldBeModal);

if (isOpen && !modeChanged) return;
// complain if dialog is not attached to the dom
if (!dialog.isConnected) {
console.warn('dialog is not attached to the dom', dialog);
return;
}

dialog.showModal();
if (isOpen) dialog.close();

if (!shouldBeOpen) return;

if (shouldBeModal) dialog.showModal();
else dialog.show();
}

export function triggerCloseForModals(
node: HTMLDialogElement | Node | RRNode,
attributeMuation: attributeMutation,
attributeMutation: attributeMutation,
) {
if (node.nodeName !== 'DIALOG' || node instanceof RRNode) return;
const dialog = node as HTMLDialogElement;
Expand All @@ -30,10 +46,8 @@ export function triggerCloseForModals(
return;
}

if (attributeMuation.attributes.rr_open === null) {
dialog.close();
}
if (attributeMuation.attributes.open === '') {
dialog.show();
if (attributeMutation.attributes.open === null) {
dialog.removeAttribute('open');
dialog.removeAttribute('rr_open');
}
}
12 changes: 4 additions & 8 deletions packages/rrweb/src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1743,10 +1743,10 @@ export class Replayer {
if (typeof attributeName === 'string') {
const value = mutation.attributes[attributeName];
if (value === null) {
(target as Element | RRElement).removeAttribute(attributeName);
if (attributeName === 'rr_open') {
if (attributeName === 'open') {
triggerCloseForModals(target, mutation);
}
(target as Element | RRElement).removeAttribute(attributeName);
} else if (typeof value === 'string') {
try {
// When building snapshot, some link styles haven't loaded. Then they are loaded, they will be inlined as incremental mutation change of attribute. We need to replace the old elements whose styles aren't inlined.
Expand Down Expand Up @@ -1803,12 +1803,8 @@ export class Replayer {
);
}

if (
attributeName === 'rr_open' &&
target.nodeName === 'DIALOG' &&
value === 'modal'
) {
triggerShowModalForModals(target);
if (attributeName === 'rr_open' && target.nodeName === 'DIALOG') {
triggerShowModalForModals(target, mutation);
}
} catch (error) {
this.warn(
Expand Down
20 changes: 12 additions & 8 deletions packages/rrweb/test/events/dialog-playback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ const events: eventWithTime[] = [
attributes: [
{
id: 27,
attributes: { open: '' },
attributes: { open: '', rr_open: 'non-modal', class: 'show' },
},
],
},
Expand All @@ -149,7 +149,7 @@ const events: eventWithTime[] = [
attributes: [
{
id: 27,
attributes: { open: null },
attributes: { open: null, class: 'closed' },
},
],
},
Expand All @@ -166,7 +166,7 @@ const events: eventWithTime[] = [
attributes: [
{
id: 27,
attributes: { rr_open: 'modal' },
attributes: { rr_open: 'modal', open: '', class: 'showModal' },
},
],
},
Expand All @@ -184,8 +184,7 @@ const events: eventWithTime[] = [
{
id: 27,
attributes: {
rr_open: null,
open: '',
rr_open: 'non-modal',
class: 'switched-from-show-modal-to-show',
},
},
Expand All @@ -206,7 +205,6 @@ const events: eventWithTime[] = [
{
id: 27,
attributes: {
open: null,
rr_open: 'modal',
class: 'switched-from-show-to-show-modal',
},
Expand Down Expand Up @@ -287,6 +285,7 @@ const events: eventWithTime[] = [
tagName: 'dialog',
attributes: {
open: '',
rr_open: 'non-modal',
style: 'outline: blue solid 1px;',
},
childNodes: [{ type: 3, textContent: 'Dialog 1', id: 25 }],
Expand Down Expand Up @@ -386,7 +385,9 @@ const events: eventWithTime[] = [
tagName: 'dialog',
attributes: {
rr_open: 'modal',
open: '',
style: 'outline: blue solid 1px;',
class: 'existing-1',
},
childNodes: [{ type: 3, textContent: 'Dialog 1', id: 25 }],
id: 24,
Expand All @@ -397,6 +398,7 @@ const events: eventWithTime[] = [
tagName: 'dialog',
attributes: {
style: 'outline: red solid 1px;',
class: 'existing-2',
},
childNodes: [{ type: 3, textContent: 'Dialog 2', id: 28 }],
id: 27,
Expand All @@ -423,14 +425,16 @@ const events: eventWithTime[] = [
adds: [
{
parentId: 22,
previousId: 27,
nextId: 31,
previousId: 23,
nextId: 24,
node: {
type: 2,
tagName: 'dialog',
attributes: {
rr_open: 'modal',
open: '',
style: 'outline: orange solid 1px;',
class: 'new-dialog',
},
childNodes: [],
id: 32,
Expand Down
5 changes: 5 additions & 0 deletions packages/rrweb/test/html/dialog.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<html>
<body>
<dialog>I'm a dialog</dialog>
</body>
</html>
Loading

0 comments on commit dcc867c

Please sign in to comment.