emberjs / ember.js

Ember.js - A JavaScript framework for creating ambitious web applications
https://emberjs.com
MIT License
22.46k stars 4.21k forks source link

[Bug] `#in-element` cannot directly render in to shadow-dom #20641

Open NullVoxPopuli opened 4 months ago

NullVoxPopuli commented 4 months ago

🐞 Describe the Bug

😕 Actual Behavior

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

import { modifier } from 'ember-modifier';

const attachShadow = modifier((element: Element, [set]: [(shadowRoot: ShadowRoot) => void]) => {
  let shadow = element.attachShadow({ mode: 'open' });

  set(shadow);
});

// index.html has the production-fingerprinted references to these links
// Ideally, we'd have some pre-processor scan everything for references to
// assets in public, but idk how to set that up
const getStyles = () => [...document.head.querySelectorAll('link')].map((link) => link.href);

export class Shadowed extends Component<{
  Element: HTMLDivElement;
  Args: {
    omitStyles?: boolean;
  };
  Blocks: { default: [] };
}> {
  @tracked shadow: ShadowRoot | undefined;

  setShadow = async (shadowRoot: ShadowRoot) => {
    await Promise.resolve();

    this.shadow = shadowRoot;
  }

  <template>
    <div data-shadow {{attachShadow this.setShadow}} ...attributes></div>

    {{#if this.shadow}}
      {{#in-element this.shadow}}
        {{#unless @omitStyles}}
          {{#each (getStyles) as |styleHref|}}
            <link rel="stylesheet" href={{styleHref}} />
          {{/each}}
        {{/unless}}

        {{yield}}
      {{/in-element}}
    {{/if}}
  </template>
}

export default Shadowed;

Used in this test:

import { find, render } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';

import { Shadowed } from 'limber/components/shadowed';

module('Rendering | <Shadowed>', function (hooks) {
  setupRenderingTest(hooks);

  test('it works', async function (assert) {
    await render(
      <template>
        out of shadow

        <Shadowed>
          in shadow
        </Shadowed>
      </template>
    );

    assert.dom().hasText('out of shadow');
    assert.dom().doesNotContainText('in shadow');
    // assort.dom forgot that ShadowDom is a thing
    // assert.dom(find('[data-shadow]')?.shadowRoot).hasText('in shadow');
    assert.ok(find('[data-shadow]')?.shadowRoot?.textContent?.includes('in shadow'));
  });
});

Causes this error:

Error occurred:

- While rendering:
  -top-level
    application
      index
        _shadowedTest
          Shadowed

[runtime.js:5723](http://localhost:4201/assets/@glimmer/runtime.js)

Error occurred:

[runtime.js:4948](http://localhost:4201/assets/@glimmer/runtime.js)
Uncaught (in promise) Error: Got [object ShadowRoot], expected:
{ nodeType: 1,tagName: typeof string,nextSibling: any }
    check debug.js:1512 # @glimmer/debug
    <anonymous> runtime.js:1508
    evaluate runtime.js:1061

and doesn't actually render in to the shadow dom: image

🤔 Expected Behavior

A clear and concise description of what you expected to happen.

🌍 Environment

I was able to get desired behavior at the cost of an extra div here:

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

import { modifier } from 'ember-modifier';

const attachShadow = modifier((element: Element, [set]: [(shadowRoot: HTMLDivElement) => void]) => {
  let shadow = element.attachShadow({ mode: 'open' });
  let div = document.createElement('div');

  shadow.appendChild(div);

  set(div);
});

// index.html has the production-fingerprinted references to these links
// Ideally, we'd have some pre-processor scan everything for references to
// assets in public, but idk how to set that up
const getStyles = () => [...document.head.querySelectorAll('link')].map((link) => link.href);

export class Shadowed extends Component<{
  Element: HTMLDivElement;
  Args: {
    omitStyles?: boolean;
  };
  Blocks: { default: [] };
}> {
  @tracked shadow: HTMLDivElement | undefined;

  setShadow = async (shadowRoot: HTMLDivElement) => {
    await Promise.resolve();

    this.shadow = shadowRoot;
  }

  <template>
    <div data-shadow {{attachShadow this.setShadow}} ...attributes></div>

    {{#if this.shadow}}
      {{#in-element this.shadow}}
        {{#unless @omitStyles}}
          {{#each (getStyles) as |styleHref|}}
            <link rel="stylesheet" href={{styleHref}} />
          {{/each}}
        {{/unless}}

        {{yield}}
      {{/in-element}}
    {{/if}}
  </template>
}

export default Shadowed;

(this change also makes the test pass, but again, at the cost of an extra div)

NullVoxPopuli commented 4 months ago

Repro: https://github.com/NullVoxPopuli/ember-source-5.6-broke-shadow-dom (check commits)

NullVoxPopuli commented 4 months ago

I have a hunch this has to do with Glimmer-vm changes :thinking: