Shadcn/uiは便利なのですが、Github Issuesを見ていただくとわかるように大量のIssueがOpenのまま存在します。
そのため、多くのバグや仕様は大抵の場合Issueがあり、誰かが地雷を踏んでいるか解決するかなどしてくれています。
今回はShadcn/uiで生成したDialogを使ったコンポーネント内で、公式ドキュメントを参考に作ったComboboxを使ったとき、かつスクロールが多く発生する程度にコンボボックス内の選択項目が多い場合に、スクロールが効かなくなる問題についてです。
関連するIssueは以下です。
解決策
上記Issueで私がコメントしているとおり、 Popover
コンポーネントに modal={true}
を追加するだけで解決します。
<Dialog>
...
<Popover modal={true} onOpenChange={setOpen} open={open}>
xxxx
</Popover>
...
</Dialog>
で、この modal
についてどこに書かれているかというと以下です。
こちらはShadcn/uiではなく、shadcn/uiが内部で使っている @radix-ui/react-popover'
のドキュメントになります。分かりづらいですねぇ。
まとめ
Shadcn/uiは様々なライブラリの組み合わせでUIを表現している都合上、今回のようなコンポーネントの組み合わせによる問題や仕様について、ドキュメントやIssueがいろんなライブラリのGithubや公式ドキュメントに散らばってしまうというデメリットがあります。
個人的にShadcn/uiを「流行ってるから選ぶ」というのは早計で、「自力で調べ上げる検索力と解決力がチームにあるか」 が重要かなと感じます。
(余談)combobox改良版
ついでに私が使ってる modal
に対応したcomboboxコンポーネントの実装コードを以下に置いておきます。
こちらのコードはChatGPTとともに生成したもので、modalとは別にユースケースとしてPlaceholderや幅を指定しないとUIが崩れてしまう問題があったため、これらもPropsに追加しています。
import * as React from 'react';
import { Check, ChevronsUpDown } from 'lucide-react';
import { Button } from './button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from './command';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from './popover';
import { cn } from "@/lib/utils"
type Option = {
label: string;
value: string;
};
type ComboboxProps = {
className?: string; // Buttonの幅(クラス名指定用)
modal?: boolean; // モーダル表示の有無(デフォルトは false)
onSelect: (value: string) => void; // 選択時のコールバック関数
options: Option[]; // 親コンポーネントから渡される key-value のデータ
placeholder?: string; // プレースホルダーのテキスト
width?: string; // 追加のカスタムクラス(幅の調整)
};
export function Combobox({
options,
placeholder = 'Please Select',
onSelect,
width = 'w-[200px]',
className = '',
modal = false,
}: ComboboxProps) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState('');
return (
<Popover modal={modal} onOpenChange={setOpen} open={open}>
<PopoverTrigger asChild>
<Button
aria-expanded={open}
className={cn(width, 'justify-between', className)}
role="combobox"
variant="outline"
>
{value ? options.find((option) => option.value === value)?.label : placeholder}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className={cn(width, 'p-100')}>
<Command>
<CommandInput placeholder={`Search ${placeholder.toLowerCase()}`} />
<CommandList>
<CommandEmpty>No options found.</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
onSelect={(currentValue) => {
setValue(currentValue === value ? '' : currentValue);
onSelect(currentValue);
setOpen(false);
}}
value={option.value}
>
<Check
className={cn(
'mr-2 h-4 w-4',
value === option.value ? 'opacity-100' : 'opacity-0'
)}
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
なお、コンポーネントのPropsの使い方についてはコード内にコメントで記述したりドキュメント書いたりという方法もありますが、Storybookを書いて開発チームで共有することを個人的にはおすすめしたいです。グラフィカルかつインタラクティブにPropsの値の違いがUIにどう影響するのかわかるためです。