Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React Typescript - Controls Addon enum control not rendering consistently #79

Closed
chrislegault opened this issue Jul 30, 2021 · 8 comments

Comments

@chrislegault
Copy link

chrislegault commented Jul 30, 2021

Created a vite app and corresponding storybook using commands:

npm init @vitejs/app my-vite-app 
npx sb@next init --builder storybook-builder-vite

Firing up the storybook and looking at the Button -> Large story the control for the size prop renders as a text input. If you disable the vite builder in the main.js and use default webpack core it will render the size prop as radio buttons which is the documented control in Controls Docs

The control for the primary boolean property and the onClick are also missing. Manually providing the control config via argTypes will render the size control correctly.

Vite:
Screen Shot 2021-07-29 at 10 04 44 PM

Webpack:
Screen Shot 2021-07-29 at 10 05 20 PM

@IanVS
Copy link
Member

IanVS commented Jul 30, 2021

Thanks for the report. I believe the control types are extracted as part of the docgen generation, which doesn't work in this builder as it's coupled to the webpack loader used in storybook. There's an effort underway to decouple them, but as far as I know it's not ready yet. #46 (comment).

@IanVS
Copy link
Member

IanVS commented Jan 21, 2022

Note: there's an experiment underway in #190, to add support for react-docgen. If you would like to try it out in your own project, you can replace the react vite plugin in your viteFinal to include @ianvs/babel-plugin-react-docgen, like so:

const reactPlugin = require('@vitejs/plugin-react');

.
.
.

  async viteFinal(config) {
    const plugins = [
      ...config.plugins.filter((plugin) => {
        return !(Array.isArray(plugin) && plugin[0].name === 'vite:react-babel');
      }),
      reactPlugin({
        exclude: [/\.stories\.(t|j)sx?$/, /node_modules/],
        babel: { plugins: [
          'babel-plugin-add-react-displayname',
          require.resolve('@ianvs/babel-plugin-react-docgen'),
        ]},
      }),

There are also some good notes in #2 (comment), if you'd like a more flexible approach.

Please provide feedback on whether the approach above meets your needs. We're also considering the use of react-docgen-typescript, but there are some tradeoffs, and it's currently implemented in storybook as a webpack loader, so there'd be some work needed to adapt it for vite (for which there is a commit in a branch: joshwooding@675506f).

IanVS pushed a commit that referenced this issue Feb 3, 2022
Related to #103 #2 #79

This PR adds doc-gen via react-docgen-typescript.

| | Before | After|
| --- | --- | --- |
| Sidebar | ![localhost_52700__path=_story_example-introduction--page](https://user-images.githubusercontent.com/12938082/152376764-b5fa8048-8e7a-47fa-b15f-c10d1b0a3feb.png) | ![localhost_55358__path=_story_example-introduction--page](https://user-images.githubusercontent.com/12938082/152375596-eed09e52-03bf-4e6d-9ce0-b8ef28bfdbee.png)
| Docs | ![image](https://user-images.githubusercontent.com/12938082/152377428-ffaa0b12-d453-4540-a0d3-b66d27371a1b.png) | ![image](https://user-images.githubusercontent.com/12938082/152378055-7d65504e-621f-48d6-a0d4-879171086832.png)

I haven't noticed any significant impact to speed. 🎉 

 It's hooked up to the storybook config so it should act as a drop-in for the webpack functionality for typescript types.
@IanVS
Copy link
Member

IanVS commented Feb 3, 2022

@chrislegault We've just released https://github.com/eirslett/storybook-builder-vite/releases/tag/v0.1.15, which I think will address this issue. Could you please check and close this issue if your problem is resolved?

@mupinnn
Copy link

mupinnn commented Apr 21, 2022

@IanVS I'm using "@storybook/builder-vite": "^0.1.29" and its have strange behavior same as this issue. I have two stories file, the one correctly generate the docs from the typescript interface and the one only generate it from the args provided on the story file and have wrong controls.

This is the one that generate the docs from args only
image

import { ComponentMeta, ComponentStory } from "@storybook/react";
import { FiTrash } from "react-icons/fi";
import Button from "./BaseButton";

export default {
  title: "Button",
  component: Button,
} as ComponentMeta<typeof Button>;

const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;

export const Base = Template.bind({});
Base.args = {
  children: "Button",
  color: "brand",
  size: "md",
  variant: "primary",
};

This is the output I expected. You can see that the props control rendered correctly as radio button when using an enum and not rendered as textbox.
image

import { ComponentMeta, ComponentStory } from "@storybook/react";
import { FiMoon } from "react-icons/fi";
import IconButton from "./IconButton";

export default {
  title: "Button/Icon Only",
  component: IconButton,
} as ComponentMeta<typeof IconButton>;

const Template: ComponentStory<typeof IconButton> = (args) => (
  <IconButton {...args} />
);

export const IconOnly = Template.bind({});
IconOnly.args = {
  color: "brand",
  icon: FiMoon,
  size: "md",
  variant: "primary",
};

The ButtonProps and IconButtonProps should generate 10 controls in the docs, but only IconButtonProps that generate it correctly; though they have and using the same interface.

types.ts

export type ButtonColors = "brand" | "gray" | "green" | "red" | "yellow";
export type ButtonVariants = "primary" | "secondary" | "ghost";
export type ButtonSizes = "sm" | "md" | "lg";

interface ButtonBaseProps {
  /** Choose between normal or full-width button */
  block?: boolean;

  /** Button colors */
  color?: ButtonColors;

  /** React element children */
  children?: React.ReactNode;

  /** Disabled state */
  disabled?: boolean;

  /** Set icon to be rendered in the button */
  icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>;

  /** Position of the icon to be rendered */
  iconPosition?: "left" | "right";

  /** Show loading indicator if set to `true` */
  isLoading?: boolean;

  /** Set loading text */
  loadingText?: string;

  /** Button sizes */
  size?: ButtonSizes;

  /** Button variants */
  variant?: ButtonVariants;
}

export type ButtonProps = ButtonBaseProps &
  React.ComponentPropsWithRef<"button">;

// For the sake of simplicity and example, I make it same as `ButtonProps` to see which one
// can generated the docs correctly.
export type IconButtonProps = ButtonBaseProps &
  React.ComponentPropsWithRef<"button">;

Might it useful to include the component file:
BaseButton.tsx

import React from "react";
import { FiLoader } from "react-icons/fi";
import twClsx from "~/utils/tw-clsx";
import { ButtonProps } from "../types";
import {
  buttonSizing,
  buttonColorVariant,
  createButtonIcon,
} from "../Button.helpers";

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      block,
      color = "brand",
      children,
      className,
      disabled,
      isLoading,
      icon,
      iconPosition = "left",
      loadingText = "Loading . . .",
      size = "md",
      variant = "primary",
      ...rest
    },
    ref
  ) => {
    return (
      <button
        className={twClsx(
          "inline-flex justify-center items-center gap-2 font-semibold rounded-md transition ease-in-out duration-200 focus:outline-none focus:ring",
          buttonColorVariant(color, variant),
          buttonSizing(size),
          [isLoading && "disabled:cursor-progress"],
          [block && "w-full"],
          [iconPosition === "left" ? "flex-row" : "flex-row-reverse"],
          className
        )}
        disabled={isLoading ?? disabled}
        ref={ref}
        {...rest}
      >
        {isLoading ? (
          <>
            <FiLoader className="animate-spin motion-reduce:hidden" />
            {loadingText}
          </>
        ) : (
          <>
            {createButtonIcon(icon)}
            {children}
          </>
        )}
      </button>
    );
  }
);

export default Button;

IconButton.tsx

import React from "react";
import { FiLoader } from "react-icons/fi";
import twClsx from "~/utils/tw-clsx";
import { IconButtonProps } from "../types";
import {
  buttonIconSizing,
  buttonColorVariant,
  createButtonIcon,
} from "../Button.helpers";

const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
  (
    {
      color = "brand",
      className,
      disabled,
      isLoading,
      icon,
      size = "md",
      variant = "primary",
      ...rest
    },
    ref
  ) => {
    return (
      <button
        className={twClsx(
          "inline-flex justify-center items-center font-semibold rounded-md transition ease-in-out duration-200 focus:outline-none focus:ring",
          buttonColorVariant(color, variant),
          buttonIconSizing(size),
          [isLoading && "disabled:cursor-progress"],
          className
        )}
        disabled={isLoading ?? disabled}
        ref={ref}
        {...rest}
      >
        {isLoading ? (
          <FiLoader className="animate-spin motion-reduce:animate-none" />
        ) : (
          createButtonIcon(icon)
        )}
      </button>
    );
  }
);

export default IconButton;

And here my storybook config file:
main.js

const path = require("path");

module.exports = {
  stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
  ],
  framework: "@storybook/react",
  core: {
    builder: "@storybook/builder-vite",
  },
  async viteFinal(config) {
    return {
      ...config,

      /**
       * Import alias support
       * @see https://github.com/storybookjs/builder-vite/issues/85#issuecomment-900831050
       */
      resolve: {
        alias: [{ find: "~", replacement: path.resolve(__dirname, "../src") }],
      },
    };
  },
};

preview.js

import "../src/styles/global.css";

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    expanded: true,
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
};

Thanks!

@IanVS
Copy link
Member

IanVS commented Apr 21, 2022

Thanks for the report. The best way for us to look into this is with a minimal reproduction. Do you think you could create a new vite storybook project that reproduces this issue? My guess is that the type definition for the icon prop is crashing typescript-react-docgen, but it would help us a ton to have a repo to poke around in. Thanks!

@mupinnn
Copy link

mupinnn commented Apr 21, 2022

Here I create a minimal reproduction for you to tinker around.

For additional behavior notes, the first time I create the BaseButton component it works like charm! The props control rendered correctly based on the type definition but when I create another component (IconButton), the props control on the prior component generated from the args not from the type definition and the new component has the right props control based on the type definition. I tried deleting the IconButton and doesn't change anything.

Thank you!

@IanVS
Copy link
Member

IanVS commented Apr 22, 2022

Thanks for reporting this, and for the reproduction, @mupinnn. @joshwooding, you'll be interested in this one. I opened joshwooding/vite-plugin-react-docgen-typescript#2, since I think it's an issue in the react-docgen-typescript plugin.

@mupinnn
Copy link

mupinnn commented Apr 22, 2022

Hey, I tried it out to change the component name to be the same as the filename and it solved my issue right now. Probably the best workaround until joshwooding/vite-plugin-react-docgen-typescript#2 got an update.

Thanks again @IanVS , I really appreciate it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants