feat(react): use popover and anchor position API (#178)

This commit is contained in:
Wesley Luyten
2025-11-11 12:28:50 -06:00
committed by GitHub
parent a62623ba41
commit f513c74a6c
15 changed files with 597 additions and 487 deletions
@@ -32,14 +32,12 @@ export default function FrostedSkin({ children, className = '' }: SkinProps): JS
<PauseIcon className="icon pause-icon" />
</PlayButton>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className="tooltip-popup surface popup-animation">
<span className="tooltip play-tooltip">Play</span>
<span className="tooltip pause-tooltip">Pause</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className="tooltip-popup surface popup-animation">
<span className="tooltip play-tooltip">Play</span>
<span className="tooltip pause-tooltip">Pause</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
<div className="time-controls">
@@ -59,13 +57,11 @@ export default function FrostedSkin({ children, className = '' }: SkinProps): JS
<TimeSlider.Thumb className="slider-thumb" />
</TimeSlider.Root>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={18} collisionPadding={12}>
<Tooltip.Popup className="surface popup-animation tooltip-popup">
<PreviewTimeDisplay className="time-display media-preview-time-display" />
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={18} collisionPadding={12}>
<Tooltip.Popup className="surface popup-animation tooltip-popup">
<PreviewTimeDisplay className="time-display media-preview-time-display" />
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
<DurationDisplay className="time-display" />
@@ -79,18 +75,16 @@ export default function FrostedSkin({ children, className = '' }: SkinProps): JS
<VolumeOffIcon className="icon volume-off-icon" />
</MuteButton>
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner side="top" sideOffset={12}>
<Popover.Popup className="surface popup-animation popover-popup">
<VolumeSlider.Root className="slider" orientation="vertical">
<VolumeSlider.Track className="slider-track">
<VolumeSlider.Progress className="slider-progress" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="slider-thumb" />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
<Popover.Positioner side="top" sideOffset={12}>
<Popover.Popup className="surface popup-animation popover-popup">
<VolumeSlider.Root className="slider" orientation="vertical">
<VolumeSlider.Track className="slider-track">
<VolumeSlider.Progress className="slider-progress" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="slider-thumb" />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Positioner>
</Popover.Root>
<Tooltip.Root delay={500}>
@@ -100,14 +94,12 @@ export default function FrostedSkin({ children, className = '' }: SkinProps): JS
<FullscreenExitIcon className="icon fullscreen-exit-icon" />
</FullscreenButton>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className="surface popup-animation tooltip-popup">
<span className="tooltip fullscreen-enter-tooltip">Enter Fullscreen</span>
<span className="tooltip fullscreen-exit-tooltip">Exit Fullscreen</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className="surface popup-animation tooltip-popup">
<span className="tooltip fullscreen-enter-tooltip">Enter Fullscreen</span>
<span className="tooltip fullscreen-exit-tooltip">Exit Fullscreen</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
</div>
</MediaContainer>
@@ -330,8 +330,6 @@
.vjs-frosted-skin .popover-popup {
margin: 0;
border: none;
box-shadow: none;
background: transparent;
padding: 0.75rem 0.25rem;
border-radius: calc(infinity * 1px);
}
@@ -45,14 +45,12 @@ export default function FrostedSkin({ children, className = '' }: SkinProps): JS
<PauseIcon className={styles.PauseIcon}></PauseIcon>
</PlayButton>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className={`${styles.TooltipPopup} ${styles.PlayTooltipPopup}`}>
<span className={styles.PlayTooltip}>Play</span>
<span className={styles.PauseTooltip}>Pause</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className={`${styles.TooltipPopup} ${styles.PlayTooltipPopup}`}>
<span className={styles.PlayTooltip}>Play</span>
<span className={styles.PauseTooltip}>Pause</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
<div className={styles.TimeControls}>
@@ -81,18 +79,16 @@ export default function FrostedSkin({ children, className = '' }: SkinProps): JS
<VolumeOffIcon className={styles.VolumeOffIcon} />
</MuteButton>
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner side="top" sideOffset={12}>
<Popover.Popup className={styles.PopoverPopup}>
<VolumeSlider.Root className={styles.SliderRoot} orientation="vertical">
<VolumeSlider.Track className={styles.SliderTrack}>
<VolumeSlider.Progress className={styles.SliderProgress} />
</VolumeSlider.Track>
<VolumeSlider.Thumb className={styles.SliderThumb} />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
<Popover.Positioner side="top" sideOffset={12}>
<Popover.Popup className={styles.PopoverPopup}>
<VolumeSlider.Root className={styles.SliderRoot} orientation="vertical">
<VolumeSlider.Track className={styles.SliderTrack}>
<VolumeSlider.Progress className={styles.SliderProgress} />
</VolumeSlider.Track>
<VolumeSlider.Thumb className={styles.SliderThumb} />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Positioner>
</Popover.Root>
<Tooltip.Root delay={600} closeDelay={0}>
@@ -102,14 +98,12 @@ export default function FrostedSkin({ children, className = '' }: SkinProps): JS
<FullscreenExitIcon className={styles.FullScreenExitIcon} />
</FullscreenButton>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className={`${styles.TooltipPopup} ${styles.FullScreenTooltipPopup}`}>
<span className={styles.FullScreenEnterTooltip}>Enter Fullscreen</span>
<span className={styles.FullScreenExitTooltip}>Exit Fullscreen</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className={`${styles.TooltipPopup} ${styles.FullScreenTooltipPopup}`}>
<span className={styles.FullScreenEnterTooltip}>Enter Fullscreen</span>
<span className={styles.FullScreenExitTooltip}>Exit Fullscreen</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
</div>
</MediaContainer>
@@ -32,14 +32,12 @@ export default function MinimalSkin({ children, className = '' }: SkinProps): JS
<PauseIcon className="icon pause-icon" />
</PlayButton>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner side="top-start" sideOffset={6} collisionPadding={12}>
<Tooltip.Popup className="popup-animation tooltip-popup">
<span className="tooltip play-tooltip">Play</span>
<span className="tooltip pause-tooltip">Pause</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
<Tooltip.Positioner side="top-start" sideOffset={6} collisionPadding={12}>
<Tooltip.Popup className="popup-animation tooltip-popup">
<span className="tooltip play-tooltip">Play</span>
<span className="tooltip pause-tooltip">Pause</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
<div className="time-display-group">
@@ -65,13 +63,11 @@ export default function MinimalSkin({ children, className = '' }: SkinProps): JS
<TimeSlider.Thumb className="slider-thumb" />
</TimeSlider.Root>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className="popup-animation tooltip-popup">
<PreviewTimeDisplay className="time-display media-preview-time-display" />
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className="popup-animation tooltip-popup">
<PreviewTimeDisplay className="time-display media-preview-time-display" />
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
<div className="button-group">
@@ -83,18 +79,16 @@ export default function MinimalSkin({ children, className = '' }: SkinProps): JS
<VolumeOffIcon className="icon volume-off-icon" />
</MuteButton>
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner side="top" sideOffset={2}>
<Popover.Popup className="popup-animation popover-popup">
<VolumeSlider.Root className="slider" orientation="vertical">
<VolumeSlider.Track className="slider-track">
<VolumeSlider.Progress className="slider-progress" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="slider-thumb" />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
<Popover.Positioner side="top" sideOffset={2}>
<Popover.Popup className="popup-animation popover-popup">
<VolumeSlider.Root className="slider" orientation="vertical">
<VolumeSlider.Track className="slider-track">
<VolumeSlider.Progress className="slider-progress" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="slider-thumb" />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Positioner>
</Popover.Root>
<Tooltip.Root delay={500} closeDelay={0}>
@@ -104,14 +98,12 @@ export default function MinimalSkin({ children, className = '' }: SkinProps): JS
<FullscreenExitAltIcon className="icon fullscreen-exit-icon" />
</FullscreenButton>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner side="top-end" sideOffset={6} collisionPadding={12}>
<Tooltip.Popup className="popup-animation tooltip-popup">
<span className="tooltip fullscreen-enter-tooltip">Enter Fullscreen</span>
<span className="tooltip fullscreen-exit-tooltip">Exit Fullscreen</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
<Tooltip.Positioner side="top-end" sideOffset={6} collisionPadding={12}>
<Tooltip.Popup className="popup-animation tooltip-popup">
<span className="tooltip fullscreen-enter-tooltip">Enter Fullscreen</span>
<span className="tooltip fullscreen-exit-tooltip">Exit Fullscreen</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
</div>
</div>
+8 -3
View File
@@ -1,6 +1,6 @@
import { contains, getDocument, getDocumentOrShadowRoot, safePolygon } from '@videojs/utils/dom';
type Placement = 'top' | 'bottom' | 'left' | 'right';
type Placement = 'top' | 'top-start' | 'top-end';
export class PopoverElement extends HTMLElement {
static get observedAttributes(): string[] {
@@ -18,9 +18,14 @@ export class PopoverElement extends HTMLElement {
this.style.setProperty('position-anchor', `--${newValue}`);
}
this.style.setProperty('top', `calc(anchor(${this.side}) - ${this.sideOffset}px)`);
const [side, alignment] = this.side.split('-');
this.style.setProperty('top', `calc(anchor(${side}) - ${this.sideOffset}px)`);
this.style.setProperty('translate', `0 -100%`);
this.style.setProperty('justify-self', 'anchor-center');
this.style.setProperty('justify-self', alignment === 'start'
? 'anchor-start'
: alignment === 'end'
? 'anchor-end'
: 'anchor-center');
}
connectedCallback(): void {
+8 -4
View File
@@ -6,7 +6,7 @@ import {
getInBoundsAdjustments,
} from '@videojs/utils/dom';
type Placement = 'top' | 'bottom' | 'left' | 'right';
type Placement = 'top' | 'top-start' | 'top-end';
export class TooltipElement extends HTMLElement {
static get observedAttributes(): string[] {
@@ -30,14 +30,18 @@ export class TooltipElement extends HTMLElement {
if (name === 'id') {
this.style.setProperty('position-anchor', `--${newValue}`);
}
this.style.setProperty('top', `calc(anchor(${this.side}) - ${this.sideOffset}px)`);
const [side, alignment] = this.side.split('-');
this.style.setProperty('top', `calc(anchor(${side}) - ${this.sideOffset}px)`);
if (this.trackCursorAxis) {
this.style.setProperty('translate', `-50% -100%`);
} else {
this.style.setProperty('translate', `0 -100%`);
this.style.setProperty('justify-self', 'anchor-center');
this.style.setProperty('justify-self', alignment === 'start'
? 'anchor-start'
: alignment === 'end'
? 'anchor-end'
: 'anchor-center');
}
}
@@ -2,14 +2,10 @@ import type { Constructor, CustomElement } from '@open-wc/context-protocol';
import { ConsumerMixin } from '@open-wc/context-protocol';
/* @TODO We need to make sure portal logic is non-brittle longer term (CJP) */
export function getTemplateHTML() {
return /* html */ `
<slot name="media"></slot>
<slot></slot>
<div id="@default_portal_id" style={ position: absolute; zIndex: 10; }>
<slot name="portal"></slot>
</div>
`;
}
-1
View File
@@ -55,7 +55,6 @@
"react": ">=16.8.0"
},
"dependencies": {
"@floating-ui/react": "^0.27.16",
"@videojs/core": "workspace:*",
"@videojs/icons": "workspace:*",
"@videojs/utils": "workspace:*"
@@ -54,8 +54,8 @@ export function useMediaContainerRef(): RefCallback<HTMLElement | null> {
* </MediaContainer>
* );
*/
export const MediaContainer: FC<PropsWithChildren<HTMLProps<HTMLDivElement> & { portalId?: string }>> = forwardRef(
({ children, portalId = '@default_portal_id', ...props }, ref) => {
export const MediaContainer: FC<PropsWithChildren<HTMLProps<HTMLDivElement>>> = forwardRef(
({ children, ...props }, ref) => {
const containerRef = useMediaContainerRef();
const composedRef = useComposedRefs(ref, containerRef);
@@ -78,11 +78,10 @@ export const MediaContainer: FC<PropsWithChildren<HTMLProps<HTMLDivElement> & {
<div
ref={composedRef}
onClick={handleClick}
data-media-container
{...props}
>
{children}
{/* @TODO We need to make sure this is non-brittle longer term (CJP) */}
<div id={portalId} style={{ position: 'absolute', zIndex: 10 }} />
</div>
);
},
+210 -112
View File
@@ -1,22 +1,6 @@
import type { OpenChangeReason, Placement } from '@floating-ui/react';
import type { MutableRefObject, ReactNode } from 'react';
import type { ReactNode } from 'react';
import {
autoUpdate,
flip,
FloatingFocusManager,
FloatingPortal,
offset,
safePolygon,
shift,
useDismiss,
useFloating,
useFocus,
useHover,
useInteractions,
useRole,
useTransitionStatus,
} from '@floating-ui/react';
import { contains, safePolygon } from '@videojs/utils/dom';
import {
Children,
@@ -26,20 +10,23 @@ import {
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
type Placement = 'top' | 'top-start' | 'top-end';
type TransitionStatus = 'initial' | 'open' | 'close' | 'unmounted';
interface PopoverContextType {
open: boolean;
setOpen: (open: boolean) => void;
openReason: OpenChangeReason | null;
refs: ReturnType<typeof useFloating>['refs'];
floatingStyles: ReturnType<typeof useFloating>['floatingStyles'];
getReferenceProps: ReturnType<typeof useInteractions>['getReferenceProps'];
getFloatingProps: ReturnType<typeof useInteractions>['getFloatingProps'];
context: ReturnType<typeof useFloating>['context'];
popupRef: React.RefObject<HTMLElement | null>;
triggerRef: React.RefObject<HTMLElement | null>;
updatePositioning: (placement: Placement, sideOffset: number) => void;
transitionStatus: ReturnType<typeof useTransitionStatus>['status'];
transitionStatus: TransitionStatus;
placement: Placement;
sideOffset: number;
}
interface PopoverRootProps {
@@ -60,16 +47,11 @@ interface PopoverPositionerProps {
}
interface PopoverPopupProps {
id?: string;
className?: string;
children: ReactNode;
}
interface PopoverPortalProps {
children: ReactNode;
root?: HTMLElement | ShadowRoot | MutableRefObject<HTMLElement | ShadowRoot | null> | null;
rootId?: string;
}
const PopoverContext = createContext<PopoverContextType | null>(null);
function usePopoverContext(): PopoverContextType {
@@ -84,125 +66,243 @@ function PopoverRoot({ openOnHover = false, delay = 0, closeDelay = 0, children
const [open, setOpen] = useState(false);
const [placement, setPlacement] = useState<Placement>('top');
const [sideOffset, setSideOffset] = useState(5);
const [openReason, setOpenReason] = useState<OpenChangeReason | null>(null);
const [transitionStatus, setTransitionStatus] = useState<TransitionStatus>('initial');
const popupRef = useRef<HTMLElement | null>(null);
const triggerRef = useRef<HTMLElement | null>(null);
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const pointerMoveHandlerRef = useRef<((event: MouseEvent) => void) | null>(null);
const { refs, floatingStyles, context } = useFloating({
open,
onOpenChange: (open, _event, reason) => {
setOpen(open);
setOpenReason(reason || null);
},
placement,
middleware: [offset(sideOffset), flip(), shift()],
whileElementsMounted: autoUpdate,
});
const clearHoverTimeout = useCallback(() => {
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
hoverTimeoutRef.current = null;
}
}, []);
const { status: transitionStatus } = useTransitionStatus(context);
const setOpenState = useCallback((newOpen: boolean) => {
if (open === newOpen) return;
const hover = useHover(context, {
enabled: openOnHover,
mouseOnly: true,
move: false,
delay: {
open: delay,
close: closeDelay,
},
handleClose: safePolygon({ blockPointerEvents: true }),
});
const focus = useFocus(context);
const dismiss = useDismiss(context);
const role = useRole(context);
setOpen(newOpen);
const { getReferenceProps, getFloatingProps } = useInteractions([hover, focus, dismiss, role]);
if (newOpen) {
setTransitionStatus('initial');
if (popupRef.current) {
popupRef.current.showPopover();
}
requestAnimationFrame(() => {
setTransitionStatus('open');
});
} else {
setTransitionStatus('close');
}
}, [open]);
useEffect(() => {
if (!popupRef.current || open) return;
const transitions = popupRef.current.getAnimations().filter(anim => anim instanceof CSSTransition);
if (transitions.length > 0) {
Promise.all(transitions.map(t => t.finished))
.then(() => popupRef.current?.hidePopover())
.catch(() => popupRef.current?.hidePopover());
} else {
popupRef.current.hidePopover();
}
}, [open, transitionStatus]);
const updatePositioning = useCallback((newPlacement: Placement, newSideOffset: number) => {
setPlacement(newPlacement);
setSideOffset(newSideOffset);
}, []);
useEffect(() => {
const trigger = triggerRef.current;
const popup = popupRef.current;
if (!trigger || !popup) return;
const abortController = new AbortController();
const { signal } = abortController;
const addPointerMoveListener = () => {
if (!globalThis.matchMedia?.('(hover: hover)')?.matches) return;
if (!pointerMoveHandlerRef.current) {
pointerMoveHandlerRef.current = safePolygon({ blockPointerEvents: true })({
placement,
elements: {
domReference: trigger,
floating: popup,
},
x: 0,
y: 0,
onClose: () => {
if (pointerMoveHandlerRef.current) {
document.documentElement.removeEventListener('pointermove', pointerMoveHandlerRef.current);
}
clearHoverTimeout();
hoverTimeoutRef.current = setTimeout(() => {
setOpenState(false);
}, closeDelay);
},
});
}
// Event listener is automatically removed when AbortController is aborted
document.documentElement.addEventListener('pointermove', pointerMoveHandlerRef.current, { signal });
};
const handlePointerEnter = (event: PointerEvent) => {
if (!openOnHover) return;
clearHoverTimeout();
if (event.currentTarget === popup) {
addPointerMoveListener();
}
if (open) {
return;
}
hoverTimeoutRef.current = setTimeout(() => {
setOpenState(true);
}, delay);
};
const handlePointerLeave = () => {
if (!openOnHover) return;
addPointerMoveListener();
};
if (globalThis.matchMedia?.('(hover: hover)')?.matches) {
// Event listeners are automatically removed when AbortController is aborted
trigger.addEventListener('pointerenter', handlePointerEnter, { signal });
trigger.addEventListener('pointerleave', handlePointerLeave, { signal });
popup.addEventListener('pointerenter', handlePointerEnter, { signal });
}
const handleFocusIn = () => {
setOpenState(true);
};
const handleFocusOut = (event: FocusEvent) => {
const relatedTarget = event.relatedTarget as HTMLElement;
if (relatedTarget && popup && contains(popup, relatedTarget)) return;
setOpenState(false);
};
// Event listeners are automatically removed when AbortController is aborted
trigger.addEventListener('focusin', handleFocusIn, { signal });
trigger.addEventListener('focusout', handleFocusOut, { signal });
popup.addEventListener('focusout', handleFocusOut, { signal });
return () => {
abortController.abort();
clearHoverTimeout();
if (pointerMoveHandlerRef.current) {
document.documentElement.removeEventListener('pointermove', pointerMoveHandlerRef.current);
}
pointerMoveHandlerRef.current = null;
};
}, [openOnHover, delay, closeDelay, open, placement, setOpenState, clearHoverTimeout]);
useEffect(() => {
if (!popupRef.current || !triggerRef.current) return;
const popup = popupRef.current;
const popupId = popup.id;
if (popupId) {
popup.style.setProperty('position-anchor', `--${popupId}`);
}
const [side, alignment] = placement.split('-');
popup.style.setProperty('top', `calc(anchor(${side}) - ${sideOffset}px)`);
popup.style.setProperty('translate', `0 -100%`);
popup.style.setProperty('justify-self', alignment === 'start'
? 'anchor-start'
: alignment === 'end'
? 'anchor-end'
: 'anchor-center');
}, [placement, sideOffset]);
const value: PopoverContextType = useMemo(() => ({
open,
setOpen,
openReason,
refs,
floatingStyles,
getReferenceProps,
getFloatingProps,
context,
setOpen: setOpenState,
popupRef,
triggerRef,
updatePositioning,
transitionStatus,
}), [open, openReason, refs, floatingStyles, getReferenceProps, getFloatingProps, context, updatePositioning, transitionStatus]);
placement,
sideOffset,
}), [open, setOpenState, updatePositioning, transitionStatus, placement, sideOffset]);
return <PopoverContext.Provider value={value}>{children}</PopoverContext.Provider>;
}
function PopoverTrigger({ children }: PopoverTriggerProps): JSX.Element {
const { refs, getReferenceProps, open } = usePopoverContext();
const { triggerRef, open } = usePopoverContext();
// eslint-disable-next-line react/no-clone-element, react/no-children-only
return cloneElement(Children.only(children) as JSX.Element, {
ref: refs.setReference,
...getReferenceProps(),
ref: triggerRef,
'data-popup-open': open ? '' : undefined,
});
}
function PopoverPositioner({ side = 'top', sideOffset = 5, children }: PopoverPositionerProps): JSX.Element | null {
const { refs, floatingStyles, updatePositioning } = usePopoverContext();
const { updatePositioning } = usePopoverContext();
useEffect(() => {
updatePositioning(side, sideOffset);
}, [side, sideOffset, updatePositioning]);
return (
<div ref={refs.setFloating} style={floatingStyles}>
{children}
</div>
);
return <>{children}</>;
}
function PopoverPopup({ className, children }: PopoverPopupProps): JSX.Element {
const { getFloatingProps, context, transitionStatus } = usePopoverContext();
const { refs, placement } = context;
const triggerElement = refs.reference.current as HTMLElement | null;
function PopoverPopup({ id, className, children }: PopoverPopupProps): JSX.Element {
const { popupRef, triggerRef, transitionStatus, placement } = usePopoverContext();
const triggerElement = triggerRef.current;
const popupId = useMemo(() => id ?? `popover-${Math.random().toString(36).substring(2, 9)}`, [id]);
useEffect(() => {
if (!popupRef.current || !triggerRef.current) return;
const popup = popupRef.current;
const trigger = triggerRef.current;
popup.setAttribute('popover', 'manual');
trigger.setAttribute('commandfor', popupId);
trigger.style.setProperty('anchor-name', `--${popupId}`);
}, [popupId, popupRef, triggerRef]);
// Copy data attributes from trigger element
const dataAttributes = triggerElement?.attributes
? Object.fromEntries(
Array.from(triggerElement.attributes)
.filter(attr => attr.name.startsWith('data-'))
.map(attr => [attr.name, attr.value]),
)
: {};
const dataAttributes = useMemo(() => {
if (!triggerElement?.attributes) return {};
return Object.fromEntries(
Array.from(triggerElement.attributes)
.filter(attr => attr.name.startsWith('data-'))
.map(attr => [attr.name, attr.value]),
);
}, [triggerElement]);
return (
<FloatingFocusManager
context={context}
modal={false}
initialFocus={-1}
returnFocus={false}
<div
ref={popupRef as React.RefObject<HTMLDivElement>}
id={popupId}
className={className}
{...dataAttributes}
data-side={placement}
data-starting-style={transitionStatus === 'initial' ? '' : undefined}
data-open={transitionStatus === 'initial' || transitionStatus === 'open' ? '' : undefined}
data-ending-style={transitionStatus === 'close' || transitionStatus === 'unmounted' ? '' : undefined}
data-closed={transitionStatus === 'close' || transitionStatus === 'unmounted' ? '' : undefined}
>
<div
className={className}
{...getFloatingProps()}
{...dataAttributes}
data-side={placement}
data-starting-style={transitionStatus === 'initial' ? '' : undefined}
data-open={transitionStatus === 'initial' || transitionStatus === 'open' ? '' : undefined}
data-ending-style={transitionStatus === 'close' || transitionStatus === 'unmounted' ? '' : undefined}
data-closed={transitionStatus === 'close' || transitionStatus === 'unmounted' ? '' : undefined}
>
{children}
</div>
</FloatingFocusManager>
);
}
function PopoverPortal({ children, root, rootId = '@default_portal_id' }: PopoverPortalProps): JSX.Element {
return (
<FloatingPortal root={root as HTMLElement} id={rootId as string}>
{children}
</FloatingPortal>
</div>
);
}
@@ -212,13 +312,11 @@ export const Popover: {
Trigger: typeof PopoverTrigger;
Positioner: typeof PopoverPositioner;
Popup: typeof PopoverPopup;
Portal: typeof PopoverPortal;
} = {
Root: PopoverRoot,
Trigger: PopoverTrigger,
Positioner: PopoverPositioner,
Popup: PopoverPopup,
Portal: PopoverPortal,
};
export default Popover;
+237 -171
View File
@@ -1,22 +1,6 @@
import type { Placement } from '@floating-ui/react';
import type { MutableRefObject, ReactNode, RefObject } from 'react';
import type { ReactNode, RefObject } from 'react';
import {
arrow,
autoUpdate,
flip,
FloatingPortal,
offset,
shift,
useClientPoint,
useDismiss,
useFloating,
useFocus,
useHover,
useInteractions,
useRole,
useTransitionStatus,
} from '@floating-ui/react';
import { contains, getBoundingClientRectWithoutTransform, getInBoundsAdjustments } from '@videojs/utils/dom';
import {
Children,
@@ -30,20 +14,26 @@ import {
useState,
} from 'react';
type Placement = 'top' | 'top-start' | 'top-end';
interface UpdatePositioningProps {
side: Placement;
sideOffset: number;
collisionPadding: number;
}
type TransitionStatus = 'initial' | 'open' | 'close' | 'unmounted';
interface TooltipContextType {
arrowRef: MutableRefObject<HTMLElement | null>;
context: ReturnType<typeof useFloating>['context'];
transitionStatus: ReturnType<typeof useTransitionStatus>['status'];
getReferenceProps: ReturnType<typeof useInteractions>['getReferenceProps'];
getFloatingProps: ReturnType<typeof useInteractions>['getFloatingProps'];
popupRef: RefObject<HTMLElement | null>;
triggerRef: RefObject<HTMLElement | null>;
open: boolean;
transitionStatus: TransitionStatus;
updatePositioning: (props: UpdatePositioningProps) => void;
trackCursorAxis?: 'x' | 'y' | 'both' | undefined;
placement: Placement;
sideOffset: number;
collisionPadding: number;
}
interface TooltipRootProps {
@@ -65,29 +55,13 @@ interface TooltipPositionerProps {
}
interface TooltipPopupProps {
id?: string;
className?: string;
children: ReactNode;
}
interface TooltipArrowProps {
className?: string;
children: ReactNode;
}
interface TooltipPortalProps {
children: ReactNode;
root?: HTMLElement | ShadowRoot | MutableRefObject<HTMLElement | ShadowRoot | null> | null;
rootId?: string;
}
const TooltipContext = createContext<TooltipContextType | null>(null);
interface TooltipPositionerContextType {
side: Placement;
}
const TooltipPositionerContext = createContext<TooltipPositionerContextType | null>(null);
function useTooltipContext(): TooltipContextType {
const context = useContext(TooltipContext);
if (!context) {
@@ -96,61 +70,16 @@ function useTooltipContext(): TooltipContextType {
return context;
}
function useTooltipPositionerContext(): TooltipPositionerContextType {
const context = useContext(TooltipPositionerContext);
if (!context) {
throw new Error('TooltipArrow must be used within TooltipPositioner');
}
return context;
}
function TooltipRoot({ delay = 0, closeDelay = 0, trackCursorAxis, children }: TooltipRootProps): JSX.Element {
const [open, setOpen] = useState(false);
const [placement, setPlacement] = useState<Placement>('top');
const [sideOffset, setSideOffset] = useState(0);
const [collisionPadding, setCollisionPadding] = useState(0);
const arrowRef = useRef<HTMLElement | null>(null);
const { context } = useFloating({
open,
onOpenChange: setOpen,
placement,
middleware: [
offset(sideOffset),
flip(),
shift({ padding: collisionPadding }),
arrow({
element: arrowRef,
}),
],
whileElementsMounted: autoUpdate,
});
const { status: transitionStatus } = useTransitionStatus(context);
const hover = useHover(context, {
// restMs: 300, // this broke the time slider tooltip, todo: find solution!
delay: {
open: delay,
close: closeDelay,
},
});
const focus = useFocus(context);
const dismiss = useDismiss(context);
const role = useRole(context, { role: 'tooltip' });
// Use client point hook when trackCursorAxis is enabled
const clientPoint = useClientPoint(context, {
axis: trackCursorAxis || 'both',
enabled: !!trackCursorAxis,
});
// Combine interactions based on whether cursor tracking is enabled
const interactions = trackCursorAxis
? [hover, focus, dismiss, role, clientPoint]
: [hover, focus, dismiss, role];
const { getReferenceProps, getFloatingProps } = useInteractions(interactions);
const [transitionStatus, setTransitionStatus] = useState<TransitionStatus>('initial');
const popupRef = useRef<HTMLElement | null>(null);
const triggerRef = useRef<HTMLElement | null>(null);
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const pointerPositionRef = useRef({ x: 0, y: 0 });
const updatePositioning = useCallback(({ side, sideOffset, collisionPadding }: UpdatePositioningProps) => {
setPlacement(side);
@@ -158,27 +87,203 @@ function TooltipRoot({ delay = 0, closeDelay = 0, trackCursorAxis, children }: T
setCollisionPadding(collisionPadding);
}, []);
const value: TooltipContextType = useMemo(() => ({
getReferenceProps,
getFloatingProps,
context,
updatePositioning,
arrowRef,
transitionStatus,
trackCursorAxis,
}), [getReferenceProps, getFloatingProps, context, updatePositioning, transitionStatus, trackCursorAxis]);
const clearHoverTimeout = useCallback(() => {
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
hoverTimeoutRef.current = null;
}
}, []);
const checkCollision = useCallback(() => {
if (!popupRef.current || !triggerRef.current || !open) return;
const mediaContainer = popupRef.current.closest('[data-media-container]') as HTMLElement | null;
if (!mediaContainer) return;
const popupRect = getBoundingClientRectWithoutTransform(popupRef.current);
const boundsRect = getBoundingClientRectWithoutTransform(mediaContainer);
const { x } = getInBoundsAdjustments(popupRect, boundsRect, collisionPadding);
if (x !== 0) {
if (trackCursorAxis) {
const currentLeft = Number.parseFloat(popupRef.current.style.left || '0');
popupRef.current.style.setProperty('left', `${currentLeft + x}px`);
} else {
popupRef.current.style.setProperty('translate', `${x}px -100%`);
}
} else {
if (trackCursorAxis) {
popupRef.current.style.setProperty('translate', '-50% -100%');
} else {
popupRef.current.style.setProperty('translate', '0 -100%');
}
}
}, [open, collisionPadding, trackCursorAxis]);
const updatePosition = useCallback(() => {
if (open && trackCursorAxis && popupRef.current) {
popupRef.current.style.setProperty('left', `${pointerPositionRef.current.x}px`);
checkCollision();
}
}, [open, trackCursorAxis, checkCollision]);
const setOpenState = useCallback(
(newOpen: boolean) => {
if (open === newOpen) return;
setOpen(newOpen);
if (newOpen) {
setTransitionStatus('initial');
if (popupRef.current) {
popupRef.current.showPopover();
}
requestAnimationFrame(() => {
setTransitionStatus('open');
checkCollision();
});
} else {
setTransitionStatus('close');
}
},
[open, checkCollision],
);
useEffect(() => {
if (!popupRef.current || open) return;
const transitions = popupRef.current.getAnimations().filter(anim => anim instanceof CSSTransition);
if (transitions.length > 0) {
Promise.all(transitions.map(t => t.finished))
.then(() => popupRef.current?.hidePopover())
.catch(() => popupRef.current?.hidePopover());
} else {
popupRef.current.hidePopover();
}
}, [open, transitionStatus]);
useEffect(() => {
const trigger = triggerRef.current;
const popup = popupRef.current;
if (!trigger || !popup) return;
const abortController = new AbortController();
const { signal } = abortController;
const handlePointerEnter = () => {
clearHoverTimeout();
hoverTimeoutRef.current = setTimeout(() => {
setOpenState(true);
}, delay);
};
const handlePointerLeave = () => {
clearHoverTimeout();
hoverTimeoutRef.current = setTimeout(() => {
setOpenState(false);
}, closeDelay);
};
const handlePointerMove = (event: PointerEvent) => {
if (trackCursorAxis) {
pointerPositionRef.current = { x: event.clientX, y: event.clientY };
if (open) {
updatePosition();
}
}
};
const handleFocusIn = () => {
setOpenState(true);
};
const handleFocusOut = (event: FocusEvent) => {
const relatedTarget = event.relatedTarget as HTMLElement;
if (relatedTarget && popup && contains(popup, relatedTarget)) return;
setOpenState(false);
};
if (globalThis.matchMedia?.('(hover: hover)')?.matches) {
// Event listeners are automatically removed when AbortController is aborted
trigger.addEventListener('pointerenter', handlePointerEnter, { signal });
trigger.addEventListener('pointerleave', handlePointerLeave, { signal });
if (trackCursorAxis) {
trigger.addEventListener('pointermove', handlePointerMove, { signal });
}
}
// Event listeners are automatically removed when AbortController is aborted
trigger.addEventListener('focusin', handleFocusIn, { signal });
trigger.addEventListener('focusout', handleFocusOut, { signal });
return () => {
abortController.abort();
clearHoverTimeout();
};
}, [delay, closeDelay, trackCursorAxis, open, setOpenState, clearHoverTimeout, updatePosition]);
useEffect(() => {
if (!popupRef.current || !triggerRef.current) return;
const popup = popupRef.current;
const popupId = popup.id;
if (popupId) {
popup.style.setProperty('position-anchor', `--${popupId}`);
}
const [side, alignment] = placement.split('-');
popup.style.setProperty('top', `calc(anchor(${side}) - ${sideOffset}px)`);
if (trackCursorAxis) {
popup.style.setProperty('translate', `-50% -100%`);
} else {
popup.style.setProperty('translate', `0 -100%`);
popup.style.setProperty('justify-self', alignment === 'start'
? 'anchor-start'
: alignment === 'end'
? 'anchor-end'
: 'anchor-center');
}
}, [placement, sideOffset, trackCursorAxis]);
useEffect(() => {
if (!popupRef.current) return;
const resizeObserver = new ResizeObserver(checkCollision);
resizeObserver.observe(popupRef.current);
return () => {
resizeObserver.disconnect();
};
}, [checkCollision]);
const value: TooltipContextType = useMemo(
() => ({
popupRef,
triggerRef,
open,
transitionStatus,
updatePositioning,
trackCursorAxis,
placement,
sideOffset,
collisionPadding,
}),
[open, transitionStatus, updatePositioning, trackCursorAxis, placement, sideOffset, collisionPadding],
);
return <TooltipContext.Provider value={value}>{children}</TooltipContext.Provider>;
}
function TooltipTrigger({ children }: TooltipTriggerProps): JSX.Element {
const { context, getReferenceProps } = useTooltipContext();
const { refs, open } = context;
const { triggerRef, open } = useTooltipContext();
// eslint-disable-next-line react/no-clone-element, react/no-children-only
return cloneElement(Children.only(children) as JSX.Element, {
ref: refs.setReference,
...getReferenceProps(),
ref: triggerRef,
'data-popup-open': open ? '' : undefined,
});
}
@@ -189,51 +294,48 @@ function TooltipPositioner({
collisionPadding = 0,
children,
}: TooltipPositionerProps): JSX.Element | null {
const { context, updatePositioning, trackCursorAxis } = useTooltipContext();
const { refs, floatingStyles } = context;
const { updatePositioning } = useTooltipContext();
// Update positioning when props change
useEffect(() => {
updatePositioning({ side, sideOffset, collisionPadding });
}, [side, sideOffset, collisionPadding, updatePositioning]);
const positionerContextValue: TooltipPositionerContextType = useMemo(() => ({
side,
}), [side]);
return (
<TooltipPositionerContext.Provider value={positionerContextValue}>
<div
ref={refs.setFloating}
style={{
...floatingStyles,
pointerEvents: trackCursorAxis ? 'none' : undefined,
}}
>
{children}
</div>
</TooltipPositionerContext.Provider>
);
return <>{children}</>;
}
function TooltipPopup({ className = '', children }: TooltipPopupProps): JSX.Element | null {
const { context, getFloatingProps, transitionStatus } = useTooltipContext();
const { refs, placement } = context;
const triggerElement = refs.reference.current as HTMLElement | null;
function TooltipPopup({ id, className = '', children }: TooltipPopupProps): JSX.Element | null {
const { popupRef, triggerRef, transitionStatus, placement } = useTooltipContext();
const triggerElement = triggerRef.current;
const popupId = useMemo(() => id ?? `tooltip-${Math.random().toString(36).substring(2, 9)}`, [id]);
useEffect(() => {
if (!popupRef.current || !triggerRef.current) return;
const popup = popupRef.current;
const trigger = triggerRef.current;
popup.setAttribute('popover', 'manual');
trigger.setAttribute('commandfor', popupId);
trigger.style.setProperty('anchor-name', `--${popupId}`);
}, [popupId, popupRef, triggerRef]);
// Copy data attributes from trigger element
const dataAttributes = triggerElement?.attributes
? Object.fromEntries(
Array.from(triggerElement.attributes)
.filter(attr => attr.name.startsWith('data-'))
.map(attr => [attr.name, attr.value]),
)
: {};
const dataAttributes = useMemo(() => {
if (!triggerElement?.attributes) return {};
return Object.fromEntries(
Array.from(triggerElement.attributes)
.filter(attr => attr.name.startsWith('data-'))
.map(attr => [attr.name, attr.value]),
);
}, [triggerElement]);
return (
<div
ref={popupRef as RefObject<HTMLDivElement>}
id={popupId}
className={className}
{...getFloatingProps()}
role="tooltip"
{...dataAttributes}
data-side={placement}
data-starting-style={transitionStatus === 'initial' ? '' : undefined}
@@ -246,53 +348,17 @@ function TooltipPopup({ className = '', children }: TooltipPopupProps): JSX.Elem
);
}
function TooltipArrow({ className = '', children }: TooltipArrowProps): JSX.Element {
const { arrowRef, context } = useTooltipContext();
const { side } = useTooltipPositionerContext();
// Get arrow positioning data from floating-ui
const { x: arrowX, y: arrowY } = context.middlewareData.arrow || { x: 0, y: 0 };
return (
<div
ref={arrowRef as RefObject<HTMLDivElement>}
className={className}
aria-hidden="true"
data-side={side}
style={{
left: arrowX != null ? `${arrowX}px` : undefined,
top: arrowY != null ? `${arrowY}px` : undefined,
}}
>
{children}
</div>
);
}
function TooltipPortal({ children, root, rootId = '@default_portal_id' }: TooltipPortalProps): JSX.Element {
return (
<FloatingPortal root={root as HTMLElement} id={rootId as string}>
{children}
</FloatingPortal>
);
}
// Export compound component
// eslint-disable-next-line react-refresh/only-export-components
export const Tooltip: {
Root: typeof TooltipRoot;
Trigger: typeof TooltipTrigger;
Positioner: typeof TooltipPositioner;
Popup: typeof TooltipPopup;
Arrow: typeof TooltipArrow;
Portal: typeof TooltipPortal;
} = {
Root: TooltipRoot,
Trigger: TooltipTrigger,
Positioner: TooltipPositioner,
Popup: TooltipPopup,
Arrow: TooltipArrow,
Portal: TooltipPortal,
};
export default Tooltip;
@@ -41,14 +41,12 @@ export default function FrostedSkin({ children, className = '' }: SkinProps): JS
<PauseIcon className={`${styles.Icon} ${styles.PauseIcon}`} />
</PlayButton>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className={`${styles.TooltipPopup} ${styles.Surface} ${styles.PopupAnimation} ${styles.PlayTooltipPopup}`}>
<span className={styles.PlayTooltip}>Play</span>
<span className={styles.PauseTooltip}>Pause</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className={`${styles.TooltipPopup} ${styles.Surface} ${styles.PopupAnimation} ${styles.PlayTooltipPopup}`}>
<span className={styles.PlayTooltip}>Play</span>
<span className={styles.PauseTooltip}>Pause</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
<div className={styles.TimeControls}>
@@ -68,13 +66,11 @@ export default function FrostedSkin({ children, className = '' }: SkinProps): JS
<TimeSlider.Thumb className={styles.SliderThumb} />
</TimeSlider.Root>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={18} collisionPadding={12}>
<Tooltip.Popup className={`${styles.Surface} ${styles.PopupAnimation} ${styles.TooltipPopup}`}>
<PreviewTimeDisplay className={styles.TimeDisplay} />
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={18} collisionPadding={12}>
<Tooltip.Popup className={`${styles.Surface} ${styles.PopupAnimation} ${styles.TooltipPopup}`}>
<PreviewTimeDisplay className={styles.TimeDisplay} />
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
<DurationDisplay className={styles.TimeDisplay} />
@@ -88,18 +84,16 @@ export default function FrostedSkin({ children, className = '' }: SkinProps): JS
<VolumeOffIcon className={`${styles.Icon} ${styles.VolumeOffIcon}`} />
</MuteButton>
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner side="top" sideOffset={12}>
<Popover.Popup className={`${styles.Surface} ${styles.PopupAnimation} ${styles.PopoverPopup}`}>
<VolumeSlider.Root className={styles.SliderRoot} orientation="vertical">
<VolumeSlider.Track className={styles.SliderTrack}>
<VolumeSlider.Progress className={styles.SliderProgress} />
</VolumeSlider.Track>
<VolumeSlider.Thumb className={styles.SliderThumb} />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
<Popover.Positioner side="top" sideOffset={12}>
<Popover.Popup className={`${styles.Surface} ${styles.PopupAnimation} ${styles.PopoverPopup}`}>
<VolumeSlider.Root className={styles.SliderRoot} orientation="vertical">
<VolumeSlider.Track className={styles.SliderTrack}>
<VolumeSlider.Progress className={styles.SliderProgress} />
</VolumeSlider.Track>
<VolumeSlider.Thumb className={styles.SliderThumb} />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Positioner>
</Popover.Root>
<Tooltip.Root delay={500}>
@@ -109,14 +103,12 @@ export default function FrostedSkin({ children, className = '' }: SkinProps): JS
<FullscreenExitIcon className={`${styles.Icon} ${styles.FullscreenExitIcon}`} />
</FullscreenButton>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className={`${styles.Surface} ${styles.PopupAnimation} ${styles.TooltipPopup} ${styles.FullscreenTooltipPopup}`}>
<span className={styles.FullscreenEnterTooltip}>Enter Fullscreen</span>
<span className={styles.FullscreenExitTooltip}>Exit Fullscreen</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className={`${styles.Surface} ${styles.PopupAnimation} ${styles.TooltipPopup} ${styles.FullscreenTooltipPopup}`}>
<span className={styles.FullscreenEnterTooltip}>Enter Fullscreen</span>
<span className={styles.FullscreenExitTooltip}>Exit Fullscreen</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
</div>
</MediaContainer>
@@ -41,14 +41,12 @@ export default function MinimalSkin({ children, className = '' }: SkinProps): JS
<PauseIcon className={`${styles.Icon} ${styles.PauseIcon}`} />
</PlayButton>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner side="top-start" sideOffset={6} collisionPadding={12}>
<Tooltip.Popup className={`${styles.PopupAnimation} ${styles.TooltipPopup} ${styles.PlayTooltipPopup}`}>
<span className={styles.PlayTooltip}>Play</span>
<span className={styles.PauseTooltip}>Pause</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
<Tooltip.Positioner side="top-start" sideOffset={6} collisionPadding={12}>
<Tooltip.Popup className={`${styles.PopupAnimation} ${styles.TooltipPopup} ${styles.PlayTooltipPopup}`}>
<span className={styles.PlayTooltip}>Play</span>
<span className={styles.PauseTooltip}>Pause</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
<div className={styles.TimeDisplayGroup}>
@@ -74,13 +72,11 @@ export default function MinimalSkin({ children, className = '' }: SkinProps): JS
<TimeSlider.Thumb className={styles.SliderThumb} />
</TimeSlider.Root>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className={`${styles.PopupAnimation} ${styles.TooltipPopup}`}>
<PreviewTimeDisplay className={styles.TimeDisplay} />
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className={`${styles.PopupAnimation} ${styles.TooltipPopup}`}>
<PreviewTimeDisplay className={styles.TimeDisplay} />
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
<div className={styles.ButtonGroup}>
@@ -92,18 +88,16 @@ export default function MinimalSkin({ children, className = '' }: SkinProps): JS
<VolumeOffIcon className={`${styles.Icon} ${styles.VolumeOffIcon}`} />
</MuteButton>
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner side="top" sideOffset={2}>
<Popover.Popup className={`${styles.PopupAnimation} ${styles.PopoverPopup}`}>
<VolumeSlider.Root className={styles.SliderRoot} orientation="vertical">
<VolumeSlider.Track className={styles.SliderTrack}>
<VolumeSlider.Progress className={styles.SliderProgress} />
</VolumeSlider.Track>
<VolumeSlider.Thumb className={styles.SliderThumb} />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
<Popover.Positioner side="top" sideOffset={2}>
<Popover.Popup className={`${styles.PopupAnimation} ${styles.PopoverPopup}`}>
<VolumeSlider.Root className={styles.SliderRoot} orientation="vertical">
<VolumeSlider.Track className={styles.SliderTrack}>
<VolumeSlider.Progress className={styles.SliderProgress} />
</VolumeSlider.Track>
<VolumeSlider.Thumb className={styles.SliderThumb} />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Positioner>
</Popover.Root>
<Tooltip.Root delay={500} closeDelay={0}>
@@ -113,14 +107,12 @@ export default function MinimalSkin({ children, className = '' }: SkinProps): JS
<FullscreenExitAltIcon className={`${styles.Icon} ${styles.FullscreenExitIcon}`} />
</FullscreenButton>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner side="top-end" sideOffset={6} collisionPadding={12}>
<Tooltip.Popup className={`${styles.PopupAnimation} ${styles.TooltipPopup} ${styles.FullscreenTooltipPopup}`}>
<span className={styles.FullscreenEnterTooltip}>Enter Fullscreen</span>
<span className={styles.FullscreenExitTooltip}>Exit Fullscreen</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
<Tooltip.Positioner side="top-end" sideOffset={6} collisionPadding={12}>
<Tooltip.Popup className={`${styles.PopupAnimation} ${styles.TooltipPopup} ${styles.FullscreenTooltipPopup}`}>
<span className={styles.FullscreenEnterTooltip}>Enter Fullscreen</span>
<span className={styles.FullscreenExitTooltip}>Exit Fullscreen</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
</div>
</div>
+1 -1
View File
@@ -131,7 +131,7 @@ const styles: MinimalSkinStyles = {
'vjs:data-starting-style:scale-0 vjs:data-starting-style:opacity-0 vjs:data-starting-style:blur-sm',
'vjs:data-ending-style:scale-0 vjs:data-ending-style:opacity-0 vjs:data-ending-style:blur-sm',
),
PopoverPopup: cn('vjs:py-2'),
PopoverPopup: cn('vjs:py-2 vjs:bg-transparent'),
TooltipPopup: cn(
'vjs:whitespace-nowrap vjs:flex vjs:flex-col vjs:rounded vjs:text-white vjs:text-xs vjs:@7xl/root:text-sm vjs:px-2 vjs:py-1 vjs:bg-white/20 vjs:backdrop-blur-3xl vjs:backdrop-saturate-150 vjs:backdrop-brightness-90 vjs:shadow-md vjs:shadow-black/5',
),
-17
View File
@@ -226,9 +226,6 @@ importers:
packages/react:
dependencies:
'@floating-ui/react':
specifier: ^0.27.16
version: 0.27.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@videojs/core':
specifier: workspace:*
version: link:../core
@@ -1056,12 +1053,6 @@ packages:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@floating-ui/react@0.27.16':
resolution: {integrity: sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==}
peerDependencies:
react: '>=17.0.0'
react-dom: '>=17.0.0'
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
@@ -7398,14 +7389,6 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@floating-ui/react@0.27.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@floating-ui/utils': 0.2.10
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
tabbable: 6.2.0
'@floating-ui/utils@0.2.10': {}
'@humanfs/core@0.19.1': {}