PlAccordion
한 번에 하나씩 펼쳐지는 섹션 묶음입니다. 무엇을 읽을지 먼저 훑어보는 참고성 내용(설정 그룹, 사양표, FAQ)에 씁니다.
import { PlAccordion, PlAccordionItem } from 'plass-ui';
<PlAccordion defaultValue={['shipping']}>
<PlAccordionItem value="shipping" title="Shipping">
Three to five working days.
</PlAccordionItem>
<PlAccordionItem value="returns" title="Returns">
Thirty days from delivery.
</PlAccordionItem>
</PlAccordion>;import 'package:plass_ui/plass_ui.dart';
PlAccordion<String>(
value: open,
onChanged: (Set<String> next) => setState(() => open = next),
items: const <PlAccordionItem<String>>[
PlAccordionItem<String>(
value: 'shipping',
title: Text('Shipping'),
child: Text('Three to five working days.'),
),
PlAccordionItem<String>(
value: 'returns',
title: Text('Returns'),
child: Text('Thirty days from delivery.'),
),
],
);Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| variant공통 | 'solid' | 'glass' | 'ghost' | 'glass' | 시트의 재질. solid는 가장 불투명한 유리, glass는 하이라이너가 있는 기본 시트, ghost는 표면 없음 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 제목과 본문의 타입 스케일, 그리고 그 둘을 감싸는 여백 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | 'default' | 'compact' | 'default' | 여백만 바꿉니다. 높이와 글자 크기는 그대로 |
| elevation공통 | 0 | 1 | 2 | 3 | 0 | 그림자 깊이. 0은 그림자 없음 |
| multiple | boolean | false | 여러 섹션을 동시에 열 수 있게 합니다 |
| value | (string | number)[] | — | 열려 있는 섹션. onValueChange와 함께 controlled로 씁니다 |
| defaultValue | (string | number)[] | — | uncontrolled일 때 처음부터 열려 있는 섹션 |
| onValueChange | (value: (string | number)[]) => void | — | 열린 섹션 집합이 바뀔 때 호출됩니다 |
| dividers | boolean | true | 섹션 사이를 헤어라인으로 나눕니다. 끄면 각 섹션이 타일이 됩니다 |
| disabled | boolean | false | 모든 섹션이 반응하지 않습니다 |
| hiddenUntilFound | boolean | false | 닫힌 패널을 DOM에 남겨 브라우저의 페이지 검색이 찾아 열 수 있게 합니다. keepMounted보다 우선합니다 |
| keepMounted | boolean | false | 닫힌 패널을 DOM에 남깁니다. 만드는 비용이 크거나 form 상태를 쥐고 있는 내용에 |
| children | ReactNode | — | PlAccordionItem 목록 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| items * | List<PlAccordionItem<T>> | — | 섹션들. children이 아니라 설명의 목록입니다 — accordion이 무엇이 열려 있고 사이의 선이 어디 가는지를 알아야 합니다 |
| value * | Set<T> | — | 열려 있는 섹션. multiple이 꺼져 있어도 집합입니다 — 닫힌 상태는 빈 집합입니다 |
| onChanged | ValueChanged<Set<T>>? | — | 다음에 열려 있어야 할 집합으로 호출됩니다. 주지 않으면 열린 상태 그대로 굳습니다 |
| multiple | bool | false | 여러 섹션을 동시에 열 수 있게 합니다 |
| variant공통 | PlassVariant | PlassVariant.glass | 시트의 재질. solid는 가장 불투명한 유리, glass는 하이라이너가 있는 기본 시트, ghost는 표면 없음 |
| size공통 | PlassSize | PlassSize.md | 제목과 본문의 타입 스케일, 그리고 그 둘을 감싸는 여백 |
| color공통 | PlassColor | PlassColor.primary | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | PlassDensity | PlassDensity.standard | 여백만 바꿉니다. 높이와 글자 크기는 그대로 |
| elevation공통 | int | 0 | 그림자 깊이. 0은 그림자 없음 |
| dividers | bool | true | 섹션 사이를 헤어라인으로 나눕니다. 끄면 각 섹션이 타일이 됩니다 |
| disabled | bool | false | 모든 섹션이 반응하지 않습니다 |
네이티브 <div> 속성은 그대로 전달됩니다. color는 위 표의 color와 충돌해서, defaultValue와 onChange는 accordion이 각각 배열형 defaultValue와 onValueChange로 쓰기 때문에 제외됩니다.
accordion은 섹션 값 타입에 대해 제네릭입니다(PlAccordion<String>, PlAccordion<Section>). 그래서 value와 onChanged가 dynamic이 아니라 타입을 가지며, 패키지의 다른 컨트롤과 마찬가지로 controlled입니다. multiple이 꺼져 있어도 value는 Set<T>입니다. 닫힌 상태도 집합이기 때문입니다. 빈 집합입니다.
PlAccordionItem
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| value | string | number | — | value / defaultValue가 이 섹션을 가리키는 이름. 생략하면 Base UI가 만들어 줍니다 |
| title | ReactNode | — | 접힘 헤더의 제목 |
| subtitle | ReactNode | — | 제목 아래 한 줄. 타입 스케일 한 단계 아래의 muted 텍스트 |
| startIcon | ReactNode | — | 제목 앞에 놓이는 내용 — 아이콘, 상태 점, 개수 |
| action | ReactNode | — | 헤더 끝, chevron 앞에 고정되는 컨트롤. trigger 바깥에 놓이므로 버튼을 넣어도 됩니다 |
| truncate | boolean | false | 제목과 부제를 각각 한 줄로 자르고 넘치면 말줄임합니다. 기본값이 false인 이유는 접힘 제목이 대개 한 문장이기 때문입니다 |
| disabled | boolean | false | 이 섹션만 접히지 않습니다. 나머지는 그대로 동작합니다 |
| children | ReactNode | — | 패널의 내용 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| value * | T | — | PlAccordion.value가 이 섹션을 가리키는 이름 |
| title | Widget? | — | 접힘 헤더의 제목 |
| subtitle | Widget? | — | 제목 아래 한 줄. 타입 스케일 한 단계 아래의 muted 텍스트 |
| startIcon | Widget? | — | 제목 앞에 놓이는 내용 — 아이콘, 상태 점, 개수 |
| action | Widget? | — | 헤더 끝, chevron 앞에 고정되는 컨트롤. 접히는 부분 바깥이라 버튼을 넣어도 됩니다 |
| truncate | bool | false | 제목과 부제를 각각 한 줄로 자르고 넘치면 말줄임합니다. 기본값이 false인 이유는 접힘 제목이 대개 한 문장이기 때문입니다 |
| disabled | bool | false | 이 섹션만 접히지 않습니다. 나머지는 그대로 동작합니다 |
| child | Widget? | — | 패널의 내용 |
size, density, dividers는 item에 주는 prop이 아니라 감싸고 있는 PlAccordion에서 내려받습니다.
섹션은 위젯이 아니라 PlAccordionItem, 즉 설명입니다. accordion은 어느 섹션이 열려 있는지, 누르면 무엇이 닫혀야 하는지, 사이의 선이 어디 들어가는지를 알아야 하는데, 불투명한 Widget에게는 그중 어느 것도 물어볼 수 없습니다.
size도 density도 dividers도 없고, 있을 수도 없습니다. 그것들은 accordion의 것이고, 타입 스케일이 두 개인 묶음은 한 장의 판이 아닙니다.
라이브러리 전체에서 공유 축(variant size color density elevation)이 뜻하는 바는 prop 규칙에 있습니다.
Examples
variant
세 가지 재질을 컨테이너 입장에서 읽은 것입니다. solid는 가장 불투명한 맑은 유리로, 주변보다 앞으로 나와 있어야 하는 판에 씁니다. glass는 Plass의 기본 시트이자 기본값입니다. ghost는 시트가 아예 없어서, 이미 시트인 PlCard 안에 넣을 때 씁니다. 사각형 안의 또 다른 사각형은 사각형 하나가 더 많은 것입니다.
셋 중 어느 것에도 색이 들어가지 않습니다. accordion이 담는 내용은 자기 색을 가지고 오기 때문에, 색 계열은 hover 틴트와 열린 섹션의 제목, focus ring까지만 닿고 거기서 멈춥니다.
multiple
기본값에서는 한 섹션을 열면 열려 있던 섹션이 닫힙니다. accordion이 collapsible을 쌓아 놓은 것과 다른 이유가 바로 이것으로, 다음을 열 때 앞의 것이 닫히는 덕분에 읽는 도중 페이지가 아래로 자라지 않습니다. multiple은 이 제약을 풉니다.
dividers
기본으로 켜져 있습니다. 양 끝까지 닿는 헤어라인이 여러 섹션을 한 장의 판으로 묶어 줍니다. 끄면 각 섹션이 자기 타일이 되고, 여백으로 구분됩니다.
title · subtitle · startIcon · action
제목과 부제는 줄바꿈됩니다. 접힘 제목은 대개 한 문장이고(FAQ는 질문의 목록입니다) 말줄임하면 독자는 문장의 끝을 잃습니다. 툴팁도 없고 확인할 방법도 없습니다. 반대로 줄바꿈이 치르는 값은 두 줄짜리 헤더인데, 높이가 변하는 것이 존재 이유인 컴포넌트에서는 그것이 값이라 하기 어렵습니다. truncate는 둘을 다시 한 줄로 되돌립니다. 데이터베이스에서 온 이름을 컨트롤 옆에 놓는 헤더를 위한 것입니다.
action은 접히는 부분 바깥에 그려집니다. 접히기도 하고 버튼도 쥐고 있는 헤더에는 누를 것이 두 개인데, 그중 하나를 다른 하나 안에 넣을 수는 없습니다.
브라우저가 <button> 안의 <button>을 파싱 단계에서 다시 쓰므로, 이것은 취향의 문제가 아닙니다.
여기서는 트리를 다시 쓰는 주체가 없지만, 컨트롤 안의 컨트롤은 한 번 누르면 두 번 발생하는 이벤트이고 스크린리더에는 버튼 안의 버튼입니다.
size
제목과 본문, 그리고 둘을 감싸는 여백이 함께 움직입니다. accordion에 주면 모든 섹션이 내려받으므로, 한 묶음 안에 타입 스케일이 두 개가 되는 일이 없습니다.
본문은 아래쪽뿐 아니라 위쪽에도 자기 여백을 둡니다. 열린 헤더는 아래 모서리가 붙은 색 띠이고, 그 모서리에서 바로 시작하는 본문은 첫 줄이 제목 밑 half leading 자리에 놓입니다. 제목과 그것을 설명하는 문단이 색만 바뀐 한 덩어리 글로 읽히게 됩니다. 헤더의 여백이 사는 것은 제목 둘레의 자리이고, 본문의 자리는 본문이 삽니다.
Controlled
value와 onValueChange를 함께 넘기면 열린 섹션 집합을 직접 쥘 수 있습니다. multiple이 꺼져 있어도 둘 다 배열입니다. 전부 닫힌 상태는 []입니다.
uncontrolled 모드는 없습니다. accordion을 움직이는 방법은 언제나 value와 onChanged입니다. multiple이 꺼져 있어도 value는 Set<T>이고(전부 닫힌 상태는 <String>{}입니다) onChanged를 주지 않으면 열려 있는 상태 그대로 굳습니다. 읽기 전용 요약은 그렇게 씁니다.
Accessibility
- 각 헤더는
aria-expanded가 붙은 진짜<button>이고,aria-controls로 자기 패널을 가리킵니다. 패널은 헤더가 이름을 붙여 주는region입니다. - Enter와 Space로 섹션을 접고 폅니다. Tab은 헤더 사이와 열린 패널 안으로 이동합니다.
hiddenUntilFound는 닫힌 패널을hidden="until-found"로 렌더링하므로, 브라우저의 페이지 검색이 그 안의 글자를 찾아 해당 섹션을 열어 줍니다.- chevron은 장식이라
aria-hidden입니다. 열림 상태는aria-expanded가 나르며, 회전만으로 전달되는 정보는 없습니다. action에 넣은 것은 자기 tab stop이 붙은 별개의 컨트롤이므로, 접근 가능한 이름도 따로 필요합니다.- 패널은
transform이 아니라 height를 애니메이션합니다. 글자가 다시 샘플링되지 않고, 열리는 동안 패널 안의 내용이 밀리지도 않습니다.
- 각 헤더는 펼쳐졌는지 접혔는지가 표시된 버튼으로 읽힙니다. 그 상태는 플래그가 나르며, chevron의 회전만으로 전달되는 정보는 없습니다.
- Enter와 Space로 섹션을 접고 폅니다. Tab은 헤더 사이와 열린 패널 안으로 이동합니다. 헤더는 저마다 자기 focus stop이 있습니다. accordion은 버튼 묶음이지 roving 그룹이 아닙니다.
- 닫힌 패널은 트리에 아예 없습니다. 열리기 전까지 그 안의 어떤 것도 닿거나 포커스되거나 읽히지 않습니다.
- chevron은 그려지되 이름이 없고, 비활성 섹션은 포인터에도 키보드에도 답하지 않습니다.
action에 넣은 것은 자기 focus stop이 붙은 별개의 컨트롤이므로, 이름도 따로 필요합니다.- 패널은 transform이 아니라 높이를 애니메이션합니다. 글자가 다시 샘플링되지 않고, 열리는 동안 패널 안의 내용이 밀리지도 않습니다. OS에서 애니메이션을 끄면 즉시 펼쳐집니다.
React 빌드와 다른 점
| React | Flutter | 이유 |
|---|---|---|
<PlAccordionItem> children | 설명 목록인 items | accordion은 어느 섹션이 열려 있는지, 누르면 무엇이 닫히는지, 선이 어디 들어가는지를 알아야 합니다. 불투명한 위젯에게는 물어볼 수 없습니다. |
defaultValue / onValueChange | value / onChanged | Flutter의 컨트롤은 controlled이고, 콜백 이름도 Flutter의 것입니다. |
string 값 | 제네릭 T | Dart에는 제네릭이 있으니 섹션 타입은 관습이 아니라 타입 검사로 지켜집니다. |
배열인 value | Set<T>인 value | 열린 섹션은 순서도 중복도 없는 집합이고, Dart에는 그 자료형이 있습니다. |
hiddenUntilFound | — | 섹션을 대신 열어 줄 브라우저 페이지 검색이 없습니다. 닫힌 패널은 그냥 만들어지지 않습니다. |
aria-expanded, aria-controls, region | 펼쳐짐이 표시된 버튼과, 있거나 없는 패널 | Flutter는 상태를 노드 자체에 적습니다. 가리킬 id가 없습니다. |
children | child | Flutter의 이름입니다. |
className, style | — | 전달할 클래스 목록도 style 속성도 없습니다. |