PlButtonGroup
함께 묶이는 버튼 한 줄입니다. 이웃과 맞닿는 모서리를 각지게 깎고, variant size color density elevation disabled를 묶음 단위로 한 번만 지정합니다.
import { PlButton, PlButtonGroup } from 'plass-ui';
<PlButtonGroup variant="glass" color="secondary">
<PlButton>Day</PlButton>
<PlButton>Week</PlButton>
<PlButton>Month</PlButton>
</PlButtonGroup>;import 'package:plass_ui/plass_ui.dart';
PlButtonGroup(
variant: PlassVariant.glass,
color: PlassColor.secondary,
children: <Widget>[
PlButton(onPressed: showDay, child: const Text('Day')),
PlButton(onPressed: showWeek, child: const Text('Week')),
PlButton(onPressed: showMonth, child: const Text('Month')),
],
);Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| variant공통 | 'solid' | 'glass' | 'ghost' | — | 그룹 전체의 재질. 지정하지 않으면 각 버튼의 기본값(solid)을 씁니다 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | — | 그룹 전체의 높이와 타입 스케일. 한 버튼만 크기가 다른 그룹을 막는 것이 이 컴포넌트의 절반입니다 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | — | 그룹 전체의 색 역할. 버튼이 스스로 정한 color가 우선합니다 |
| density공통 | 'default' | 'compact' | — | 그룹 전체의 가로 패딩 |
| elevation공통 | 0 | 1 | 2 | 3 | — | 그룹 전체의 그림자 깊이 |
| orientation공통 | 'horizontal' | 'vertical' | 'horizontal' | 버튼이 늘어서는 방향. vertical은 동등한 액션을 쌓은 메뉴입니다 |
| disabled | boolean | — | 그룹의 모든 버튼을 한 번에 비활성화합니다 |
| fullWidth | boolean | false | 컨테이너 너비만큼 늘리고 버튼끼리 너비를 똑같이 나눠 갖습니다 |
| children | ReactNode | — | 버튼들. 진짜 PlButton으로 남습니다 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| children * | List<Widget> | — | 버튼들, 순서대로. 하나의 child가 아니라 목록인 건 그룹이 양 끝이 누구인지 알아야 모서리를 정할 수 있기 때문입니다 |
| variant공통 | PlassVariant? | — | 그룹 전체의 재질. 지정하지 않으면 각 버튼의 기본값(solid)을 씁니다 |
| size공통 | PlassSize? | — | 그룹 전체의 높이와 타입 스케일. 한 버튼만 크기가 다른 그룹을 막는 것이 이 컴포넌트의 절반입니다 |
| color공통 | PlassColor? | — | 그룹 전체의 색 역할. 버튼이 스스로 정한 color가 우선합니다 |
| density공통 | PlassDensity? | — | 그룹 전체의 가로 패딩 |
| elevation공통 | int? | — | 그룹 전체의 그림자 깊이 |
| orientation공통 | PlassOrientation | PlassOrientation.horizontal | 버튼이 늘어서는 방향. vertical은 동등한 액션을 쌓은 메뉴입니다 |
| disabled | bool? | — | 그룹의 모든 버튼을 한 번에 비활성화합니다 |
| fullWidth | bool | false | 컨테이너 너비만큼 늘리고 버튼끼리 너비를 똑같이 나눠 갖습니다 |
나머지 <div> 속성은 그대로 통과합니다. color는 위 표의 color와 이름이 겹쳐 제외했습니다.
children이 하나의 child가 아니라 목록인 것은 Flutter의 관례이기도 하지만, 그것만은 아닙니다. 그룹은 어느 멤버가 양 끝에 있는지 알아야 어느 모서리를 깎을지 정할 수 있고, 불투명한 subtree 하나만 받은 widget은 그걸 알 수 없습니다.
축들은 PlButton과 PlIconButton에서도 nullable입니다. PlassVariant?, PlassSize?, int?. Dart에는 기본값과 실제로 넘어온 값을 구분할 방법이 없기 때문입니다. 거기서 null은 이 버튼은 말하지 않았다 는 뜻이고, 그래야 그룹이 대신 답할 수 있습니다.
다섯 개의 스타일 축에는 자기 기본값이 없습니다. 그룹이 지정하지 않은 축은 각 버튼이 자기 기본값으로 돌아가는 축이라, 아무 prop도 주지 않은 그룹은 모서리 말고는 아무것도 바꾸지 않습니다. 버튼이 직접 지정한 축은 그룹보다 우선합니다. secondary 액션 줄에 danger 버튼 하나가 섞이는 것은 실제로 있는 일입니다.
공유 축(variant size color density elevation)이 라이브러리 전체에서 무엇을 뜻하는지는 prop 규약에 있습니다.
PlButtonGroup과 PlSegmentedButton
버튼은 진짜 PlButton으로 남고, 그 무엇도 대체되지 않습니다. 그룹이 하는 일은 모서리 넷을 깎고 prop 여섯 개를 물려주는 것뿐입니다. 선택 상태를 관리하지 않고, value도 없으며, 어느 버튼도 고른 것 이 되지 않습니다.
여럿 중 하나를 고르는 컨트롤(뷰 전환, 모드 토글)은 PlSegmentedButton입니다. 그쪽이 roving focus와 radiogroup semantics까지 갖춘 진짜 그 컨트롤입니다.
Examples
variant
이음매를 처리해야 하는 것은 glass 하나뿐입니다. 테두리를 그리는 유일한 variant이기도 해서, glass 키 둘이 맞닿으면 hairline이 두 겹으로 겹쳐 페이지의 다른 모든 선보다 두 배로 무거워집니다. 그래서 뒤쪽을 1px 당겨 두 키가 선 하나를 나눠 쓰게 합니다.
solid는 그렇게 하면 안 됩니다. 겹칠 테두리가 없고, 겹치면 한 키의 그러데이션이 다음 키의 시작을 덮습니다.
import { PlButton, PlButtonGroup } from 'plass-ui';
export default function ButtonGroupVariants() {
return (
<div className="flex flex-wrap items-center gap-4">
<PlButtonGroup variant="solid">
<PlButton>Cut</PlButton>
<PlButton>Copy</PlButton>
<PlButton>Paste</PlButton>
</PlButtonGroup>
<PlButtonGroup variant="glass" color="secondary">
<PlButton>Cut</PlButton>
<PlButton>Copy</PlButton>
<PlButton>Paste</PlButton>
</PlButtonGroup>
<PlButtonGroup variant="ghost" color="secondary">
<PlButton>Cut</PlButton>
<PlButton>Copy</PlButton>
<PlButton>Paste</PlButton>
</PlButtonGroup>
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class ButtonGroupVariants extends StatelessWidget {
const ButtonGroupVariants({super.key});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 16,
runSpacing: 16,
children: <Widget>[
for (final PlassVariant variant in PlassVariant.values)
PlButtonGroup(
variant: variant,
color: variant == PlassVariant.solid ? PlassColor.primary : PlassColor.secondary,
children: <Widget>[
PlButton(onPressed: () {}, child: const Text('Cut')),
PlButton(onPressed: () {}, child: const Text('Copy')),
PlButton(onPressed: () {}, child: const Text('Paste')),
],
),
],
);
}
}size
한 번만 지정하니 버튼 하나만 크기가 어긋날 수 없습니다. 높이는 라이브러리의 컨트롤 사다리 그대로입니다.
import { PlButtonGroup, PlButton } from 'plass-ui';
const sizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
export default function ButtonGroupSizes() {
return (
<div className="flex flex-col items-start gap-3">
{sizes.map((size) => (
<PlButtonGroup key={size} size={size} variant="glass" color="secondary">
<PlButton>Back</PlButton>
<PlButton>Forward</PlButton>
</PlButtonGroup>
))}
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class ButtonGroupSizes extends StatelessWidget {
const ButtonGroupSizes({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: <Widget>[
for (final PlassSize size in PlassSize.values)
PlButtonGroup(
size: size,
variant: PlassVariant.glass,
color: PlassColor.secondary,
children: <Widget>[
PlButton(onPressed: () {}, child: const Text('Back')),
PlButton(onPressed: () {}, child: const Text('Forward')),
],
),
],
);
}
}orientation
vertical은 줄을 세로로 쌓고, 옆면 대신 위아래를 각지게 깎습니다. 동등한 액션을 쌓은 메뉴에 쓰고, 기본값이 horizontal인 것은 툴바가 그 모양이기 때문입니다.
import { PlButton, PlButtonGroup } from 'plass-ui';
export default function ButtonGroupOrientation() {
return (
<PlButtonGroup orientation="vertical" variant="glass" color="secondary">
<PlButton>Rename</PlButton>
<PlButton>Duplicate</PlButton>
<PlButton color="danger">Delete</PlButton>
</PlButtonGroup>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class ButtonGroupOrientation extends StatelessWidget {
const ButtonGroupOrientation({super.key});
@override
Widget build(BuildContext context) {
return PlButtonGroup(
orientation: PlassOrientation.vertical,
variant: PlassVariant.glass,
color: PlassColor.secondary,
children: <Widget>[
PlButton(onPressed: () {}, child: const Text('Rename')),
PlButton(onPressed: () {}, child: const Text('Duplicate')),
PlButton(color: PlassColor.danger, onPressed: () {}, child: const Text('Delete')),
],
);
}
}fullWidth
그룹을 컨테이너 너비만큼 늘리고 버튼끼리 너비를 똑같이 나눠 갖게 합니다. 카드 아래 액션 세 개가 서로 다른 길이의 단어 셋이 아니라 똑같은 삼등분이 됩니다.
import { PlButton, PlButtonGroup } from 'plass-ui';
export default function ButtonGroupFullWidth() {
return (
<PlButtonGroup fullWidth variant="glass" color="secondary" className="max-w-sm">
<PlButton>Deny</PlButton>
<PlButton>Ask</PlButton>
<PlButton>Allow</PlButton>
</PlButtonGroup>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class ButtonGroupFullWidth extends StatelessWidget {
const ButtonGroupFullWidth({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 360,
child: PlButtonGroup(
fullWidth: true,
variant: PlassVariant.glass,
color: PlassColor.secondary,
children: <Widget>[
PlButton(onPressed: () {}, child: const Text('Deny')),
PlButton(onPressed: () {}, child: const Text('Ask')),
PlButton(onPressed: () {}, child: const Text('Allow')),
],
),
);
}
}Accessibility
- 그룹은
role="group"입니다. 줄 자체에 이름이 필요하면aria-label을 주세요. 이것이 세 개 놓인 바는 이름 없는 그룹 세 개입니다. role="toolbar"가 아니고 roving focus도 없습니다. 그 role은 키보드 동작에 대한 약속이고, 여기서는 버튼 하나하나가 각자의 focus 정거장입니다. 평범한<button>semantics가 이미 말하고 있는 그대로입니다.- 모서리는 logical property로 깎으므로, RTL에서는 첫 버튼이 오른쪽에 오고 깎이는 면도 따라갑니다.
- 버튼마다 stacking context가 생겨, border box 바깥에 그려지는 focus ring이 뒤에 오는 이웃에 덮이지 않습니다.
- 그룹의
disabled는 안의 모든 버튼을 끕니다. 버튼이 직접 지정한disabled는 그대로 우선합니다.
React 빌드와 다른 점
| React | Flutter | 이유 |
|---|---|---|
임의의 children | children: List<Widget> | 어느 모서리를 깎을지 정하려면 그룹이 양 끝이 누구인지 알아야 합니다. |
| glass 키를 1px 당겨 hairline 둘을 겹칩니다 | 이웃과 맞닿는 면을 아예 그리지 않습니다 | Flutter에는 음수 margin이 없고(EdgeInsets가 non-negative를 assert합니다), 대안은 Transform인데 이 라이브러리는 컨트롤에 transform을 걸지 않습니다. 둘 다 이음매마다 hairline 하나라는 같은 결과에 닿습니다. |
| 축을 그냥 빼면 됩니다 | 같은 파라미터가 nullable입니다 | Dart는 기본값과 넘어온 값을 구분하지 못하므로, 말하지 않았다 를 타입이 담을 수 있는 값으로 만들어야 합니다. |
className, style, 네이티브 속성 | — | 통과시킬 class 목록도 style 속성도 없습니다. |