Material UIで新しいバリエーションのボタンを作る

# はじめに
[Material UI](https://mui.com/)はReactのコンポーネントライブラリで、Material Designに沿ったコンポーネントを簡単に利用することができます(現在の最新バージョンではMaterial Design 2に対応しています)。この記事ではMaterial UIをカスタマイズして定義されていないバリエーションのボタンを作成します。

# テーマの設定
Material UIのテーマの設定は`ThemeProvider`を用いて行うことができます。下の例では`App`配下にそのテーマが適用されます。
```tsx
<ThemeProvider theme={theme}>
  <App />
</ThemeProvider>
```
`theme`は`createTheme`関数によって作成されます。`createTheme`の引数に渡すオブジェクトでカスタマイズしたい内容を記述します。Primaryカラーなど色についての設定や、文字の大きさなどTypographyの設定などを行うことができます。今回はMaterial UIが提供しているコンポーネントごとにカスタマイズする設定を利用します。
例えばボタンの横幅を最大にする引数を常にオンにしたいときは以下のようにします。
```ts
const theme = createTheme({
  components: {
    MuiButton: {
      defaultProps: {
        fullWidth: true,
      },
    },
  },
});
```
ボタンのfontSizeを20pxにしたいときは以下のようにします。
```ts
const theme = createTheme({
  components: {
    MuiButton: {
      styleOverrides: {
        root: {
          fontSize: '20px',
        },
      },
    },
  },
});
```

# 目標
Material UIのボタンは引数のvariantを変更することで3種類のボタンを作成することができます。

https://codesandbox.io/embed/button-variant-moeqc0?fontsize=14&hidenavigation=1&module=%2Fdemo.tsx&theme=dark

この記事ではこの三つの他にBootstrapライクなボタンをvariantにbootstarpを指定することで表示できることを目標とします。

# BootStrapのボタンを観察する
こちらが今回実装するBootstrapのボタンです。先ほど紹介したMaterial UIのContainedボタンと似ていますが挙動やpaddingも違いますし、微妙な変化ですが色もフォントも異なります。

https://codesandbox.io/embed/bootstrap-button-z8w2kl?fontsize=14&hidenavigation=1&theme=dark


# 実装
ボタンの`variant`にbootstrapも指定可能にして、bootstrapを指定した場合にはbootstrapのstyleを当てるようにします。
これらは全て`createTheme`によって作成できます。`variants`は新しいバリエーションを作成するためのキーです。バリューには配列を取り、設定を一つずつ記述します。設定は`props`と`style`の二つをとります。`style`には反映したいcssを記述します。またこの`style`は`props`に指定した`props`がButtonに設定された時に反映されます。今回は新しく`variant`にbootstrapするので`{ variant: "bootstrap" }`にしています。もし、`color`にprimaryが指定されているときだけ`style`を反映したいときは`{ variant: "bootstrap", color: "primary" }`にすることでできます。
```ts
const theme = createTheme({
  components: {
    MuiButton: {
      variants: [
        {
          props: { variant: "bootstrap" },
          style: {
            color: "white",
            boxShadow: "none",
            textTransform: "none",
            fontSize: 16,
            padding: "6px 12px",
            border: "1px solid",
            lineHeight: 1.5,
            backgroundColor: "#0063cc",
            borderColor: "#0063cc",
            fontFamily: [
              "-apple-system",
              "BlinkMacSystemFont",
              '"Segoe UI"',
              "Roboto",
              '"Helvetica Neue"',
              "Arial",
              "sans-serif",
              '"Apple Color Emoji"',
              '"Segoe UI Emoji"',
              '"Segoe UI Symbol"'
            ].join(","),
            "&:hover": {
              backgroundColor: "#0069d9",
              borderColor: "#0062cc",
              boxShadow: "none"
            },
            "&:active": {
              boxShadow: "none",
              backgroundColor: "#0062cc",
              borderColor: "#005cbf"
            },
            "&:focus": {
              boxShadow: "0 0 0 0.2rem rgba(0,123,255,.5)"
            }
          }
        }
      ]
    }
  }
});
```

typescriptを利用の方はここで型エラーが出るはずです。なぜならMaterial UIのButtonの[実装](https://github.com/mui/material-ui/blob/master/packages/mui-material/src/Button/Button.d.ts#L85)を見ると、Buttonの型が以下の様になっているからです。
```ts
export interface ButtonPropsVariantOverrides {}
{
  variant?: OverridableStringUnion<
    'text' | 'outlined' | 'contained',
    ButtonPropsVariantOverrides
  >;
}
```
`ButtonPropsVariantOverrides`は空ですから`'text'`と`'outlined'`、`'contained'`しかとれないようになっています。これでは`variant`に`'bootstrap'`を与えても型が解決されずエラーになってしまいます。Material UIでは拡張されることに備えて`ButtonPropsVariantOverrides`を用意しています。これを下のようにthemeを定義するタイミングで拡張してあげれば型のエラーが出なくなるようにできます。
```ts
declare module "@mui/material/Button" {
  interface ButtonPropsVariantOverrides {
    bootstrap: true;
  }
}

```

これらの設定を全て完了させると、以下のようにMaterial UIでBootstrapのボタンを作ることができます。クリックすると波紋が広がる機能はBootstrapにはないので`disableRipple`をpropsに与えています。

https://codesandbox.io/embed/button-variant-bootstrap-y5890o?fontsize=14&hidenavigation=1&module=%2Ftheme.ts&theme=dark

# さいごに
アプリケーションの開発では、Material UIのコンポーネントに実装されているバリエーションでは足りないケースが出てくる時があります。そうなった時にButtonコンポーネントをアプリケーション側で作成し直すことや特別なケースだと毎回実装するような運用も考えられます。今回紹介した拡張方法ではどのコンポーネントを使って良いか迷うことや、毎回実装することによる高い工数やミスを避けることができるので利用してみてはいかがでしょうか。