From 13e872c838feddf7e2e468977a150efa24b797c5 Mon Sep 17 00:00:00 2001
From: Yann Braga
Date: Fri, 8 Mar 2024 11:48:55 +0100
Subject: [PATCH 1/2] React: Support all React component types in JSX Decorator
---
.../react/src/docs/jsxDecorator.test.tsx | 105 ++++++++++++------
.../renderers/react/src/docs/jsxDecorator.tsx | 43 ++++++-
2 files changed, 109 insertions(+), 39 deletions(-)
diff --git a/code/renderers/react/src/docs/jsxDecorator.test.tsx b/code/renderers/react/src/docs/jsxDecorator.test.tsx
index bfb20fdd5f0d..6ed0f0eda179 100644
--- a/code/renderers/react/src/docs/jsxDecorator.test.tsx
+++ b/code/renderers/react/src/docs/jsxDecorator.test.tsx
@@ -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(hello
, {})).toMatchInlineSnapshot(`
@@ -139,53 +152,71 @@ describe('renderJsx', () => {
});
it('Profiler', () => {
- function ProfilerComponent({ children }: any) {
- return (
+ expect(
+ renderJsx(
{}}>
- {children}
-
- );
- }
-
- expect(renderJsx(createElement(ProfilerComponent, {}, 'I am Profiler'), {}))
- .toMatchInlineSnapshot(`
-
- I am Profiler
-
+ I am in a Profiler
+ ,
+ {}
+ )
+ ).toMatchInlineSnapshot(`
+ {}}
+ >
+
+ I am in a Profiler
+
+
`);
});
it('StrictMode', () => {
- function StrictModeComponent({ children }: any) {
- return (
-
- {children}
-
- );
+ expect(renderJsx(I am StrictMode, {})).toMatchInlineSnapshot(`
+
+ I am StrictMode
+
+ `);
+ });
+
+ it('displayName coming from docgenInfo', () => {
+ function BasicComponent({ label }: any) {
+ return ;
}
+ BasicComponent.__docgenInfo = {
+ description: 'Some description',
+ methods: [],
+ displayName: 'Button',
+ props: {},
+ };
- expect(renderJsx(createElement(StrictModeComponent, {}, 'I am StrictMode'), {}))
- .toMatchInlineSnapshot(`
-
- I am StrictMode
-
- `);
+ expect(
+ renderJsx(
+ createElement(
+ BasicComponent,
+ {
+ label: Abcd
,
+ },
+ undefined
+ )
+ )
+ ).toMatchInlineSnapshot(`
} />`);
});
it('Suspense', () => {
- function SuspenseComponent({ children }: any) {
- return (
+ expect(
+ renderJsx(
- {children}
-
- );
- }
-
- expect(renderJsx(createElement(SuspenseComponent, {}, 'I am Suspense'), {}))
- .toMatchInlineSnapshot(`
-
- I am Suspense
-
+ I am in Suspense
+ ,
+ {}
+ )
+ ).toMatchInlineSnapshot(`
+
+
+ I am in Suspense
+
+
`);
});
diff --git a/code/renderers/react/src/docs/jsxDecorator.tsx b/code/renderers/react/src/docs/jsxDecorator.tsx
index 3395a3860557..26cdc238995b 100644
--- a/code/renderers/react/src/docs/jsxDecorator.tsx
+++ b/code/renderers/react/src/docs/jsxDecorator.tsx
@@ -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,
};
}
From e887915a5ee79fde5c46e4782630252e14400b21 Mon Sep 17 00:00:00 2001
From: Norbert de Langen
Date: Fri, 8 Mar 2024 12:12:21 +0100
Subject: [PATCH 2/2] fixes
---
code/renderers/react/src/docs/jsxDecorator.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/code/renderers/react/src/docs/jsxDecorator.tsx b/code/renderers/react/src/docs/jsxDecorator.tsx
index 26cdc238995b..5b1391246c9d 100644
--- a/code/renderers/react/src/docs/jsxDecorator.tsx
+++ b/code/renderers/react/src/docs/jsxDecorator.tsx
@@ -29,7 +29,7 @@ const toPascalCase = (str: string) => str.charAt(0).toUpperCase() + str.slice(1)
* @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 symbolDescription: string = elementType.toString().replace(/^Symbol\((.*)\)$/, '$1');
const reactComponentName = symbolDescription
.split('.')
@@ -74,7 +74,7 @@ type JSXOptions = Options & {
};
/** Apply the users parameters and render the jsx for a story */
-export const renderJsx = (code: React.ReactElement, options: JSXOptions) => {
+export const renderJsx = (code: React.ReactElement, options?: JSXOptions) => {
if (typeof code === 'undefined') {
logger.warn('Too many skip or undefined component');
return null;