You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ReactJS问题:MultiSelect组件在Dialog内无法正常选择

问题描述

基于shadcn组件库开发的自定义MultiSelect组件,在主组件中使用完全正常:

<MultiSelect
    id="zone"
    placeholder="Zone search"
    onSelectionChange={setZones}
    endpoint="/zones/search"
/>

但将其嵌入到EstateSelector组件的Dialog中时,下拉菜单可正常展开,却无法选择任何选项,点击会穿透下拉菜单触发底层元素的交互。

相关组件代码如下:

EstateSelector.jsx

return (
        <>
            <Dialog>
                <DialogTrigger className="w-full">
                    <div className="flex justify-between h-9 w-full rounded-md border border-input bg-transparent px-2 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground md:text-sm">
                        <ChevronRight className="mt-1 size-4" />
                    </div>
                </DialogTrigger>
                <DialogContent className="lg:min-w-[800px] lg:min-h-[400px] min-w-full">
                    <DialogHeader>
                        <DialogTitle>{placeholder}</DialogTitle>
                        <DialogDescription className="pt-3">
                            <Command>
                                <div className="flex px-1 gap-1 pt-1 mb-2">
                                    <Select
                                        className="w-1/3"
                                        onValueChange={setSelectedStatus}
                                    >
                                        <SelectTrigger className="w-[90%] overflow-hidden">
                                            <SelectValue placeholder="Stato" />
                                        </SelectTrigger>
                                        <SelectContent>
                                            <SelectGroup>
                                                <SelectItem value={null}>Tutti gli Stati</SelectItem>
                                            </SelectGroup>
                                        </SelectContent>
                                    </Select>
                                    <MultiSelect
                                        id="zone"
                                        placeholder="Zone search"
                                        isMulti={false}
                                        onSelectionChange={setSelectedZone}
                                        endpoint="/zones/search"
                                    ></MultiSelect>
                                </div>
                                {loading && <CommandEmpty>Caricamento...</CommandEmpty>}
                                {error && <CommandEmpty>{error}</CommandEmpty>}
                                {!loading && !error && searchTerm && (
                                    <CommandEmpty>Nessun immobile trovato.</CommandEmpty>
                                )}
                                <CommandGroup>
                                    <CommandList>
                                        {options.map((option) => (
                                            <CommandItem
                                                key={'estate_' + option.id}
                                                value={`${option.id} ${option.titolo_interno}`}
                                                onSelect={() => {
                                                    handleSelectChange({
                                                        id: option.id,
                                                        value: option.titolo_interno
                                                    });
                                                }}
                                                className={cn(
                                                    '',
                                                    selectedItems.some(item => item.id === option.id) ? 'bg-blue-200 hover:bg-blue-200' : 'hover:bg-gray-50'
                                                )}
                                            >
                                                <Check
                                                    className={cn(
                                                        'absolute left-5 size-2  text-white bg-blue-500 p-0.5 rounded-full',
                                                        selectedItems.some(item => item.id === option.id) ? 'opacity-100' : 'opacity-0'
                                                    )}
                                                />
                                                <img
                                                    src={`/storage/images/estates/${user.branch_id}/${option?.thumb?.thumb}`}
                                                    className="size-10 rounded"
                                                    loading="lazy"
                                                    onError={(e) => {
                                                        e.target.onerror = null;
                                                        e.target.src = `/images/${imagePlaceholder}`;
                                                    }}
                                                    alt={option.titolo_interno}
                                                />
                                                <div className="flex gap-1">
                                                    <span className="ml-2 font-bold">{'#' + option.id}</span>: {option.titolo_interno}
                                                </div>
                                            </CommandItem>
                                        ))}
                                    </CommandList>
                                </CommandGroup>
                            </Command>
                        </DialogDescription>
                    </DialogHeader>
                </DialogContent>
            </Dialog>
        </>
    );

MultiSelect.jsx

return (
        <Popover
            open={open}
            onOpenChange={setOpen}
        >
            <PopoverTrigger asChild>
                <Button
                    variant="outline"
                    role="combobox"
                    aria-expanded={open}
                    className="w-full justify-between px-3"
                >
                    <div className="flex gap-1 justify-start overflow-hidden">
                        {selectedItems?.length ? (
                            isMulti && selectedItems.length > cutItemsAfter ? (
                                <Badge variant="secondary">
                                    {selectedItems.length} voci selezionate...
                                </Badge>
                            ) : (
                                selectedItems.map((item, i) => (
                                    <Badge
                                        key={i}
                                        onClick={(e) => {
                                            e.stopPropagation();
                                            removeItem(item.id);
                                        }}
                                    >
                                        {item.value}
                                    </Badge>
                                ))
                            )
                        ) : (
                            <span>{placeholder}...</span>
                        )}
                    </div>
                    <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
                </Button>
            </PopoverTrigger>
            <PopoverContent className="min-w-[380px] lg:min-w-[400px] p-0">
                <Command>
                    <CommandInput
                        className="border-0 ring-0 focus:ring-0"
                        placeholder={placeholder + '...'}
                        value={endpoint ? searchTerm : undefined}
                        onValueChange={endpoint ? setSearchTerm : undefined}
                    />
                    {loading && <CommandEmpty>Caricamento...</CommandEmpty>}
                    {error && <CommandEmpty>{error}</CommandEmpty>}
                    {!loading && !error && searchTerm && (
                        <CommandEmpty>Nessuna voce trovata.</CommandEmpty>
                    )}
                    <CommandList>
                        <CommandGroup>
                            {orderedOptions
                                .filter(option => !hideId || hideId !== option.id)
                                .map((option) => (
                                    <CommandItem
                                        onMouseOver={(e) => {
                                            console.log(e);
                                        }}
                                        key={option.id}
                                        value={`${option.id} ${option.label}`}
                                        onSelect={() => {
                                            handleSelectChange({ id: option.id, value: option.label });
                                            if (!isMulti) setOpen(false); // Chiudi il popover in modalità single select
                                        }}
                                        className={cn(
                                            '',
                                            selectedItems.some(item => item.id === option.id)
                                                ? 'bg-blue-200 hover:bg-blue-200'
                                                : 'hover:bg-gray-50'
                                        )}
                                    >
                                        <Check
                                            className={cn(
                                                'mr-2 h-4 w-4',
                                                selectedItems.some(item => item.id === option.id)
                                                    ? 'opacity-100'
                                                    : 'opacity-0'
                                            )}
                                        />
                                        {option.label}
                                    </CommandItem>
                                ))}
                        </CommandGroup>
                    </CommandList>
                </Command>
            </PopoverContent>
        </Popover>
    );
问题原因
  1. 层级与事件穿透:shadcn的Dialog默认通过Portal挂载到body,且拥有较高z-index;MultiSelect的Popover同样使用Portal,但默认z-index低于Dialog,导致下拉菜单实际渲染在Dialog下方,点击事件被Dialog底层元素捕获,而非Popover的CommandItem。
  2. Command组件嵌套冲突:EstateSelector中把MultiSelect包裹在外层Command组件内,而MultiSelect自身也使用了Command组件。外层Command会拦截点击事件并尝试处理,导致MultiSelect的CommandItem点击无法触发预期逻辑。
修复方案

方案1:提升Popover层级

修改MultiSelect的PopoverContent,添加更高的z-index确保其在Dialog之上:

<PopoverContent className="min-w-[380px] lg:min-w-[400px] p-0 z-[1000]">

方案2:解除Command组件嵌套

将EstateSelector中MultiSelect所在的容器移到外层Command之外,避免事件拦截:

<DialogDescription className="pt-3">
    {/* 把MultiSelect和Select移到Command外面 */}
    <div className="flex px-1 gap-1 pt-1 mb-2">
        <Select
            className="w-1/3"
            onValueChange={setSelectedStatus}
        >
            <SelectTrigger className="w-[90%] overflow-hidden">
                <SelectValue placeholder="Stato" />
            </SelectTrigger>
            <SelectContent>
                <SelectGroup>
                    <SelectItem value={null}>Tutti gli Stati</SelectItem>
                </SelectGroup>
            </SelectContent>
        </Select>
        <MultiSelect
            id="zone"
            placeholder="Zone search"
            isMulti={false}
            onSelectionChange={setSelectedZone}
            endpoint="/zones/search"
        ></MultiSelect>
    </div>
    {/* 保留原来的Command内容,仅用于处理下方的选项列表 */}
    <Command>
        {loading && <CommandEmpty>Caricamento...</CommandEmpty>}
        {error && <CommandEmpty>{error}</CommandEmpty>}
        {!loading && !error && searchTerm && (
            <CommandEmpty>Nessun immobile trovato.</CommandEmpty>
        )}
        <CommandGroup>
            <CommandList>
                {options.map((option) => (
                    <CommandItem
                        key={'estate_' + option.id}
                        value={`${option.id} ${option.titolo_interno}`}
                        onSelect={() => {
                            handleSelectChange({
                                id: option.id,
                                value: option.titolo_interno
                            });
                        }}
                        className={cn(
                            '',
                            selectedItems.some(item => item.id === option.id) ? 'bg-blue-200 hover:bg-blue-200' : 'hover:bg-gray-50'
                        )}
                    >
                        <Check
                            className={cn(
                                'absolute left-5 size-2  text-white bg-blue-500 p-0.5 rounded-full',
                                selectedItems.some(item => item.id === option.id) ? 'opacity-100' : 'opacity-0'
                            )}
                        />
                        <img
                            src={`/storage/images/estates/${user.branch_id}/${option?.thumb?.thumb}`}
                            className="size-10 rounded"
                            loading="lazy"
                            onError={(e) => {
                                e.target.onerror = null;
                                e.target.src = `/images/${imagePlaceholder}`;
                            }}
                            alt={option.titolo_interno}
                        />
                        <div className="flex gap-1">
                            <span className="ml-2 font-bold">{'#' + option.id}</span>: {option.titolo_interno}
                        </div>
                    </CommandItem>
                ))}
            </CommandList>
        </CommandGroup>
    </Command>
</DialogDescription>

方案3:禁用Dialog的点击穿透(可选)

如果上述方案仍有问题,可以在DialogContent上添加onClick={(e) => e.stopPropagation()},阻止事件冒泡到Dialog底层,但这可能影响其他交互,需谨慎使用。

内容的提问来源于stack exchange,提问作者Mr.Web

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 05:55:54