mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat: add HTML tooltip component (#40)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { Placement } from '@floating-ui/dom';
|
||||
|
||||
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
|
||||
import { uniqueId } from '../utils/element-utils';
|
||||
|
||||
export class MediaPopoverRoot extends HTMLElement {
|
||||
#open = false;
|
||||
@@ -42,12 +43,16 @@ export class MediaPopoverRoot extends HTMLElement {
|
||||
return this.querySelector('media-popover-trigger') as MediaPopoverTrigger | null;
|
||||
}
|
||||
|
||||
get #portalElement(): MediaPopoverPortal | null {
|
||||
return this.querySelector('media-popover-portal') as MediaPopoverPortal | null;
|
||||
}
|
||||
|
||||
get #positionerElement(): MediaPopoverPositioner | null {
|
||||
return this.querySelector('media-popover-positioner') as MediaPopoverPositioner | null;
|
||||
return this.#portalElement?.querySelector('media-popover-positioner') as MediaPopoverPositioner | null;
|
||||
}
|
||||
|
||||
get #popupElement(): MediaPopoverPopup | null {
|
||||
return this.querySelector('media-popover-popup') as MediaPopoverPopup | null;
|
||||
return this.#portalElement?.querySelector('media-popover-popup') as MediaPopoverPopup | null;
|
||||
}
|
||||
|
||||
#setOpen(open: boolean): void {
|
||||
@@ -138,6 +143,53 @@ export class MediaPopoverTrigger extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
export class MediaPopoverPortal extends HTMLElement {
|
||||
#portal: HTMLElement | null = null;
|
||||
|
||||
connectedCallback(): void {
|
||||
this.style.display = 'contents';
|
||||
this.#setupPortal();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.#cleanupPortal();
|
||||
}
|
||||
|
||||
querySelector(selector: string): HTMLElement | null {
|
||||
return this.#portal?.querySelector(selector) ?? null;
|
||||
}
|
||||
|
||||
handleEvent(event: Event): void {
|
||||
this.dispatchEvent(new Event(event.type, { bubbles: true }));
|
||||
}
|
||||
|
||||
#setupPortal(): void {
|
||||
const portalId = this.getAttribute('root-id');
|
||||
if (!portalId) return;
|
||||
|
||||
const portalContainer = (this.getRootNode() as ShadowRoot | Document).getElementById(portalId);
|
||||
if (!portalContainer) return;
|
||||
|
||||
portalContainer.addEventListener('mouseenter', this);
|
||||
portalContainer.addEventListener('mouseleave', this);
|
||||
|
||||
this.#portal = document.createElement('div');
|
||||
this.#portal.id = uniqueId();
|
||||
portalContainer.append(this.#portal);
|
||||
|
||||
this.#portal.append(...this.children);
|
||||
}
|
||||
|
||||
#cleanupPortal(): void {
|
||||
if (!this.#portal) return;
|
||||
|
||||
// Move children back to the portal element
|
||||
this.append(...this.#portal.children);
|
||||
this.#portal.remove();
|
||||
this.#portal = null;
|
||||
}
|
||||
}
|
||||
|
||||
export class MediaPopoverPositioner extends HTMLElement {
|
||||
connectedCallback(): void {
|
||||
this.style.display = 'contents';
|
||||
@@ -148,7 +200,6 @@ export class MediaPopoverPositioner extends HTMLElement {
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
left: '0',
|
||||
zIndex: '1000',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -177,6 +228,10 @@ if (!globalThis.customElements.get('media-popover-trigger')) {
|
||||
globalThis.customElements.define('media-popover-trigger', MediaPopoverTrigger);
|
||||
}
|
||||
|
||||
if (!globalThis.customElements.get('media-popover-portal')) {
|
||||
globalThis.customElements.define('media-popover-portal', MediaPopoverPortal);
|
||||
}
|
||||
|
||||
if (!globalThis.customElements.get('media-popover-positioner')) {
|
||||
globalThis.customElements.define('media-popover-positioner', MediaPopoverPositioner);
|
||||
}
|
||||
@@ -188,11 +243,13 @@ if (!globalThis.customElements.get('media-popover-popup')) {
|
||||
export const Popover: {
|
||||
Root: typeof MediaPopoverRoot;
|
||||
Trigger: typeof MediaPopoverTrigger;
|
||||
Portal: typeof MediaPopoverPortal;
|
||||
Positioner: typeof MediaPopoverPositioner;
|
||||
Popup: typeof MediaPopoverPopup;
|
||||
} = {
|
||||
Root: MediaPopoverRoot,
|
||||
Trigger: MediaPopoverTrigger,
|
||||
Portal: MediaPopoverPortal,
|
||||
Positioner: MediaPopoverPositioner,
|
||||
Popup: MediaPopoverPopup,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
import type { Placement } from '@floating-ui/dom';
|
||||
import type { MediaContainer } from '@/media-container';
|
||||
|
||||
import { autoUpdate, computePosition, flip, offset, shift, arrow } from '@floating-ui/dom';
|
||||
import { uniqueId } from '../utils/element-utils';
|
||||
|
||||
export class MediaTooltipRoot extends HTMLElement {
|
||||
#open = false;
|
||||
#hoverTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
#cleanup: (() => void) | null = null;
|
||||
#arrowElement: HTMLElement | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener('mouseenter', this);
|
||||
this.addEventListener('mouseleave', this);
|
||||
}
|
||||
|
||||
handleEvent(event: Event): void {
|
||||
if (event.type === 'mouseenter') {
|
||||
this.#handleMouseEnter();
|
||||
} else if (event.type === 'mouseleave') {
|
||||
this.#handleMouseLeave();
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
this.#updateVisibility();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.#clearHoverTimeout();
|
||||
this.#cleanup?.();
|
||||
}
|
||||
|
||||
static get observedAttributes(): string[] {
|
||||
return ['delay', 'close-delay'];
|
||||
}
|
||||
|
||||
get delay(): number {
|
||||
return parseInt(this.getAttribute('delay') ?? '600', 10);
|
||||
}
|
||||
|
||||
get closeDelay(): number {
|
||||
return parseInt(this.getAttribute('close-delay') ?? '0', 10);
|
||||
}
|
||||
|
||||
get #triggerElement(): MediaTooltipTrigger | null {
|
||||
return this.querySelector('media-tooltip-trigger') as MediaTooltipTrigger | null;
|
||||
}
|
||||
|
||||
get #portalElement(): MediaTooltipPortal | null {
|
||||
return this.querySelector('media-tooltip-portal') as MediaTooltipPortal | null;
|
||||
}
|
||||
|
||||
get #positionerElement(): MediaTooltipPositioner | null {
|
||||
return this.#portalElement?.querySelector('media-tooltip-positioner') as MediaTooltipPositioner | null;
|
||||
}
|
||||
|
||||
get #popupElement(): MediaTooltipPopup | null {
|
||||
return this.#portalElement?.querySelector('media-tooltip-popup') as MediaTooltipPopup | null;
|
||||
}
|
||||
|
||||
#setOpen(open: boolean): void {
|
||||
if (this.#open === open) return;
|
||||
|
||||
this.#open = open;
|
||||
this.#updateVisibility();
|
||||
|
||||
if (open) {
|
||||
this.#setupFloating();
|
||||
} else {
|
||||
this.#cleanup?.();
|
||||
this.#cleanup = null;
|
||||
}
|
||||
}
|
||||
|
||||
#updateVisibility(): void {
|
||||
this.toggleAttribute('data-open', this.#open);
|
||||
this.style.display = 'contents';
|
||||
|
||||
if (this.#popupElement) {
|
||||
this.#popupElement.style.display = this.#open ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Update trigger aria-expanded
|
||||
const triggerElement = this.#triggerElement?.firstElementChild as HTMLElement;
|
||||
if (triggerElement) {
|
||||
triggerElement.setAttribute('aria-expanded', this.#open.toString());
|
||||
}
|
||||
}
|
||||
|
||||
#setupFloating(): void {
|
||||
if (!this.#triggerElement || !this.#popupElement) return;
|
||||
|
||||
const trigger = this.#triggerElement.firstElementChild as HTMLElement;
|
||||
const popup = this.#popupElement;
|
||||
|
||||
if (!trigger || !popup) return;
|
||||
|
||||
const placement = this.#positionerElement?.side ?? 'top';
|
||||
const sideOffset = this.#positionerElement?.sideOffset ?? 0;
|
||||
const collisionPadding = this.#positionerElement?.collisionPadding ?? 0;
|
||||
const mediaContainer = this.closest('media-container') as MediaContainer;
|
||||
|
||||
this.#arrowElement = popup.querySelector('media-tooltip-arrow') as HTMLElement;
|
||||
|
||||
const updatePosition = () => {
|
||||
const middleware = [
|
||||
offset(sideOffset),
|
||||
flip(),
|
||||
shift({
|
||||
boundary: mediaContainer,
|
||||
padding: collisionPadding
|
||||
}),
|
||||
];
|
||||
|
||||
// Add arrow middleware if arrow element exists
|
||||
if (this.#arrowElement) {
|
||||
middleware.push(arrow({ element: this.#arrowElement }));
|
||||
}
|
||||
|
||||
computePosition(trigger, popup, {
|
||||
placement,
|
||||
middleware,
|
||||
}).then(({ x, y, middlewareData }: { x: number; y: number; middlewareData: any }) => {
|
||||
Object.assign(popup.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
});
|
||||
|
||||
// Position arrow if it exists
|
||||
if (this.#arrowElement && middlewareData.arrow) {
|
||||
const { x: arrowX, y: arrowY } = middlewareData.arrow;
|
||||
Object.assign(this.#arrowElement.style, {
|
||||
left: arrowX != null ? `${arrowX}px` : undefined,
|
||||
top: arrowY != null ? `${arrowY}px` : undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
updatePosition();
|
||||
this.#cleanup = autoUpdate(trigger, popup, updatePosition);
|
||||
}
|
||||
|
||||
#clearHoverTimeout(): void {
|
||||
if (this.#hoverTimeout) {
|
||||
clearTimeout(this.#hoverTimeout);
|
||||
this.#hoverTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
#handleMouseEnter(): void {
|
||||
this.#clearHoverTimeout();
|
||||
this.#hoverTimeout = globalThis.setTimeout(() => {
|
||||
this.#setOpen(true);
|
||||
}, this.delay);
|
||||
}
|
||||
|
||||
#handleMouseLeave(): void {
|
||||
this.#clearHoverTimeout();
|
||||
this.#hoverTimeout = globalThis.setTimeout(() => {
|
||||
this.#setOpen(false);
|
||||
}, this.closeDelay);
|
||||
}
|
||||
}
|
||||
|
||||
export class MediaTooltipTrigger extends HTMLElement {
|
||||
connectedCallback(): void {
|
||||
this.style.display = 'contents';
|
||||
|
||||
const triggerElement = this.firstElementChild as HTMLElement;
|
||||
if (triggerElement) {
|
||||
triggerElement.setAttribute('aria-describedby', 'tooltip');
|
||||
triggerElement.setAttribute('aria-expanded', 'false');
|
||||
|
||||
const mutationObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes') {
|
||||
const rootElement = this.closest('media-tooltip-root') as MediaTooltipRoot;
|
||||
let popupElement = rootElement.querySelector('media-tooltip-popup') as MediaTooltipPopup;
|
||||
|
||||
if (!popupElement) {
|
||||
const portalElement = rootElement.querySelector('media-tooltip-portal') as MediaTooltipPortal;
|
||||
if (!portalElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
popupElement = portalElement.querySelector('media-tooltip-popup') as MediaTooltipPopup;
|
||||
if (!popupElement) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const attributeName = mutation.attributeName;
|
||||
if (!attributeName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attributeValue = triggerElement.getAttribute(attributeName);
|
||||
if (attributeValue !== null) {
|
||||
popupElement.setAttribute(attributeName, attributeValue);
|
||||
} else {
|
||||
popupElement.removeAttribute(attributeName);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
mutationObserver.observe(triggerElement, {
|
||||
attributes: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MediaTooltipPortal extends HTMLElement {
|
||||
#portal: HTMLElement | null = null;
|
||||
|
||||
connectedCallback(): void {
|
||||
this.style.display = 'contents';
|
||||
this.#setupPortal();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.#cleanupPortal();
|
||||
}
|
||||
|
||||
querySelector(selector: string): HTMLElement | null {
|
||||
return this.#portal?.querySelector(selector) ?? null;
|
||||
}
|
||||
|
||||
#setupPortal(): void {
|
||||
const portalId = this.getAttribute('root-id');
|
||||
if (!portalId) return;
|
||||
|
||||
const portalContainer = (this.getRootNode() as ShadowRoot | Document).getElementById(portalId);
|
||||
if (!portalContainer) return;
|
||||
|
||||
this.#portal = document.createElement('div');
|
||||
this.#portal.id = uniqueId();
|
||||
portalContainer.append(this.#portal);
|
||||
|
||||
this.#portal.append(...this.children);
|
||||
}
|
||||
|
||||
#cleanupPortal(): void {
|
||||
if (!this.#portal) return;
|
||||
|
||||
// Move children back to the portal element
|
||||
this.append(...this.#portal.children);
|
||||
this.#portal.remove();
|
||||
this.#portal = null;
|
||||
}
|
||||
}
|
||||
|
||||
export class MediaTooltipPositioner extends HTMLElement {
|
||||
connectedCallback(): void {
|
||||
this.style.display = 'contents';
|
||||
|
||||
const popup = this.firstElementChild as HTMLElement;
|
||||
if (popup) {
|
||||
Object.assign(popup.style, {
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
left: '0',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get side(): Placement {
|
||||
return (this.getAttribute('side') as Placement) ?? 'top';
|
||||
}
|
||||
|
||||
get sideOffset(): number {
|
||||
return parseInt(this.getAttribute('side-offset') ?? '0', 10);
|
||||
}
|
||||
|
||||
get collisionPadding(): number {
|
||||
return parseInt(this.getAttribute('collision-padding') ?? '0', 10);
|
||||
}
|
||||
}
|
||||
|
||||
export class MediaTooltipPopup extends HTMLElement {
|
||||
connectedCallback(): void {
|
||||
this.setAttribute('role', 'tooltip');
|
||||
this.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
export class MediaTooltipArrow extends HTMLElement {
|
||||
connectedCallback(): void {
|
||||
this.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.customElements.get('media-tooltip-root')) {
|
||||
globalThis.customElements.define('media-tooltip-root', MediaTooltipRoot);
|
||||
}
|
||||
|
||||
if (!globalThis.customElements.get('media-tooltip-trigger')) {
|
||||
globalThis.customElements.define('media-tooltip-trigger', MediaTooltipTrigger);
|
||||
}
|
||||
|
||||
if (!globalThis.customElements.get('media-tooltip-portal')) {
|
||||
globalThis.customElements.define('media-tooltip-portal', MediaTooltipPortal);
|
||||
}
|
||||
|
||||
if (!globalThis.customElements.get('media-tooltip-positioner')) {
|
||||
globalThis.customElements.define('media-tooltip-positioner', MediaTooltipPositioner);
|
||||
}
|
||||
|
||||
if (!globalThis.customElements.get('media-tooltip-popup')) {
|
||||
globalThis.customElements.define('media-tooltip-popup', MediaTooltipPopup);
|
||||
}
|
||||
|
||||
if (!globalThis.customElements.get('media-tooltip-arrow')) {
|
||||
globalThis.customElements.define('media-tooltip-arrow', MediaTooltipArrow);
|
||||
}
|
||||
|
||||
export const Tooltip: {
|
||||
Root: typeof MediaTooltipRoot;
|
||||
Trigger: typeof MediaTooltipTrigger;
|
||||
Portal: typeof MediaTooltipPortal;
|
||||
Positioner: typeof MediaTooltipPositioner;
|
||||
Popup: typeof MediaTooltipPopup;
|
||||
Arrow: typeof MediaTooltipArrow;
|
||||
} = {
|
||||
Root: MediaTooltipRoot,
|
||||
Trigger: MediaTooltipTrigger,
|
||||
Portal: MediaTooltipPortal,
|
||||
Positioner: MediaTooltipPositioner,
|
||||
Popup: MediaTooltipPopup,
|
||||
Arrow: MediaTooltipArrow,
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
@@ -9,9 +9,13 @@ import '../components/media-fullscreen-button';
|
||||
import '../components/media-duration-display';
|
||||
import '../components/media-current-time-display';
|
||||
import '../components/media-popover';
|
||||
import '../components/media-tooltip';
|
||||
import '@vjs-10/html-icons';
|
||||
import { uniqueId } from '../utils/element-utils';
|
||||
|
||||
export function getTemplateHTML() {
|
||||
const portalId = uniqueId();
|
||||
|
||||
return /* html */ `
|
||||
${MediaSkin.getTemplateHTML()}
|
||||
<style>
|
||||
@@ -241,17 +245,61 @@ export function getTemplateHTML() {
|
||||
background-color: #007bff;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.portal {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Tooltip Component Styles */
|
||||
media-tooltip-popup {
|
||||
background: rgb(20 20 30 / .9);
|
||||
color: rgb(238 238 238);
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
media-tooltip-popup[data-paused] .play-tooltip,
|
||||
media-tooltip-popup:not([data-paused]) .pause-tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
media-tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip,
|
||||
media-tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<media-container>
|
||||
<slot name="media" slot="media"></slot>
|
||||
<div id="${portalId}" class="portal"></div>
|
||||
<div class="overlay">
|
||||
<div class="spacer"></div>
|
||||
<div class="control-bar">
|
||||
<!-- NOTE: We can decide if we further want to provide a further, "themed" media-play-button that comes with baked in default styles and icons. (CJP) -->
|
||||
<media-play-button class="button">
|
||||
<media-play-icon class="icon play-icon"></media-play-icon>
|
||||
<media-pause-icon class="icon pause-icon"></media-pause-icon>
|
||||
</media-play-button>
|
||||
<media-tooltip-root delay="600" close-delay="0">
|
||||
<media-tooltip-trigger>
|
||||
<media-play-button class="button">
|
||||
<media-play-icon class="icon play-icon"></media-play-icon>
|
||||
<media-pause-icon class="icon pause-icon"></media-pause-icon>
|
||||
</media-play-button>
|
||||
</media-tooltip-trigger>
|
||||
<media-tooltip-portal root-id="${portalId}">
|
||||
<media-tooltip-positioner side="top" side-offset="8" collision-padding="8">
|
||||
<media-tooltip-popup>
|
||||
<span class="tooltip play-tooltip">Play</span>
|
||||
<span class="tooltip pause-tooltip">Pause</span>
|
||||
</media-tooltip-popup>
|
||||
</media-tooltip-positioner>
|
||||
</media-tooltip-portal>
|
||||
</media-tooltip-root>
|
||||
<!-- Use the show-remaining attribute to show count down/remaining time -->
|
||||
<media-current-time-display show-remaining></media-current-time-display>
|
||||
<media-time-range-root>
|
||||
@@ -270,21 +318,35 @@ export function getTemplateHTML() {
|
||||
<media-volume-off-icon class="icon volume-off-icon"></media-volume-off-icon>
|
||||
</media-mute-button>
|
||||
</media-popover-trigger>
|
||||
<media-popover-positioner side="top" side-offset="0">
|
||||
<media-popover-popup>
|
||||
<media-volume-range-root orientation="vertical">
|
||||
<media-volume-range-track>
|
||||
<media-volume-range-progress></media-volume-range-progress>
|
||||
</media-volume-range-track>
|
||||
<media-volume-range-thumb></media-volume-range-thumb>
|
||||
</media-volume-range-root>
|
||||
</media-popover-popup>
|
||||
</media-popover-positioner>
|
||||
<media-popover-portal root-id="${portalId}">
|
||||
<media-popover-positioner side="top" side-offset="0">
|
||||
<media-popover-popup>
|
||||
<media-volume-range-root orientation="vertical">
|
||||
<media-volume-range-track>
|
||||
<media-volume-range-progress></media-volume-range-progress>
|
||||
</media-volume-range-track>
|
||||
<media-volume-range-thumb></media-volume-range-thumb>
|
||||
</media-volume-range-root>
|
||||
</media-popover-popup>
|
||||
</media-popover-positioner>
|
||||
</media-popover-portal>
|
||||
</media-popover-root>
|
||||
<media-fullscreen-button class="button">
|
||||
<media-fullscreen-enter-icon class="icon fullscreen-enter-icon"></media-fullscreen-enter-icon>
|
||||
<media-fullscreen-exit-icon class="icon fullscreen-exit-icon"></media-fullscreen-exit-icon>
|
||||
</media-fullscreen-button>
|
||||
<media-tooltip-root delay="600" close-delay="0">
|
||||
<media-tooltip-trigger>
|
||||
<media-fullscreen-button class="button">
|
||||
<media-fullscreen-enter-icon class="icon fullscreen-enter-icon"></media-fullscreen-enter-icon>
|
||||
<media-fullscreen-exit-icon class="icon fullscreen-exit-icon"></media-fullscreen-exit-icon>
|
||||
</media-fullscreen-button>
|
||||
</media-tooltip-trigger>
|
||||
<media-tooltip-portal root-id="${portalId}">
|
||||
<media-tooltip-positioner side="top" side-offset="8" collision-padding="8">
|
||||
<media-tooltip-popup>
|
||||
<span class="tooltip fullscreen-enter-tooltip">Enter Fullscreen</span>
|
||||
<span class="tooltip fullscreen-exit-tooltip">Exit Fullscreen</span>
|
||||
</media-tooltip-popup>
|
||||
</media-tooltip-positioner>
|
||||
</media-tooltip-portal>
|
||||
</media-tooltip-root>
|
||||
</div>
|
||||
</div>
|
||||
</media-container>
|
||||
|
||||
@@ -7,3 +7,15 @@ export function namedNodeMapToObject(namedNodeMap: NamedNodeMap): Record<string,
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
let id = 0;
|
||||
|
||||
/**
|
||||
* Generates a unique ID for an element.
|
||||
* React's useId() is more complex but might not be needed for this use case.
|
||||
* @returns A unique ID.
|
||||
*/
|
||||
export function uniqueId(): string {
|
||||
id++;
|
||||
return `:h${id}:`;
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ interface PopoverPopupProps {
|
||||
|
||||
interface PopoverPortalProps {
|
||||
children: ReactNode;
|
||||
container?: HTMLElement | ShadowRoot | React.MutableRefObject<HTMLElement | ShadowRoot | null> | null;
|
||||
id?: string;
|
||||
root?: HTMLElement | ShadowRoot | React.MutableRefObject<HTMLElement | ShadowRoot | null> | null;
|
||||
rootId?: string;
|
||||
}
|
||||
|
||||
const PopoverContext = createContext<PopoverContextType | null>(null);
|
||||
@@ -149,8 +149,8 @@ function PopoverPopup({ className, children }: PopoverPopupProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverPortal({ children, container, id }: PopoverPortalProps): JSX.Element {
|
||||
return <FloatingPortal root={container as HTMLElement} id={id}>{children}</FloatingPortal>;
|
||||
function PopoverPortal({ children, root, rootId }: PopoverPortalProps): JSX.Element {
|
||||
return <FloatingPortal root={root as HTMLElement} id={rootId as string}>{children}</FloatingPortal>;
|
||||
}
|
||||
|
||||
// Export compound component
|
||||
|
||||
@@ -65,8 +65,8 @@ interface TooltipArrowProps {
|
||||
|
||||
interface TooltipPortalProps {
|
||||
children: ReactNode;
|
||||
container?: HTMLElement | ShadowRoot | React.MutableRefObject<HTMLElement | ShadowRoot | null> | null;
|
||||
id?: string;
|
||||
root?: HTMLElement | ShadowRoot | React.MutableRefObject<HTMLElement | ShadowRoot | null> | null;
|
||||
rootId?: string;
|
||||
}
|
||||
|
||||
const TooltipContext = createContext<TooltipContextType | null>(null);
|
||||
@@ -226,8 +226,8 @@ function TooltipArrow({ className = '', children }: TooltipArrowProps): JSX.Elem
|
||||
);
|
||||
}
|
||||
|
||||
function TooltipPortal({ children, container, id }: TooltipPortalProps): JSX.Element {
|
||||
return <FloatingPortal root={container as HTMLElement} id={id as string}>{children}</FloatingPortal>;
|
||||
function TooltipPortal({ children, root, rootId }: TooltipPortalProps): JSX.Element {
|
||||
return <FloatingPortal root={root as HTMLElement} id={rootId as string}>{children}</FloatingPortal>;
|
||||
}
|
||||
|
||||
// Export compound component
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function MediaSkinDefault({ children, className = '' }: SkinProps
|
||||
<PauseIcon className={styles.PauseIcon}></PauseIcon>
|
||||
</PlayButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal id={portalId}>
|
||||
<Tooltip.Portal rootId={portalId}>
|
||||
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
|
||||
<Tooltip.Popup className={`${styles.TooltipPopup} ${styles.PlayTooltipPopup}`}>
|
||||
<span className={styles.PlayTooltip}>Play</span>
|
||||
@@ -81,7 +81,7 @@ export default function MediaSkinDefault({ children, className = '' }: SkinProps
|
||||
<VolumeOffIcon className={styles.VolumeOffIcon} />
|
||||
</MuteButton>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal id={portalId}>
|
||||
<Popover.Portal rootId={portalId}>
|
||||
<Popover.Positioner side="top" sideOffset={12}>
|
||||
<Popover.Popup className={styles.PopoverPopup}>
|
||||
<VolumeRange.Root className={styles.SliderRoot} orientation="vertical">
|
||||
@@ -102,7 +102,7 @@ export default function MediaSkinDefault({ children, className = '' }: SkinProps
|
||||
<FullscreenExitIcon className={styles.FullScreenExitIcon} />
|
||||
</FullscreenButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal id={portalId}>
|
||||
<Tooltip.Portal rootId={portalId}>
|
||||
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
|
||||
<Tooltip.Popup className={`${styles.TooltipPopup} ${styles.FullScreenTooltipPopup}`}>
|
||||
<span className={styles.FullScreenEnterTooltip}>Enter Fullscreen</span>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { cn } from "../../utils/cn";
|
||||
|
||||
const styles: MediaDefaultSkinStyles = {
|
||||
MediaContainer: cn(
|
||||
'media-container relative @container/root group/root overflow-clip',
|
||||
'relative @container/root group/root overflow-clip',
|
||||
// Base typography
|
||||
'text-sm',
|
||||
// Prevent rounded corners in fullscreen.
|
||||
|
||||
Reference in New Issue
Block a user