mirror of
https://github.com/mastodon/mastodon.git
synced 2025-10-05 16:42:47 +00:00
adds element handler prop with new props arg
This commit is contained in:
parent
f046106612
commit
5a9d9d1132
|
@ -4,6 +4,7 @@ import classNames from 'classnames';
|
|||
|
||||
import type { CustomEmojiMapArg } from '@/mastodon/features/emoji/types';
|
||||
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
||||
import type { OnElementHandler } from '@/mastodon/utils/html';
|
||||
import { htmlStringToComponents } from '@/mastodon/utils/html';
|
||||
import { polymorphicForwardRef } from '@/types/polymorphic';
|
||||
|
||||
|
@ -14,6 +15,7 @@ interface EmojiHTMLProps {
|
|||
htmlString: string;
|
||||
extraEmojis?: CustomEmojiMapArg;
|
||||
className?: string;
|
||||
onElement?: OnElementHandler;
|
||||
}
|
||||
|
||||
export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
||||
|
@ -23,13 +25,15 @@ export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
|||
htmlString,
|
||||
as: asProp = 'div', // Rename for syntax highlighting
|
||||
className = '',
|
||||
onElement,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const contents = useMemo(
|
||||
() => htmlStringToComponents(htmlString, { onText: textToEmojis }),
|
||||
[htmlString],
|
||||
() =>
|
||||
htmlStringToComponents(htmlString, { onText: textToEmojis, onElement }),
|
||||
[htmlString, onElement],
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -46,6 +50,7 @@ export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
|||
);
|
||||
},
|
||||
);
|
||||
ModernEmojiHTML.displayName = 'ModernEmojiHTML';
|
||||
|
||||
export const LegacyEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
||||
(props, ref) => {
|
||||
|
@ -54,6 +59,7 @@ export const LegacyEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
|||
htmlString,
|
||||
extraEmojis,
|
||||
className,
|
||||
onElement,
|
||||
...rest
|
||||
} = props;
|
||||
const Wrapper = asElement ?? 'div';
|
||||
|
@ -67,6 +73,7 @@ export const LegacyEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
|||
);
|
||||
},
|
||||
);
|
||||
LegacyEmojiHTML.displayName = 'LegacyEmojiHTML';
|
||||
|
||||
export const EmojiHTML = isModernEmojiEnabled()
|
||||
? ModernEmojiHTML
|
||||
|
|
|
@ -53,13 +53,19 @@ describe('html', () => {
|
|||
|
||||
it('calls onElement callback', () => {
|
||||
const input = '<p>lorem ipsum</p>';
|
||||
const onElement = vi.fn(
|
||||
(element: HTMLElement, children: React.ReactNode[]) =>
|
||||
React.createElement(element.tagName.toLowerCase(), {}, ...children),
|
||||
const onElement = vi.fn<html.OnElementHandler>(
|
||||
(element, props, children) =>
|
||||
React.createElement(
|
||||
element.tagName.toLowerCase(),
|
||||
props,
|
||||
...children,
|
||||
),
|
||||
);
|
||||
html.htmlStringToComponents(input, { onElement });
|
||||
expect(onElement).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.objectContaining({ tagName: 'P' }),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
expect.objectContaining({ key: expect.any(String) }),
|
||||
expect.arrayContaining(['lorem ipsum']),
|
||||
{},
|
||||
);
|
||||
|
@ -71,6 +77,8 @@ describe('html', () => {
|
|||
const output = html.htmlStringToComponents(input, { onElement });
|
||||
expect(onElement).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.objectContaining({ tagName: 'P' }),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
expect.objectContaining({ key: expect.any(String) }),
|
||||
expect.arrayContaining(['lorem ipsum']),
|
||||
{},
|
||||
);
|
||||
|
|
|
@ -32,14 +32,21 @@ interface QueueItem {
|
|||
depth: number;
|
||||
}
|
||||
|
||||
export interface HTMLToStringOptions<Arg extends Record<string, unknown>> {
|
||||
export type OnElementHandler<
|
||||
Arg extends Record<string, unknown> = Record<string, unknown>,
|
||||
> = (
|
||||
element: HTMLElement,
|
||||
props: Record<string, unknown>,
|
||||
children: React.ReactNode[],
|
||||
extra: Arg,
|
||||
) => React.ReactNode;
|
||||
|
||||
export interface HTMLToStringOptions<
|
||||
Arg extends Record<string, unknown> = Record<string, unknown>,
|
||||
> {
|
||||
maxDepth?: number;
|
||||
onText?: (text: string, extra: Arg) => React.ReactNode;
|
||||
onElement?: (
|
||||
element: HTMLElement,
|
||||
children: React.ReactNode[],
|
||||
extra: Arg,
|
||||
) => React.ReactNode;
|
||||
onElement?: OnElementHandler<Arg>;
|
||||
onAttribute?: (
|
||||
name: string,
|
||||
value: string,
|
||||
|
@ -125,9 +132,57 @@ export function htmlStringToComponents<Arg extends Record<string, unknown>>(
|
|||
const children: React.ReactNode[] = [];
|
||||
let element: React.ReactNode = undefined;
|
||||
|
||||
// Generate props from attributes.
|
||||
const key = `html-${uniqueIdCounter++}`; // Get the current key and then increment it.
|
||||
const props: Record<string, unknown> = { key };
|
||||
for (const attr of node.attributes) {
|
||||
let name = attr.name.toLowerCase();
|
||||
|
||||
// Custom attribute handler.
|
||||
if (onAttribute) {
|
||||
const result = onAttribute(
|
||||
name,
|
||||
attr.value,
|
||||
node.tagName.toLowerCase(),
|
||||
extraArgs,
|
||||
);
|
||||
if (result) {
|
||||
const [cbName, value] = result;
|
||||
props[cbName] = value;
|
||||
}
|
||||
} else {
|
||||
// Check global attributes first, then tag-specific ones.
|
||||
const globalAttr = globalAttributes[name];
|
||||
const tagAttr = tagInfo.attributes?.[name];
|
||||
|
||||
// Exit if neither global nor tag-specific attribute is allowed.
|
||||
if (!globalAttr && !tagAttr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rename if needed.
|
||||
if (typeof tagAttr === 'string') {
|
||||
name = tagAttr;
|
||||
} else if (typeof globalAttr === 'string') {
|
||||
name = globalAttr;
|
||||
}
|
||||
|
||||
let value: string | boolean | number = attr.value;
|
||||
|
||||
// Handle boolean attributes.
|
||||
if (value === 'true') {
|
||||
value = true;
|
||||
} else if (value === 'false') {
|
||||
value = false;
|
||||
}
|
||||
|
||||
props[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// If onElement is provided, use it to create the element.
|
||||
if (onElement) {
|
||||
const component = onElement(node, children, extraArgs);
|
||||
const component = onElement(node, props, children, extraArgs);
|
||||
|
||||
// Check for undefined to allow returning null.
|
||||
if (component !== undefined) {
|
||||
|
@ -137,53 +192,6 @@ export function htmlStringToComponents<Arg extends Record<string, unknown>>(
|
|||
|
||||
// If the element wasn't created, use the default conversion.
|
||||
if (element === undefined) {
|
||||
const props: Record<string, unknown> = {};
|
||||
props.key = `html-${uniqueIdCounter++}`; // Get the current key and then increment it.
|
||||
for (const attr of node.attributes) {
|
||||
let name = attr.name.toLowerCase();
|
||||
|
||||
// Custom attribute handler.
|
||||
if (onAttribute) {
|
||||
const result = onAttribute(
|
||||
name,
|
||||
attr.value,
|
||||
node.tagName.toLowerCase(),
|
||||
extraArgs,
|
||||
);
|
||||
if (result) {
|
||||
const [cbName, value] = result;
|
||||
props[cbName] = value;
|
||||
}
|
||||
} else {
|
||||
// Check global attributes first, then tag-specific ones.
|
||||
const globalAttr = globalAttributes[name];
|
||||
const tagAttr = tagInfo.attributes?.[name];
|
||||
|
||||
// Exit if neither global nor tag-specific attribute is allowed.
|
||||
if (!globalAttr && !tagAttr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rename if needed.
|
||||
if (typeof tagAttr === 'string') {
|
||||
name = tagAttr;
|
||||
} else if (typeof globalAttr === 'string') {
|
||||
name = globalAttr;
|
||||
}
|
||||
|
||||
let value: string | boolean | number = attr.value;
|
||||
|
||||
// Handle boolean attributes.
|
||||
if (value === 'true') {
|
||||
value = true;
|
||||
} else if (value === 'false') {
|
||||
value = false;
|
||||
}
|
||||
|
||||
props[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
element = React.createElement(
|
||||
tagName,
|
||||
props,
|
||||
|
|
Loading…
Reference in New Issue
Block a user