rmariuzzo / react-new-window

🔲 Pop new windows in React, using `window.open`.

Home Page:https://rmariuzzo.github.io/react-new-window/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Issue with missing styles when using copyStyles with @emotion

karibertils opened this issue · comments

When using emotion css lib the copyStyles={true} does not work correctly.

Emotion injects css styles into DOM head at the time the components are mounted. When a new popup using copyStyles={true} has opened, it copies all the existing styles but misses the styles that will be injected.

This is further complicated when there is delay until a component inside the popup are mounted. For example when components need to wait for data, or won't show until state changes happen.

One solution might be to have an interval every 50-100ms monitoring when new styles get added to the parent, and then adding them to the popups

@karibertils Any chance you have an example of how to add styles to the pop-up after X ms? I tried delaying the mount of the component but it won't work.

@rmariuzzo
I would very much appreciate it if you'd be able to handle some of the issues / PRs that had piled up, the relatively low activity on this repo is stopping the package from being a lot more popular

commented

I had this need too, since I was using MUI. I was able to get it to work using the below wrapper. @rmariuzzo I'd be happy to open a PR if you think it's appropriate. Best wishes fighting the cancer. I prayed for strength and healing for you.

MuiCompatNewWindow.tsx

import { FC, useCallback, useRef } from "react";
import NewWindow, { INewWindowProps } from "react-new-window";

/**
 * Wrapper for react-new-window that works with MUI (and Emotion).
 *
 * Same interface as react-new-window.
 */
const MUICompatNewWindow: FC<INewWindowProps> = ({
  onOpen,
  onUnload,
  ...props
}) => {
  const mutationObserverRef = useRef<MutationObserver | null>(null);

  const compatOnOpen = useCallback(
    (childWindow: Window) => {
      const childHead = childWindow.document.head;
      const mutationObserver = new MutationObserver((mutationList) => {
        mutationList
          .flatMap((mutation) => Array.from(mutation.addedNodes))
          .filter((node) => node instanceof HTMLStyleElement)
          .forEach((styleEl) => {
            childHead.appendChild(styleEl.cloneNode(true));
          });
      });
      mutationObserver.observe(document.head, { childList: true });
      mutationObserverRef.current = mutationObserver;
      onOpen?.(childWindow);
    },
    [onOpen]
  );

  const compatOnUnload = useCallback(() => {
    mutationObserverRef.current?.disconnect();
    onUnload?.();
  }, [onUnload]);

  return (
    <NewWindow onOpen={compatOnOpen} onUnload={compatOnUnload} {...props} />
  );
};

export default MUICompatNewWindow;

The revised code offers targeted enhancements over the original => epeterson320 comment

  1. Efficient Mutation Observing: In the original implementation, using multiple popups would lead to an increase in mutation observers observing the parent's head. The new approach consolidates this by having a single observer for all popups, effectively reducing the overhead and preventing potential performance issues.

  2. Addition of copyStyle Function: The copyStyle function is a new addition, providing a straightforward way to replicate existing styles in new popups. This ensures that popups are consistently styled, even when they are opened after the initial style elements have been added to the document head.

import { useEffect, useId } from 'react';
import NewWindow, { INewWindowProps } from 'react-new-window';

const subscriber = new Map<string, Document['head']>();

const styles = new Set<HTMLStyleElement>();

const addStyle = (style: HTMLStyleElement) => {
  styles.add(style);
  Array.from(subscriber.values()).forEach(head => {
    head.appendChild(style.cloneNode(true));
  });
};

/**
 *  Use this if a popup mounts and there are un-copied styles present
 *  onOpen={window => {
 *      subscriber.set(id, window.document.head);
 *      copyStyle(window.document.head);
 *      //...
 *  }
 *
 * This function is kept as a utility to handle situations where previously stored styles
 * haven't been copied to the popup.
 * For now, in my project, it's not being used since the existing styles are properly loaded into the popup.
 */

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const copyStyle = (target: Document['head']) => {
  styles.forEach(style => target.appendChild(style.cloneNode(true)));
};

const mutationObserver = new MutationObserver(mutationList => {
  mutationList.forEach(({ addedNodes, removedNodes }) => {
    [...addedNodes].forEach(node => {
      if (!(node instanceof HTMLStyleElement)) return;
      addStyle(node);
    });
    [...removedNodes].forEach(node => {
      if (!(node instanceof HTMLStyleElement)) return;
      styles.has(node) && styles.delete(node);
    });
  });
});

mutationObserver.observe(document.head, { childList: true });

export default function NewWindowPopup({ onOpen, ...rest }: INewWindowProps) {
  const id = useId();

  useEffect(() => {
    return () => {
      subscriber.delete(id);
    };
  }, []);

  return (
    <NewWindow
      onOpen={window => {
        if (!subscriber.has(id)) subscriber.set(id, window.document.head);
        onOpen?.(window);
      }}
      {...rest}
    />
  );
}