Menu | CoUI
본문으로 건너뛰기
LogoCoUI

Menu

정적 메뉴 패널 컴포넌트

목차

Menu 는 테두리가 있는 패널 안에 풍부한 메뉴 항목 목록을 표시하는 정적(인라인) 메뉴 컴포넌트입니다. 항목은 CoreMenuItem 의 이름 있는 팩토리 생성자로 만듭니다 — 헤딩 라벨, 액션, 체크박스, 라디오, 구분선, 간격, 그리고 인라인 중첩 서브메뉴까지 지원합니다.

기본 사용법 (Basic Usage)#

Menu(
  ariaLabel: 'Main menu',
  items: [
    const CoreMenuItem.label('Navigation'),
    CoreMenuItem.action('Home', onPressed: () {}, active: true),
    CoreMenuItem.action('Profile', onPressed: () {}),
    const CoreMenuItem.divider(),
    CoreMenuItem.checkbox('Show grid', onChanged: (_) {}, checked: true),
    CoreMenuItem.action('Settings', disabled: true),
  ],
)
Menu(
  ariaLabel: 'Main menu',
  items: [
    const CoreMenuItem.label('Navigation'),
    CoreMenuItem.action('Home', onPressed: () {}, active: true),
    CoreMenuItem.action('Profile', onPressed: () {}),
    const CoreMenuItem.divider(),
    CoreMenuItem.checkbox('Show grid', onChanged: (_) {}, checked: true),
    CoreMenuItem.action('Settings', disabled: true),
  ],
)

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 액션이나 네비게이션 항목 목록을 패널로 직접 표시할 때
  • leading/trailing 아이콘, 체크박스, 라디오를 포함한 메뉴가 필요할 때
  • 인라인으로 펼쳐지는 계층적 서브메뉴 구조가 필요할 때

대신 다른 컴포넌트를 사용하세요:

  • NavigationMenu: 명령 액션 행 대신 사이트 링크와 풍부한 드롭다운 콘텐츠가 필요할 때
  • Tree: 명령 목록 대신 데이터 계층을 탐색할 때
  • Popup: 항목 의미·탐색 없이 플로팅 패널 표면만 조립할 때
  • DropdownMenu: 인라인 패널 대신 트리거 앵커의 오버레이 메뉴일 때
  • ContextMenu: 포인터 위치에서 여는 컨텍스트 메뉴일 때
  • Menubar: 가로 바에서 여러 메뉴 트리거를 전환해야 할 때
  • Select: 액션 실행 대신 폼 값을 선택할 때

Props / Parameters#

W 는 Flutter의 Widget / Web의 Jaspr Component입니다. CoreMenuItem의 leading / trailing 같은 콘텐츠 슬롯도 같은 플랫폼 타입입니다.

속성타입기본값설명
items List<CoreMenuItem<W>> 필수 패널에 렌더링할 메뉴 항목
vertical bool true false 면 항목을 가로로 배치
size CoreMenuItemSize CoreMenuItemSize.defaultSize(md, 36px) 행 밀도 — sm 32 / md 36 / lg 48. 메뉴가 소유하는 값이라 모든 대화형 행( MenuItem )에 같은 값을 넘깁니다. 한 패널 안에서 높이가 섞이지 않습니다
ariaLabel String? null 메뉴 컨테이너의 접근성 이름
menuStyle CoreMenuStyle? null 인스턴스별 chrome 스타일
onActivate void Function()? null 서브메뉴 없는 action의 onPressed보다 먼저 호출하는 호스트 닫기 요청

CoreMenuItem 팩토리#

팩토리설명
CoreMenuItem.label(text, {leading, trailing})비대화 섹션 헤딩
CoreMenuItem.action(text, {description, tone, leading, trailing, shortcut, onPressed, href, submenu, active, disabled}) 대화형 액션 항목 (submenu 지정 시 인라인 서브메뉴 트리거)
CoreMenuItem.checkbox(text, {description, tone, trailing, onChanged, checked, disabled}) 체크박스 토글 항목
CoreMenuItem.radio(text, {description, tone, trailing, onChanged, selected, disabled}) 라디오 단일 선택 항목
CoreMenuItem.divider()시각적 구분선
CoreMenuItem.gap(size)고정 크기 간격

action / checkbox / radio 세 팩토리가 공유하는 행 속성:

속성타입기본값설명
description String? null 라벨 아래 두 번째 줄. 있으면 그 행만 size 의 최소 높이를 넘어 자라고 두 줄이 위쪽 정렬됩니다
tone CoreMenuItemTone CoreMenuItemTone.defaultTone(neutral) critical 이면 라벨과 글리프가 error 색으로 읽히고, hover / focus / pressed 에서는 워시 대신 errorContainer 채움에 onErrorContainer 글자로 바뀝니다. 색이 유일한 단서라 파괴적 항목은 그룹 마지막, 보통 구분선 뒤에 둡니다

행은 MenuItem 이 그립니다#

action / checkbox / radio 항목은 Menu 가 직접 그리지 않고 MenuItem 을 합성합니다 — label 행·구분선·간격만 Menu 자신의 것입니다. 팩토리 필드는 다음처럼 행 파라미터로 옮겨집니다.

CoreMenuItemMenuItem
label / description / tone / shortcut / leading / trailing 같은 이름의 슬롯 (Text 로 감싸서)
checked / selected (토글) · active (action) selected + mark: Icon(LucideIcons.check) (라디오는 점)
패널에 토글 행이 하나라도 있음모든 행에 reserveMark: true — 라벨 정렬 유지
submenu 비어있지 않음 hasSubmenu: true (셰브론, shortcut 은 숨김)
disabledenabled: false — 포커스는 받되 활성화만 무시
Menu.sizesize
CoreMenuStyle.itemStyle (→ effectiveItemStyle) menuItemStyle

폭#

패널 폭은 CoreMenuStyle.defaultPopupStyle 이 minWidth 240 / maxWidth 280 (CoreLayout.layout240 / layout280) 으로 진술하고 합성된 Popup 이 그립니다 — 내용이 그 사이에서 폭을 정하고, 위젯은 폭을 따로 박지 않습니다. 다른 폭이 필요하면 menuStyle: CoreMenuStyle(popupStyle: CorePopupStyle(minWidth: …, maxWidth: …)) 로 덮습니다.

스타일 시스템 (Style System)#

Menu 의 모든 chrome / dimensional 오버라이드는 단일 CoreMenuStyle 슬롯으로 흐릅니다.

시맨틱 vs 스타일#

  • 시맨틱 / behaviour: 위젯 파라미터로 직접 (vertical, size, ariaLabel, onActivate)
  • chrome / dimensional: CoreMenuStyle 한 곳으로 — 패널 박스는 popupStyle: CorePopupStyle?, 대화형 행은 itemStyle: CoreMenuItemStyle?, 헤딩 행은 labelTextStyle (labelSmall 역할 그대로, 12 Medium) · labelBottomInset (헤딩 아래 4 — 위와 좌우는 행의 안쪽 여백을 따름), 구분선은 dividerStyle · dividerInset (세로 메뉴에서 구분선 위아래 4), 인라인 서브메뉴는 submenuStyle / submenuIndent / leadingColumnWidth

Resolve chain#

CoreMenuStyle.defaultX (디자인 시스템 기본값)
  → CoreMenuTheme.style                  // 프로젝트 공통
  → widget.menuStyle                     // 인스턴스별

행 chrome 은 한 단계 더 흐릅니다 — Menu resolver 가 CoreMenuStyle.effectiveItemStyle 을 각 MenuItem 에 raw-forward 하고, 행의 resolver 가 size / tone 사다리와 나머지 기본값을 채웁니다.

CoreMenuStyle.defaultItemStyle
  → CoreMenuStyle.itemStyle                   // Menu 의 슬롯
  → MenuItem(menuItemStyle: …)                // 행 resolver — defaultsBySize / defaultsByTone 위에 overlay

행 chrome — 여백, 하이라이트 반경, 아이콘 열 ↔ 라벨 사이 간격, 단축키 힌트, 마크 / 셰브런 글리프 — 은 전부 itemStyle: CoreMenuItemStyle? 한 슬롯으로 들어갑니다.

defaultItemStyle 이 바꾸는 것은 반경 하나입니다 — 패널 안의 행은 사이즈와 무관하게 6 입니다. 패널 모서리 12 에서 패널 패딩 6 을 뺀 파생값이라 어느 사이즈에서도 동심입니다. 혼자 선 MenuItem 의 반경은 행 높이 × 0.25 이지만, 그대로 따르면 패널 안의 lg 행(12)이 바깥 모서리 12 와 같아져 패딩 6 만큼 안쪽에서 바깥보다 둥글어집니다. 세로 패널 · 가로 바 · 인라인 서브메뉴, 그리고 Menu 를 합성하는 DropdownMenu · ContextMenu · Menubar 패널이 모두 이 값을 받습니다.

CoreMenuStyle 필드#

필드타입설명
popupStyle CorePopupStyle? Nested panel-box chrome style — forwarded straight to the composed Popup(popupStyle: …) , which resolves the box background / border / radius / padding / shadow / min-width and the inter-row spacing. Only the menu-divergent fields are pre-set in [defaultPopupStyle]; the rest fall back to [CorePopupStyle]'s own defaults inside the popup resolver (raw-forward). Overlaid by the per-instance value.
submenuStyle CoreMenuStyle? Nested style for an inline nested submenu — the submenu is itself a Menu (an indented child), so this slot carries a full [CoreMenuStyle] that the resolver raw-forwards to the child's menuStyle (the child self-resolves, exactly like dividerStyle forwards to Separator / chevronIconStyle to Icon ). Defaults to [defaultSubmenuStyle] (a flush, box-less panel) so inline submenus read as an indented group; a designer can override this slot to give nested submenus their own box / item chrome. Overlaid by the per-instance value on top of the default.
itemStyle CoreMenuItemStyle? Nested chrome of every interactive row — the MenuItem the menu composes. null → [defaultItemStyle]; resolvers raw-forward [effectiveItemStyle] to each row (the MenuItem resolver fills the size / tone ladders). Size and tone themselves are not here — the menu passes size as a prop and each entry carries its own tone.
labelTextStyle CoreTextStyle? Nested text style for non-interactive label / header entries ( CoreMenuItemKind.label ) — overlaid on the [defaultLabelTextStyle] ( labelSmall role + variant colour) and forwarded to the entry's Text / <span> as a paint-ready style. Carries the heading weight / colour so both platforms drive the caption typography from this single slot.
dividerStyle CoreDividerStyle? Nested divider style for the divider entry — forwarded straight to Divider(dividerStyle: …) , which resolves the colour / thickness (rule 2.2 child-component nested slot). Overlaid on [defaultDividerStyle].
labelBottomInset double? Space left below a label / heading entry (logical px). The heading's top and sides follow the row inset (the forwarded row chrome, else the size ladder) so its text lines up with the rows; only the bottom is its own. null → [defaultLabelBottomInset].
dividerInset double? Space kept on each side of a divider entry across the menu's cross axis (logical px) — above and below the rule in a column menu, either side of it in a bar. null → [defaultDividerInset].
leadingColumnWidth double? Leading-icon column width (logical px).
submenuIndent double? Per-level submenu indentation (logical px).

사용 예#

Menu(
  size: CoreMenuItemSize.sm,
  items: items,
  menuStyle: const CoreMenuStyle(
    popupStyle: CorePopupStyle(
      borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
    ),
    itemStyle: CoreMenuItemStyle(
      selectedBackgroundColor: CoreColor.token(CoreColors.secondary),
      selectedForegroundColor: CoreColor.token(CoreColors.onSecondary),
    ),
  ),
)

패널 박스 chrome 은 popupStyle 이, 행 chrome 은 itemStyle 이 받습니다 — Menu 자신은 그 값을 다시 해석하지 않고 Popup 과 MenuItem 에 그대로 넘깁니다(합성 컴포넌트가 자기 것을 풉니다).

위젯 슬롯 — leading / trailing 은 위젯 그대로#

CoreMenuItem 의 leading / trailing 은 W? 슬롯이라 아이콘·인디케이터 ·Text chain 을 그대로 받습니다(shadcn asChild 패턴). 슬롯 안 위젯의 시맨틱은 호출자가 정하고, 행은 위치만 잡습니다. CoreMenuStyle.itemStyle.leadingIconStyle 같은 nested 슬롯은 그 자리에 놓인 Icon 의 chrome 미세 조정용입니다 — 어떤 위젯을 놓을지는 슬롯이 정합니다.

CoreMenuItem.action(
  'Sync',
  leading: const Icon(LucideIcons.refreshCw),
  trailing: Text('Beta').labelSmall.onSurfaceVariant,
  onPressed: () {},
)

빠른 오버라이드 (Chain)#

이미 만든 Menu 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.

class MenuChainExample extends StatelessWidget {
  const MenuChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Menu(
      ariaLabel: 'Main menu',
      items: [
        const CoreMenuItem.label('Navigation'),
        CoreMenuItem.action('Home', onPressed: () {}, active: true),
        CoreMenuItem.action('Profile', onPressed: () {}),
        const CoreMenuItem.divider(),
        CoreMenuItem.checkbox('Show grid', onChanged: (_) {}, checked: true),
        CoreMenuItem.action('Settings', disabled: true),
      ],
    ).withStyle(
      const CoreMenuStyle(
        itemStyle: CoreMenuItemStyle(
          padding: CoreEdgeInsets.symmetric(
            horizontal: CoreSpace.space20,
            vertical: CoreSpace.space12,
          ),
          contentGapStyle: CoreGapStyle(size: CoreSpace.space12),
          markIconStyle: CoreIconStyle(
            color: CoreColor.token(CoreColors.tertiary),
          ),
        ),
        dividerStyle: CoreDividerStyle(
          color: CoreColor.token(CoreColors.primary),
          thickness: CoreStrokeWidth.stroke2,
        ),
      ),
    );
  }
}
class MenuChainExample extends StatelessComponent {
  const MenuChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return Menu(
      ariaLabel: 'Main menu',
      items: [
        const CoreMenuItem.label('Navigation'),
        CoreMenuItem.action('Home', onPressed: () {}, active: true),
        CoreMenuItem.action('Profile', onPressed: () {}),
        const CoreMenuItem.divider(),
        CoreMenuItem.checkbox('Show grid', onChanged: (_) {}, checked: true),
        CoreMenuItem.action('Settings', disabled: true),
      ],
    ).withStyle(
      const CoreMenuStyle(
        itemStyle: CoreMenuItemStyle(
          padding: CoreEdgeInsets.symmetric(
            horizontal: CoreSpace.space20,
            vertical: CoreSpace.space12,
          ),
          contentGapStyle: CoreGapStyle(size: CoreSpace.space12),
          markIconStyle: CoreIconStyle(
            color: CoreColor.token(CoreColors.tertiary),
          ),
        ),
        dividerStyle: CoreDividerStyle(
          color: CoreColor.token(CoreColors.primary),
          thickness: CoreStrokeWidth.stroke2,
        ),
      ),
    );
  }
}

변형 (Variants)#

서브메뉴 (인라인 중첩)#

Menu(
  items: [
    CoreMenuItem.action('File', onPressed: () {}),
    CoreMenuItem.action(
      'Export',
      submenu: [
        CoreMenuItem.action('PDF', onPressed: () {}),
        CoreMenuItem.action('CSV', onPressed: () {}),
      ],
    ),
  ],
)

서브메뉴는 부모 항목 아래에 한 단계 들여쓰기되어 인라인으로 펼쳐집니다.

가로 메뉴#

Menu(
  vertical: false,
  items: [
    CoreMenuItem.action('Home', onPressed: () {}),
    CoreMenuItem.action('Docs', onPressed: () {}),
  ],
)

체크 / 라디오 항목#

Menu(
  items: [
    CoreMenuItem.checkbox('Auto save', onChanged: (_) {}, checked: true),
    CoreMenuItem.radio('Compact', onChanged: (_) {}, selected: true),
    CoreMenuItem.radio('Comfortable', onChanged: (_) {}),
  ],
)

동작 스펙 (Behavior)#

항목 활성화#

  • action: 서브메뉴가 없으면 onActivate(호스트 닫기 요청) → onPressed 순서. 서브메뉴 부모는 호스트 닫기를 요청하지 않습니다. disabled 면 무시 — 행은 disabledForegroundColor 로 그려지되 포커스는 받습니다
  • checkbox: 활성화 시 onChanged(!checked) 호출, 체크 시 mark 칸에 체크 글리프 + selected 채움
  • radio: 활성화 시 onChanged(true) 호출, 선택 시 mark 칸에 점 글리프
    • selected 채움
  • 패널에 토글 행이 하나라도 있으면 모든 행이 mark 칸 폭을 예약합니다 (reserveMark) — 일부 행만 켜져도 라벨이 어긋나지 않습니다
  • checkbox / radio 값은 호출부가 갱신해야 선택 표시가 바뀝니다. href는 data-href 정보이며 Menu가 자동 이동하지 않습니다. Menu 자체는 표시 패널이며 오버레이 열기·닫기는 호스트가 담당합니다.

Hover / Focus / Pressed#

  • 행이 스스로 상태 레이어를 그립니다 — 전경색을 itemStyle.stateLayer (hover 8% / focus 8% / pressed 12%) 알파로 워시합니다. 자세한 규칙은 MenuItem 의 동작 스펙.

밀도 (size)#

  • size 는 메뉴 한 곳에서 정하고 모든 행에 같은 값이 갑니다 — sm 32 / md 36 / lg 48. description 이 있는 행만 그 최소 높이를 넘어 자랍니다.

긴 목록#

  • Flutter: 세로 Menu 는 받은 높이가 행에 모자라면 행을 패널 안에서 스크롤합니다. 높이를 제한하는 쪽은 호스트입니다 — DropdownMenu · ContextMenu · Menubar 의 패널이 뷰포트가 남기는 높이로 제한됩니다. 제한이 없는 곳에서는 행이 그대로 펼쳐지고, 가로 Menu 는 스크롤하지 않습니다.
  • Web: Menu 는 파라미터를 더하지 않고, 호스트가 위치 엔진이 정한 높이를 내부 범위로 알려 주면 그 높이를 따릅니다. 그러면 Menu 가 패널 상자에 max-height 를 걸고 행을 상자 안의 목록에 담아 그 목록만 overflow-y: auto 로 스크롤합니다. 상자는 스크롤하지 않으므로 테두리·모서리·그림자와, 상자 안에 그려지는 글래스 림이 행과 함께 밀려나지 않고 제자리에 있습니다. 높이를 알리지 않는 곳에서는 행이 상자의 직접 자식으로 그대로 펼쳐집니다.

서브메뉴#

  • CoreMenuItem.action 에 submenu 를 지정하면 shortcut 자리에 chevron 아이콘이 그려지고(shortcut 은 숨김), 항목 아래에 중첩 Menu 가 인라인으로 렌더됩니다.
  • 인라인 서브메뉴는 부모 패널의 스크롤에 함께 실립니다. 자기 스크롤러를 따로 만들지 않습니다.

사용 가이드라인 (Usage Guidelines)#

✅ Do#

독립적으로 쓸 때는 ariaLabel 지정

Menu(
  ariaLabel: 'Main menu',
  items: [
    CoreMenuItem.action('Home', onPressed: () {}),
    CoreMenuItem.action('Profile', onPressed: () {}),
  ],
)

Web 은 role="menu" 만으로는 이름 붙은 조상 트리거가 없으면 스크린 리더가 메뉴의 목적을 알 수 없습니다. ariaLabel 은 Flutter Semantics 컨테이너 라벨로도 동일하게 노출됩니다.


❌ Don't#

커스텀 오버레이 안에서 onActivate 연결을 빠뜨리지 않기

// ❌ onActivate 미연결 — 액션 클릭 후에도 패널이 열린 채로 남음
Popover(
  content: Menu(items: actionItems),
)

// ✅ onActivate 로 닫기 로직 연결
Popover(
  content: Menu(
    items: actionItems,
    onActivate: closePopover,
  ),
)

Menu 자신은 열림/닫힘 상태를 관리하지 않습니다 — action 항목이 활성화되면 onActivate 가 호출될 뿐이므로, 직접 만든 오버레이 안에 Menu 를 넣었다면 그 콜백으로 닫기 로직을 연결해야 합니다(DropdownMenu/ContextMenu/Menubar 는 이미 이 배선을 대신 해줍니다).

접근성 (Accessibility)#

Menu 는 항목 목록을 그립니다. 열고 닫기·포커스·Escape 는 이것을 합성하는 호스트(DropdownMenu·ContextMenu·Menubar·NavigationMenu)와 그 오버레이가 소유하므로, 아래는 두 층을 나눠 적습니다.

역할#

  • Flutter — 루트 CoUISemantics가 vertical이면 menu, 가로이면 menuBar 역할과 ariaLabel을 노출합니다. MenuRowScope 안의 MenuItem은 menuItem 역할·enabled·selected를 사용하며 이름은 행 콘텐츠에서 옵니다.
  • Web — 컨테이너는 role="menu"(세로) / role="menubar"(가로) + aria-orientation. 행 role 은 항목 종류에서 파생됩니다 — action menuitem, checkbox menuitemcheckbox + aria-checked, radio menuitemradio + aria-checked. Menu 가 attributes 로 MenuItem 에 통과시키고(MenuItem 단독 기본값은 menuitem), 서브메뉴 트리거는 aria-haspopup="menu", 비활성 행은 aria-disabled="true", active 행은 aria-current 를 답니다.

키보드#

키FlutterWeb
Enter / Space포커스된 항목 활성화포커스된 항목 활성화
Tab 항목마다 개별 tab stop 메뉴 전체가 한 tab stop (roving tabindex — 활성 행 0, 나머지 -1)
ArrowUp / ArrowDown 프레임워크 방향성 순회 (메뉴 의미론 없음) 이전 / 다음 행, 끝에서 순환
Home / End없음첫 / 마지막 행
문자 키없음그 글자로 시작하는 다음 행으로 점프 (typeahead)

Web 의 화살표·Home/End·typeahead 는 비활성 행도 건너뛰지 않습니다 — WAI-ARIA APG 대로 enabled: false 행은 포커스를 받되 활성화만 무시합니다 (inert 지 invisible 이 아닙니다). Flutter 는 아직 메뉴 탐색을 구현하지 않았습니다 — 화살표가 포커스를 옮기는 것은 프레임워크의 방향성 순회이지 메뉴 탐색이 아닙니다(순환·Home/End·typeahead 없음).

Escape 는 Menu 자신이 아니라 호스트가 제공합니다:

호스트FlutterWeb
DropdownMenu · ContextMenu · NavigationMenu 닫힘 닫힘
Menubar없음닫힘

Flutter Menubar 는 non-modal 로 열려 오버레이의 Escape 배선을 받지 않습니다.

브라우저 검증 — 이 컴포넌트의 Web 키보드 이동은 야간 파리티 스윕의 키보드 프로브(tool/parity/keyboard_probe.dart)가 하이드레이션된 문서 사이트에서 실제로 키를 눌러, 포커스가 기대한 항목으로 옮겨 갔는지 확인합니다. 핸들러가 달렸는지가 아니라 포커스가 어디에 있는지를 봅니다.

스크린 리더#

  • Flutter — 접근 가능한 이름의 출처는 ariaLabel 하나입니다(Semantics 컨테이너 라벨). 항목의 역할·체크 상태는 전달되지 않습니다.
  • Web — 항목 상태를 aria-checked(checkbox/radio 항목) · aria-current (현재 항목) · aria-disabled 로 알리고, 방향은 aria-orientation 으로 전달합니다.

항목 수와 위치는 통보되지 않습니다 — 리더가 "N개 중 3번째" 를 읽으려면 aria-setsize/aria-posinset 이 필요한데 양쪽 다 없습니다. 메뉴가 열리고 닫힌 사실도 live region 으로 통보되지 않고, 트리거의 확장 상태로만 노출됩니다.

포커스 관리#

  • 진입 — Menu 자신은 포커스를 옮기지 않습니다. Flutter 의 modal 호스트 (DropdownMenu·ContextMenu·NavigationMenu)는 오버레이가 열릴 때 패널 스코프로 포커스를 옮깁니다(첫 항목이 아니라 패널입니다).
  • 트랩 — Flutter modal 호스트에서는 Tab 이 패널 안에 갇히고, 배경이 스크린 리더에서 가려집니다. Web 은 갇히지 않아 메뉴(한 tab stop) 다음 Tab 이 패널 밖으로 나갑니다.
  • 이탈 — 닫을 때 트리거로 포커스를 되돌리는 코드가 양 플랫폼 어디에도 없습니다.

알려진 제약#

  • Flutter 는 항목 수만큼 tab stop 이 생깁니다 (Web 은 하나).
  • 메뉴가 닫힌 뒤 포커스가 어디로 가는지 보장되지 않습니다.
  • 포커스 트랩과 배경 격리가 Flutter modal 호스트에만 있어, 같은 메뉴라도 플랫폼과 호스트에 따라 키보드 경험이 다릅니다.

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
패널menu / menuBar 시맨틱 안 Popuprole=menu / menubar의 flex 패널
항목 CoreMenuItem 팩토리 (양쪽 동일 API) CoreMenuItem 팩토리 (양쪽 동일 API)
구분선의미 있는 Divider의미 있는 Divider (role=separator)
서브메뉴인라인 중첩 Menu인라인 중첩 Menu
아이콘IconIcon
ARIA Flutter Semantics role / aria-* 속성
항목 탐색항목별 Tab · Enter / Spaceroving · 화살표 · Home / End · typeahead
긴 목록 세로 Menu 가 받은 높이 안에서 스크롤 호스트가 알린 높이를 상자의 max-height 로 걸고, 상자 안의 목록이 overflow-y: auto
  • DropdownMenu: 트리거로 떠오르는 오버레이 메뉴
  • ContextMenu: 우클릭/롱프레스 컨텍스트 메뉴
  • Menubar: 가로 애플리케이션 메뉴바
  • Popover: 임의의 콘텐츠를 표시하는 팝업