mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
146 lines
4.6 KiB
TypeScript
146 lines
4.6 KiB
TypeScript
'use client';
|
|
|
|
import { type TooltipProps as CoreTooltipProps, TooltipCore, TooltipDataAttrs } from '@videojs/core';
|
|
import {
|
|
createTooltip,
|
|
createTransition,
|
|
type PositioningBoundary,
|
|
type TooltipChangeDetails,
|
|
} from '@videojs/core/dom';
|
|
import { useSnapshot } from '@videojs/store/react';
|
|
import { isUndefined } from '@videojs/utils/predicate';
|
|
import type { ReactNode } from 'react';
|
|
import { useEffect, useState } from 'react';
|
|
import { useOptionalContainer } from '../../player/context';
|
|
import { useDestroy } from '../../utils/use-destroy';
|
|
import { useLatestRef } from '../../utils/use-latest-ref';
|
|
import { useSafeId } from '../../utils/use-safe-id';
|
|
import { useOptionalControlsContext } from '../controls/context';
|
|
import { usePositionedState } from '../hooks/use-positioned-state';
|
|
import { type TooltipContent, TooltipContextProvider } from './context';
|
|
import { useTooltipGroup } from './group-context';
|
|
|
|
export interface TooltipRootProps extends CoreTooltipProps {
|
|
/** Boundary used to constrain the popup size. */
|
|
boundary?: PositioningBoundary;
|
|
/** Called when the tooltip open state changes (fires immediately, before animations). */
|
|
onOpenChange?: (open: boolean, details: TooltipChangeDetails) => void;
|
|
/** Called after open/close animations complete. */
|
|
onOpenChangeComplete?: (open: boolean) => void;
|
|
children?: ReactNode;
|
|
}
|
|
|
|
export function TooltipRoot({
|
|
open: controlledOpen,
|
|
defaultOpen = TooltipCore.defaultProps.defaultOpen,
|
|
onOpenChange: onOpenChangeProp,
|
|
onOpenChangeComplete: onOpenChangeCompleteProp,
|
|
delay = TooltipCore.defaultProps.delay,
|
|
closeDelay = TooltipCore.defaultProps.closeDelay,
|
|
disableHoverablePopup = TooltipCore.defaultProps.disableHoverablePopup,
|
|
disabled = TooltipCore.defaultProps.disabled,
|
|
boundary = 'container',
|
|
children,
|
|
...coreProps
|
|
}: TooltipRootProps): ReactNode {
|
|
const container = useOptionalContainer();
|
|
const controls = useOptionalControlsContext();
|
|
const [core] = useState(() => new TooltipCore(coreProps));
|
|
core.setProps(coreProps);
|
|
|
|
const isControlled = !isUndefined(controlledOpen);
|
|
|
|
const groupFromContext = useTooltipGroup();
|
|
|
|
// Keep refs that always point to the latest values so the
|
|
// createTooltip closure never reads stale props.
|
|
const onOpenChangeRef = useLatestRef(onOpenChangeProp);
|
|
const onOpenChangeCompleteRef = useLatestRef(onOpenChangeCompleteProp);
|
|
const delayRef = useLatestRef(delay);
|
|
const closeDelayRef = useLatestRef(closeDelay);
|
|
const disableHoverablePopupRef = useLatestRef(disableHoverablePopup);
|
|
const disabledRef = useLatestRef(disabled);
|
|
const groupRef = useLatestRef(groupFromContext);
|
|
|
|
const [tooltip] = useState(() => {
|
|
const instance = createTooltip({
|
|
transition: createTransition(),
|
|
onOpenChange: (nextOpen: boolean, details: TooltipChangeDetails) => {
|
|
onOpenChangeRef.current?.(nextOpen, details);
|
|
},
|
|
onOpenChangeComplete: (nextOpen: boolean) => {
|
|
onOpenChangeCompleteRef.current?.(nextOpen);
|
|
},
|
|
delay: () => delayRef.current,
|
|
closeDelay: () => closeDelayRef.current,
|
|
disableHoverablePopup: () => disableHoverablePopupRef.current,
|
|
disabled: () => disabledRef.current,
|
|
group: () => groupRef.current,
|
|
});
|
|
|
|
// Apply defaultOpen on creation (uncontrolled only)
|
|
if (!isControlled && defaultOpen) {
|
|
instance.open();
|
|
}
|
|
|
|
return instance;
|
|
});
|
|
|
|
const [content, setContent] = useState<TooltipContent | undefined>();
|
|
|
|
const anchorName = useSafeId();
|
|
const popupId = useSafeId('tooltip');
|
|
|
|
// Sync controlled open prop -> internal input state.
|
|
useEffect(() => {
|
|
if (isUndefined(controlledOpen)) return;
|
|
|
|
const { active: inputOpen } = tooltip.input.current;
|
|
if (controlledOpen === inputOpen) return;
|
|
|
|
if (controlledOpen) {
|
|
tooltip.open();
|
|
} else {
|
|
tooltip.close();
|
|
}
|
|
}, [controlledOpen, tooltip]);
|
|
|
|
useEffect(() => {
|
|
if (isUndefined(controls?.state.visible)) return;
|
|
if (controls.state.visible) return;
|
|
|
|
tooltip.close('imperative-action');
|
|
}, [controls?.state.visible, tooltip]);
|
|
|
|
useDestroy(tooltip);
|
|
|
|
const input = useSnapshot(tooltip.input);
|
|
core.setInput(input);
|
|
const { state, preferredSide, setPositionedSide } = usePositionedState(core.getState());
|
|
|
|
return (
|
|
<TooltipContextProvider
|
|
value={{
|
|
core,
|
|
tooltip,
|
|
state,
|
|
preferredSide,
|
|
setPositionedSide,
|
|
stateAttrMap: TooltipDataAttrs,
|
|
anchorName,
|
|
popupId,
|
|
content,
|
|
setContent,
|
|
boundary,
|
|
container,
|
|
}}
|
|
>
|
|
{children}
|
|
</TooltipContextProvider>
|
|
);
|
|
}
|
|
|
|
export namespace TooltipRoot {
|
|
export type Props = TooltipRootProps;
|
|
}
|