feat(packages): error dialog component (#1077)

This commit is contained in:
Sam Potts
2026-03-31 17:00:45 -07:00
committed by GitHub
parent 29465609c5
commit 3430fe1a49
42 changed files with 612 additions and 153 deletions
@@ -0,0 +1,18 @@
'use client';
import type { MediaError } from '@videojs/core';
import { createContext, useContext } from 'react';
export interface ErrorDialogContextValue {
lastError: MediaError | null;
}
const ErrorDialogContext = createContext<ErrorDialogContextValue | null>(null);
export const ErrorDialogContextProvider = ErrorDialogContext.Provider;
export function useErrorDialogContext(): ErrorDialogContextValue {
const ctx = useContext(ErrorDialogContext);
if (!ctx) throw new Error('ErrorDialog compound components must be used within an ErrorDialog.Root');
return ctx;
}
@@ -0,0 +1,39 @@
'use client';
import type { AlertDialogCore } from '@videojs/core';
import { forwardRef } from 'react';
import type { UIComponentProps } from '../../utils/types';
import { renderElement } from '../../utils/use-render';
import { useAlertDialogContext } from '../alert-dialog/context';
import { useErrorDialogContext } from './context';
const FALLBACK_MESSAGE = 'An error occurred. Please try again.';
export interface ErrorDialogDescriptionProps extends UIComponentProps<'p', AlertDialogCore.State> {}
export const ErrorDialogDescription = forwardRef<HTMLParagraphElement, ErrorDialogDescriptionProps>(
function ErrorDialogDescription({ render, className, style, children, ...elementProps }, forwardedRef) {
const { state, stateAttrMap } = useAlertDialogContext();
const { lastError } = useErrorDialogContext();
return renderElement(
'p',
{ render, className, style },
{
state,
stateAttrMap,
ref: forwardedRef,
props: [
{ id: state.descriptionId, children: children ?? lastError?.message ?? FALLBACK_MESSAGE },
elementProps,
],
}
);
}
);
export namespace ErrorDialogDescription {
export type Props = ErrorDialogDescriptionProps;
export type State = AlertDialogCore.State;
}
@@ -0,0 +1,76 @@
'use client';
import { AlertDialogDataAttrs, ErrorDialogCore } from '@videojs/core';
import { createAlertDialog, createTransition, selectError } from '@videojs/core/dom';
import { useSnapshot } from '@videojs/store/react';
import type { ReactNode } from 'react';
import { useEffect, useRef, useState } from 'react';
import { usePlayer } from '../../player/context';
import { useDestroy } from '../../utils/use-destroy';
import { useLatestRef } from '../../utils/use-latest-ref';
import { useSafeId } from '../../utils/use-safe-id';
import { AlertDialogContextProvider } from '../alert-dialog/context';
import { ErrorDialogContextProvider } from './context';
export interface ErrorDialogRootProps {
children?: ReactNode;
}
export function ErrorDialogRoot({ children }: ErrorDialogRootProps): ReactNode {
const [core] = useState(() => new ErrorDialogCore());
const errorState = usePlayer(selectError);
const lastError = useRef(errorState?.error ?? null);
if (errorState?.error) lastError.current = errorState.error;
const errorStateRef = useLatestRef(errorState);
const [dialog] = useState(() =>
createAlertDialog({
transition: createTransition(),
onOpenChange: (nextOpen: boolean) => {
if (!nextOpen) {
errorStateRef.current?.dismissError();
}
},
})
);
const titleId = useSafeId('error-dialog-title');
const descriptionId = useSafeId('error-dialog-desc');
core.setTitleId(titleId);
core.setDescriptionId(descriptionId);
useEffect(() => {
const hasError = Boolean(errorState?.error);
const { active: isOpen } = dialog.input.current;
if (hasError && !isOpen) {
dialog.open();
} else if (!hasError && isOpen) {
dialog.close();
}
}, [errorState?.error, dialog]);
useDestroy(dialog);
const input = useSnapshot(dialog.input);
core.setInput(input);
const state = core.getState();
if (!errorState) return null;
return (
<ErrorDialogContextProvider value={{ lastError: lastError.current }}>
<AlertDialogContextProvider value={{ core, dialog, state, stateAttrMap: AlertDialogDataAttrs }}>
{children}
</AlertDialogContextProvider>
</ErrorDialogContextProvider>
);
}
export namespace ErrorDialogRoot {
export type Props = ErrorDialogRootProps;
}
@@ -0,0 +1,17 @@
export {
AlertDialogClose as Close,
type AlertDialogCloseProps as CloseProps,
} from '../alert-dialog/alert-dialog-close';
export {
AlertDialogPopup as Popup,
type AlertDialogPopupProps as PopupProps,
} from '../alert-dialog/alert-dialog-popup';
export {
AlertDialogTitle as Title,
type AlertDialogTitleProps as TitleProps,
} from '../alert-dialog/alert-dialog-title';
export {
ErrorDialogDescription as Description,
type ErrorDialogDescriptionProps as DescriptionProps,
} from './error-dialog-description';
export { ErrorDialogRoot as Root, type ErrorDialogRootProps as RootProps } from './error-dialog-root';
@@ -0,0 +1,2 @@
export { type ErrorDialogContextValue, useErrorDialogContext } from './context';
export * as ErrorDialog from './index.parts';