player icon indicating copy to clipboard operation
player copied to clipboard

Settings and library popover always being appended as last div element, because menuContainer is being ignored

Open taran2k opened this issue 1 year ago • 4 comments

Current Behavior:

The newly added menuContainer property is being ignored, and popovers are still added as the latest div element instead of being within the scope of the provided menuContainer element, making it so that the chapters popover as well as the settings popover are unusable when a video is being played inside a modal using the HTMLDialogElement API.

Expected Behavior:

Expected behaviour is for the given menuContainer element to be the container inside which the chapters and settings menu render.

Steps To Reproduce:

import '@material/web/button/elevated-button';
import '@material/web/iconbutton/icon-button';
import '@material/web/icon/icon';

import {LitElement, html, css} from 'lit';
import {customElement, property, query} from 'lit/decorators.js'; // eslint-disable-line import/extensions

import {type MediaPlayerElement} from 'vidstack/elements';
import {VidstackPlayer, VidstackPlayerLayout} from 'vidstack/global/player';

import vidstackStyles from 'vidstack/player/styles/default/layouts/video.css?url';
import defaultStyles from 'vidstack/player/styles/default/theme.css?url';

import {DisableBodyScroll} from './mixins/disableBodyScroll';
import {type ModalElement} from './modal';
import './modal'; // eslint-disable-line no-duplicate-imports

@customElement('video-content')
// eslint-disable-next-line wc/file-name-matches-element -- impossible filename
export class VideoContentElement extends DisableBodyScroll(LitElement) {
  @property({type: String, attribute: 'video'})
  public accessor fileUuid = '';

  @query('#modal')
  private accessor modal: ModalElement | null = null;

  @query('#video-player')
  private accessor videoPlayer: HTMLVideoElement | null = null;

  private _vidstack: MediaPlayerElement | null = null;

  static styles = css`
    video:not(.vidstack:fullscreen video) {
      max-height: calc(90vh - 2rem); /* This is a hack to ensure proper sizing */
    }

    #video-player {
      aspect-ratio: unset;
    }
  `;

  render() {
    return html`
      <link rel="stylesheet" href="${defaultStyles}" />
      <link rel="stylesheet" href="${vidstackStyles}" />

      <md-elevated-button
        class="dialog-button"
        @click="${this._openModal}"
      >
        Open Video Modal
      </md-elevated-button>

      <modal-element
        id="modal"
        @open="${this._onOpenModal}"
        @close="${this._onCloseModal}"
      >
        <span slot="title">Demo Video</span>
        <vidstack-player id="video-player"></vidstack-player>
      </modal-element>
    `;
  }

  private async _initializePlayer() {
    const menuContainerElement = document.getElementById('modal');
    const player = await VidstackPlayer.create({
      target: this.videoPlayer!,
      src: 'https://files.vidstack.io/sprite-fight/720p.mp4',
      viewType: 'video',
      streamType: 'on-demand',
      logLevel: 'warn',
      crossOrigin: true,
      playsInline: true,
      title: 'Sprite Fight',
      poster: 'https://files.vidstack.io/sprite-fight/poster.webp',
      layout: new VidstackPlayerLayout({
        thumbnails: 'https://files.vidstack.io/sprite-fight/thumbnails.vtt',
        menuContainer: menuContainerElement,
      }),
      tracks: [
        {
          src: 'https://files.vidstack.io/sprite-fight/subs/english.vtt',
          label: 'English',
          language: 'en-US',
          kind: 'subtitles',
          type: 'vtt',
          default: true,
        },
        {
          src: 'https://files.vidstack.io/sprite-fight/subs/spanish.vtt',
          label: 'Spanish',
          language: 'es-ES',
          kind: 'subtitles',
          type: 'vtt',
        },
        {
          src: 'https://files.vidstack.io/sprite-fight/chapters.vtt',
          language: 'en-US',
          kind: 'chapters',
          type: 'vtt',
          default: true,
        },
      ],
    });

    return player;
  }

  private async _openModal() {
    this.modal?.open();
  }

  private async _onOpenModal() {
    this._disableBodyScroll();
    this._vidstack?.play(); // If the video already exists, resume playing it.
    this._vidstack ??= await this._initializePlayer(); // Otherwise initialize it.
  }

  private _onCloseModal() {
    if (this._vidstack) {
      this._vidstack.pause();
    }
    this._restoreBodyScroll();
  }
}

Environment:

Video Reconstruction

Schermopname van 2024-06-20 08-22-55.webm

taran2k avatar Jun 20 '24 06:06 taran2k

Thanks, I'll look into it!

mihar-22 avatar Jun 20 '24 06:06 mihar-22

Great. Might be worthy to note that the overall support for video rendering inside the modal is a bit tacky, as shown by the necessity for

  static styles = css`
    video:not(.vidstack:fullscreen video) {
      max-height: calc(90vh - 2rem); /* This is a hack to ensure proper sizing */
    }

    #video-player {
      aspect-ratio: unset;
    }
  `;

to have the video render correctly.

taran2k avatar Jun 20 '24 06:06 taran2k

@mihar-22 Seems like the issue was on my end, trying to call DOM elements the wrong way. Was solved by adding a container within the modal element with id menu-container, and fetching it through

  @query('#menu-container')
  private accessor menuContainerElement: HTMLElement | null = null;

which makes it so this works just fine:

      layout: new VidstackPlayerLayout({
        ...defaultVidstackLayout,
        menuContainer: this.menuContainerElement,
      }),

taran2k avatar Jun 20 '24 10:06 taran2k

A new issue did arrive now though, only the settings and chapters container pop up like they should. But all nested containers don't (playback speed, audio settings, etc.)...

Schermopname van 2024-06-20 12-17-17.webm

taran2k avatar Jun 20 '24 10:06 taran2k

You'll need to provide me a reproduction because this is working in my testing. Closing until one is provided, thanks!

mihar-22 avatar Jul 05 '24 03:07 mihar-22

@mihar-22 Could you reopen the issue?

I've created a simple recreation of the issue by simulating the way components are loaded within the shadow DOM. As can be seen, when a modal is loaded within a shadow DOM, issues arise. Below is the reproduction.

Reproduction

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Video Player Modal Example</title>
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
</head>

<body>
    <main>
        <button id="openButton">Open Video Modal</button>

        <foo-element></foo-element>
    </main>

    <script type="module">
        class FooElement extends HTMLElement {
            constructor() {
                super();
                const shadow = this.attachShadow({ mode: "open" });

                // External stylesheets within shadow DOM
                const themeLink = document.createElement("link");
                themeLink.setAttribute("rel", "stylesheet");
                themeLink.setAttribute("href", "https://cdn.vidstack.io/player/theme.css");
                shadow.appendChild(themeLink);

                const videoLink = document.createElement("link");
                videoLink.setAttribute("rel", "stylesheet");
                videoLink.setAttribute("href", "https://cdn.vidstack.io/player/video.css");
                shadow.appendChild(videoLink);

                const dialog = document.createElement("dialog");
                dialog.id = "videoModal";
                const video = document.createElement("div");
                video.id = "videoPlayer";
                const menu = document.createElement("div");
                menu.id = "menuContainer";
                dialog.appendChild(video);
                dialog.appendChild(menu);
                shadow.appendChild(dialog);
            }
        }
        customElements.define("foo-element", FooElement);
    </script>

    <script type="module">
        import { VidstackPlayer, VidstackPlayerLayout } from 'https://cdn.vidstack.io/player';

        document.addEventListener('DOMContentLoaded', () => {
            const openButton = document.getElementById('openButton');
            const fooElement = document.querySelector('foo-element');

            openButton.addEventListener('click', async () => {
                const shadow = fooElement.shadowRoot;
                const videoModal = shadow.getElementById('videoModal');
                const videoPlayerContainer = shadow.getElementById('videoPlayer');
                const menuContainer = shadow.getElementById('menuContainer');

                videoModal.showModal();

                const player = await VidstackPlayer.create({
                    target: videoPlayerContainer,
                    src: 'https://files.vidstack.io/sprite-fight/720p.mp4',
                    viewType: 'video',
                    streamType: 'on-demand',
                    logLevel: 'warn',
                    crossOrigin: true,
                    playsInline: true,
                    title: 'Sprite Fight',
                    poster: 'https://files.vidstack.io/sprite-fight/poster.webp',
                    layout: new VidstackPlayerLayout({
                        thumbnails: 'https://files.vidstack.io/sprite-fight/thumbnails.vtt',
                        menuContainer: menuContainer,
                    }),
                    tracks: [
                        {
                            src: 'https://files.vidstack.io/sprite-fight/subs/english.vtt',
                            label: 'English',
                            language: 'en-US',
                            kind: 'subtitles',
                            type: 'vtt',
                            default: true,
                        },
                        {
                            src: 'https://files.vidstack.io/sprite-fight/subs/spanish.vtt',
                            label: 'Spanish',
                            language: 'es-ES',
                            kind: 'subtitles',
                            type: 'vtt',
                        },
                        {
                            src: 'https://files.vidstack.io/sprite-fight/chapters.vtt',
                            language: 'en-US',
                            kind: 'chapters',
                            type: 'vtt',
                            default: true,
                        },
                    ],
                });

                videoModal.addEventListener('close', () => {
                    videoPlayerContainer.innerHTML = '';
                });
            });
        });
    </script>
</body>

</html>

taran2k avatar Jul 05 '24 13:07 taran2k

@mihar-22 Just checking in, any chance the issue could be reopened? Can't do it myself.

taran2k avatar Jul 15 '24 06:07 taran2k

Thanks for the repro @taran2k, found the issue and I'll release the fix some time today or tomorrow.

mihar-22 avatar Jul 19 '24 03:07 mihar-22

Thanks for the issue and the solution!

We are seeing the similar thingy with [email protected], so I wonder if this is related or some other issue. @taran2k was the issue resolved for you?

amureki avatar Jul 29 '24 09:07 amureki

Thank you @mihar-22! This solved the problems we were having. @amureki, yes, the most recent release resolved our issue.

taran2k avatar Jul 29 '24 09:07 taran2k