mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-09 00:19:13 +08:00
React: Support all React component types in JSX Decorator
This commit is contained in:
parent
fc222b9b97
commit
13e872c838
@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import React, { StrictMode, createElement, Profiler } from 'react';
|
||||
import type { Mock } from 'vitest';
|
||||
@ -5,7 +6,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import PropTypes from 'prop-types';
|
||||
import { addons, useEffect } from '@storybook/preview-api';
|
||||
import { SNIPPET_RENDERED } from '@storybook/docs-tools';
|
||||
import { renderJsx, jsxDecorator } from './jsxDecorator';
|
||||
import { renderJsx, jsxDecorator, getReactSymbolName } from './jsxDecorator';
|
||||
|
||||
vi.mock('@storybook/preview-api');
|
||||
const mockedAddons = vi.mocked(addons);
|
||||
@ -16,6 +17,18 @@ expect.addSnapshotSerializer({
|
||||
test: (val) => typeof val === 'string',
|
||||
});
|
||||
|
||||
describe('converts React Symbol to displayName string', () => {
|
||||
const symbolCases = [
|
||||
['react.suspense', 'React.Suspense'],
|
||||
['react.strict_mode', 'React.StrictMode'],
|
||||
['react.server_context.defaultValue', 'React.ServerContext.DefaultValue'],
|
||||
];
|
||||
|
||||
it.each(symbolCases)('"%s" to "%s"', (symbol, expectedValue) => {
|
||||
expect(getReactSymbolName(Symbol(symbol))).toEqual(expectedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderJsx', () => {
|
||||
it('basic', () => {
|
||||
expect(renderJsx(<div>hello</div>, {})).toMatchInlineSnapshot(`
|
||||
@ -139,53 +152,71 @@ describe('renderJsx', () => {
|
||||
});
|
||||
|
||||
it('Profiler', () => {
|
||||
function ProfilerComponent({ children }: any) {
|
||||
return (
|
||||
expect(
|
||||
renderJsx(
|
||||
<Profiler id="profiler-test" onRender={() => {}}>
|
||||
<div>{children}</div>
|
||||
</Profiler>
|
||||
);
|
||||
}
|
||||
|
||||
expect(renderJsx(createElement(ProfilerComponent, {}, 'I am Profiler'), {}))
|
||||
.toMatchInlineSnapshot(`
|
||||
<ProfilerComponent>
|
||||
I am Profiler
|
||||
</ProfilerComponent>
|
||||
<div>I am in a Profiler</div>
|
||||
</Profiler>,
|
||||
{}
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
<React.Profiler
|
||||
id="profiler-test"
|
||||
onRender={() => {}}
|
||||
>
|
||||
<div>
|
||||
I am in a Profiler
|
||||
</div>
|
||||
</React.Profiler>
|
||||
`);
|
||||
});
|
||||
|
||||
it('StrictMode', () => {
|
||||
function StrictModeComponent({ children }: any) {
|
||||
return (
|
||||
<StrictMode>
|
||||
<div>{children}</div>
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
expect(renderJsx(<StrictMode>I am StrictMode</StrictMode>, {})).toMatchInlineSnapshot(`
|
||||
<React.StrictMode>
|
||||
I am StrictMode
|
||||
</React.StrictMode>
|
||||
`);
|
||||
});
|
||||
|
||||
expect(renderJsx(createElement(StrictModeComponent, {}, 'I am StrictMode'), {}))
|
||||
.toMatchInlineSnapshot(`
|
||||
<StrictModeComponent>
|
||||
I am StrictMode
|
||||
</StrictModeComponent>
|
||||
`);
|
||||
it('displayName coming from docgenInfo', () => {
|
||||
function BasicComponent({ label }: any) {
|
||||
return <button>{label}</button>;
|
||||
}
|
||||
BasicComponent.__docgenInfo = {
|
||||
description: 'Some description',
|
||||
methods: [],
|
||||
displayName: 'Button',
|
||||
props: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
renderJsx(
|
||||
createElement(
|
||||
BasicComponent,
|
||||
{
|
||||
label: <p>Abcd</p>,
|
||||
},
|
||||
undefined
|
||||
)
|
||||
)
|
||||
).toMatchInlineSnapshot(`<Button label={<p>Abcd</p>} />`);
|
||||
});
|
||||
|
||||
it('Suspense', () => {
|
||||
function SuspenseComponent({ children }: any) {
|
||||
return (
|
||||
expect(
|
||||
renderJsx(
|
||||
<React.Suspense fallback={null}>
|
||||
<div>{children}</div>
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
expect(renderJsx(createElement(SuspenseComponent, {}, 'I am Suspense'), {}))
|
||||
.toMatchInlineSnapshot(`
|
||||
<SuspenseComponent>
|
||||
I am Suspense
|
||||
</SuspenseComponent>
|
||||
<div>I am in Suspense</div>
|
||||
</React.Suspense>,
|
||||
{}
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
<React.Suspense fallback={null}>
|
||||
<div>
|
||||
I am in Suspense
|
||||
</div>
|
||||
</React.Suspense>
|
||||
`);
|
||||
});
|
||||
|
||||
|
@ -8,9 +8,39 @@ import { addons, useEffect } from '@storybook/preview-api';
|
||||
import type { StoryContext, ArgsStoryFn, PartialStoryFn } from '@storybook/types';
|
||||
import { SourceType, SNIPPET_RENDERED, getDocgenSection } from '@storybook/docs-tools';
|
||||
import { logger } from '@storybook/client-logger';
|
||||
import { isMemo, isForwardRef } from './lib';
|
||||
|
||||
import type { ReactRenderer } from '../types';
|
||||
|
||||
const toPascalCase = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
|
||||
|
||||
/**
|
||||
* Converts a React symbol to a React-like displayName
|
||||
*
|
||||
* Symbols come from here
|
||||
* https://github.com/facebook/react/blob/338dddc089d5865761219f02b5175db85c54c489/packages/react-devtools-shared/src/backend/ReactSymbols.js
|
||||
*
|
||||
* E.g.
|
||||
* Symbol(react.suspense) -> React.Suspense
|
||||
* Symbol(react.strict_mode) -> React.StrictMode
|
||||
* Symbol(react.server_context.defaultValue) -> React.ServerContext.DefaultValue
|
||||
*
|
||||
* @param {Symbol} elementType - The symbol to convert
|
||||
* @returns {string | null} A displayName for the Symbol in case elementType is a Symbol; otherwise, null.
|
||||
*/
|
||||
export const getReactSymbolName = (elementType: any): string => {
|
||||
const symbolDescription = elementType.toString().replace(/^Symbol\((.*)\)$/, '$1');
|
||||
|
||||
const reactComponentName = symbolDescription
|
||||
.split('.')
|
||||
.map((segment) => {
|
||||
// Split segment by underscore to handle cases like 'strict_mode' separately, and PascalCase them
|
||||
return segment.split('_').map(toPascalCase).join('');
|
||||
})
|
||||
.join('.');
|
||||
return reactComponentName;
|
||||
};
|
||||
|
||||
// Recursively remove "_owner" property from elements to avoid crash on docs page when passing components as an array prop (#17482)
|
||||
// Note: It may be better to use this function only in development environment.
|
||||
function simplifyNodeForStringify(node: ReactNode): ReactNode {
|
||||
@ -91,10 +121,19 @@ export const renderJsx = (code: React.ReactElement, options: JSXOptions) => {
|
||||
*
|
||||
* Cannot read properties of undefined (reading '__docgenInfo').
|
||||
*/
|
||||
} else if (renderedJSX?.type && getDocgenSection(renderedJSX.type, 'displayName')) {
|
||||
} else {
|
||||
displayNameDefaults = {
|
||||
// To get exotic component names resolving properly
|
||||
displayName: (el: any): string => getDocgenSection(el.type, 'displayName'),
|
||||
displayName: (el: any): string =>
|
||||
el.type.displayName || typeof el.type === 'symbol'
|
||||
? getReactSymbolName(el.type)
|
||||
: null ||
|
||||
getDocgenSection(el.type, 'displayName') ||
|
||||
(el.type.name !== '_default' ? el.type.name : null) ||
|
||||
(typeof el.type === 'function' ? 'No Display Name' : null) ||
|
||||
(isForwardRef(el.type) ? el.type.render.name : null) ||
|
||||
(isMemo(el.type) ? el.type.type.name : null) ||
|
||||
el.type,
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user