Dropdown Menu
DropdownMenuは、クリックやホバーによって表示される選択肢の一覧から、ユーザーが操作を選べるコンポーネントです。
<script lang="ts">
import DropdownMenu from '$lib/components/ui/modules/DropdownMenu.svelte';
let subMenus = [
{
label: 'ラベル',
items: [
{ label: 'メニュー' },
{ label: 'メニュー', shortCutText: '⌘K' },
],
},
];
let menus = [
{
label: 'ラベル',
items: [
{ label: 'メニュー', shortCutText: '⌘K' },
{ label: 'メニュー' },
],
},
{
items: [
{ label: 'メニュー' },
{ label: 'メニュー', subMenus },
{ label: 'メニュー' },
],
},
];
</script>
<DropdownMenu class="max-w-72" {menus} />
プロパティ
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を使うときは、以下のコンポーネントもダウンロードが必要です。
使い方
<script lang="ts">
import DropdownMenu from '$lib/components/ui/modules/DropdownMenu.svelte';
let subMenus = [
{
label: 'ラベル',
items: [
{ label: 'メニュー' },
{ label: 'メニュー', shortCutText: '⌘K' },
],
},
];
let menus = [
{
label: 'ラベル',
items: [
{ label: 'メニュー', shortCutText: '⌘K' },
{ label: 'メニュー' },
],
},
{
items: [
{ label: 'メニュー' },
{ label: 'メニュー', subMenus },
{ label: 'メニュー' },
],
},
];
</script>
<DropdownMenu class="max-w-72" {menus} />
サンプル
Default
特に操作が行われていない、デフォルトの状態です。
<script lang="ts">
import DropdownMenu from '$lib/components/ui/modules/DropdownMenu.svelte';
let subMenus = [
{
label: 'ラベル',
items: [
{ label: 'メニュー' },
{ label: 'メニュー', shortCutText: '⌘K' },
],
},
];
let menus = [
{
label: 'ラベル',
items: [
{ label: 'メニュー', shortCutText: '⌘K' },
{ label: 'メニュー' },
],
},
{
items: [
{ label: 'メニュー' },
{ label: 'メニュー', subMenus },
{ label: 'メニュー' },
],
},
];
</script>
<DropdownMenu class="max-w-72" {menus} />
Disabled
選択不可の状態です。
<script lang="ts">
import DropdownMenu from '$lib/components/ui/modules/DropdownMenu.svelte';
let subMenus = [
{
label: 'ラベル',
items: [
{ label: 'メニュー' },
{ label: 'メニュー', shortCutText: '⌘K' },
],
},
];
let menus = [
{
label: 'ラベル',
items: [
{ label: 'メニュー', shortCutText: '⌘K', disabled: true },
{ label: 'メニュー' },
],
},
{
items: [
{ label: 'メニュー' },
{ label: 'メニュー', subMenus, disabled: true },
{ label: 'メニュー' },
],
},
];
</script>
<DropdownMenu class="max-w-72" {menus} />