PlMenu
무언가를 눌렀을 때 나타나는 동작 목록입니다. roving 포커스와 타이프어헤드, 안전 삼각형이 딸린 서브메뉴, 그리고 그 모두를 스크린 리더에게 뜻이 통하게 하는 role까지 들어 있습니다.
import { PlButton, PlMenu, PlMenuItem, PlMenuSeparator } from 'plass-ui';
<PlMenu trigger={<PlButton variant="glass">Actions</PlButton>}>
<PlMenuItem shortcut="⌘X">Cut</PlMenuItem>
<PlMenuItem shortcut="⌘C">Copy</PlMenuItem>
<PlMenuSeparator />
<PlMenuItem color="danger">Delete</PlMenuItem>
</PlMenu>;import 'package:plass_ui/plass_ui.dart';
PlMenu(
items: <PlMenuEntry>[
PlMenuItem(label: 'Cut', shortcut: '⌘X', onPressed: cut),
PlMenuItem(label: 'Copy', shortcut: '⌘C', onPressed: copy),
const PlMenuSeparator(),
PlMenuItem(label: 'Delete', color: PlassColor.danger, onPressed: remove),
],
trigger: (BuildContext context, VoidCallback open, bool isOpen) =>
PlButton(onPressed: open, variant: PlassVariant.glass, child: const Text('Actions')),
);Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| trigger | ReactElement | — | 메뉴를 여는 요소. 선택 사항입니다 — 밖에서 여는 controlled 메뉴에는 트리거가 필요 없습니다 |
| open · defaultOpen · onOpenChange | boolean · boolean · (open: boolean) => void | — | 열림 상태. controlled와 uncontrolled 양쪽 |
| side | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | 트리거의 어느 가장자리에 매달릴지 |
| align | 'start' | 'center' | 'end' | 'start' | 그 가장자리를 따라 어디에 놓일지 |
| sideOffset | number | 6 | 트리거와의 거리(px) |
| modal | boolean | true | 열려 있는 동안 뒤 페이지를 가져갈지 |
| openOnHover | boolean | false | 클릭뿐 아니라 호버로도 엽니다. 메뉴 바를 위한 것입니다 |
| loopFocus | boolean | true | 화살표 키가 마지막 행에서 첫 행으로 돌아갈지 |
| disabled | boolean | false | 트리거가 아무것도 열지 않습니다 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 팝업의 반경, 타입 스케일, 행 패딩 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. 행마다 덮어쓸 수 있습니다 |
| density공통 | 'default' | 'compact' | 'default' | 행의 패딩만 바꿉니다 |
| children | ReactNode | — | 행들 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| items * | List<PlMenuEntry> | — | 행들. children이 아니라 설명의 목록입니다 — 메뉴가 강조 이동과 타이프어헤드를 직접 소유해야 합니다 |
| trigger * | Widget Function(BuildContext, VoidCallback open, bool isOpen) | — | 메뉴를 여는 것. 위젯이 아니라 빌더입니다 — 트리거는 거의 언제나 자기가 열려 있는지 알고 싶어 합니다 |
| size공통 | PlassSize | PlassSize.md | 팝업의 반경, 타입 스케일, 행 패딩 |
| color공통 | PlassColor | PlassColor.primary | 의미론적 색 역할. 행마다 덮어쓸 수 있습니다 |
| density공통 | PlassDensity | PlassDensity.standard | 행의 패딩만 바꿉니다 |
| side | PlassSide | PlassSide.bottom | 트리거의 어느 가장자리에 매달릴지 |
| align | PlassAlign | PlassAlign.start | 그 가장자리를 따라 어디에 놓일지 |
| sideOffset | double | 6 | 트리거와의 거리(px) |
| loopFocus | bool | true | 화살표 키가 마지막 행에서 첫 행으로 돌아갈지 |
| disabled | bool | false | 트리거가 아무것도 열지 않습니다 |
| onOpenChange | ValueChanged<bool>? | — | 메뉴가 열리거나 닫힐 때마다 호출됩니다 |
| label | String? | — | 스크린 리더가 팝업을 부르는 이름 |
PlMenuItem
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| onClick | (event: MouseEvent) => void | — | 행이 하는 일. 주지 않고 링크도 아니면 행은 라벨입니다 |
| href · target | string | — | 행을 진짜 <a>로 렌더링합니다. 링크들의 메뉴는 링크여야 합니다 |
| startIcon · endIcon | ReactNode | — | 라벨 앞뒤의 슬롯 |
| shortcut | ReactNode | — | 같은 일을 하는 키 조합. 행 끝에 흐리게 놓입니다. 텍스트일 뿐이고 바인딩은 앱의 몫입니다 |
| description | ReactNode | — | 라벨 아래 한 줄. 한 단계 작고 흐립니다 |
| color | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | — | 행의 색 가족을 다시 겨눕니다 — 삭제하는 행에 danger |
| closeOnClick | boolean | true | 행을 고르면 메뉴가 닫힐지 |
| disabled | boolean | false | 쓸 수 없습니다. 목록에는 남고 타이프어헤드에도 잡힙니다 |
| label | string | — | 라벨이 평범한 문자열이 아닐 때 타이프어헤드가 맞춰 볼 문자열 |
| children | ReactNode | — | 라벨 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| label * | String | — | 라벨. 위젯이 아니라 String입니다 — 그려지고, 안내되고, 타이프어헤드가 맞춰 보는 대상입니다 |
| onPressed | VoidCallback? | — | 행이 하는 일. 주지 않고 링크도 아니면 행은 라벨입니다 |
| startIcon · endIcon | Widget? | — | 라벨 앞뒤의 슬롯 |
| shortcut | String? | — | 같은 일을 하는 키 조합. 행 끝에 흐리게 놓입니다. 텍스트일 뿐이고 바인딩은 앱의 몫입니다 |
| description | String? | — | 라벨 아래 한 줄. 한 단계 작고 흐립니다 |
| color | PlassColor? | — | 행의 색 가족을 다시 겨눕니다 — 삭제하는 행에 danger |
| closeOnPress | bool | true | 행을 고르면 메뉴가 닫힐지 |
| disabled | bool | false | 쓸 수 없습니다. 목록에는 남고 타이프어헤드에도 잡힙니다 |
PlMenuCheckboxItem과 PlMenuRadioItem
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| checked · defaultChecked · onCheckedChange | boolean · boolean · (checked: boolean) => void | — | 체크 상태 |
| value * | string | number | — | PlMenuRadioItem에만: 이 행이 그룹을 무엇으로 설정할지 |
| closeOnClick | boolean | false | 체크했을 때 메뉴가 닫힐지. 평범한 행과 달리 기본이 false입니다 |
| endIcon · shortcut · description · color · disabled · label | — | — | PlMenuItem과 같습니다 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| checked · onChanged | bool · ValueChanged<bool>? | — | 체크 상태. controlled입니다 — defaultChecked는 없습니다 |
| selected · onPressed | bool · VoidCallback? | — | PlMenuRadioItem에만: 이 행이 고른 것인지, 그리고 고르면 무엇을 하는지. 값을 쥔 그룹이 아니라 행이 듣습니다 |
| closeOnPress | bool | false | 체크했을 때 메뉴가 닫힐지. 평범한 행과 달리 기본이 false입니다 |
| label · endIcon · shortcut · description · color · disabled | — | — | PlMenuItem과 같습니다 |
PlMenuSubmenu
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| label | ReactNode | — | 서브메뉴를 여는 행의 라벨 |
| startIcon | ReactNode | — | 라벨 앞의 슬롯 |
| side | 'top' | 'right' | 'bottom' | 'left' | 'right' | 부모 행의 어느 가장자리에서 열릴지 |
| sideOffset | number | 4 | 부모 메뉴와의 거리(px) |
| disabled | boolean | false | 서브메뉴를 열 수 없습니다 |
| children | ReactNode | — | 중첩된 행들 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| label * | String | — | 서브메뉴를 여는 행의 라벨 |
| items * | List<PlMenuEntry> | — | 중첩된 행들 |
| startIcon | Widget? | — | 라벨 앞의 슬롯 |
| disabled | bool | false | 서브메뉴를 열 수 없습니다 |
PlContextMenu
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| content * | ReactNode | — | PlMenu 안에 쓰는 것과 똑같은 행들 |
| children * | ReactNode | — | 오른쪽 클릭이나 길게 누르기에 응답하는 영역 |
| open · defaultOpen · onOpenChange | boolean · boolean · (open: boolean) => void | — | 열림 상태 |
| loopFocus | boolean | true | 화살표 키가 마지막 행에서 첫 행으로 돌아갈지 |
| disabled | boolean | false | 영역이 아무것도 열지 않습니다 |
| size · color · density공통 | — | — | PlMenu와 같습니다 |
Flutter 패키지에는 아직 PlContextMenu가 없습니다.
variant은 없습니다. PlModal에 없는 것과 같은 이유입니다. 세 재질은 "이 표면이 페이지에 대해 얼마나 자기를 선언하는가"에 답하는데, 포인터를 가져간 팝업은 이미 그 질문에 답했습니다. elevation도 없습니다. 메뉴는 진짜로 떠 있고, 그것이 사다리가 존재하는 유일한 경우라서 맨 윗단에 고정되어 있습니다. 라이브러리 전체에서 공유 축이 뜻하는 바는 prop 규칙에 있습니다.
Examples
items 배열
데이터가 아니라 조합입니다. PlSelect의 정반대이고, 의도한 것입니다.
select의 옵션은 호출자가 이미 가진 목록에서 나온 값이라 데이터입니다. 메뉴의 행은 코드입니다. 각각 다른 핸들러, 다른 아이콘, 때로는 링크, 때로는 서브메뉴. 데이터로 넘기면 행이 취할 수 있는 모양마다 변형을 둔 items 타입이 되는데, 그것은 판별 유니온으로 쓴 컴포넌트 트리입니다.
조합이 아니라 설명입니다. 두 패키지가 컴포넌트의 모양에 대해 의견이 갈리는 유일한 자리입니다.
강제된 것입니다. React가 조합할 수 있는 이유는 Base UI가 행이 쓰인 DOM을 읽기 때문입니다. 누가 목록을 건네주지 않아도 행을 찾고, 세고, roving 강조를 옮기고, 타이프어헤드를 맞춰 봅니다. 여기에는 걸어 다닐 트리가 없어서 메뉴에게 말해 주어야 합니다. PlAccordion, PlTabs, PlSelect가 모두 설명을 받는 것과 같은 이유입니다.
PlMenuEntry는 판별자를 단 클래스 하나가 아니라 sealed 계층입니다. 그래야 행이 서로 다른 종류의 것이 되고, 그 위의 switch가 검사를 받습니다.
import { PlButton, PlMenu, PlMenuItem, PlMenuSeparator } from 'plass-ui';
const Star = () => (
<svg viewBox="0 0 16 16" fill="currentColor">
<path d="m8 1.6 1.86 3.9 4.14.56-3.02 2.9.76 4.24L8 11.16 4.26 13.2l.76-4.24L2 6.06l4.14-.56z" />
</svg>
);
export default function MenuRows() {
return (
<PlMenu trigger={<PlButton variant="glass">Rows</PlButton>}>
<PlMenuItem startIcon={<Star />} shortcut="⌘D">
With an icon
</PlMenuItem>
<PlMenuItem description="A second line, one step down and muted">
With a description
</PlMenuItem>
<PlMenuItem href="https://plass.cdget.com" target="_blank">
A real link
</PlMenuItem>
<PlMenuItem disabled>Unavailable</PlMenuItem>
<PlMenuSeparator />
<PlMenuItem color="danger">Delete everything</PlMenuItem>
</PlMenu>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
import 'package:plass_ui_example/demos/glyphs.dart';
class MenuRows extends StatelessWidget {
const MenuRows({super.key});
@override
Widget build(BuildContext context) {
return PlMenu(
items: const <PlMenuEntry>[
PlMenuItem(label: 'With an icon', startIcon: StarGlyph(), shortcut: '⌘D'),
PlMenuItem(
label: 'With a description',
description: 'A second line, one step down and muted',
),
PlMenuItem(label: 'Unavailable', disabled: true),
PlMenuSeparator(),
PlMenuItem(label: 'Delete everything', color: PlassColor.danger),
],
trigger: (BuildContext context, VoidCallback open, bool isOpen) =>
PlButton(onPressed: open, variant: PlassVariant.glass, child: const Text('Rows')),
);
}
}행의 color
행은 자기 색 가족을 부를 수 있습니다. 삭제하는 행에 danger. 그리고 슬롯을 행에서 다시 선언하기 때문에, 색조 · 얇은 선 · 글자가 하나만 파랗게 남는 일 없이 한꺼번에 넘어갑니다.
기본값 옆에 클래스를 덧붙이는 대신 분기합니다. 한 요소에 붙은 명시도가 같은 Tailwind 유틸리티 둘은 쓴 순서가 아니라 생성된 스타일시트 안의 순서로 승부가 나므로, 덧붙인 강조색은 어떤 빌드에서는 아무 일도 하지 않고 어떤 빌드에서는 동작했을 것입니다.
그룹과 구분선
그룹의 라벨은 행이 아니라 제목입니다. 고를 수 없고, 타이프어헤드에도 잡히지 않으며, Base UI가 그 아래 행들과 연결해 줍니다.
import { PlButton, PlMenu, PlMenuGroup, PlMenuItem, PlMenuSeparator } from 'plass-ui';
export default function MenuGroups() {
return (
<PlMenu trigger={<PlButton variant="glass">Grouped</PlButton>}>
<PlMenuGroup label="Edit">
<PlMenuItem shortcut="⌘X">Cut</PlMenuItem>
<PlMenuItem shortcut="⌘C">Copy</PlMenuItem>
</PlMenuGroup>
<PlMenuSeparator />
<PlMenuGroup label="Document">
<PlMenuItem shortcut="⌘S">Save</PlMenuItem>
<PlMenuItem shortcut="⌘P">Print</PlMenuItem>
</PlMenuGroup>
</PlMenu>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class MenuGroups extends StatelessWidget {
const MenuGroups({super.key});
@override
Widget build(BuildContext context) {
return PlMenu(
items: const <PlMenuEntry>[
PlMenuGroup(
label: 'Edit',
items: <PlMenuEntry>[
PlMenuItem(label: 'Cut', shortcut: '⌘X'),
PlMenuItem(label: 'Copy', shortcut: '⌘C'),
],
),
PlMenuSeparator(),
PlMenuGroup(
label: 'Document',
items: <PlMenuEntry>[
PlMenuItem(label: 'Save', shortcut: '⌘S'),
PlMenuItem(label: 'Print', shortcut: '⌘P'),
],
),
],
trigger: (BuildContext context, VoidCallback open, bool isOpen) =>
PlButton(onPressed: open, variant: PlassVariant.glass, child: const Text('Grouped')),
);
}
}체크와 선택
체크 행은 틱으로, 라디오 행은 점으로 표시됩니다. PlCheckbox와 PlRadioGroup이 다른 모든 곳에서 하는 것과 같은 구분입니다: 틱은 "그리고", 점은 "대신에".
둘 다 골랐을 때 열린 채로 남는 것이 기본입니다. 평범한 행의 기본값 true와 반대입니다. 체크할 것들의 목록은 하나보다 많이 체크하는 목록이기 때문입니다.
라디오 그룹은 없습니다. 이 패키지의 모든 입력이 controlled이므로, PlMenuRadioItem은 자기가 고른 것인지를 듣고 눌렸다는 사실을 보고합니다. 값을 쥔 그룹은 라이브러리에서 보고하고 잊지 않는 유일한 것이 되었을 것입니다.
import { useState } from 'react';
import {
PlButton,
PlMenu,
PlMenuCheckboxItem,
PlMenuGroup,
PlMenuRadioGroup,
PlMenuRadioItem,
PlMenuSeparator
} from 'plass-ui';
export default function MenuSelection() {
const [wrap, setWrap] = useState(true);
const [minimap, setMinimap] = useState(false);
const [layout, setLayout] = useState<string | number>('list');
return (
<PlMenu trigger={<PlButton variant="glass">View</PlButton>}>
<PlMenuGroup label="Show">
<PlMenuCheckboxItem checked={wrap} onCheckedChange={setWrap} shortcut="⌥Z">
Word wrap
</PlMenuCheckboxItem>
<PlMenuCheckboxItem checked={minimap} onCheckedChange={setMinimap}>
Minimap
</PlMenuCheckboxItem>
</PlMenuGroup>
<PlMenuSeparator />
<PlMenuGroup label="Layout">
<PlMenuRadioGroup value={layout} onValueChange={setLayout}>
<PlMenuRadioItem value="list">List</PlMenuRadioItem>
<PlMenuRadioItem value="grid">Grid</PlMenuRadioItem>
<PlMenuRadioItem value="columns">Columns</PlMenuRadioItem>
</PlMenuRadioGroup>
</PlMenuGroup>
</PlMenu>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class MenuSelection extends StatefulWidget {
const MenuSelection({super.key});
@override
State<MenuSelection> createState() => _MenuSelectionState();
}
class _MenuSelectionState extends State<MenuSelection> {
bool _wrap = true;
bool _minimap = false;
String _layout = 'list';
@override
Widget build(BuildContext context) {
return PlMenu(
items: <PlMenuEntry>[
PlMenuGroup(
label: 'Show',
items: <PlMenuEntry>[
PlMenuCheckboxItem(
label: 'Word wrap',
shortcut: '⌥Z',
checked: _wrap,
onChanged: (bool next) => setState(() => _wrap = next),
),
PlMenuCheckboxItem(
label: 'Minimap',
checked: _minimap,
onChanged: (bool next) => setState(() => _minimap = next),
),
],
),
const PlMenuSeparator(),
PlMenuGroup(
label: 'Layout',
items: <PlMenuEntry>[
for (final String layout in <String>['list', 'grid', 'columns'])
PlMenuRadioItem(
label: layout,
selected: _layout == layout,
onPressed: () => setState(() => _layout = layout),
),
],
),
],
trigger: (BuildContext context, VoidCallback open, bool isOpen) =>
PlButton(onPressed: open, variant: PlassVariant.glass, child: const Text('View')),
);
}
}서브메뉴
서브메뉴를 여는 행은 다른 모든 항목과 같은 행이고, 셰브런을 하나 달고 있을 뿐입니다. 호버로, Enter로, 그리고 그쪽을 가리키는 화살표 키로 열립니다. 대각선으로 움직여도 닫히지 않습니다. Base UI가 포인터에서 팝업까지의 안전 삼각형을 추적합니다.
중첩에는 제한이 없습니다. 서브메뉴는 자식을 그 자체가 메뉴인 팝업 안에 렌더링하므로, 서브메뉴의 서브메뉴에 다른 컴포넌트가 필요하지 않습니다.
import { PlButton, PlMenu, PlMenuItem, PlMenuSeparator, PlMenuSubmenu } from 'plass-ui';
export default function MenuSubmenus() {
return (
<PlMenu trigger={<PlButton variant="glass">Share</PlButton>}>
<PlMenuItem>Copy link</PlMenuItem>
<PlMenuSeparator />
<PlMenuSubmenu label="Send to">
<PlMenuItem>Email</PlMenuItem>
<PlMenuItem>Message</PlMenuItem>
<PlMenuSubmenu label="More">
<PlMenuItem>Print</PlMenuItem>
<PlMenuItem>Fax, apparently</PlMenuItem>
</PlMenuSubmenu>
</PlMenuSubmenu>
<PlMenuSubmenu label="Export as">
<PlMenuItem>PDF</PlMenuItem>
<PlMenuItem>Markdown</PlMenuItem>
</PlMenuSubmenu>
</PlMenu>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class MenuSubmenus extends StatelessWidget {
const MenuSubmenus({super.key});
@override
Widget build(BuildContext context) {
return PlMenu(
items: const <PlMenuEntry>[
PlMenuItem(label: 'Copy link'),
PlMenuSeparator(),
PlMenuSubmenu(
label: 'Send to',
items: <PlMenuEntry>[
PlMenuItem(label: 'Email'),
PlMenuItem(label: 'Message'),
PlMenuSubmenu(
label: 'More',
items: <PlMenuEntry>[
PlMenuItem(label: 'Print'),
PlMenuItem(label: 'Fax, apparently'),
],
),
],
),
PlMenuSubmenu(
label: 'Export as',
items: <PlMenuEntry>[
PlMenuItem(label: 'PDF'),
PlMenuItem(label: 'Markdown'),
],
),
],
trigger: (BuildContext context, VoidCallback open, bool isOpen) =>
PlButton(onPressed: open, variant: PlassVariant.glass, child: const Text('Share')),
);
}
}size와 density
size는 팝업의 반경, 타입 스케일, 그리고 행 패딩 사다리를 정합니다. density는 패딩만 건드립니다.
행은 시트 트랙이 아니라 자기 패딩 트랙을 씁니다. PlList의 행은 다른 무언가가 너비를 정한 시트를 가로지르지만, 메뉴의 행은 가장 긴 라벨만큼만 넓은 팝업 안에 있습니다. 시트 트랙의 px-5는 "Cut"이라고 적힌 메뉴에 40px를 더하고, 그렇게 다섯 줄짜리 메뉴가 다이얼로그만큼 넓어집니다.
import { PlButton, PlMenu, PlMenuItem } from 'plass-ui';
export default function MenuSizes() {
return (
<div className="flex flex-wrap items-center gap-3">
{(['sm', 'md', 'lg'] as const).map((size) => (
<PlMenu
key={size}
size={size}
trigger={
<PlButton size={size} variant="glass">
{size}
</PlButton>
}
>
<PlMenuItem shortcut="⌘X">Cut</PlMenuItem>
<PlMenuItem shortcut="⌘C">Copy</PlMenuItem>
</PlMenu>
))}
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class MenuSizes extends StatelessWidget {
const MenuSizes({super.key});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: <Widget>[
for (final PlassSize size in <PlassSize>[PlassSize.sm, PlassSize.md, PlassSize.lg])
PlMenu(
size: size,
items: const <PlMenuEntry>[
PlMenuItem(label: 'Cut', shortcut: '⌘X'),
PlMenuItem(label: 'Copy', shortcut: '⌘C'),
],
trigger: (BuildContext context, VoidCallback open, bool isOpen) => PlButton(
size: size,
onPressed: open,
variant: PlassVariant.glass,
child: Text(size.name),
),
),
],
);
}
}PlContextMenu
버튼이 아니라 오른쪽 클릭이나 길게 누르기로 열리는 같은 메뉴입니다.
행을 content로, 영역을 children으로 받습니다. PlMenu가 아니라 PlTooltip의 모양인데, 여기서 트리거는 넘겨주는 요소 하나가 아니라 페이지의 한 영역이고, 감싸이는 것이 바로 그 영역이기 때문입니다. 팝업은 앵커가 아니라 포인터 위치에 놓이고, 길게 누르기가 있어야 터치 화면에서 닿을 수 있습니다.
import { PlContextMenu, PlMenuItem, PlMenuSeparator } from 'plass-ui';
export default function MenuContextDemo() {
return (
<PlContextMenu
content={
<>
<PlMenuItem shortcut="⌘R">Rename</PlMenuItem>
<PlMenuItem shortcut="⌘D">Duplicate</PlMenuItem>
<PlMenuSeparator />
<PlMenuItem color="danger">Delete</PlMenuItem>
</>
}
>
<div className="flex h-28 w-full max-w-sm items-center justify-center rounded-(--plass-radius-lg) border border-dashed border-(--plass-border) text-sm text-(--plass-muted-fg)">
Right-click, or press and hold
</div>
</PlContextMenu>
);
}Accessibility
- Base UI의 Menu 위에 있습니다. 메뉴를 떠다니는
<div>목록이 아니라 메뉴로 만드는 것 전부를 그쪽이 맡습니다:menu와menuitemrole, 화살표 키의 roving 포커스, Home과 End, 타이프어헤드, Esc, 바깥 클릭으로 닫기, 그리고 트리거로 포커스 되돌리기. href가 있는 행은 진짜<a>입니다. 링크가 아닌 링크들의 메뉴는 새 탭으로 열 수도, 복사할 수도 없고, 스크린 리더에게는 그 하나하나에 대해 틀린 말을 합니다.- 행에는 포커스 링이 없습니다. Base UI가 강조된 행 자체로 포커스를 옮기므로, 링이 있으면 화살표를 누를 때마다 팝업 안에 사각형이 그려집니다. 색조가 포커스 표시이고, 그래서 마우스가 받는 것과 같은 표시가 됩니다.
- 행에 불이 들어오는 기준은
:hover가 아니라data-highlighted입니다. 그래서 키보드와 포인터가 같은 행에 불을 켭니다. - 비활성 행도 목록에 남고 타이프어헤드에도 잡힙니다. 쓸 수 없을 때 사라지는 행은 길이가 바뀌는 메뉴입니다.
- 팝업은 불투명도만 애니메이션합니다. 미끄러져 들어오는 메뉴는 이미 누르려던 행을 옮겨 버리고, 그것이 메뉴가 절대 해서는 안 되는 단 하나입니다.
- 팝업이 떠 있는 동안 포커스는 트리거에 남습니다.
PlSelect가 하는 것과 같고 이유도 같습니다. 행은 오버레이에 그려지고, 함께 들려 올라간 포커스 스코프는 그 키를 어떻게 다룰지 아는 위젯에서 키보드를 빼앗아 갑니다. 화살표 ·Home·End·Esc·Enter· 타이프어헤드가 모두 거기에 묶여 있습니다. - 포인터는 화살표 키가 옮기는 것과 같은 강조를 옮깁니다. 그래서 마우스와 키보드가 두 행이 아니라 한 행에 불을 켜고, 바깥 메뉴의 행으로 옮겨 가는 것이 옆에 열려 있던 서브메뉴를 닫습니다.
- 행은 이름과 동작을 든 버튼 노드입니다. 체크된 행은 checked로, 고른 행은 상호 배타적 집합 안에서 selected로 표시됩니다. 안에 그려진 것은 전부 제외되므로 글리프가 읽을 것 하나를 더 만들지 않습니다.
- 서브메뉴를 여는 화살표는 쓰기 방향을 따릅니다. RTL에서는 반대로 움직입니다.
- 비활성 행도 목록에 남고 타이프어헤드에도 잡힙니다. 쓸 수 없을 때 사라지는 행은 길이가 바뀌는 메뉴입니다.
React 빌드와 다른 점
| React | Flutter | 이유 |
|---|---|---|
| 조합된 행 | items: List<PlMenuEntry> | Base UI는 행이 쓰인 DOM을 읽습니다. 여기에는 걸어 다닐 트리가 없어서 메뉴에게 무엇을 들고 있는지 말해 주어야 합니다. |
행의 children | String인 label | 그려지고, 안내되고, 타이프어헤드가 맞춰 보는 대상입니다. 셋 다 될 수 있는 것은 문자열뿐입니다. |
PlMenuRadioGroup | PlMenuRadioItem.selected | 이 패키지의 모든 입력이 controlled입니다. 값을 쥔 그룹은 그렇지 않은 유일한 것이 되었을 것입니다. |
요소인 trigger | 빌더인 trigger | 메뉴를 여는 콜백과 열려 있는지를 함께 받습니다. 열린 동안 켜져 있는 트리거에 필요한 것이 그것입니다. |
행의 href | — | 링크 요소도 없고 Flutter 앱을 크롤링하는 것도 없습니다. 라우터를 부르는 자리는 onPressed입니다. |
modal | — | 팝업은 화면 위에 덮이는 것이 아니라 앵커에 매달립니다. 바깥에 떨어진 누름이 그것을 닫습니다. |
PlContextMenu | — | 이 패키지가 도는 모든 플랫폼에서 같은 뜻을 갖는 오른쪽 클릭 제스처가 없습니다. 길게 눌러 여는 메뉴는 onLongPress와 앱이 직접 여는 PlMenu입니다. |
className, style | — | 전달할 클래스 목록도 style 속성도 없습니다. |