From b02cf08fc6692eb0e367070b278f4183f49d3685 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Thu, 14 Mar 2024 18:32:54 +0100 Subject: [PATCH 1/8] Add --- react/src/components/SlotContainer.tsx | 15 +++++++++++++++ react/src/components/index.ts | 1 + src/UIContainer.ts | 3 ++- src/components/MenuGroup.ts | 16 +++++++++++----- src/components/RadioGroup.ts | 4 ++-- src/components/SlotContainer.css | 11 +++++++++++ src/components/SlotContainer.ts | 24 ++++++++++++++++++++++++ src/components/index.ts | 1 + src/util/CommonUtils.ts | 14 ++++++++++++++ 9 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 react/src/components/SlotContainer.tsx create mode 100644 src/components/SlotContainer.css create mode 100644 src/components/SlotContainer.ts diff --git a/react/src/components/SlotContainer.tsx b/react/src/components/SlotContainer.tsx new file mode 100644 index 00000000..87d76b13 --- /dev/null +++ b/react/src/components/SlotContainer.tsx @@ -0,0 +1,15 @@ +import { createComponent } from '@lit/react'; +import { SlotContainer as SlotContainerElement } from '@theoplayer/web-ui'; +import * as React from 'react'; + +/** + * See {@link @theoplayer/web-ui!SlotContainer | SlotContainer in @theoplayer/web-ui}. + * + * @group Components + */ +export const SlotContainer = createComponent({ + tagName: 'theoplayer-slot-container', + displayName: 'SlotContainer', + elementClass: SlotContainerElement, + react: React +}); diff --git a/react/src/components/index.ts b/react/src/components/index.ts index 1a0c0478..f6156b64 100644 --- a/react/src/components/index.ts +++ b/react/src/components/index.ts @@ -45,4 +45,5 @@ export * from './GestureReceiver'; export * from './PreviewTimeDisplay'; export * from './PreviewThumbnail'; export * from './LiveButton'; +export * from './SlotContainer'; export * from './ads/index'; diff --git a/src/UIContainer.ts b/src/UIContainer.ts index 71f78762..86813363 100644 --- a/src/UIContainer.ts +++ b/src/UIContainer.ts @@ -7,6 +7,7 @@ import { arrayRemove, containsComposedNode, getFocusableChildren, + getSlottedElements, getTvFocusChildren, isElement, isHTMLElement, @@ -1075,7 +1076,7 @@ declare global { function getVisibleRect(slot: HTMLSlotElement): Rectangle | undefined { let result: Rectangle | undefined; - const children = slot.assignedNodes().filter(isHTMLElement); + const children = getSlottedElements(slot).filter(isHTMLElement); for (const child of children) { if (getComputedStyle(child).opacity !== '0') { const childRect = Rectangle.fromRect(child.getBoundingClientRect()); diff --git a/src/components/MenuGroup.ts b/src/components/MenuGroup.ts index ecd30f0e..7e363ad0 100644 --- a/src/components/MenuGroup.ts +++ b/src/components/MenuGroup.ts @@ -1,7 +1,16 @@ import * as shadyCss from '@webcomponents/shadycss'; import menuGroupCss from './MenuGroup.css'; import { Attribute } from '../util/Attribute'; -import { arrayFind, arrayFindIndex, fromArrayLike, isElement, isHTMLElement, noOp, upgradeCustomElementIfNeeded } from '../util/CommonUtils'; +import { + arrayFind, + arrayFindIndex, + fromArrayLike, + getSlottedElements, + isElement, + isHTMLElement, + noOp, + upgradeCustomElementIfNeeded +} from '../util/CommonUtils'; import { CLOSE_MENU_EVENT, type CloseMenuEvent } from '../events/CloseMenuEvent'; import { TOGGLE_MENU_EVENT, type ToggleMenuEvent } from '../events/ToggleMenuEvent'; import { isBackKey } from '../util/KeyCode'; @@ -255,10 +264,7 @@ export class MenuGroup extends HTMLElement { * this listener to each nested ``. */ private readonly _onMenuListChange = () => { - const children: Element[] = [ - ...fromArrayLike(this.shadowRoot!.children), - ...(this._menuSlot ? this._menuSlot.assignedNodes({ flatten: true }).filter(isElement) : []) - ]; + const children: Element[] = [...fromArrayLike(this.shadowRoot!.children), ...(this._menuSlot ? getSlottedElements(this._menuSlot) : [])]; const upgradePromises: Array> = []; for (const child of children) { if (!isMenuElement(child)) { diff --git a/src/components/RadioGroup.ts b/src/components/RadioGroup.ts index 0c68a6b6..0c77df49 100644 --- a/src/components/RadioGroup.ts +++ b/src/components/RadioGroup.ts @@ -2,7 +2,7 @@ import * as shadyCss from '@webcomponents/shadycss'; import { isArrowKey, KeyCode } from '../util/KeyCode'; import { RadioButton } from './RadioButton'; import { createEvent } from '../util/EventUtils'; -import { arrayFind, isElement, noOp, upgradeCustomElementIfNeeded } from '../util/CommonUtils'; +import { arrayFind, getSlottedElements, isElement, noOp, upgradeCustomElementIfNeeded } from '../util/CommonUtils'; import { StateReceiverMixin } from './StateReceiverMixin'; import { Attribute } from '../util/Attribute'; import type { DeviceType } from '../util/DeviceType'; @@ -75,7 +75,7 @@ export class RadioGroup extends StateReceiverMixin(HTMLElement, ['deviceType']) } private readonly _onSlotChange = () => { - const children = this._slot.assignedNodes({ flatten: true }).filter(isElement); + const children = getSlottedElements(this._slot); const upgradePromises: Array> = []; for (const child of children) { if (!isRadioButton(child)) { diff --git a/src/components/SlotContainer.css b/src/components/SlotContainer.css new file mode 100644 index 00000000..48572ac8 --- /dev/null +++ b/src/components/SlotContainer.css @@ -0,0 +1,11 @@ +:host { + display: contents; +} + +slot, +::slotted(*) { + /* + * Inherit opacity from parent container, so auto-hide works. + */ + opacity: inherit; +} diff --git a/src/components/SlotContainer.ts b/src/components/SlotContainer.ts new file mode 100644 index 00000000..a139673c --- /dev/null +++ b/src/components/SlotContainer.ts @@ -0,0 +1,24 @@ +import { createTemplate } from '../util/TemplateUtils'; +import slotContainerCss from './SlotContainer.css'; + +const template = createTemplate('theoplayer-slot-container', ``); + +export class SlotContainer extends HTMLElement { + constructor() { + super(); + const shadowRoot = this.attachShadow({ mode: 'open' }); + shadowRoot.appendChild(template().content.cloneNode(true)); + } +} + +customElements.define('theoplayer-slot-container', SlotContainer); + +export function isSlotContainer(element: Element): element is SlotContainer { + return element.nodeName.toLowerCase() === 'theoplayer-slot-container'; +} + +declare global { + interface HTMLElementTagNameMap { + 'theoplayer-slot-container': SlotContainer; + } +} diff --git a/src/components/index.ts b/src/components/index.ts index 9a8253c0..f55509e9 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -45,5 +45,6 @@ export * from './GestureReceiver'; export * from './PreviewTimeDisplay'; export * from './PreviewThumbnail'; export * from './LiveButton'; +export * from './SlotContainer'; export * from './ads/index'; export { type StateReceiverElement, type StateReceiverPropertyMap, StateReceiverMixin } from './StateReceiverMixin'; diff --git a/src/util/CommonUtils.ts b/src/util/CommonUtils.ts index eba52858..4bde8d40 100644 --- a/src/util/CommonUtils.ts +++ b/src/util/CommonUtils.ts @@ -1,4 +1,5 @@ import { Attribute } from './Attribute'; +import { isSlotContainer } from '../components'; export type Constructor = abstract new (...args: any[]) => T; @@ -158,6 +159,19 @@ export function getChildren(element: Element): ArrayLike { return []; } +export function getSlottedElements(slot: HTMLSlotElement): Element[] { + const elements: Element[] = []; + for (const node of slot.assignedNodes({ flatten: true })) { + if (isElement(node)) { + elements.push(node); + if (isSlotContainer(node)) { + elements.push(...fromArrayLike(node.children)); + } + } + } + return elements; +} + export function getTvFocusChildren(element: Element): HTMLElement[] | undefined { if (!isHTMLElement(element)) { return; From 6c3c8cb1f44d374d450a43f30147a412be081633 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Thu, 14 Mar 2024 18:40:33 +0100 Subject: [PATCH 2/8] Use in React UI containers --- react/src/DefaultUI.tsx | 11 +++++------ react/src/UIContainer.tsx | 15 +++++++-------- react/src/slotted.tsx | 29 +---------------------------- 3 files changed, 13 insertions(+), 42 deletions(-) diff --git a/react/src/DefaultUI.tsx b/react/src/DefaultUI.tsx index 14245a34..7ad17247 100644 --- a/react/src/DefaultUI.tsx +++ b/react/src/DefaultUI.tsx @@ -5,8 +5,7 @@ import type { ChromelessPlayer } from 'theoplayer/chromeless'; import { createComponent, type WebComponentProps } from '@lit/react'; import { usePlayer } from './util'; import { PlayerContext } from './context'; -import { Slotted, SlottedInPlace } from './slotted'; -import type { Menu } from './components'; +import { type Menu, SlotContainer } from './components'; const RawDefaultUI = createComponent({ tagName: 'theoplayer-default-ui', @@ -93,10 +92,10 @@ export const DefaultUI = (props: DefaultUIProps) => { return ( - {title} - {topControlBar} - {bottomControlBar} - {menu} + {title} + {topControlBar} + {bottomControlBar} + {menu} ); diff --git a/react/src/UIContainer.tsx b/react/src/UIContainer.tsx index 88d79d5c..d4910bc4 100644 --- a/react/src/UIContainer.tsx +++ b/react/src/UIContainer.tsx @@ -5,8 +5,7 @@ import type { ChromelessPlayer } from 'theoplayer/chromeless'; import { createComponent, type WebComponentProps } from '@lit/react'; import { usePlayer } from './util'; import { PlayerContext } from './context'; -import { Slotted, SlottedInPlace } from './slotted'; -import type { ChromecastButton, ErrorDisplay, Menu, PlayButton, TimeRange } from './components'; +import { type ChromecastButton, type ErrorDisplay, type Menu, type PlayButton, SlotContainer, type TimeRange } from './components'; const RawUIContainer = createComponent({ tagName: 'theoplayer-ui', @@ -134,13 +133,13 @@ export const UIContainer = (props: UIContainerProps) => { return ( - {topChrome} - {middleChrome} - {centeredChrome} - {centeredLoading} + {topChrome} + {middleChrome} + {centeredChrome} + {centeredLoading} {bottomChrome} - {menu} - {error} + {menu} + {error} ); diff --git a/react/src/slotted.tsx b/react/src/slotted.tsx index def6c134..39efdf45 100644 --- a/react/src/slotted.tsx +++ b/react/src/slotted.tsx @@ -1,11 +1,10 @@ import * as React from 'react'; -import { Children, cloneElement, Fragment, isValidElement, type ReactNode } from 'react'; +import { type ReactNode } from 'react'; export interface SlottedProps { slot: string; children?: ReactNode; } - /** * A component that puts its children inside a specific slot of a custom element. */ @@ -18,29 +17,3 @@ export const Slotted = ({ slot, children }: SlottedProps) => {
); }; - -/** - * A component that puts its children inside a specific slot of a custom element, - * by adding a `slot` property directly to each child. - * - * This should be used with caution! If a component is wrapped in another component that doesn't forward all props - * (such as a ``), then the slot property might not end up at the desired child. - */ -export const SlottedInPlace = ({ slot, children }: SlottedProps): ReactNode => { - if (!children) { - return null; - } - return Children.map(children, (child) => cloneWithSlot(slot, child)); -}; - -function cloneWithSlot(slot: string, child: T): ReactNode { - if (isValidElement(child)) { - if (child.type === Fragment) { - return cloneElement(child, undefined, ); - } else { - return cloneElement(child, { slot }); - } - } else { - return ; - } -} From 7795d62c994297ffef6774cf22e643cea2f47c2e Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Thu, 14 Mar 2024 18:47:16 +0100 Subject: [PATCH 3/8] Export element class only --- src/components/index.ts | 2 +- src/util/CommonUtils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/index.ts b/src/components/index.ts index f55509e9..8b2f92a7 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -45,6 +45,6 @@ export * from './GestureReceiver'; export * from './PreviewTimeDisplay'; export * from './PreviewThumbnail'; export * from './LiveButton'; -export * from './SlotContainer'; +export { SlotContainer } from './SlotContainer'; export * from './ads/index'; export { type StateReceiverElement, type StateReceiverPropertyMap, StateReceiverMixin } from './StateReceiverMixin'; diff --git a/src/util/CommonUtils.ts b/src/util/CommonUtils.ts index 4bde8d40..1b673c97 100644 --- a/src/util/CommonUtils.ts +++ b/src/util/CommonUtils.ts @@ -1,5 +1,5 @@ import { Attribute } from './Attribute'; -import { isSlotContainer } from '../components'; +import { isSlotContainer } from '../components/SlotContainer'; export type Constructor = abstract new (...args: any[]) => T; From 267df870ebea598e038da04a83116204559bb516 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Thu, 14 Mar 2024 18:55:04 +0100 Subject: [PATCH 4/8] Document `` --- react/src/components/SlotContainer.tsx | 1 + src/components/SlotContainer.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/react/src/components/SlotContainer.tsx b/react/src/components/SlotContainer.tsx index 87d76b13..3ccb1fa6 100644 --- a/react/src/components/SlotContainer.tsx +++ b/react/src/components/SlotContainer.tsx @@ -6,6 +6,7 @@ import * as React from 'react'; * See {@link @theoplayer/web-ui!SlotContainer | SlotContainer in @theoplayer/web-ui}. * * @group Components + * @internal */ export const SlotContainer = createComponent({ tagName: 'theoplayer-slot-container', diff --git a/src/components/SlotContainer.ts b/src/components/SlotContainer.ts index a139673c..97acb056 100644 --- a/src/components/SlotContainer.ts +++ b/src/components/SlotContainer.ts @@ -3,6 +3,21 @@ import slotContainerCss from './SlotContainer.css'; const template = createTemplate('theoplayer-slot-container', ``); +/** + * `` - A container that can be assigned to a slot, + * and behaves as if all its children are directly assigned to that slot. + * + * This behaves approximately like a regular `
` with style `display: contents`, + * but receives some special treatment from e.g. {@link MenuGroup | ``} + * which normally expects its {@link Menu | menu}s to be slotted in as direct children. + * Those menus can also be children of a `` instead. + * + * This is an internal component, used mainly by Open Video UI for React. + * You shouldn't need this under normal circumstances. + * + * @group Components + * @internal + */ export class SlotContainer extends HTMLElement { constructor() { super(); From 4d6661006d8957ef9450a811630e537dd662174a Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 18 Mar 2024 09:23:04 +0100 Subject: [PATCH 5/8] Avoid creating empty s --- react/src/DefaultUI.tsx | 8 ++++---- react/src/UIContainer.tsx | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/react/src/DefaultUI.tsx b/react/src/DefaultUI.tsx index 7ad17247..855f9c09 100644 --- a/react/src/DefaultUI.tsx +++ b/react/src/DefaultUI.tsx @@ -92,10 +92,10 @@ export const DefaultUI = (props: DefaultUIProps) => { return ( - {title} - {topControlBar} - {bottomControlBar} - {menu} + {title && {title}} + {topControlBar && {topControlBar}} + {bottomControlBar && {bottomControlBar}} + {menu && {menu}} ); diff --git a/react/src/UIContainer.tsx b/react/src/UIContainer.tsx index d4910bc4..3845f3e3 100644 --- a/react/src/UIContainer.tsx +++ b/react/src/UIContainer.tsx @@ -133,13 +133,13 @@ export const UIContainer = (props: UIContainerProps) => { return ( - {topChrome} - {middleChrome} - {centeredChrome} - {centeredLoading} + {topChrome && {topChrome}} + {middleChrome && {middleChrome}} + {centeredChrome && {centeredChrome}} + {centeredLoading && {centeredLoading}} {bottomChrome} - {menu} - {error} + {menu && {menu}} + {error && {error}} ); From 98c6cbac1a5ab9480cf5cebae921bff587207462 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 18 Mar 2024 09:24:13 +0100 Subject: [PATCH 6/8] Update test snapshots --- react/test/ssr.test.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/react/test/ssr.test.mjs b/react/test/ssr.test.mjs index 91b8a891..b135819a 100644 --- a/react/test/ssr.test.mjs +++ b/react/test/ssr.test.mjs @@ -23,8 +23,8 @@ describe('Server-side rendering (SSR)', () => { ); const expected = '' + - '
' + - '
' + + '' + + '' + '
'; assert.equal(actual, expected); }); @@ -43,7 +43,7 @@ describe('Server-side rendering (SSR)', () => { ); const expected = '' + - '
' + + '' + '' + '
'; assert.equal(actual, expected); From 82515d153b32212313d2bf156ab2d64984e2073d Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 18 Mar 2024 09:31:46 +0100 Subject: [PATCH 7/8] Update changelog --- CHANGELOG.md | 4 ++++ react/CHANGELOG.md | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2b76117..c3c4c895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ > - 🏠 Internal > - 💅 Polish +## Unreleased + +- 🚀 Added ``. ([#55](https://github.com/THEOplayer/web-ui/pull/55)) + ## v1.7.1 (2024-02-15) - 💅 Export `version` in public API. ([#53](https://github.com/THEOplayer/web-ui/pull/53)) diff --git a/react/CHANGELOG.md b/react/CHANGELOG.md index 79652d3f..0c11f2cb 100644 --- a/react/CHANGELOG.md +++ b/react/CHANGELOG.md @@ -10,6 +10,11 @@ > - 🏠 Internal > - 💅 Polish +## Unreleased + +- 🐛 Fixed `topChrome`, `middleChrome` and `centeredChrome` slots not auto-hiding in ``. ([#55](https://github.com/THEOplayer/web-ui/pull/55)) +- 🚀 Added ``. ([#55](https://github.com/THEOplayer/web-ui/pull/55)) + ## v1.7.1 (2024-02-15) - 🐛 Fix "Warning: useLayoutEffect does nothing on the server" when using `@theoplayer/react-ui` in Node. ([#52](https://github.com/THEOplayer/web-ui/pull/52)) From c665af8e3543518ca98565a6c9223f10567425ab Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 18 Mar 2024 10:48:32 +0100 Subject: [PATCH 8/8] Fix no-auto-hide --- react/CHANGELOG.md | 1 + src/components/SlotContainer.css | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/react/CHANGELOG.md b/react/CHANGELOG.md index 0c11f2cb..796a71fb 100644 --- a/react/CHANGELOG.md +++ b/react/CHANGELOG.md @@ -13,6 +13,7 @@ ## Unreleased - 🐛 Fixed `topChrome`, `middleChrome` and `centeredChrome` slots not auto-hiding in ``. ([#55](https://github.com/THEOplayer/web-ui/pull/55)) +- 🐛 Fixed `no-auto-hide` attribute not working for React components. ([#55](https://github.com/THEOplayer/web-ui/pull/55)) - 🚀 Added ``. ([#55](https://github.com/THEOplayer/web-ui/pull/55)) ## v1.7.1 (2024-02-15) diff --git a/src/components/SlotContainer.css b/src/components/SlotContainer.css index 48572ac8..1cb4b51e 100644 --- a/src/components/SlotContainer.css +++ b/src/components/SlotContainer.css @@ -3,7 +3,7 @@ } slot, -::slotted(*) { +::slotted(:not([no-auto-hide])) { /* * Inherit opacity from parent container, so auto-hide works. */