1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

shadcn/ui の Dialog 内で Select を用いると SelectContent にフォーカスされない

Last updated at Posted at 2025-02-15

shadcn/ui を利用してアプリケーションを作成していると、Dialog コンポーネント内で Select コンポーネントを使用する際、SelectContent にフォーカスが当たらず、選択肢の開閉時にエラーが発生するという問題に直面することがあります。
本記事では、この現象の原因と解決方法、そして Dialog 内で正しく動作する SelectContent の実装例について紹介します。

事象の詳細

Dialog 内で Select コンポーネントを利用して選択を行うと、以下のような現象が発生します。

  • エラーメッセージ: Select の開閉時にエラーが表示される
  • フォーカスの不具合: 選択肢が表示されても、SelectContent にフォーカスが当たらない
Blocked aria-hidden on an element because its descendant retained focus. 
The focus must not be hidden from assistive technology users. 
Avoid using aria-hidden on a focused element or its ancestor. 
Consider using the inert attribute instead, which will also prevent focus. 
For more details, see the aria-hidden section of the WAI-ARIA specification at https://w3c.github.io/aria/#aria-hidden.

詳細な仕様は下記の公式ドキュメントを参照してください。

原因の特定

原因は、shadcn/ui の標準実装にあります。
具体的には、SelectContent の内部で SelectPrimitive.Portal が使用されており、これにより SelectContent が Dialog の外側にある DOM ツリー上に作成されてしまいます。
その結果、Dialog のフォーカスマネジメントの仕組みによって、正しくフォーカスが当たらなくなるという問題が発生しています。

解決策

この問題を解決するためには、Dialog 内で利用する Select コンポーネント専用に、Portal を使用しない実装を追加する必要があります。
shadcn/ui の標準実装を元に、Dialog 向けの SelectContent を以下のように実装することで、問題を解決できます。

修正前

const SelectContent = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
  <SelectPrimitive.Portal>
    <SelectPrimitive.Content
      ref={ref}
      className={cn(
        "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
        position === "popper" &&
          "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
        className
      )}
      position={position}
      {...props}
    >
      <SelectScrollUpButton />
      <SelectPrimitive.Viewport
        className={cn(
          "p-1",
          position === "popper" &&
            "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
        )}
      >
        {children}
      </SelectPrimitive.Viewport>
      <SelectScrollDownButton />
    </SelectPrimitive.Content>
  </SelectPrimitive.Portal>
));

追加する SelectContent の実装例

Portal を除去し、Dialog 内の DOM ツリーに直接レンダリングすることで、フォーカスの問題を解決します。

const SelectContentForDialog = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
  <SelectPrimitive.Content
    ref={ref}
    className={cn(
      "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
      position === "popper" &&
        "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
      className
    )}
    position={position}
    {...props}
  >
    <SelectScrollUpButton />
    <SelectPrimitive.Viewport
      className={cn(
        "p-1",
        position === "popper" &&
          "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
      )}
    >
      {children}
    </SelectPrimitive.Viewport>
    <SelectScrollDownButton />
  </SelectPrimitive.Content>
));

この実装では、Portal コンポーネントを使用していないため、SelectContentForDialog は Dialog の子要素として直接 DOM に配置され、フォーカス管理が正常に行われるようになります。

注意点

この方法は Portal を削除するため、document.body 直下に SelectContent がマウントされず、Dialog 内にマウントされます。副作用もあり、Dialog の描画領域を超えて SelectContent を表示するのが難しくなります。

まとめ

Dialog 内で Select コンポーネントを使用した際に発生するフォーカスの問題は、SelectContent が Portal を利用していることに起因していました。
Dialog の外にレンダリングされると、Dialog のフォーカスマネジメントから外れてしまい、エラーや意図しない挙動が発生します。
今回紹介した修正方法では、Dialog 向けに Portal を使用しない SelectContent を実装することで、この問題を解消しています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?