mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(react): use popover and anchor position API (#178)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
|
||||
Generated
-17
@@ -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': {}
|
||||
|
||||
Reference in New Issue
Block a user