PlNavigationMenu
사이트의 내비게이션입니다. 목적지의 행이고, 그중 일부는 더 많은 목적지가 든 패널을 엽니다. 모든 행이 진짜 링크이고, 이것이 menu가 아닌 이유가 전부 거기 있습니다.
import { PlNavigationMenu, PlNavigationMenuItem, PlNavigationMenuLink } from 'plass-ui';
<PlNavigationMenu>
<PlNavigationMenuItem label="Product" columns={2}>
<PlNavigationMenuLink href="/analytics" title="Analytics" description="Numbers over time" />
</PlNavigationMenuItem>
<PlNavigationMenuItem label="Pricing" href="/pricing" />
</PlNavigationMenu>;import 'package:plass_ui/plass_ui.dart';
PlNavigationMenu(
items: <PlNavigationMenuItem>[
PlNavigationMenuItem(
label: 'Product',
columns: 2,
links: <PlNavigationMenuLink>[
PlNavigationMenuLink(title: 'Analytics', onPressed: openAnalytics),
],
),
PlNavigationMenuItem(label: 'Pricing', onPressed: openPricing),
],
);Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 행의 높이와 타입 스케일. 패널의 반경과 여백도 함께 갑니다 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. hover와 열린 패널과 focus ring까지 갑니다 — 시트에는 색이 들어가지 않습니다 |
| density공통 | 'default' | 'compact' | 'default' | 여백만 바꿉니다 |
| orientation공통 | 'horizontal' | 'vertical' | 'horizontal' | 행이 늘어서는 방향. vertical은 패널이 옆으로 열리는 nav rail입니다 |
| value | string | null | — | 어느 항목의 패널이 열려 있는지, value로. nullish는 닫힘입니다 |
| defaultValue | string | null | — | 어느 것이 열린 채로 시작할지 |
| onValueChange | (value: string | null) => void | — | 열린 패널이 바뀔 때 |
| delay | number | — | 패널이 열리기까지 포인터가 머무는 시간, 밀리초 |
| closeDelay | number | — | 포인터가 떠난 뒤 패널이 남는 시간, 밀리초 |
| sideOffset | number | 8 | 행에서 떨어진 거리, 픽셀 |
| children | ReactNode | — | 항목들 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| size공통 | PlassSize | PlassSize.md | 행의 높이와 타입 스케일. 패널의 반경과 여백도 함께 갑니다 |
| color공통 | PlassColor | PlassColor.primary | 의미론적 색 역할. hover와 열린 패널과 focus ring까지 갑니다 — 시트에는 색이 들어가지 않습니다 |
| density공통 | PlassDensity | PlassDensity.standard | 여백만 바꿉니다 |
| items * | List<PlNavigationMenuItem> | — | 항목들. 여기서는 조합된 자식이 아니라 데이터입니다 — 어느 항목이 양 끝인지, 어느 것이 열려 있는지를 행이 알아야 하기 때문입니다 |
| initialValue | String? | — | 어느 항목의 패널이 열린 채로 시작할지. controlled 모드는 없습니다 — 어느 패널이 열려 있는지는 앱의 데이터가 아니라 포인터와 키보드의 것입니다 |
| onValueChanged | ValueChanged<String?>? | — | 열린 패널이 바뀔 때 |
| orientation공통 | PlassOrientation | PlassOrientation.horizontal | 행이 늘어서는 방향. vertical은 패널이 옆으로 열리는 nav rail입니다 |
| delay | Duration | Duration(milliseconds: 50) | 패널이 열리기까지 포인터가 머무는 시간, 밀리초 |
| closeDelay | Duration | Duration(milliseconds: 100) | 포인터가 떠난 뒤 패널이 남는 시간, 밀리초 |
| sideOffset | double | 8 | 행에서 떨어진 거리, 픽셀 |
| semanticLabel | String? | — | 내비게이션 영역이 불리는 이름. 화면에 둘 이상 있을 때 반드시 써야 합니다 |
네이티브 <nav> 속성은 모두 그대로 전달됩니다. color는 여기서 Plass의 prop이라, defaultValue와 onChange는 value와 onValueChange로 표기하기 때문에 제외됩니다.
PlNavigationMenuItem
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| label * | ReactNode | — | 행에 쓰이는 단어 |
| href | string | — | 패널을 여는 대신 그냥 링크로 만듭니다. href만 있고 children이 없는 항목은 목적지이고, 그렇게 알려집니다 |
| target | string | — | 링크가 열리는 곳. 이 탭이 아니면 rel에 noopener noreferrer가 합쳐집니다 |
| rel | string | — | 링크의 rel |
| startIcon | ReactNode | — | 라벨 앞에 놓이는 내용 |
| value | string | — | controlled 메뉴에서 항목을 식별합니다 |
| disabled | boolean | false | 쓸 수 없습니다. 단어는 행에 남고 아무것도 열지 않습니다 |
| columns | number | 1 | 패널이 링크를 몇 열로 배치할지 |
| children | ReactNode | — | 패널의 내용. 보통 PlNavigationMenuLink들입니다 |
| className | string | — | 행에 쓰인 단어에 붙는 class. 컴포넌트 자신의 class를 대체하지 않고 함께 적용됩니다 |
| style | CSSProperties | — | 행에 쓰인 단어에 붙는 inline style. 컴포넌트가 쓴 custom property 위에 적용됩니다 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| label * | String | — | 행에 쓰이는 단어 |
| value | String? | — | 메뉴의 값 안에서 항목을 식별합니다. 없으면 label이 쓰입니다 |
| onPressed | VoidCallback? | — | 패널을 여는 대신 목적지로 만듭니다. 여기에는 href가 없습니다 — 목적지가 어디인지는 앱의 라우터가 정합니다 |
| startIcon | Widget? | — | 라벨 앞에 놓이는 내용 |
| disabled | bool | false | 쓸 수 없습니다. 단어는 행에 남고 아무것도 열지 않습니다 |
| columns | int | 1 | 패널이 링크를 몇 열로 배치할지 |
| links | List<PlNavigationMenuLink> | const [] | 패널의 내용. 보통 PlNavigationMenuLink들입니다 |
PlNavigationMenuLink
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| href * | string | — | 어디로 가는지 |
| title * | ReactNode | — | 행의 이름 |
| description | ReactNode | — | 그 아래 한 줄. 타입 스케일 한 단계 아래의 muted 텍스트 |
| startIcon | ReactNode | — | 제목 앞의 글리프 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| title * | String | — | 행의 이름 |
| description | String? | — | 그 아래 한 줄. 타입 스케일 한 단계 아래의 muted 텍스트 |
| startIcon | Widget? | — | 제목 앞의 글리프 |
| onPressed * | VoidCallback? | — | 어디로 가는지. 이 패키지에는 navigator가 없으므로 그것을 정하는 자리가 여기입니다 |
공용 축이 라이브러리 전체에서 무엇을 뜻하는지는 prop 규약에 있습니다.
PlNavigationMenu와 PlMenu
차이는 행이 무엇이냐입니다.
PlMenu는 액션을 담습니다. 행은 menuitem이고, 전체가 화살표 키를 가두는 위젯이며, 하나를 고르면 닫힙니다.
이것은 링크를 담습니다. 진짜 <a>로 채워진 <nav>이고, 그것이 브라우저의 링크 목록, 포인터 아래 상태 표시줄, 가운데 클릭 메뉴, 크롤러의 색인에 그것들을 올립니다. 클릭 핸들러가 달린 <div>인 목적지는 그중 어디에도 없습니다.
행이 무언가를 하면 menu를, 행이 어딘가로 가면 이것을 쓰세요.
Examples
링크인 항목, 여는 항목
href만 있고 children이 없는 항목은 링크입니다. children이 있는 항목은 trigger와 패널입니다.
차이는 겉모습이 아닙니다. 앞의 것은 목적지로, 뒤의 것은 펼쳐지는 것으로 알려지므로, 스크린 리더가 둘 중 무엇을 누르려는지 미리 말해 줍니다.
import { PlNavigationMenu, PlNavigationMenuItem, PlNavigationMenuLink } from 'plass-ui';
export default function NavigationMenuStates() {
return (
<PlNavigationMenu>
<PlNavigationMenuItem label="A destination" href="#" />
<PlNavigationMenuItem label="A panel">
<PlNavigationMenuLink href="#" title="Somewhere" />
</PlNavigationMenuItem>
<PlNavigationMenuItem label="Unavailable" disabled>
<PlNavigationMenuLink href="#" title="Nowhere" />
</PlNavigationMenuItem>
<PlNavigationMenuItem label="Status page" href="https://example.com" target="_blank" />
</PlNavigationMenu>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class NavigationMenuStates extends StatelessWidget {
const NavigationMenuStates({super.key});
@override
Widget build(BuildContext context) {
return PlNavigationMenu(
items: <PlNavigationMenuItem>[
PlNavigationMenuItem(label: 'A destination', onPressed: () {}),
PlNavigationMenuItem(
label: 'A panel',
links: <PlNavigationMenuLink>[PlNavigationMenuLink(title: 'Somewhere', onPressed: () {})],
),
const PlNavigationMenuItem(
label: 'Unavailable',
disabled: true,
links: <PlNavigationMenuLink>[PlNavigationMenuLink(title: 'Nowhere')],
),
],
);
}
}columns
패널이 링크를 몇 열로 배치할지입니다. PlNavigationMenuLink가 한 행이고, title과 그 아래의 muted description, 그리고 앞의 글리프를 가질 수 있습니다.
한 번에 하나의 패널만 열려 있고, 닫혔다 다시 열리는 대신 항목 사이를 크기를 바꾸며 이동합니다. 행을 가로지르는 것이 셋이 아니라 하나의 표면으로 읽히는 이유가 그것입니다.
import { PlNavigationMenu, PlNavigationMenuItem, PlNavigationMenuLink } from 'plass-ui';
export default function NavigationMenuColumns() {
return (
<PlNavigationMenu>
<PlNavigationMenuItem label="One column">
<PlNavigationMenuLink href="#" title="Overview" />
<PlNavigationMenuLink href="#" title="Changelog" />
</PlNavigationMenuItem>
<PlNavigationMenuItem label="Three columns" columns={3}>
{['Analytics', 'Billing', 'Audit log', 'Integrations', 'Webhooks', 'Exports'].map(
(title) => (
<PlNavigationMenuLink key={title} href="#" title={title} />
)
)}
</PlNavigationMenuItem>
</PlNavigationMenu>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class NavigationMenuColumns extends StatelessWidget {
const NavigationMenuColumns({super.key});
@override
Widget build(BuildContext context) {
return PlNavigationMenu(
items: <PlNavigationMenuItem>[
PlNavigationMenuItem(
label: 'One column',
links: <PlNavigationMenuLink>[
PlNavigationMenuLink(title: 'Overview', onPressed: () {}),
PlNavigationMenuLink(title: 'Changelog', onPressed: () {}),
],
),
PlNavigationMenuItem(
label: 'Three columns',
columns: 3,
links: <PlNavigationMenuLink>[
for (final String title in <String>[
'Analytics',
'Billing',
'Audit log',
'Integrations',
'Webhooks',
'Exports',
])
PlNavigationMenuLink(title: title, onPressed: () {}),
],
),
],
);
}
}orientation
vertical은 패널이 아래가 아니라 옆으로 열리는 nav rail입니다. 화살표 키는 어느 쪽이든 따라갑니다.
import { PlNavigationMenu, PlNavigationMenuItem, PlNavigationMenuLink } from 'plass-ui';
export default function NavigationMenuOrientation() {
return (
<div className="w-48">
<PlNavigationMenu orientation="vertical" size="sm">
<PlNavigationMenuItem label="Overview" href="#" />
<PlNavigationMenuItem label="Reports">
<PlNavigationMenuLink href="#" title="Usage" />
<PlNavigationMenuLink href="#" title="Revenue" />
</PlNavigationMenuItem>
<PlNavigationMenuItem label="Settings" href="#" />
</PlNavigationMenu>
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class NavigationMenuOrientation extends StatelessWidget {
const NavigationMenuOrientation({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 192,
child: PlNavigationMenu(
orientation: PlassOrientation.vertical,
size: PlassSize.sm,
items: <PlNavigationMenuItem>[
PlNavigationMenuItem(label: 'Overview', onPressed: () {}),
PlNavigationMenuItem(
label: 'Reports',
links: <PlNavigationMenuLink>[
PlNavigationMenuLink(title: 'Usage', onPressed: () {}),
PlNavigationMenuLink(title: 'Revenue', onPressed: () {}),
],
),
PlNavigationMenuItem(label: 'Settings', onPressed: () {}),
],
),
);
}
}행의 표면
쉬고 있을 때 항목은 페이지 자신의 단어입니다. 채움도, 가장자리도, 그림자도 없습니다. 사이트 위쪽을 가로지르는 테두리 상자 다섯 개는 내비게이션이 아니라 툴바이고, 내비게이션은 손이 갈 때까지 글자로 읽혀야 합니다.
색 계열은 포인터와 함께, 그리고 열린 패널과 함께 도착합니다. 시트 자체에는 색이 들어가지 않습니다. 패널은 PlMenu와 PlPopover가 그리는 것과 같은 서리 유리입니다.
다른 곳에서 열리는 링크
항목의 target은 <a>에서 하는 일을 그대로 하고, 이 탭이 아닌 곳으로 열리면 요청된 rel에 noopener noreferrer가 합쳐집니다.
대체가 아니라 합침입니다. rel을 손으로 쓰는 흔한 이유는 nofollow나 sponsored인데, 그것을 덮어쓰기로 적으면 다른 곳에서 열리는 링크의 보호가 조용히 사라집니다.
React 빌드와 다른 점
| React | Flutter | 이유 |
|---|---|---|
항목과 링크의 href | onPressed | 이 패키지에는 navigator도, 해석할 주소도 없습니다. 목적지가 어디인지는 앱 자신의 라우터의 몫입니다. |
조합된 PlNavigationMenuItem 자식 | 데이터인 items: List<PlNavigationMenuItem> | 한 번에 하나의 패널만 열어 두려면 행이 어느 항목이 어느 것인지 알아야 하고, 셀 수 있는 것이 리스트입니다. |
value / defaultValue | initialValue | String?으로는 "호출자가 말하지 않았다"와 "닫혔다고 말했다"를 구분할 수 없어서, controlled 모드는 바깥에서 닫을 수 없는 모드가 됩니다. 어느 패널이 열려 있는지는 앱이 아니라 포인터의 상태입니다. |
| 항목 사이에서 크기가 바뀌는 하나의 패널 | 항목마다 하나씩, 페이드 | 크기 변화는 Base UI가 나가는 패널과 들어오는 패널을 재어 그 사이를 애니메이션하는 것입니다. 여기서는 각 항목이 자기 팝업을 앵커하므로, 행을 가로지르면 패널이 자라는 대신 바뀝니다. |
target과 합쳐지는 rel | — | 앵커가 없으니 지킬 rel도 없습니다. |
<nav> landmark | SemanticsRole.navigation | 프레임워크 자신의 이름으로 된 같은 landmark입니다. |
className, style, 네이티브 속성 | — | 전달할 class 목록도 style 속성도 없습니다. |
Accessibility
- 진짜
<a>로 채워진 진짜<nav>입니다. 이 컴포넌트의 주장이 전부 그것이고, 아래의 모든 것이 거기서 따라 나옵니다. - 키보드는 Base UI의 것입니다. 화살표 키가 행을 따라 움직이고, Enter와 Space가 패널을 열고, Esc가 닫으며 focus는 trigger로 돌아가고, Tab이 열린 패널의 링크로 들어갑니다.
- trigger는
aria-expanded를 보고하므로, 누르면 무엇이 일어날지 미리 알려집니다. disabled항목은 단어를 행에 남기고 아무것도 열지 않습니다. 색을 바꾸는 대신 흐려지는데, 라이브러리 전체에서disabled가 그렇게 보입니다.- 팝업은
<body>끝으로 portal되고 positioner가.plass-portal을 지닙니다. CSS reset을 범위 지정한 호스트가 같은 reset을 거는 자리가 그것입니다. - 패널이 미끄러지는 대신 셰브런이 돕니다. 여기서 포인터 아래에서 움직이는 것은 없습니다.