update packages and add valign
This commit is contained in:
138
scripts/reveal.js/react/src/Code.tsx
Normal file
138
scripts/reveal.js/react/src/Code.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { useContext, useLayoutEffect, useMemo, useRef } from 'react';
|
||||
import { RevealContext } from './context';
|
||||
import type { CodeProps } from './types';
|
||||
|
||||
type HighlightPlugin = {
|
||||
highlightBlock?: (block: HTMLElement) => void;
|
||||
};
|
||||
|
||||
function normalizeCode(code: string) {
|
||||
const lines = code.replace(/\r\n/g, '\n').split('\n');
|
||||
|
||||
while (lines.length && lines[0].trim().length === 0) lines.shift();
|
||||
while (lines.length && lines[lines.length - 1].trim().length === 0) lines.pop();
|
||||
|
||||
if (!lines.length) return '';
|
||||
|
||||
const minIndent = lines
|
||||
.filter((line) => line.trim().length > 0)
|
||||
.reduce(
|
||||
(acc, line) => Math.min(acc, line.match(/^\s*/)?.[0].length ?? 0),
|
||||
Number.POSITIVE_INFINITY
|
||||
);
|
||||
|
||||
return lines.map((line) => line.slice(minIndent)).join('\n');
|
||||
}
|
||||
|
||||
function cleanupGeneratedFragments(block: HTMLElement) {
|
||||
const pre = block.parentElement;
|
||||
if (!pre) return;
|
||||
|
||||
// RevealHighlight creates extra <code.fragment> nodes for each highlight step (e.g. "1|3").
|
||||
// Remove previously generated nodes before re-highlighting to avoid duplicate steps.
|
||||
Array.from(pre.children).forEach((child) => {
|
||||
if (
|
||||
child !== block &&
|
||||
child instanceof HTMLElement &&
|
||||
child.tagName === 'CODE' &&
|
||||
child.classList.contains('fragment')
|
||||
) {
|
||||
child.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function Code({
|
||||
children,
|
||||
code,
|
||||
language,
|
||||
trim = true,
|
||||
lineNumbers,
|
||||
startFrom,
|
||||
noEscape,
|
||||
codeClassName,
|
||||
codeStyle,
|
||||
codeProps,
|
||||
className,
|
||||
style,
|
||||
...rest
|
||||
}: CodeProps) {
|
||||
const deck = useContext(RevealContext);
|
||||
const codeRef = useRef<HTMLElement>(null);
|
||||
const lastHighlightSignatureRef = useRef<string>('');
|
||||
|
||||
const rawCode = typeof code === 'string' ? code : typeof children === 'string' ? children : '';
|
||||
const normalizedCode = useMemo(() => (trim ? normalizeCode(rawCode) : rawCode), [rawCode, trim]);
|
||||
const lineNumbersValue =
|
||||
lineNumbers === true
|
||||
? ''
|
||||
: lineNumbers === false || lineNumbers == null
|
||||
? undefined
|
||||
: String(lineNumbers);
|
||||
const codeClasses = [language, codeClassName].filter(Boolean).join(' ');
|
||||
const preClasses = ['code-wrapper', className].filter(Boolean).join(' ');
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const block = codeRef.current;
|
||||
if (!block || !deck) return;
|
||||
|
||||
const plugin = deck.getPlugin?.('highlight') as HighlightPlugin | undefined;
|
||||
if (!plugin || typeof plugin.highlightBlock !== 'function') return;
|
||||
|
||||
const highlightSignature = [
|
||||
normalizedCode,
|
||||
language || '',
|
||||
codeClassName || '',
|
||||
lineNumbersValue == null ? '__none__' : `lineNumbers:${lineNumbersValue}`,
|
||||
startFrom == null ? '' : String(startFrom),
|
||||
noEscape ? '1' : '0',
|
||||
].join('::');
|
||||
|
||||
if (
|
||||
lastHighlightSignatureRef.current === highlightSignature &&
|
||||
block.getAttribute('data-highlighted') === 'yes'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanupGeneratedFragments(block);
|
||||
block.textContent = normalizedCode;
|
||||
block.removeAttribute('data-highlighted');
|
||||
block.classList.remove('hljs');
|
||||
block.classList.remove('has-highlights');
|
||||
|
||||
// Restore source attributes before each highlight call since RevealHighlight mutates
|
||||
// data-line-numbers on the original block when it expands multi-step highlights.
|
||||
if (lineNumbersValue == null) block.removeAttribute('data-line-numbers');
|
||||
else block.setAttribute('data-line-numbers', lineNumbersValue);
|
||||
if (startFrom == null) block.removeAttribute('data-ln-start-from');
|
||||
else block.setAttribute('data-ln-start-from', String(startFrom));
|
||||
if (noEscape) block.setAttribute('data-noescape', '');
|
||||
else block.removeAttribute('data-noescape');
|
||||
|
||||
plugin.highlightBlock(block);
|
||||
|
||||
const slide = typeof block.closest === 'function' ? block.closest('section') : null;
|
||||
if (slide && typeof deck.syncFragments === 'function') {
|
||||
deck.syncFragments(slide as HTMLElement);
|
||||
}
|
||||
|
||||
lastHighlightSignatureRef.current = highlightSignature;
|
||||
}, [deck, normalizedCode, language, codeClassName, lineNumbersValue, startFrom, noEscape]);
|
||||
|
||||
return (
|
||||
<pre className={preClasses} style={style} {...rest}>
|
||||
<code
|
||||
{...codeProps}
|
||||
ref={codeRef}
|
||||
className={codeClasses || undefined}
|
||||
style={codeStyle}
|
||||
data-line-numbers={lineNumbersValue}
|
||||
data-ln-start-from={startFrom}
|
||||
data-noescape={noEscape ? '' : undefined}
|
||||
>
|
||||
{normalizedCode}
|
||||
</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
256
scripts/reveal.js/react/src/Deck.tsx
Normal file
256
scripts/reveal.js/react/src/Deck.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState, type Ref, type RefObject } from 'react';
|
||||
import Reveal from 'reveal.js';
|
||||
import type { RevealApi } from 'reveal.js';
|
||||
import { RevealContext } from './context';
|
||||
import type { DeckProps } from './types';
|
||||
|
||||
const DEFAULT_PLUGINS: NonNullable<DeckProps['plugins']> = [];
|
||||
type DeckEventHandler = NonNullable<DeckProps['onSync']>;
|
||||
type SlideStructureNode = number | [number, SlideStructureNode[]];
|
||||
type CurrentRef<T> = { current: T };
|
||||
|
||||
// Shallow-compare config objects so that re-renders where the parent creates a new object
|
||||
// literal with identical values do not trigger an unnecessary configure() call.
|
||||
function hasShallowConfigChanges(prev: DeckProps['config'], next: DeckProps['config']) {
|
||||
if (prev === next) return false;
|
||||
if (!prev || !next) return prev !== next;
|
||||
|
||||
const prevKeys = Object.keys(prev);
|
||||
const nextKeys = Object.keys(next);
|
||||
|
||||
if (prevKeys.length !== nextKeys.length) return true;
|
||||
|
||||
for (const key of prevKeys) {
|
||||
if (!(key in next)) return true;
|
||||
if ((prev as Record<string, unknown>)[key] !== (next as Record<string, unknown>)[key]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function setRef<T>(ref: Ref<T | null> | undefined, value: T | null) {
|
||||
if (!ref) return;
|
||||
if (typeof ref === 'function') {
|
||||
ref(value);
|
||||
} else {
|
||||
(ref as RefObject<T | null>).current = value;
|
||||
}
|
||||
}
|
||||
|
||||
function isSectionElement(element: Element): element is HTMLElement {
|
||||
return element.tagName === 'SECTION';
|
||||
}
|
||||
|
||||
function getSectionStructure(
|
||||
container: Element,
|
||||
slideIds: WeakMap<HTMLElement, number>,
|
||||
nextSlideIdRef: CurrentRef<number>
|
||||
): SlideStructureNode[] {
|
||||
return Array.from(container.children)
|
||||
.filter(isSectionElement)
|
||||
.map((section) => {
|
||||
let id = slideIds.get(section);
|
||||
if (id === undefined) {
|
||||
id = nextSlideIdRef.current++;
|
||||
slideIds.set(section, id);
|
||||
}
|
||||
|
||||
const childSlides = getSectionStructure(section, slideIds, nextSlideIdRef);
|
||||
return childSlides.length > 0 ? [id, childSlides] : id;
|
||||
});
|
||||
}
|
||||
|
||||
function getSlidesStructureSignature(
|
||||
slidesElement: HTMLElement | null,
|
||||
slideIds: WeakMap<HTMLElement, number>,
|
||||
nextSlideIdRef: CurrentRef<number>
|
||||
) {
|
||||
if (!slidesElement) return '[]';
|
||||
return JSON.stringify(getSectionStructure(slidesElement, slideIds, nextSlideIdRef));
|
||||
}
|
||||
|
||||
export function Deck({
|
||||
config,
|
||||
plugins = DEFAULT_PLUGINS,
|
||||
onReady,
|
||||
onSync,
|
||||
onSlideSync,
|
||||
onSlideChange,
|
||||
onSlideTransitionEnd,
|
||||
onFragmentShown,
|
||||
onFragmentHidden,
|
||||
onOverviewShown,
|
||||
onOverviewHidden,
|
||||
onPaused,
|
||||
onResumed,
|
||||
deckRef,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
}: DeckProps) {
|
||||
const deckDivRef = useRef<HTMLDivElement>(null);
|
||||
const slidesDivRef = useRef<HTMLDivElement>(null);
|
||||
const revealRef = useRef<RevealApi | null>(null);
|
||||
const [deck, setDeck] = useState<RevealApi | null>(null);
|
||||
|
||||
// Plugins are init-only in reveal.js; we register them once when creating the instance.
|
||||
const initialPluginsRef = useRef<NonNullable<DeckProps['plugins']>>(plugins);
|
||||
|
||||
// configure() performs its own sync in Reveal; this flag prevents us from running an
|
||||
// immediate second sync in the next layout effect pass.
|
||||
const skipNextSyncRef = useRef(false);
|
||||
|
||||
// Track the last config reference we applied so we can skip redundant configure() calls.
|
||||
const appliedConfigRef = useRef<DeckProps['config']>(config);
|
||||
const lastSyncedSlidesSignatureRef = useRef<string | null>(null);
|
||||
const slideIdsRef = useRef(new WeakMap<HTMLElement, number>());
|
||||
const nextSlideIdRef = useRef(1);
|
||||
const mountedRef = useRef(false);
|
||||
const teardownRequestRef = useRef(0);
|
||||
|
||||
// Create the Reveal instance once on mount and destroy it on unmount.
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
teardownRequestRef.current += 1;
|
||||
|
||||
if (!revealRef.current) {
|
||||
const instance = new Reveal(deckDivRef.current!, {
|
||||
...config,
|
||||
plugins: initialPluginsRef.current,
|
||||
});
|
||||
// Capture the config that was passed to the constructor so the configure
|
||||
// effect can later detect whether anything actually changed.
|
||||
appliedConfigRef.current = config;
|
||||
revealRef.current = instance;
|
||||
|
||||
instance.initialize().then(() => {
|
||||
if (!mountedRef.current || revealRef.current !== instance) return;
|
||||
setDeck(instance);
|
||||
onReady?.(instance);
|
||||
});
|
||||
} else if (revealRef.current.isReady()) {
|
||||
// React StrictMode unmounts and remounts every effect. On the second mount
|
||||
// the instance is already live, so skip construction. The isReady() guard
|
||||
// ensures we only expose it once initialization has fully completed.
|
||||
setDeck(revealRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
const instance = revealRef.current;
|
||||
if (!instance) return;
|
||||
|
||||
// Defer teardown to the next microtask. In StrictMode the component
|
||||
// remounts immediately, incrementing teardownRequestRef before the
|
||||
// microtask runs. The stale request number causes the callback to bail
|
||||
// out, preventing the instance from being destroyed on a live component.
|
||||
const teardownRequest = ++teardownRequestRef.current;
|
||||
Promise.resolve().then(() => {
|
||||
if (mountedRef.current || teardownRequestRef.current !== teardownRequest) return;
|
||||
if (revealRef.current !== instance) return;
|
||||
|
||||
try {
|
||||
instance.destroy();
|
||||
} catch (e) {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
|
||||
if (revealRef.current === instance) {
|
||||
revealRef.current = null;
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Keep consumer refs in sync, including when the ref prop itself changes.
|
||||
useEffect(() => {
|
||||
setRef(deckRef, deck);
|
||||
return () => setRef(deckRef, null);
|
||||
}, [deckRef, deck]);
|
||||
|
||||
// Attach and detach Reveal event listeners from the provided callbacks.
|
||||
useEffect(() => {
|
||||
if (!deck) return;
|
||||
|
||||
const events: [string, DeckEventHandler | undefined][] = [
|
||||
['sync', onSync],
|
||||
['slidesync', onSlideSync],
|
||||
['slidechanged', onSlideChange],
|
||||
['slidetransitionend', onSlideTransitionEnd],
|
||||
['fragmentshown', onFragmentShown],
|
||||
['fragmenthidden', onFragmentHidden],
|
||||
['overviewshown', onOverviewShown],
|
||||
['overviewhidden', onOverviewHidden],
|
||||
['paused', onPaused],
|
||||
['resumed', onResumed],
|
||||
];
|
||||
|
||||
const bound = events.filter((e): e is [string, DeckEventHandler] => e[1] != null);
|
||||
for (const [name, handler] of bound) {
|
||||
deck.on(name, handler);
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const [name, handler] of bound) {
|
||||
deck.off(name, handler);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
deck,
|
||||
onSync,
|
||||
onSlideSync,
|
||||
onSlideChange,
|
||||
onSlideTransitionEnd,
|
||||
onFragmentShown,
|
||||
onFragmentHidden,
|
||||
onOverviewShown,
|
||||
onOverviewHidden,
|
||||
onPaused,
|
||||
onResumed,
|
||||
]);
|
||||
|
||||
// Re-apply config after init and mark that configure already performed a sync.
|
||||
useLayoutEffect(() => {
|
||||
if (!deck || !revealRef.current?.isReady()) return;
|
||||
if (!hasShallowConfigChanges(appliedConfigRef.current, config)) return;
|
||||
|
||||
skipNextSyncRef.current = true;
|
||||
revealRef.current.configure(config ?? {});
|
||||
appliedConfigRef.current = config;
|
||||
}, [deck, config]);
|
||||
|
||||
// Sync Reveal's internal slide bookkeeping only when the rendered slide
|
||||
// structure changes. Avoid triggering sync for child changes.
|
||||
useLayoutEffect(() => {
|
||||
const shouldSkip = skipNextSyncRef.current;
|
||||
skipNextSyncRef.current = false;
|
||||
const slidesStructureSignature = getSlidesStructureSignature(
|
||||
slidesDivRef.current,
|
||||
slideIdsRef.current,
|
||||
nextSlideIdRef
|
||||
);
|
||||
|
||||
if (shouldSkip) {
|
||||
lastSyncedSlidesSignatureRef.current = slidesStructureSignature;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!revealRef.current?.isReady()) return;
|
||||
if (lastSyncedSlidesSignatureRef.current === slidesStructureSignature) return;
|
||||
|
||||
revealRef.current.sync();
|
||||
lastSyncedSlidesSignatureRef.current = slidesStructureSignature;
|
||||
});
|
||||
|
||||
return (
|
||||
<RevealContext.Provider value={deck}>
|
||||
<div className={className ? `reveal ${className}` : 'reveal'} style={style} ref={deckDivRef}>
|
||||
<div className="slides" ref={slidesDivRef}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</RevealContext.Provider>
|
||||
);
|
||||
}
|
||||
76
scripts/reveal.js/react/src/Fragment.tsx
Normal file
76
scripts/reveal.js/react/src/Fragment.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
Children,
|
||||
Fragment as ReactFragment,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
type CSSProperties,
|
||||
type ReactElement,
|
||||
} from 'react';
|
||||
import type { FragmentProps } from './types';
|
||||
|
||||
type FragmentChildProps = {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
'data-fragment-index'?: number;
|
||||
};
|
||||
|
||||
function mergeClassNames(...classNames: Array<string | undefined>) {
|
||||
return classNames.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function mergeStyles(
|
||||
childStyle: CSSProperties | undefined,
|
||||
style: CSSProperties | undefined
|
||||
) {
|
||||
if (!childStyle) return style;
|
||||
if (!style) return childStyle;
|
||||
|
||||
return {
|
||||
...childStyle,
|
||||
...style,
|
||||
};
|
||||
}
|
||||
|
||||
export function Fragment({
|
||||
animation,
|
||||
index,
|
||||
as,
|
||||
asChild,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
}: FragmentProps) {
|
||||
const classes = mergeClassNames('fragment', animation, className);
|
||||
|
||||
if (asChild) {
|
||||
let child: ReactElement<FragmentChildProps>;
|
||||
try {
|
||||
child = Children.only(children) as ReactElement<FragmentChildProps>;
|
||||
} catch {
|
||||
throw new Error('Fragment with asChild expects exactly one React element child.');
|
||||
}
|
||||
|
||||
if (!isValidElement(child) || child.type === ReactFragment) {
|
||||
throw new Error('Fragment with asChild expects exactly one non-Fragment React element child.');
|
||||
}
|
||||
|
||||
const fragmentChildProps: FragmentChildProps = {
|
||||
className: mergeClassNames(child.props.className, classes),
|
||||
style: mergeStyles(child.props.style, style),
|
||||
};
|
||||
|
||||
if (index !== undefined) {
|
||||
fragmentChildProps['data-fragment-index'] = index;
|
||||
}
|
||||
|
||||
return cloneElement(child, fragmentChildProps);
|
||||
}
|
||||
|
||||
const Tag = as ?? 'span';
|
||||
|
||||
return (
|
||||
<Tag className={classes} style={style} data-fragment-index={index}>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
180
scripts/reveal.js/react/src/Slide.tsx
Normal file
180
scripts/reveal.js/react/src/Slide.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { useContext, useLayoutEffect, useRef } from 'react';
|
||||
import type { RevealApi } from 'reveal.js';
|
||||
import { RevealContext } from './context';
|
||||
import type {
|
||||
SlideAutoAnimateProps,
|
||||
SlideBackgroundProps,
|
||||
SlideDataAttributeValue,
|
||||
SlideProps,
|
||||
SlideRevealProps,
|
||||
} from './types';
|
||||
|
||||
const EMPTY_DATA_ATTRIBUTES_SIGNATURE = '[]';
|
||||
type SlideShorthandProps = SlideBackgroundProps & SlideAutoAnimateProps & SlideRevealProps;
|
||||
|
||||
const SLIDE_DATA_ATTRIBUTES: Record<keyof SlideShorthandProps, `data-${string}`> = {
|
||||
background: 'data-background',
|
||||
backgroundImage: 'data-background-image',
|
||||
backgroundVideo: 'data-background-video',
|
||||
backgroundVideoLoop: 'data-background-video-loop',
|
||||
backgroundVideoMuted: 'data-background-video-muted',
|
||||
backgroundIframe: 'data-background-iframe',
|
||||
backgroundColor: 'data-background-color',
|
||||
backgroundGradient: 'data-background-gradient',
|
||||
backgroundSize: 'data-background-size',
|
||||
backgroundPosition: 'data-background-position',
|
||||
backgroundRepeat: 'data-background-repeat',
|
||||
backgroundOpacity: 'data-background-opacity',
|
||||
backgroundTransition: 'data-background-transition',
|
||||
visibility: 'data-visibility',
|
||||
autoAnimate: 'data-auto-animate',
|
||||
autoAnimateId: 'data-auto-animate-id',
|
||||
autoAnimateRestart: 'data-auto-animate-restart',
|
||||
autoAnimateUnmatched: 'data-auto-animate-unmatched',
|
||||
autoAnimateEasing: 'data-auto-animate-easing',
|
||||
autoAnimateDuration: 'data-auto-animate-duration',
|
||||
autoAnimateDelay: 'data-auto-animate-delay',
|
||||
transition: 'data-transition',
|
||||
transitionSpeed: 'data-transition-speed',
|
||||
autoSlide: 'data-autoslide',
|
||||
notes: 'data-notes',
|
||||
backgroundInteractive: 'data-background-interactive',
|
||||
preload: 'data-preload',
|
||||
};
|
||||
|
||||
type SlideElementProps = Omit<SlideProps, 'children' | keyof SlideShorthandProps>;
|
||||
|
||||
function getDataAttributesSignature(attributes: SlideElementProps) {
|
||||
return JSON.stringify(
|
||||
Object.entries(attributes)
|
||||
.filter(([key]) => key.startsWith('data-'))
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
);
|
||||
}
|
||||
|
||||
function getSlideAttributes(
|
||||
attributes: SlideElementProps,
|
||||
shorthandProps: SlideShorthandProps
|
||||
): SlideElementProps {
|
||||
const resolvedAttributes = { ...attributes } as SlideElementProps;
|
||||
const resolvedDataAttributes = resolvedAttributes as unknown as Record<
|
||||
string,
|
||||
SlideDataAttributeValue
|
||||
>;
|
||||
|
||||
for (const [propName, dataAttributeName] of Object.entries(SLIDE_DATA_ATTRIBUTES) as Array<
|
||||
[keyof SlideShorthandProps, `data-${string}`]
|
||||
>) {
|
||||
if (resolvedDataAttributes[dataAttributeName] !== undefined) continue;
|
||||
|
||||
const value = shorthandProps[propName];
|
||||
if (value === undefined) continue;
|
||||
|
||||
if (value === false) {
|
||||
if (propName === 'autoAnimateUnmatched') {
|
||||
resolvedDataAttributes[dataAttributeName] = 'false';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
resolvedDataAttributes[dataAttributeName] = typeof value === 'boolean' ? '' : value;
|
||||
}
|
||||
|
||||
return resolvedAttributes;
|
||||
}
|
||||
|
||||
export function Slide({
|
||||
children,
|
||||
background,
|
||||
backgroundImage,
|
||||
backgroundVideo,
|
||||
backgroundVideoLoop,
|
||||
backgroundVideoMuted,
|
||||
backgroundIframe,
|
||||
backgroundColor,
|
||||
backgroundGradient,
|
||||
backgroundSize,
|
||||
backgroundPosition,
|
||||
backgroundRepeat,
|
||||
backgroundOpacity,
|
||||
backgroundTransition,
|
||||
visibility,
|
||||
autoAnimate,
|
||||
autoAnimateId,
|
||||
autoAnimateRestart,
|
||||
autoAnimateUnmatched,
|
||||
autoAnimateEasing,
|
||||
autoAnimateDuration,
|
||||
autoAnimateDelay,
|
||||
transition,
|
||||
transitionSpeed,
|
||||
autoSlide,
|
||||
notes,
|
||||
backgroundInteractive,
|
||||
preload,
|
||||
...rest
|
||||
}: SlideProps) {
|
||||
const deck = useContext(RevealContext);
|
||||
const slideRef = useRef<HTMLElement>(null);
|
||||
const lastSyncedDeckRef = useRef<RevealApi | null>(null);
|
||||
const lastSyncedSignatureRef = useRef<string | null>(null);
|
||||
const slideAttributes = getSlideAttributes(rest, {
|
||||
background,
|
||||
backgroundImage,
|
||||
backgroundVideo,
|
||||
backgroundVideoLoop,
|
||||
backgroundVideoMuted,
|
||||
backgroundIframe,
|
||||
backgroundColor,
|
||||
backgroundGradient,
|
||||
backgroundSize,
|
||||
backgroundPosition,
|
||||
backgroundRepeat,
|
||||
backgroundOpacity,
|
||||
backgroundTransition,
|
||||
visibility,
|
||||
autoAnimate,
|
||||
autoAnimateId,
|
||||
autoAnimateRestart,
|
||||
autoAnimateUnmatched,
|
||||
autoAnimateEasing,
|
||||
autoAnimateDuration,
|
||||
autoAnimateDelay,
|
||||
transition,
|
||||
transitionSpeed,
|
||||
autoSlide,
|
||||
notes,
|
||||
backgroundInteractive,
|
||||
preload,
|
||||
});
|
||||
const dataAttributesSignature = getDataAttributesSignature(slideAttributes);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const slide = slideRef.current;
|
||||
if (!deck || !slide || typeof deck.syncSlide !== 'function') return;
|
||||
|
||||
// The first render for a given deck instance is handled by the parent Deck.sync() pass.
|
||||
// syncSlide() is only safe once Reveal is aware of this slide.
|
||||
if (lastSyncedDeckRef.current !== deck) {
|
||||
lastSyncedDeckRef.current = deck;
|
||||
lastSyncedSignatureRef.current = dataAttributesSignature;
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataAttributesSignature === EMPTY_DATA_ATTRIBUTES_SIGNATURE) return;
|
||||
|
||||
const sameDeck = lastSyncedDeckRef.current === deck;
|
||||
const sameDataAttributes = lastSyncedSignatureRef.current === dataAttributesSignature;
|
||||
if (sameDeck && sameDataAttributes) return;
|
||||
|
||||
deck.syncSlide(slide);
|
||||
lastSyncedDeckRef.current = deck;
|
||||
lastSyncedSignatureRef.current = dataAttributesSignature;
|
||||
}, [deck, dataAttributesSignature]);
|
||||
|
||||
return (
|
||||
<section ref={slideRef} {...slideAttributes}>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
9
scripts/reveal.js/react/src/Stack.tsx
Normal file
9
scripts/reveal.js/react/src/Stack.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { StackProps } from './types';
|
||||
|
||||
export function Stack({ className, style, children }: StackProps) {
|
||||
return (
|
||||
<section className={className} style={style}>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
142
scripts/reveal.js/react/src/__tests__/Code.test.tsx
Normal file
142
scripts/reveal.js/react/src/__tests__/Code.test.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { Code } from '../Code';
|
||||
import { RevealContext } from '../context';
|
||||
|
||||
describe('Code', () => {
|
||||
it('renders pre/code and trims multiline template literals by default', () => {
|
||||
const { container } = render(
|
||||
<Code language="javascript">{`
|
||||
function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
`}</Code>
|
||||
);
|
||||
|
||||
const pre = container.querySelector('pre');
|
||||
const code = container.querySelector('pre > code');
|
||||
|
||||
expect(pre).toBeInTheDocument();
|
||||
expect(pre).toHaveClass('code-wrapper');
|
||||
expect(code).toHaveClass('javascript');
|
||||
expect(code?.textContent).toBe('function add(a, b) {\n\treturn a + b;\n}');
|
||||
});
|
||||
|
||||
it('can disable trimming', () => {
|
||||
const source = '\n\tconst value = 1;\n';
|
||||
const { container } = render(<Code trim={false}>{source}</Code>);
|
||||
const code = container.querySelector('pre > code');
|
||||
|
||||
expect(code?.textContent).toBe(source);
|
||||
});
|
||||
|
||||
it('maps line number props to reveal data attributes', () => {
|
||||
const { container } = render(
|
||||
<Code language="js" lineNumbers="|2,4-6|8" startFrom={10}>
|
||||
{`console.log('hello')`}
|
||||
</Code>
|
||||
);
|
||||
|
||||
const code = container.querySelector('pre > code');
|
||||
expect(code).toHaveAttribute('data-line-numbers', '|2,4-6|8');
|
||||
expect(code).toHaveAttribute('data-ln-start-from', '10');
|
||||
});
|
||||
|
||||
it('invokes the Reveal highlight plugin when registered on the deck', () => {
|
||||
const highlightBlock = vi.fn((block: HTMLElement) => {
|
||||
block.setAttribute('data-highlighted', 'yes');
|
||||
});
|
||||
const deck = {
|
||||
getPlugin: vi.fn().mockReturnValue({ highlightBlock }),
|
||||
syncFragments: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const { container } = render(
|
||||
<RevealContext.Provider value={deck}>
|
||||
<section>
|
||||
<Code language="javascript">{`console.log('hello')`}</Code>
|
||||
</section>
|
||||
</RevealContext.Provider>
|
||||
);
|
||||
|
||||
const code = container.querySelector('pre > code');
|
||||
expect(deck.getPlugin).toHaveBeenCalledWith('highlight');
|
||||
expect(highlightBlock).toHaveBeenCalledWith(code);
|
||||
expect(deck.syncFragments).toHaveBeenCalledWith(expect.any(HTMLElement));
|
||||
});
|
||||
|
||||
it('rehighlights updated code without accumulating generated fragments', () => {
|
||||
const highlightBlock = vi.fn((block: HTMLElement) => {
|
||||
block.setAttribute('data-highlighted', 'yes');
|
||||
const fragment = block.cloneNode(true) as HTMLElement;
|
||||
fragment.classList.add('fragment');
|
||||
block.parentElement?.appendChild(fragment);
|
||||
});
|
||||
|
||||
const deck = {
|
||||
getPlugin: vi.fn().mockReturnValue({ highlightBlock }),
|
||||
syncFragments: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const { container, rerender } = render(
|
||||
<RevealContext.Provider value={deck}>
|
||||
<section>
|
||||
<Code lineNumbers="|1|2">{`console.log('one')`}</Code>
|
||||
</section>
|
||||
</RevealContext.Provider>
|
||||
);
|
||||
|
||||
expect(container.querySelectorAll('pre > code.fragment')).toHaveLength(1);
|
||||
|
||||
rerender(
|
||||
<RevealContext.Provider value={deck}>
|
||||
<section>
|
||||
<Code lineNumbers="|1|2">{`console.log('two')`}</Code>
|
||||
</section>
|
||||
</RevealContext.Provider>
|
||||
);
|
||||
|
||||
expect(highlightBlock).toHaveBeenCalledTimes(2);
|
||||
expect(container.querySelectorAll('pre > code.fragment')).toHaveLength(1);
|
||||
expect(container.querySelector('pre > code:not(.fragment)')).toHaveTextContent("console.log('two')");
|
||||
});
|
||||
|
||||
it('restores full line-number steps before rehighlighting after plugin mutation', () => {
|
||||
const seenLineNumbers: string[] = [];
|
||||
const highlightBlock = vi.fn((block: HTMLElement) => {
|
||||
seenLineNumbers.push(block.getAttribute('data-line-numbers') || '');
|
||||
block.setAttribute('data-highlighted', 'yes');
|
||||
|
||||
// Mimic RevealHighlight: original block is rewritten to the first step and
|
||||
// an extra fragment block is appended for following steps.
|
||||
block.setAttribute('data-line-numbers', '1');
|
||||
const fragment = block.cloneNode(true) as HTMLElement;
|
||||
fragment.classList.add('fragment');
|
||||
fragment.setAttribute('data-line-numbers', '3');
|
||||
block.parentElement?.appendChild(fragment);
|
||||
});
|
||||
|
||||
const deck = {
|
||||
getPlugin: vi.fn().mockReturnValue({ highlightBlock }),
|
||||
syncFragments: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const { rerender } = render(
|
||||
<RevealContext.Provider value={deck}>
|
||||
<section>
|
||||
<Code language="javascript" lineNumbers="1|3">{`console.log('one')`}</Code>
|
||||
</section>
|
||||
</RevealContext.Provider>
|
||||
);
|
||||
|
||||
rerender(
|
||||
<RevealContext.Provider value={deck}>
|
||||
<section>
|
||||
<Code language="ts" lineNumbers="1|3">{`console.log('one')`}</Code>
|
||||
</section>
|
||||
</RevealContext.Provider>
|
||||
);
|
||||
|
||||
expect(seenLineNumbers).toEqual(['1|3', '1|3']);
|
||||
});
|
||||
});
|
||||
450
scripts/reveal.js/react/src/__tests__/Deck.test.tsx
Normal file
450
scripts/reveal.js/react/src/__tests__/Deck.test.tsx
Normal file
@@ -0,0 +1,450 @@
|
||||
import { render, act, cleanup } from '@testing-library/react';
|
||||
import { StrictMode } from 'react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const mockApi = vi.hoisted(() => ({
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
destroy: vi.fn(),
|
||||
sync: vi.fn(),
|
||||
isReady: vi.fn().mockReturnValue(true),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
configure: vi.fn(),
|
||||
}));
|
||||
|
||||
const RevealConstructor = vi.hoisted(() =>
|
||||
vi.fn(function () {
|
||||
return mockApi;
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock('reveal.js', () => ({ default: RevealConstructor }));
|
||||
|
||||
import { Deck, Slide, useReveal } from '../index';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockApi.initialize.mockResolvedValue(undefined);
|
||||
mockApi.isReady.mockReturnValue(true);
|
||||
});
|
||||
|
||||
describe('Deck', () => {
|
||||
it('renders .reveal > .slides structure', async () => {
|
||||
let container: HTMLElement;
|
||||
await act(async () => {
|
||||
({ container } = render(
|
||||
<Deck>
|
||||
<Slide>Hello</Slide>
|
||||
</Deck>
|
||||
));
|
||||
});
|
||||
|
||||
const reveal = container!.querySelector('.reveal');
|
||||
expect(reveal).toBeInTheDocument();
|
||||
|
||||
const slides = reveal?.querySelector('.slides');
|
||||
expect(slides).toBeInTheDocument();
|
||||
|
||||
const section = slides?.querySelector('section');
|
||||
expect(section).toHaveTextContent('Hello');
|
||||
});
|
||||
|
||||
it('calls new Reveal() and initialize() on mount', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<Deck config={{ transition: 'slide', hash: true }}>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
expect(RevealConstructor).toHaveBeenCalledWith(
|
||||
expect.any(HTMLElement),
|
||||
expect.objectContaining({
|
||||
transition: 'slide',
|
||||
hash: true,
|
||||
plugins: [],
|
||||
})
|
||||
);
|
||||
expect(mockApi.initialize).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls destroy() on unmount', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<Deck>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
expect(mockApi.destroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls sync() after render when ready', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<Deck>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockApi.sync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not call sync() when slide content changes without structural changes', async () => {
|
||||
let rerender: ReturnType<typeof render>['rerender'];
|
||||
await act(async () => {
|
||||
({ rerender } = render(
|
||||
<Deck>
|
||||
<Slide>
|
||||
<p>Tick 1</p>
|
||||
</Slide>
|
||||
</Deck>
|
||||
));
|
||||
});
|
||||
|
||||
mockApi.sync.mockClear();
|
||||
|
||||
await act(async () => {
|
||||
rerender(
|
||||
<Deck>
|
||||
<Slide>
|
||||
<p>Tick 2</p>
|
||||
</Slide>
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockApi.sync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not call sync() twice when configure() already syncs', async () => {
|
||||
mockApi.configure.mockImplementation(() => {
|
||||
mockApi.sync();
|
||||
});
|
||||
|
||||
let rerender: ReturnType<typeof render>['rerender'];
|
||||
await act(async () => {
|
||||
({ rerender } = render(
|
||||
<Deck config={{ transition: 'slide' }}>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
));
|
||||
});
|
||||
|
||||
mockApi.configure.mockClear();
|
||||
mockApi.sync.mockClear();
|
||||
|
||||
await act(async () => {
|
||||
rerender(
|
||||
<Deck config={{ transition: 'convex' }}>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockApi.configure).toHaveBeenCalledTimes(1);
|
||||
expect(mockApi.sync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls sync() when keyed slides are reordered', async () => {
|
||||
let rerender: ReturnType<typeof render>['rerender'];
|
||||
await act(async () => {
|
||||
({ rerender } = render(
|
||||
<Deck>
|
||||
{['first', 'second'].map((label) => (
|
||||
<Slide key={label}>{label}</Slide>
|
||||
))}
|
||||
</Deck>
|
||||
));
|
||||
});
|
||||
|
||||
mockApi.sync.mockClear();
|
||||
|
||||
await act(async () => {
|
||||
rerender(
|
||||
<Deck>
|
||||
{['second', 'first'].map((label) => (
|
||||
<Slide key={label}>{label}</Slide>
|
||||
))}
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockApi.sync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls sync() after children change even when isReady() was false during the config-change render', async () => {
|
||||
let rerender: ReturnType<typeof render>['rerender'];
|
||||
await act(async () => {
|
||||
({ rerender } = render(
|
||||
<Deck config={{ transition: 'slide' }}>
|
||||
<Slide>Slide 1</Slide>
|
||||
</Deck>
|
||||
));
|
||||
});
|
||||
|
||||
// Make isReady() return true for the configure effect but false for the sync effect,
|
||||
// simulating Reveal briefly being non-ready during configuration. Without the fix the
|
||||
// skip flag would never be reset and the subsequent sync() call would be wrongly skipped.
|
||||
mockApi.isReady.mockReturnValueOnce(true).mockReturnValueOnce(false);
|
||||
|
||||
await act(async () => {
|
||||
rerender(
|
||||
<Deck config={{ transition: 'convex' }}>
|
||||
<Slide>Slide 1</Slide>
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
mockApi.isReady.mockReturnValue(true);
|
||||
mockApi.sync.mockClear();
|
||||
|
||||
await act(async () => {
|
||||
rerender(
|
||||
<Deck config={{ transition: 'convex' }}>
|
||||
<Slide>Slide 1</Slide>
|
||||
<Slide>Slide 2</Slide>
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockApi.sync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not reconfigure when config object is recreated with same values', async () => {
|
||||
let rerender: ReturnType<typeof render>['rerender'];
|
||||
await act(async () => {
|
||||
({ rerender } = render(
|
||||
<Deck config={{ transition: 'slide', hash: true }}>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
));
|
||||
});
|
||||
|
||||
mockApi.configure.mockClear();
|
||||
|
||||
await act(async () => {
|
||||
rerender(
|
||||
<Deck config={{ transition: 'slide', hash: true }}>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockApi.configure).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fires onReady callback after initialization', async () => {
|
||||
const onReady = vi.fn();
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<Deck onReady={onReady}>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
expect(onReady).toHaveBeenCalledWith(mockApi);
|
||||
});
|
||||
|
||||
it('wires onSlideChange to the slidechanged event', async () => {
|
||||
const onSlideChange = vi.fn();
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<Deck onSlideChange={onSlideChange}>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockApi.on).toHaveBeenCalledWith('slidechanged', onSlideChange);
|
||||
});
|
||||
|
||||
it('wires onSlideSync to the slidesync event', async () => {
|
||||
const onSlideSync = vi.fn();
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<Deck onSlideSync={onSlideSync}>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockApi.on).toHaveBeenCalledWith('slidesync', onSlideSync);
|
||||
});
|
||||
|
||||
it('cleans up event listeners on unmount', async () => {
|
||||
const onSlideChange = vi.fn();
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<Deck onSlideChange={onSlideChange}>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
expect(mockApi.off).toHaveBeenCalledWith('slidechanged', onSlideChange);
|
||||
});
|
||||
|
||||
it('passes plugins to the Reveal constructor', async () => {
|
||||
const fakePlugin = () => ({ id: 'fake', init: () => {} });
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<Deck plugins={[fakePlugin]}>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
expect(RevealConstructor).toHaveBeenCalledWith(
|
||||
expect.any(HTMLElement),
|
||||
expect.objectContaining({ plugins: [fakePlugin] })
|
||||
);
|
||||
});
|
||||
|
||||
it('registers plugins only on initialization', async () => {
|
||||
const pluginA = () => ({ id: 'a', init: () => {} });
|
||||
const pluginB = () => ({ id: 'b', init: () => {} });
|
||||
|
||||
let rerender: ReturnType<typeof render>['rerender'];
|
||||
await act(async () => {
|
||||
({ rerender } = render(
|
||||
<Deck plugins={[pluginA]}>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
));
|
||||
});
|
||||
|
||||
mockApi.configure.mockClear();
|
||||
|
||||
await act(async () => {
|
||||
rerender(
|
||||
<Deck plugins={[pluginB]}>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
expect(RevealConstructor).toHaveBeenCalledTimes(1);
|
||||
expect(RevealConstructor).toHaveBeenCalledWith(
|
||||
expect.any(HTMLElement),
|
||||
expect.objectContaining({ plugins: [pluginA] })
|
||||
);
|
||||
expect(mockApi.configure).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('applies className and style to the .reveal div', async () => {
|
||||
let container: HTMLElement;
|
||||
await act(async () => {
|
||||
({ container } = render(
|
||||
<Deck className="custom" style={{ height: '400px' }}>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
));
|
||||
});
|
||||
|
||||
const reveal = container!.querySelector('.reveal');
|
||||
expect(reveal).toHaveClass('reveal', 'custom');
|
||||
expect(reveal).toHaveStyle({ height: '400px' });
|
||||
});
|
||||
|
||||
it('does not initialize twice in StrictMode', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<StrictMode>
|
||||
<Deck>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
</StrictMode>
|
||||
);
|
||||
});
|
||||
|
||||
expect(RevealConstructor).toHaveBeenCalledTimes(1);
|
||||
expect(mockApi.initialize).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('updates deckRef when the ref prop changes', async () => {
|
||||
const refA = vi.fn();
|
||||
const refB = vi.fn();
|
||||
|
||||
let rerender: ReturnType<typeof render>['rerender'];
|
||||
let unmount: ReturnType<typeof render>['unmount'];
|
||||
await act(async () => {
|
||||
({ rerender, unmount } = render(
|
||||
<Deck deckRef={refA}>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
));
|
||||
});
|
||||
|
||||
expect(refA).toHaveBeenCalledWith(mockApi);
|
||||
|
||||
await act(async () => {
|
||||
rerender(
|
||||
<Deck deckRef={refB}>
|
||||
<Slide>Test</Slide>
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
expect(refA).toHaveBeenCalledWith(null);
|
||||
expect(refB).toHaveBeenCalledWith(mockApi);
|
||||
|
||||
await act(async () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
expect(refB).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useReveal', () => {
|
||||
it('returns the RevealApi instance inside a Deck', async () => {
|
||||
let hookResult: any = undefined;
|
||||
|
||||
function Inspector() {
|
||||
hookResult = useReveal();
|
||||
return null;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<Deck>
|
||||
<Slide>
|
||||
<Inspector />
|
||||
</Slide>
|
||||
</Deck>
|
||||
);
|
||||
});
|
||||
|
||||
expect(hookResult).toBe(mockApi);
|
||||
});
|
||||
|
||||
it('returns null outside of a Deck', () => {
|
||||
let hookResult: any = 'not-null';
|
||||
|
||||
function Inspector() {
|
||||
hookResult = useReveal();
|
||||
return null;
|
||||
}
|
||||
|
||||
render(<Inspector />);
|
||||
expect(hookResult).toBeNull();
|
||||
});
|
||||
});
|
||||
122
scripts/reveal.js/react/src/__tests__/Fragment.test.tsx
Normal file
122
scripts/reveal.js/react/src/__tests__/Fragment.test.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Fragment } from '../Fragment';
|
||||
|
||||
describe('Fragment', () => {
|
||||
it('renders with the "fragment" class', () => {
|
||||
const { container } = render(<Fragment>Hello</Fragment>);
|
||||
const el = container.querySelector('.fragment');
|
||||
expect(el).toBeInTheDocument();
|
||||
expect(el).toHaveTextContent('Hello');
|
||||
});
|
||||
|
||||
it('applies the animation class', () => {
|
||||
const { container } = render(<Fragment animation="fade-up">Animated</Fragment>);
|
||||
|
||||
const el = container.querySelector('.fragment');
|
||||
expect(el).toHaveClass('fragment', 'fade-up');
|
||||
});
|
||||
|
||||
it('sets data-fragment-index when index is provided', () => {
|
||||
const { container } = render(<Fragment index={2}>Indexed</Fragment>);
|
||||
|
||||
const el = container.querySelector('.fragment');
|
||||
expect(el).toHaveAttribute('data-fragment-index', '2');
|
||||
});
|
||||
|
||||
it('does not set data-fragment-index when index is omitted', () => {
|
||||
const { container } = render(<Fragment>No index</Fragment>);
|
||||
const el = container.querySelector('.fragment');
|
||||
expect(el?.getAttribute('data-fragment-index')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders as a <span> by default', () => {
|
||||
const { container } = render(<Fragment>Default</Fragment>);
|
||||
const el = container.querySelector('.fragment');
|
||||
expect(el?.tagName).toBe('SPAN');
|
||||
});
|
||||
|
||||
it('renders as a custom element when "as" is provided', () => {
|
||||
const { container } = render(<Fragment as="div">Block</Fragment>);
|
||||
|
||||
const el = container.querySelector('.fragment');
|
||||
expect(el?.tagName).toBe('DIV');
|
||||
});
|
||||
|
||||
it('combines fragment, animation, and custom className', () => {
|
||||
const { container } = render(
|
||||
<Fragment animation="grow" className="custom">
|
||||
Combined
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
const el = container.querySelector('.fragment');
|
||||
expect(el).toHaveClass('fragment', 'grow', 'custom');
|
||||
});
|
||||
|
||||
it('passes style through', () => {
|
||||
const { container } = render(<Fragment style={{ opacity: 0.5 }}>Styled</Fragment>);
|
||||
|
||||
const el = container.querySelector('.fragment');
|
||||
expect(el).toHaveStyle({ opacity: '0.5' });
|
||||
});
|
||||
|
||||
it('renders the child element directly when asChild is provided', () => {
|
||||
const { container } = render(
|
||||
<Fragment asChild animation="fade-up">
|
||||
<li>Item</li>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
expect(container.firstElementChild?.tagName).toBe('LI');
|
||||
expect(container.firstElementChild).toHaveClass('fragment', 'fade-up');
|
||||
expect(container.firstElementChild).toHaveTextContent('Item');
|
||||
});
|
||||
|
||||
it('merges child className and style when asChild is provided', () => {
|
||||
const { container } = render(
|
||||
<Fragment asChild animation="grow" className="outer" style={{ opacity: 0.5 }}>
|
||||
<div className="inner" style={{ color: 'red' }}>
|
||||
Merged
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
const el = container.firstElementChild;
|
||||
expect(el).toHaveClass('inner', 'fragment', 'grow', 'outer');
|
||||
expect(el).toHaveStyle({ color: 'rgb(255, 0, 0)', opacity: '0.5' });
|
||||
});
|
||||
|
||||
it('sets data-fragment-index on the child when asChild is provided', () => {
|
||||
const { container } = render(
|
||||
<Fragment asChild index={2}>
|
||||
<div data-fragment-index={1}>Indexed</div>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
expect(container.firstElementChild).toHaveAttribute('data-fragment-index', '2');
|
||||
});
|
||||
|
||||
it('throws when asChild receives multiple children', () => {
|
||||
expect(() =>
|
||||
render(
|
||||
<Fragment asChild>
|
||||
<span>One</span>
|
||||
<span>Two</span>
|
||||
</Fragment>
|
||||
)
|
||||
).toThrow('Fragment with asChild expects exactly one React element child.');
|
||||
});
|
||||
|
||||
it('throws when asChild receives a React.Fragment child', () => {
|
||||
expect(() =>
|
||||
render(
|
||||
<Fragment asChild>
|
||||
<>
|
||||
<span>One</span>
|
||||
</>
|
||||
</Fragment>
|
||||
)
|
||||
).toThrow('Fragment with asChild expects exactly one non-Fragment React element child.');
|
||||
});
|
||||
});
|
||||
296
scripts/reveal.js/react/src/__tests__/Slide.test.tsx
Normal file
296
scripts/reveal.js/react/src/__tests__/Slide.test.tsx
Normal file
@@ -0,0 +1,296 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { Slide } from '../Slide';
|
||||
import { RevealContext } from '../context';
|
||||
|
||||
describe('Slide', () => {
|
||||
it('renders as a <section> element', () => {
|
||||
const { container } = render(<Slide>Hello</Slide>);
|
||||
const section = container.querySelector('section');
|
||||
expect(section).toBeInTheDocument();
|
||||
expect(section).toHaveTextContent('Hello');
|
||||
});
|
||||
|
||||
it('passes data-* attributes through', () => {
|
||||
const { container } = render(
|
||||
<Slide data-background="#ff0000" data-transition="zoom" data-auto-animate="">
|
||||
Content
|
||||
</Slide>
|
||||
);
|
||||
|
||||
const section = container.querySelector('section');
|
||||
expect(section).toHaveAttribute('data-background', '#ff0000');
|
||||
expect(section).toHaveAttribute('data-transition', 'zoom');
|
||||
expect(section).toHaveAttribute('data-auto-animate', '');
|
||||
});
|
||||
|
||||
it('maps background shorthand props to slide data attributes', () => {
|
||||
const { container } = render(
|
||||
<Slide
|
||||
background="#111827"
|
||||
backgroundImage="https://example.com/hero.png"
|
||||
backgroundColor="#000000"
|
||||
backgroundOpacity={0.4}
|
||||
backgroundTransition="zoom"
|
||||
backgroundVideoLoop
|
||||
>
|
||||
Content
|
||||
</Slide>
|
||||
);
|
||||
|
||||
const section = container.querySelector('section');
|
||||
expect(section).toHaveAttribute('data-background', '#111827');
|
||||
expect(section).toHaveAttribute('data-background-image', 'https://example.com/hero.png');
|
||||
expect(section).toHaveAttribute('data-background-color', '#000000');
|
||||
expect(section).toHaveAttribute('data-background-opacity', '0.4');
|
||||
expect(section).toHaveAttribute('data-background-transition', 'zoom');
|
||||
expect(section).toHaveAttribute('data-background-video-loop', '');
|
||||
});
|
||||
|
||||
it('maps visibility and auto-animate shorthand props to slide data attributes', () => {
|
||||
const { container } = render(
|
||||
<Slide
|
||||
visibility="hidden"
|
||||
autoAnimate
|
||||
autoAnimateId="hero"
|
||||
autoAnimateRestart
|
||||
autoAnimateUnmatched={false}
|
||||
autoAnimateEasing="ease-out"
|
||||
autoAnimateDuration={0.6}
|
||||
autoAnimateDelay={0.1}
|
||||
>
|
||||
Content
|
||||
</Slide>
|
||||
);
|
||||
|
||||
const section = container.querySelector('section');
|
||||
expect(section).toHaveAttribute('data-visibility', 'hidden');
|
||||
expect(section).toHaveAttribute('data-auto-animate', '');
|
||||
expect(section).toHaveAttribute('data-auto-animate-id', 'hero');
|
||||
expect(section).toHaveAttribute('data-auto-animate-restart', '');
|
||||
expect(section).toHaveAttribute('data-auto-animate-unmatched', 'false');
|
||||
expect(section).toHaveAttribute('data-auto-animate-easing', 'ease-out');
|
||||
expect(section).toHaveAttribute('data-auto-animate-duration', '0.6');
|
||||
expect(section).toHaveAttribute('data-auto-animate-delay', '0.1');
|
||||
});
|
||||
|
||||
it('maps common Reveal slide shorthand props to data attributes', () => {
|
||||
const { container } = render(
|
||||
<Slide
|
||||
transition="zoom-in fade-out"
|
||||
transitionSpeed="fast"
|
||||
autoSlide={1500}
|
||||
notes="Speaker notes"
|
||||
backgroundInteractive
|
||||
preload
|
||||
>
|
||||
Content
|
||||
</Slide>
|
||||
);
|
||||
|
||||
const section = container.querySelector('section');
|
||||
expect(section).toHaveAttribute('data-transition', 'zoom-in fade-out');
|
||||
expect(section).toHaveAttribute('data-transition-speed', 'fast');
|
||||
expect(section).toHaveAttribute('data-autoslide', '1500');
|
||||
expect(section).toHaveAttribute('data-notes', 'Speaker notes');
|
||||
expect(section).toHaveAttribute('data-background-interactive', '');
|
||||
expect(section).toHaveAttribute('data-preload', '');
|
||||
});
|
||||
|
||||
it('prefers explicit data-* attributes over shorthand props', () => {
|
||||
const { container } = render(
|
||||
<Slide
|
||||
background="#111827"
|
||||
backgroundColor="#000000"
|
||||
autoAnimate
|
||||
visibility="hidden"
|
||||
transition="zoom"
|
||||
notes="Shorthand notes"
|
||||
preload
|
||||
data-background="#222222"
|
||||
data-background-color="#333333"
|
||||
data-auto-animate=""
|
||||
data-visibility="uncounted"
|
||||
data-transition="fade"
|
||||
data-notes="Raw notes"
|
||||
data-preload=""
|
||||
>
|
||||
Content
|
||||
</Slide>
|
||||
);
|
||||
|
||||
const section = container.querySelector('section');
|
||||
expect(section).toHaveAttribute('data-background', '#222222');
|
||||
expect(section).toHaveAttribute('data-background-color', '#333333');
|
||||
expect(section).toHaveAttribute('data-auto-animate', '');
|
||||
expect(section).toHaveAttribute('data-visibility', 'uncounted');
|
||||
expect(section).toHaveAttribute('data-transition', 'fade');
|
||||
expect(section).toHaveAttribute('data-notes', 'Raw notes');
|
||||
expect(section).toHaveAttribute('data-preload', '');
|
||||
});
|
||||
|
||||
it('applies className and style', () => {
|
||||
const { container } = render(
|
||||
<Slide className="intro" style={{ fontSize: '20px' }}>
|
||||
Styled
|
||||
</Slide>
|
||||
);
|
||||
|
||||
const section = container.querySelector('section');
|
||||
expect(section).toHaveClass('intro');
|
||||
expect(section).toHaveStyle({ fontSize: '20px' });
|
||||
});
|
||||
|
||||
it('passes id and aria attributes', () => {
|
||||
const { container } = render(
|
||||
<Slide id="slide-1" aria-label="Introduction">
|
||||
Accessible
|
||||
</Slide>
|
||||
);
|
||||
|
||||
const section = container.querySelector('section');
|
||||
expect(section).toHaveAttribute('id', 'slide-1');
|
||||
expect(section).toHaveAttribute('aria-label', 'Introduction');
|
||||
});
|
||||
|
||||
it('renders children of any type', () => {
|
||||
const { container } = render(
|
||||
<Slide>
|
||||
<h1>Title</h1>
|
||||
<p>Paragraph</p>
|
||||
</Slide>
|
||||
);
|
||||
|
||||
const section = container.querySelector('section');
|
||||
expect(section?.querySelector('h1')).toHaveTextContent('Title');
|
||||
expect(section?.querySelector('p')).toHaveTextContent('Paragraph');
|
||||
});
|
||||
|
||||
it('calls syncSlide when data-* attributes change after mount', () => {
|
||||
const deck = {
|
||||
syncSlide: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const { container, rerender } = render(
|
||||
<RevealContext.Provider value={deck}>
|
||||
<Slide data-background-color="#111">Content</Slide>
|
||||
</RevealContext.Provider>
|
||||
);
|
||||
|
||||
const section = container.querySelector('section');
|
||||
expect(section).toBeInTheDocument();
|
||||
expect(deck.syncSlide).not.toHaveBeenCalled();
|
||||
deck.syncSlide.mockClear();
|
||||
|
||||
rerender(
|
||||
<RevealContext.Provider value={deck}>
|
||||
<Slide data-background-color="#222">Content</Slide>
|
||||
</RevealContext.Provider>
|
||||
);
|
||||
|
||||
expect(deck.syncSlide).toHaveBeenCalledTimes(1);
|
||||
expect(deck.syncSlide).toHaveBeenCalledWith(container.querySelector('section'));
|
||||
});
|
||||
|
||||
it('calls syncSlide when shorthand background props change after mount', () => {
|
||||
const deck = {
|
||||
syncSlide: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const { container, rerender } = render(
|
||||
<RevealContext.Provider value={deck}>
|
||||
<Slide backgroundColor="#111">Content</Slide>
|
||||
</RevealContext.Provider>
|
||||
);
|
||||
|
||||
expect(deck.syncSlide).not.toHaveBeenCalled();
|
||||
deck.syncSlide.mockClear();
|
||||
|
||||
rerender(
|
||||
<RevealContext.Provider value={deck}>
|
||||
<Slide backgroundColor="#222">Content</Slide>
|
||||
</RevealContext.Provider>
|
||||
);
|
||||
|
||||
expect(deck.syncSlide).toHaveBeenCalledTimes(1);
|
||||
expect(deck.syncSlide).toHaveBeenCalledWith(container.querySelector('section'));
|
||||
});
|
||||
|
||||
it('calls syncSlide when shorthand Reveal slide props change after mount', () => {
|
||||
const deck = {
|
||||
syncSlide: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const { container, rerender } = render(
|
||||
<RevealContext.Provider value={deck}>
|
||||
<Slide autoSlide={1000}>Content</Slide>
|
||||
</RevealContext.Provider>
|
||||
);
|
||||
|
||||
expect(deck.syncSlide).not.toHaveBeenCalled();
|
||||
deck.syncSlide.mockClear();
|
||||
|
||||
rerender(
|
||||
<RevealContext.Provider value={deck}>
|
||||
<Slide autoSlide={2000}>Content</Slide>
|
||||
</RevealContext.Provider>
|
||||
);
|
||||
|
||||
expect(deck.syncSlide).toHaveBeenCalledTimes(1);
|
||||
expect(deck.syncSlide).toHaveBeenCalledWith(container.querySelector('section'));
|
||||
});
|
||||
|
||||
it('does not call syncSlide on first render even when data-* attributes are present', () => {
|
||||
const deck = {
|
||||
syncSlide: vi.fn(),
|
||||
} as any;
|
||||
|
||||
render(
|
||||
<RevealContext.Provider value={deck}>
|
||||
<Slide data-background="#111">Content</Slide>
|
||||
</RevealContext.Provider>
|
||||
);
|
||||
|
||||
expect(deck.syncSlide).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not call syncSlide for non-data prop updates', () => {
|
||||
const deck = {
|
||||
syncSlide: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const { rerender } = render(
|
||||
<RevealContext.Provider value={deck}>
|
||||
<Slide data-background-color="#111" className="a">
|
||||
Content
|
||||
</Slide>
|
||||
</RevealContext.Provider>
|
||||
);
|
||||
|
||||
deck.syncSlide.mockClear();
|
||||
|
||||
rerender(
|
||||
<RevealContext.Provider value={deck}>
|
||||
<Slide data-background-color="#111" className="b">
|
||||
Content
|
||||
</Slide>
|
||||
</RevealContext.Provider>
|
||||
);
|
||||
|
||||
expect(deck.syncSlide).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not call syncSlide on mount when no data-* attributes are present', () => {
|
||||
const deck = {
|
||||
syncSlide: vi.fn(),
|
||||
} as any;
|
||||
|
||||
render(
|
||||
<RevealContext.Provider value={deck}>
|
||||
<Slide className="plain">Content</Slide>
|
||||
</RevealContext.Provider>
|
||||
);
|
||||
|
||||
expect(deck.syncSlide).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
47
scripts/reveal.js/react/src/__tests__/Stack.test.tsx
Normal file
47
scripts/reveal.js/react/src/__tests__/Stack.test.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Stack } from '../Stack';
|
||||
import { Slide } from '../Slide';
|
||||
|
||||
describe('Stack', () => {
|
||||
it('renders as a <section> element', () => {
|
||||
const { container } = render(
|
||||
<Stack>
|
||||
<Slide>A</Slide>
|
||||
<Slide>B</Slide>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const sections = container.querySelectorAll('section');
|
||||
expect(sections).toHaveLength(3); // 1 outer (Stack) + 2 inner (Slides)
|
||||
});
|
||||
|
||||
it('creates nested section structure for vertical slides', () => {
|
||||
const { container } = render(
|
||||
<Stack>
|
||||
<Slide>First</Slide>
|
||||
<Slide>Second</Slide>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const outer = container.querySelector('section');
|
||||
expect(outer).toBeInTheDocument();
|
||||
|
||||
const inner = outer?.querySelectorAll(':scope > section');
|
||||
expect(inner).toHaveLength(2);
|
||||
expect(inner?.[0]).toHaveTextContent('First');
|
||||
expect(inner?.[1]).toHaveTextContent('Second');
|
||||
});
|
||||
|
||||
it('applies className and style to the outer section', () => {
|
||||
const { container } = render(
|
||||
<Stack className="my-stack" style={{ padding: '10px' }}>
|
||||
<Slide>Content</Slide>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const outer = container.querySelector('section');
|
||||
expect(outer).toHaveClass('my-stack');
|
||||
expect(outer).toHaveStyle({ padding: '10px' });
|
||||
});
|
||||
});
|
||||
1
scripts/reveal.js/react/src/__tests__/setup.ts
Normal file
1
scripts/reveal.js/react/src/__tests__/setup.ts
Normal file
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
4
scripts/reveal.js/react/src/context.ts
Normal file
4
scripts/reveal.js/react/src/context.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createContext } from 'react';
|
||||
import type { RevealApi } from 'reveal.js';
|
||||
|
||||
export const RevealContext = createContext<RevealApi | null>(null);
|
||||
15
scripts/reveal.js/react/src/index.ts
Normal file
15
scripts/reveal.js/react/src/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useContext } from 'react';
|
||||
import { RevealContext } from './context';
|
||||
|
||||
export { Deck } from './Deck';
|
||||
export { Slide } from './Slide';
|
||||
export { Stack } from './Stack';
|
||||
export { Fragment } from './Fragment';
|
||||
export { Code } from './Code';
|
||||
export { RevealContext } from './context';
|
||||
|
||||
export type { DeckProps, SlideProps, StackProps, FragmentProps, CodeProps } from './types';
|
||||
|
||||
export function useReveal() {
|
||||
return useContext(RevealContext);
|
||||
}
|
||||
127
scripts/reveal.js/react/src/types.ts
Normal file
127
scripts/reveal.js/react/src/types.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import type { CSSProperties, ReactNode, ElementType, Ref, ReactElement } from 'react';
|
||||
import type {
|
||||
FragmentAnimation,
|
||||
RevealApi,
|
||||
RevealConfig,
|
||||
RevealPlugin,
|
||||
RevealPluginFactory,
|
||||
TransitionSpeed,
|
||||
TransitionStyle,
|
||||
} from 'reveal.js';
|
||||
|
||||
type DeckConfig = RevealConfig;
|
||||
type DeckPlugin = RevealPlugin | RevealPluginFactory;
|
||||
type RevealEventHandler = Parameters<RevealApi['on']>[1];
|
||||
|
||||
export type DeckProps = {
|
||||
config?: Omit<DeckConfig, 'plugins'>;
|
||||
/** Registered during deck initialization only. Subsequent prop updates are ignored. */
|
||||
plugins?: DeckPlugin[];
|
||||
onReady?: (deck: RevealApi) => void;
|
||||
onSync?: RevealEventHandler;
|
||||
onSlideSync?: RevealEventHandler;
|
||||
onSlideChange?: RevealEventHandler;
|
||||
onSlideTransitionEnd?: RevealEventHandler;
|
||||
onFragmentShown?: RevealEventHandler;
|
||||
onFragmentHidden?: RevealEventHandler;
|
||||
onOverviewShown?: RevealEventHandler;
|
||||
onOverviewHidden?: RevealEventHandler;
|
||||
onPaused?: RevealEventHandler;
|
||||
onResumed?: RevealEventHandler;
|
||||
deckRef?: Ref<RevealApi | null>;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export type SlideDataAttributeValue = string | number | boolean | undefined;
|
||||
|
||||
export type SlideDataAttributes = {
|
||||
[key: `data-${string}`]: SlideDataAttributeValue;
|
||||
};
|
||||
|
||||
export type SlideBackgroundProps = {
|
||||
background?: string;
|
||||
backgroundImage?: string;
|
||||
backgroundVideo?: string;
|
||||
backgroundVideoLoop?: boolean;
|
||||
backgroundVideoMuted?: boolean;
|
||||
backgroundIframe?: string;
|
||||
backgroundColor?: string;
|
||||
backgroundGradient?: string;
|
||||
backgroundSize?: string;
|
||||
backgroundPosition?: string;
|
||||
backgroundRepeat?: string;
|
||||
backgroundOpacity?: number | string;
|
||||
backgroundTransition?: TransitionStyle;
|
||||
};
|
||||
|
||||
export type SlideVisibility = 'hidden' | 'uncounted';
|
||||
|
||||
export type SlideAutoAnimateProps = {
|
||||
visibility?: SlideVisibility;
|
||||
autoAnimate?: boolean;
|
||||
autoAnimateId?: string;
|
||||
autoAnimateRestart?: boolean;
|
||||
autoAnimateUnmatched?: boolean;
|
||||
autoAnimateEasing?: string;
|
||||
autoAnimateDuration?: number | string;
|
||||
autoAnimateDelay?: number | string;
|
||||
};
|
||||
|
||||
export type SlideRevealProps = {
|
||||
transition?: string;
|
||||
transitionSpeed?: TransitionSpeed;
|
||||
autoSlide?: number | string;
|
||||
notes?: string;
|
||||
backgroundInteractive?: boolean;
|
||||
preload?: boolean;
|
||||
};
|
||||
|
||||
export type SlideProps = React.HTMLAttributes<HTMLElement> &
|
||||
SlideDataAttributes &
|
||||
SlideBackgroundProps &
|
||||
SlideAutoAnimateProps &
|
||||
SlideRevealProps & {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export type StackProps = {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
type FragmentBaseProps = {
|
||||
animation?: FragmentAnimation;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
index?: number;
|
||||
};
|
||||
|
||||
type FragmentElementProps = FragmentBaseProps & {
|
||||
asChild?: false;
|
||||
as?: ElementType;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
type FragmentAsChildProps = FragmentBaseProps & {
|
||||
asChild: true;
|
||||
as?: never;
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
export type FragmentProps = FragmentElementProps | FragmentAsChildProps;
|
||||
|
||||
export type CodeProps = Omit<React.HTMLAttributes<HTMLPreElement>, 'children'> & {
|
||||
children?: string;
|
||||
code?: string;
|
||||
language?: string;
|
||||
trim?: boolean;
|
||||
lineNumbers?: boolean | string;
|
||||
startFrom?: number;
|
||||
noEscape?: boolean;
|
||||
codeClassName?: string;
|
||||
codeStyle?: CSSProperties;
|
||||
codeProps?: Omit<React.HTMLAttributes<HTMLElement>, 'children' | 'className' | 'style'>;
|
||||
};
|
||||
Reference in New Issue
Block a user