type Props = Partial<{
children: ReactNode;
type: ButtonHTMLAttributes<HTMLButtonElement>['type'];
disabled: ButtonHTMLAttributes<HTMLButtonElement>['disabled'];
tabIndex: ButtonHTMLAttributes<HTMLButtonElement>['tabIndex'];
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
}>;
export const Button = ({ children, type, disabled, tabIndex, onClick }: Props) => (
<button
type={type}
disabled={disabled}
tabIndex={tabIndex}
onClick={onClick}
>
{children}
</button>
);
フォームやアクションのトリガーといった本来の用途であれば、この実装で問題ない。
<Button onClick={handleClick}>Submit</Button>
const handleClick = () => {
window.location.href = 'http://example.com';
};
<Button onClick={handleClick}>Link</Button>
これだと cmd
キー付きクリックやコンテキストメニューによる「新しいタブで開く」といった <a />
が持つ機能まで再現出来ない。
<a href="http://example.com">
<Button>Link</Button>
</a>
Button
部分はあくまで意匠としての役割に徹し、画面遷移といったビジネスロジックの責務は素直に <a />
に委ねる。これなら「アンカーリンク」に期待される振る舞いは全て得られるが、 <a />
の子要素に Button
のようなインタラクティブ要素を持たせるのは NG と HTML の仕様書に明記されている。
Content model: Transparent, but there must be no interactive content descendant, a element descendant, or descendant with the tabindex attribute specified.
つまりこのようにレンダリングされるのは NG ということである。
<!-- 結果 -->
<a href="http://example.com">
<button>Link</button>
</a>
type Props = Partial<
{
children: ReactNode;
} & XOR<
{
type: ButtonHTMLAttributes<HTMLButtonElement>['type'];
disabled: ButtonHTMLAttributes<HTMLButtonElement>['disabled'];
tabIndex: ButtonHTMLAttributes<HTMLButtonElement>['tabIndex'];
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
},
{
/**
* button 要素としての機能を全て無効化する。
* <a /> でラップしたいときなどに指定すること。
*/
noop: true;
}
>
>;
export const Button = ({ type, children, disabled, onClick, tabIndex, noop }: Props) =>
!noop ? (
<button
type={type}
tabIndex={onClick ? tabIndex : -1}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
) : (
<span tabIndex={-1}>
{children}
</span>
);
noop
というプロパティを生やす。これを指定すると <button>
ではなく <span>
としてレンダリングされる。
<a href="http://example.com">
<Button noop>Link</Button>
</a>
<!-- 結果 -->
<a href="http://example.com">
<span>Link</span>
</a>
noop
を指定して span
としてレンダリングということは、onClick
のような button 要素特有のプロパティや tabIndex
は不要となるため、型パズルを駆使してプロパティの排他制御を行っている。
型の排他制御にはts-xorを使うのがおすすめ。
HTML のインタラクティブ要素
<a>
<audio>
(if the controls attribute is present)<button>
<details>
<embed>
<iframe>
<img>
(if the usemap attribute is present)<input>
(if the type attribute is not in the hidden state)<keygen>
<label>
<menu>
(if the type attribute is in the toolbar state)<object>
(if the usemap attribute is present)<select>
<textarea>
<video>
(if the controls attribute is present)