Dropdown Menu

DropdownMenuは、クリックやホバーによって表示される選択肢の一覧から、ユーザーが操作を選べるコンポーネントです。

プロパティ

DropdownMenuは、以下のプロパティをサポートしています。

名前 デフォルト値 説明
menus MenuProps[] 階層の配列を渡します。

MenuProps

MenuPropsは、各階層の情報を表すオブジェクトです。

名前 デフォルト値 説明
label string 階層のラベルです。
items MenuItemProps[] 階層のメニュー。

MenuItemProps

MenuItemPropsは、メニューの情報を表すオブジェクトです。

名前 デフォルト値 説明
label string メニューのラベルです。
disabled boolean false 操作できるかどうか。
shortCutText string メニュー右側に表示するテキスト。
startContent Snippet<[]> 左側に表示する要素。
subMenus MenusProps[] ネストしたメニューの情報。

インストールの手順

以下のコンポーネントのコードを、使いたいプロジェクトにコピー&ペーストします。
パスは実際のプロジェクトの構成にあわせて更新します。

modules/DropdownMenu.svelte
        <!--
@component
## 概要
- クリックやホバーによって表示される選択肢の一覧から、ユーザーが操作を選べるコンポーネントです

## 機能
- 渡した配列をメニューとして表示できる

## Props
- menus: メニューとして表示する値の配列

## Usage
```svelte
<DropdownMenu {menus} />
```
-->

<script lang="ts" module>
  import type { Snippet } from 'svelte';
  import type { ClassValue } from 'svelte/elements';
  import { cva, type VariantProps } from 'class-variance-authority';

  export const dropdownMenuVariants = cva('w-full p-2 bg-base-container-default border border-base-stroke-default rounded-md');

  export const menuVariants = cva('flex justify-between w-full px-2 py-1.5 rounded-sm outline-primary align-middle text-sm cursor-pointer menu hover:bg-base-container-accent/90 focus-visible:outline-[0.125rem] focus-visible:outline-offset-[0.125rem] focus-visible:outline-primary', {
    variants: {
      /** 操作できるかどうか */
      disabled: {
        true: ['opacity-50 pointer-events-none'],
        false: [],
      },
    },
  });

  export interface MenuItemProps {
    /** メニューのラベル */
    label: string;
    /** 操作できるかどうか */
    disabled?: boolean;
    /** 右側に表示するテキスト(矢印アイコンを表示している場合表示できない) */
    shortCutText?: string;
    /** 左側に配置するコンテンツ */
    startContent?: Snippet<[]>;
    /** サブメニューとして表示する値(矢印アイコンが表示される) */
    subMenus?: MenusProps[];
    onClick?: () => void;
  }

  export interface MenusProps {
    /** メニューセクションのラベル */
    label?: string;
    /** メニューとして表示する値 */
    items: MenuItemProps[];
  }

  export type DropdownMenuVariants = VariantProps<typeof dropdownMenuVariants>;

  export interface DropdownMenuProps extends DropdownMenuVariants {
    /** クラス */
    class?: ClassValue;
    /** メニューとして表示したい要素の配列 */
    menus: MenusProps[];
    children?: Snippet<[]>;
  }
</script>

<script lang="ts">
  import Separator from '$lib/components/ui/atoms/Separator.svelte';
  import DropdownMenu from '$lib/components/ui/modules/DropdownMenu.svelte';
  import { ChevronRight } from '@lucide/svelte';

  let { menus, class: className }: DropdownMenuProps = $props();

  let dropdownMenuElement = $state<HTMLElement>();
  let subMenuElement = $state<HTMLElement[]>([]);

  let showLeft = $state(false);

  let dropdownMenuVariantClass = $derived(dropdownMenuVariants({ class: className }));

  function onKeyDown(e, action) {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      action();
    }
  }

  function calcShowPosition(index) {
    if (!subMenuElement[index] || !dropdownMenuElement) return false;

    let sub_menu_width = subMenuElement[index].offsetWidth;

    let rect = dropdownMenuElement.getBoundingClientRect();

    showLeft = window.innerWidth < rect.right + sub_menu_width;
  }
</script>

<div class={dropdownMenuVariantClass} bind:this={dropdownMenuElement}>
  {#each menus as menu, index}
    <div class="w-full">
      {#if index !== 0}
        <div class="py-1">
          <Separator />
        </div>
      {/if}
      {#if menu.label}
        <div class="px-2 py-1.5 font-semibold">
          {menu.label}
        </div>
      {/if}

      {#each menu.items as item, i}
        <div class="relative w-full menu-container" class:pointer-events-none={item.disabled}>
          <div class={menuVariants({ disabled: item.disabled })} tabindex={item.disabled ? -1 : 0} role="button" onclick={item.onClick} onkeydown={(e) => onKeyDown(e, item.onClick)} onmouseenter={() => calcShowPosition(i)}>
            {#if item.startContent}
              <div class="flex items-center justify-center shrink-0 mr-2">
                {@render item.startContent()}
              </div>
            {/if}

            <div class="w-full whitespace-nowrap">
              {item.label}
            </div>

            {#if item.subMenus}
              <ChevronRight size="1rem" />
            {/if}

            {#if item.shortCutText && !item.subMenus}
              <div class="flex items-center justify-center shrink-0 text-base-foreground-muted text-xs ml-2">
                {item.shortCutText}
              </div>
            {/if}
          </div>
          {#if item.subMenus}
            <div class={['submenu absolute top-0', showLeft ? 'right-full' : 'left-full']} bind:this={subMenuElement[i]}>
              <DropdownMenu menus={item.subMenus} />
            </div>
          {/if}
        </div>
      {/each}
    </div>
  {/each}
</div>

<style>
  /* submenuを開くアニメーションを管理 */
  .submenu {
    opacity: 0;
    transform: scale(0.8);
    transform-origin: left top;
    pointer-events: none;
    visibility: hidden;

    /* 出入り両方のアニメを transition で */
    transition:
      opacity 0.18s ease,
      transform 0.18s ease,
      visibility 0s linear 0.18s;
  }

  .menu-container:hover > .submenu {
    opacity: 1;
    transform: scale(1);
    pointer-events: auto;
    visibility: visible;

    /* 表示時は visibility の遅延をなくす */
    transition:
      opacity 0.18s ease,
      transform 0.18s ease,
      visibility 0s;
  }

  .menu-container:focus-within > .submenu {
    opacity: 1;
    transform: scale(1);
    pointer-events: auto;
    visibility: visible;

    /* 表示時は visibility の遅延をなくす */
    transition:
      opacity 0.18s ease,
      transform 0.18s ease,
      visibility 0s;
  }

  .menu-container:hover > .menu {
    background-color: var(--color-base-container-accent);
  }
</style>

      

依存コンポーネント

DropdownMenuを使うときは、以下のコンポーネントもダウンロードが必要です。

使い方


サンプル

Default

特に操作が行われていない、デフォルトの状態です。

Disabled

選択不可の状態です。