mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(packages): compound tooltips with label and shortcut parts (#1494)
This commit is contained in:
@@ -11,7 +11,7 @@ import { usePlayer } from '../player/context';
|
||||
import type { renderElement as renderElementFn } from '../utils/use-render';
|
||||
import { renderElement } from '../utils/use-render';
|
||||
import { useButton } from './hooks/use-button';
|
||||
import { useAriaKeyShortcuts } from './hotkey/use-aria-key-shortcuts';
|
||||
import { useHotkeyShortcut } from './hotkey/use-hotkey-shortcut';
|
||||
import { useOptionalMenuTriggerChildContext } from './menu/context';
|
||||
import { useOptionalTooltipContext } from './tooltip/context';
|
||||
|
||||
@@ -22,13 +22,24 @@ interface MediaButtonConfig<Core extends Required<MediaButtonComponent>> {
|
||||
selector: Selector<object, InferMediaState<Core> | undefined>;
|
||||
action: (core: Core, state: InferMediaState<Core>) => void;
|
||||
hotkeyAction?: string;
|
||||
hotkeyValue?: (props: Record<string, unknown>) => number | undefined;
|
||||
tooltipLabel?: (core: Core, state: InferComponentState<Core>) => string | undefined;
|
||||
}
|
||||
|
||||
/** Creates a media button React component from a core class and config. */
|
||||
export function createMediaButton<Core extends Required<MediaButtonComponent>, Props extends object>(
|
||||
config: MediaButtonConfig<Core>
|
||||
): ForwardRefExoticComponent<Props & RefAttributes<HTMLButtonElement>> {
|
||||
const { displayName, core: CoreClass, stateAttrMap, selector, action, hotkeyAction } = config;
|
||||
const {
|
||||
displayName,
|
||||
core: CoreClass,
|
||||
stateAttrMap,
|
||||
selector,
|
||||
action,
|
||||
hotkeyAction,
|
||||
hotkeyValue,
|
||||
tooltipLabel,
|
||||
} = config;
|
||||
|
||||
// Props that exist in the core's defaultProps are routed to setProps; the rest go to the DOM element.
|
||||
const corePropKeys = new Set(Object.keys(CoreClass.defaultProps));
|
||||
@@ -52,8 +63,9 @@ export function createMediaButton<Core extends Required<MediaButtonComponent>, P
|
||||
|
||||
const tooltipCtx = useOptionalTooltipContext();
|
||||
const menuTriggerChild = useOptionalMenuTriggerChildContext();
|
||||
const setTooltipContent = tooltipCtx?.setContent;
|
||||
const feature = usePlayer(selector);
|
||||
const shortcuts = useAriaKeyShortcuts(hotkeyAction);
|
||||
const shortcut = useHotkeyShortcut(hotkeyAction, hotkeyValue?.(coreProps));
|
||||
|
||||
const [core] = useState(() => new CoreClass());
|
||||
|
||||
@@ -75,20 +87,21 @@ export function createMediaButton<Core extends Required<MediaButtonComponent>, P
|
||||
if (feature) core.setMedia(feature);
|
||||
const state = feature ? (core.getState() as State) : null;
|
||||
const label = state ? core.getLabel(state) : undefined;
|
||||
const tooltipText = state ? (tooltipLabel?.(core, state) ?? label) : undefined;
|
||||
|
||||
// Forward label to tooltip popup content when inside a Tooltip.Root.
|
||||
useLayoutEffect(() => {
|
||||
if (!tooltipCtx) return;
|
||||
tooltipCtx.setContent(label);
|
||||
return () => tooltipCtx.setContent(undefined);
|
||||
}, [tooltipCtx, label]);
|
||||
if (!setTooltipContent) return;
|
||||
setTooltipContent(tooltipText ? { label: tooltipText, shortcut: shortcut.shortcut } : undefined);
|
||||
return () => setTooltipContent(undefined);
|
||||
}, [setTooltipContent, tooltipText, shortcut.shortcut]);
|
||||
|
||||
if (!feature || !state) {
|
||||
if (__DEV__) logMissingFeature(displayName, selector.displayName ?? displayName);
|
||||
return null;
|
||||
}
|
||||
|
||||
const attrs = { ...core.getAttrs(state), 'aria-keyshortcuts': shortcuts };
|
||||
const attrs = { ...core.getAttrs(state), 'aria-keyshortcuts': shortcut.aria };
|
||||
|
||||
return renderElement(
|
||||
'button',
|
||||
@@ -97,7 +110,7 @@ export function createMediaButton<Core extends Required<MediaButtonComponent>, P
|
||||
state,
|
||||
stateAttrMap,
|
||||
ref: [forwardedRef, buttonRef],
|
||||
props: [attrs, elementProps, getButtonProps()],
|
||||
props: [getButtonProps(), elementProps, attrs],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { PlayerContextProvider, type PlayerContextValue } from '../../../player/context';
|
||||
import { createMockStore } from '../../../testing/mocks';
|
||||
import { Hotkey } from '../hotkey';
|
||||
import { useHotkeyShortcut } from '../use-hotkey-shortcut';
|
||||
|
||||
function createContextValue(container: HTMLElement): PlayerContextValue {
|
||||
return {
|
||||
store: createMockStore() as any,
|
||||
media: null,
|
||||
setMedia: vi.fn(),
|
||||
container,
|
||||
setContainer: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function Wrapper({ children, value }: { children: ReactNode; value: PlayerContextValue }) {
|
||||
return <PlayerContextProvider value={value}>{children}</PlayerContextProvider>;
|
||||
}
|
||||
|
||||
function Shortcut({ keys }: { keys: string }) {
|
||||
const shortcut = useHotkeyShortcut('togglePaused');
|
||||
|
||||
return (
|
||||
<>
|
||||
<span data-testid="shortcut">{shortcut.shortcut}</span>
|
||||
<span data-testid="aria">{shortcut.aria}</span>
|
||||
<Hotkey keys={keys} action="togglePaused" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
describe('useHotkeyShortcut', () => {
|
||||
it('updates when hotkey registrations change', async () => {
|
||||
const container = document.createElement('div');
|
||||
const value = createContextValue(container);
|
||||
const { rerender } = render(
|
||||
<Wrapper value={value}>
|
||||
<Shortcut keys="k" />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
await waitFor(() => expect(document.querySelector('[data-testid="shortcut"]')?.textContent).toBe('K'));
|
||||
expect(document.querySelector('[data-testid="aria"]')?.textContent).toBe('k');
|
||||
|
||||
rerender(
|
||||
<Wrapper value={value}>
|
||||
<Shortcut keys="p" />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
await waitFor(() => expect(document.querySelector('[data-testid="shortcut"]')?.textContent).toBe('P'));
|
||||
expect(document.querySelector('[data-testid="aria"]')?.textContent).toBe('p');
|
||||
});
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { findHotkeyCoordinator } from '@videojs/core/dom';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useContainer } from '../../player/context';
|
||||
|
||||
export function useAriaKeyShortcuts(action: string | undefined): string | undefined {
|
||||
const container = useContainer();
|
||||
return useMemo(() => {
|
||||
if (!container || !action) return undefined;
|
||||
return findHotkeyCoordinator(container)?.getAriaKeys(action);
|
||||
}, [container, action]);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { getHotkeyCoordinator, type HotkeyShortcutDetails } from '@videojs/core/dom';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useContainer } from '../../player/context';
|
||||
|
||||
export function useHotkeyShortcut(action: string | undefined, value?: number | undefined): HotkeyShortcutDetails {
|
||||
const container = useContainer();
|
||||
|
||||
const [shortcut, setShortcut] = useState<HotkeyShortcutDetails>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (!container || !action) {
|
||||
setShortcut({});
|
||||
return;
|
||||
}
|
||||
|
||||
const coordinator = getHotkeyCoordinator(container);
|
||||
const update = () => setShortcut(coordinator.getShortcut(action, value));
|
||||
|
||||
update();
|
||||
return coordinator.subscribeShortcutChanges(update);
|
||||
}, [container, action, value]);
|
||||
|
||||
return shortcut;
|
||||
}
|
||||
@@ -29,5 +29,5 @@ export function useHotkey(options: UseHotkeyOptions): void {
|
||||
disabled,
|
||||
onActivate: (event, key) => onActivateRef.current(event, key),
|
||||
});
|
||||
}, [container, keys, target, repeatable, disabled, onActivateRef]);
|
||||
}, [container, keys, target, repeatable, disabled]);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export const LiveButton = forwardRef<HTMLButtonElement, LiveButtonProps>(
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!tooltipCtx) return;
|
||||
tooltipCtx.setContent(labelText);
|
||||
tooltipCtx.setContent(labelText ? { label: labelText } : undefined);
|
||||
return () => tooltipCtx.setContent(undefined);
|
||||
}, [tooltipCtx, labelText]);
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ export const PlaybackRateButton = createMediaButton<PlaybackRateButtonCore, Play
|
||||
stateAttrMap: PlaybackRateButtonDataAttrs,
|
||||
selector: selectPlaybackRate,
|
||||
action: (core, state) => core.cycle(state),
|
||||
hotkeyAction: 'speedUp',
|
||||
});
|
||||
|
||||
export namespace PlaybackRateButton {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { popup } from '@videojs/skins/default/tailwind/video.tailwind';
|
||||
import type { ReactNode } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { PlayerContextProvider, type PlayerContextValue } from '../../../player/context';
|
||||
import { createMockStore } from '../../../testing/mocks';
|
||||
import { Hotkey } from '../../hotkey/hotkey';
|
||||
import { Tooltip } from '../../tooltip';
|
||||
import { PlaybackRateButton } from '../playback-rate-button';
|
||||
|
||||
function createContextValue(container: HTMLElement): PlayerContextValue {
|
||||
return {
|
||||
store: createMockStore({
|
||||
playbackRates: [0.5, 1, 1.5, 2],
|
||||
playbackRate: 1,
|
||||
setPlaybackRate: vi.fn(),
|
||||
}) as any,
|
||||
media: null,
|
||||
setMedia: vi.fn(),
|
||||
container,
|
||||
setContainer: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function Wrapper({ children, value }: { children: ReactNode; value: PlayerContextValue }) {
|
||||
return <PlayerContextProvider value={value}>{children}</PlayerContextProvider>;
|
||||
}
|
||||
|
||||
describe('PlaybackRateButton', () => {
|
||||
it('uses the core label and the speed-up shortcut', async () => {
|
||||
const container = document.createElement('div');
|
||||
const value = createContextValue(container);
|
||||
|
||||
render(
|
||||
<Wrapper value={value}>
|
||||
<Tooltip.Root defaultOpen>
|
||||
<Tooltip.Trigger render={<PlaybackRateButton data-testid="button" />} />
|
||||
<Tooltip.Popup data-testid="popup">
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className={popup.tooltipShortcut} />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
<Hotkey keys=">" action="speedUp" />
|
||||
<Hotkey keys="<" action="speedDown" />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
const button = document.querySelector('[data-testid="button"]');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[data-testid="popup"] span')?.textContent).toBe('Playback rate 1');
|
||||
expect(document.querySelector('[data-testid="popup"] kbd')?.textContent).toBe('>');
|
||||
});
|
||||
expect(button?.getAttribute('aria-label')).toBe('Playback rate 1');
|
||||
expect(button?.getAttribute('aria-keyshortcuts')).toBe('>');
|
||||
});
|
||||
});
|
||||
@@ -31,6 +31,8 @@ export const SeekButton = createMediaButton<SeekButtonCore, SeekButtonProps>({
|
||||
stateAttrMap: SeekButtonDataAttrs,
|
||||
selector: selectTime,
|
||||
action: (core, state) => core.seek(state),
|
||||
hotkeyAction: 'seekStep',
|
||||
hotkeyValue: (props) => (typeof props.seconds === 'number' ? props.seconds : SeekButtonCore.defaultProps.seconds),
|
||||
});
|
||||
|
||||
export namespace SeekButton {
|
||||
|
||||
@@ -4,6 +4,11 @@ import type { StateAttrMap, TooltipCore } from '@videojs/core';
|
||||
import type { MediaContainer, PositioningBoundary, TooltipApi } from '@videojs/core/dom';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export interface TooltipContent {
|
||||
label?: string | undefined;
|
||||
shortcut?: string | undefined;
|
||||
}
|
||||
|
||||
export interface TooltipContextValue {
|
||||
core: TooltipCore;
|
||||
tooltip: TooltipApi;
|
||||
@@ -11,8 +16,8 @@ export interface TooltipContextValue {
|
||||
stateAttrMap: StateAttrMap<TooltipCore.State>;
|
||||
anchorName: string;
|
||||
popupId: string;
|
||||
content: string | undefined;
|
||||
setContent: (content: string | undefined) => void;
|
||||
content: TooltipContent | undefined;
|
||||
setContent: (content: TooltipContent | undefined) => void;
|
||||
boundary: PositioningBoundary;
|
||||
container: MediaContainer | null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export { TooltipArrow as Arrow, type TooltipArrowProps as ArrowProps } from './tooltip-arrow';
|
||||
export { TooltipLabel as Label, type TooltipLabelProps as LabelProps } from './tooltip-label';
|
||||
export { TooltipPopup as Popup, type TooltipPopupProps as PopupProps } from './tooltip-popup';
|
||||
export { TooltipProvider as Provider, type TooltipProviderProps as ProviderProps } from './tooltip-provider';
|
||||
export { TooltipRoot as Root, type TooltipRootProps as RootProps } from './tooltip-root';
|
||||
export { TooltipShortcut as Shortcut, type TooltipShortcutProps as ShortcutProps } from './tooltip-shortcut';
|
||||
export { TooltipTrigger as Trigger, type TooltipTriggerProps as TriggerProps } from './tooltip-trigger';
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { type TooltipContextValue, useOptionalTooltipContext, useTooltipContext } from './context';
|
||||
export { type TooltipContent, type TooltipContextValue, useOptionalTooltipContext, useTooltipContext } from './context';
|
||||
export * as Tooltip from './index.parts';
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { popup } from '@videojs/skins/default/tailwind/video.tailwind';
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { Tooltip, useOptionalTooltipContext } from '..';
|
||||
|
||||
function TooltipContent({ label, shortcut }: { label?: string; shortcut?: string }) {
|
||||
const tooltip = useOptionalTooltipContext();
|
||||
const setContent = tooltip?.setContent;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setContent?.({ label, shortcut });
|
||||
return () => setContent?.(undefined);
|
||||
}, [setContent, label, shortcut]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
describe('Tooltip', () => {
|
||||
it('renders label and kbd shortcut with skin popup.tooltipShortcut from context', async () => {
|
||||
const { container } = render(
|
||||
<Tooltip.Root defaultOpen>
|
||||
<TooltipContent label="Play" shortcut="K" />
|
||||
<Tooltip.Popup data-testid="popup">
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className={popup.tooltipShortcut} />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('[data-testid="popup"] span')?.textContent).toBe('Play');
|
||||
const hint = container.querySelector('[data-testid="popup"] kbd');
|
||||
expect(hint?.textContent).toBe('K');
|
||||
expect(hint?.localName).toBe('kbd');
|
||||
});
|
||||
expect(container.querySelector('[data-testid="popup"] span')?.getAttribute('class')).toBeNull();
|
||||
});
|
||||
|
||||
it('omits shortcut without a shortcut value', async () => {
|
||||
const { container } = render(
|
||||
<Tooltip.Root defaultOpen>
|
||||
<TooltipContent label="Play" />
|
||||
<Tooltip.Popup data-testid="popup">
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className={popup.tooltipShortcut} />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
);
|
||||
|
||||
await waitFor(() => expect(container.querySelector('[data-testid="popup"] span')?.textContent).toBe('Play'));
|
||||
expect(container.querySelector('[data-testid="popup"] kbd')).toBeNull();
|
||||
});
|
||||
|
||||
it('passes TooltipState to custom popup render functions', async () => {
|
||||
const { container } = render(
|
||||
<Tooltip.Root defaultOpen>
|
||||
<TooltipContent label="Play" shortcut="K" />
|
||||
<Tooltip.Popup
|
||||
render={(props, state) => (
|
||||
<div {...props} data-open={String(state.open)} data-side={state.side} data-testid="popup">
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className={popup.tooltipShortcut} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Tooltip.Root>
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(container.querySelector('[data-testid="popup"]')?.getAttribute('data-open')).toBe('true')
|
||||
);
|
||||
expect(container.querySelector('[data-testid="popup"]')?.getAttribute('data-side')).toBe('top');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import type { TooltipState } from '@videojs/core';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import type { UIComponentProps } from '../../utils/types';
|
||||
import { renderElement } from '../../utils/use-render';
|
||||
import { useTooltipContext } from './context';
|
||||
|
||||
export interface TooltipLabelProps extends UIComponentProps<'span', TooltipState> {}
|
||||
|
||||
/** Tooltip body label; defaults to context `content.label` from the linked trigger. */
|
||||
export const TooltipLabel = forwardRef<HTMLSpanElement, TooltipLabelProps>(function TooltipLabel(
|
||||
{ render, className, style, children, ...elementProps },
|
||||
forwardedRef
|
||||
) {
|
||||
const { state, stateAttrMap, content } = useTooltipContext();
|
||||
const body = children !== undefined ? children : (content?.label ?? '');
|
||||
|
||||
return renderElement(
|
||||
'span',
|
||||
{ render, className, style },
|
||||
{
|
||||
state,
|
||||
stateAttrMap,
|
||||
ref: forwardedRef,
|
||||
props: [elementProps, { children: body }],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export namespace TooltipLabel {
|
||||
export type Props = TooltipLabelProps;
|
||||
export type State = TooltipState;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import type { TooltipState } from '@videojs/core';
|
||||
import { TooltipCSSVars } from '@videojs/core';
|
||||
import { TooltipCSSVars, type TooltipState } from '@videojs/core';
|
||||
import {
|
||||
getAnchorPositionStyle,
|
||||
getPopupPositionRect,
|
||||
@@ -18,6 +17,8 @@ import type { UIComponentProps } from '../../utils/types';
|
||||
import { useComposedRefs } from '../../utils/use-composed-refs';
|
||||
import { renderElement } from '../../utils/use-render';
|
||||
import { useTooltipContext } from './context';
|
||||
import { TooltipLabel } from './tooltip-label';
|
||||
import { TooltipShortcut } from './tooltip-shortcut';
|
||||
|
||||
export interface TooltipPopupProps extends UIComponentProps<'div', TooltipState> {}
|
||||
|
||||
@@ -25,10 +26,10 @@ const POPUP_RESET: CSSProperties = { position: 'fixed', inset: 'auto', margin: 0
|
||||
|
||||
/** Container for the tooltip content. Positioned relative to the trigger using CSS anchor positioning with a JavaScript fallback. */
|
||||
export const TooltipPopup = forwardRef<HTMLDivElement, TooltipPopupProps>(function TooltipPopup(
|
||||
{ render, className, style, ...elementProps },
|
||||
{ render, className, style, children, ...elementProps },
|
||||
forwardedRef
|
||||
) {
|
||||
const { core, tooltip, state, stateAttrMap, anchorName, popupId, content, boundary, container } = useTooltipContext();
|
||||
const { core, tooltip, state, stateAttrMap, anchorName, popupId, boundary, container } = useTooltipContext();
|
||||
const internalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const popupRef = useCallback(
|
||||
@@ -49,7 +50,7 @@ export const TooltipPopup = forwardRef<HTMLDivElement, TooltipPopupProps>(functi
|
||||
|
||||
// CSS Anchor Positioning — computed from state, no measurement needed.
|
||||
// `position-anchor` is set imperatively in the ref callback above
|
||||
// because React's style prop silently drops unrecognised CSS properties.
|
||||
// because React's style prop silently drops unrecognized CSS properties.
|
||||
const anchorStyle = useMemo(() => {
|
||||
if (!supportsAnchorPositioning()) return null;
|
||||
const { positionAnchor: _, ...rest } = getAnchorPositionStyle(
|
||||
@@ -159,6 +160,16 @@ export const TooltipPopup = forwardRef<HTMLDivElement, TooltipPopupProps>(functi
|
||||
return null;
|
||||
}
|
||||
|
||||
const body =
|
||||
children !== undefined ? (
|
||||
children
|
||||
) : (
|
||||
<>
|
||||
<TooltipLabel />
|
||||
<TooltipShortcut />
|
||||
</>
|
||||
);
|
||||
|
||||
// Remap DOM focus events to React synthetic event names.
|
||||
const { onFocusOut, ...restPopupProps } = tooltip.popupProps;
|
||||
|
||||
@@ -175,8 +186,7 @@ export const TooltipPopup = forwardRef<HTMLDivElement, TooltipPopupProps>(functi
|
||||
style: positioningStyle,
|
||||
...core.getPopupAttrs(state),
|
||||
},
|
||||
// Forwarded content as default children — explicit children override.
|
||||
{ children: content },
|
||||
{ children: body },
|
||||
{ ...restPopupProps, onBlur: onFocusOut },
|
||||
elementProps,
|
||||
],
|
||||
|
||||
@@ -16,7 +16,7 @@ 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 { TooltipContextProvider } from './context';
|
||||
import { type TooltipContent, TooltipContextProvider } from './context';
|
||||
import { useTooltipGroup } from './group-context';
|
||||
|
||||
export interface TooltipRootProps extends CoreTooltipProps {
|
||||
@@ -85,7 +85,7 @@ export function TooltipRoot({
|
||||
return instance;
|
||||
});
|
||||
|
||||
const [content, setContent] = useState<string | undefined>();
|
||||
const [content, setContent] = useState<TooltipContent | undefined>();
|
||||
|
||||
const anchorName = useSafeId();
|
||||
const popupId = useSafeId('tooltip');
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import type { TooltipState } from '@videojs/core';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import type { UIComponentProps } from '../../utils/types';
|
||||
import { renderElement } from '../../utils/use-render';
|
||||
import { useTooltipContext } from './context';
|
||||
|
||||
export interface TooltipShortcutProps extends UIComponentProps<'kbd', TooltipState> {}
|
||||
|
||||
/** Keyboard shortcut hint; apply skin `className` (CSS: `media-tooltip__kbd`; Tailwind: `popup.tooltipShortcut`). */
|
||||
export const TooltipShortcut = forwardRef<HTMLElement, TooltipShortcutProps>(function TooltipShortcut(
|
||||
{ render, className, style, children, ...elementProps },
|
||||
forwardedRef
|
||||
) {
|
||||
const { state, stateAttrMap, content } = useTooltipContext();
|
||||
const shortcut = children !== undefined && children !== null ? children : (content?.shortcut ?? null);
|
||||
|
||||
if (!shortcut) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return renderElement(
|
||||
'kbd',
|
||||
{ render, className, style },
|
||||
{
|
||||
state,
|
||||
stateAttrMap,
|
||||
ref: forwardedRef,
|
||||
props: [elementProps, { children: shortcut }],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export namespace TooltipShortcut {
|
||||
export type Props = TooltipShortcutProps;
|
||||
export type State = TooltipState;
|
||||
}
|
||||
Reference in New Issue
Block a user