PlPagination
긴 목록 아래에 놓이는 페이지 번호 줄입니다. 줄 안의 모든 버튼이 진짜 PlButton이라, 같은 size의 다른 컨트롤과 나란히 놓아도 기준선이 맞습니다.
import { PlPagination } from 'plass-ui';
<PlPagination count={12} page={page} onPageChange={setPage} />;import 'package:plass_ui/plass_ui.dart';
PlPagination(
count: 12,
page: page,
onPageChanged: (int next) => setState(() => page = next),
);Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| variant공통 | 'solid' | 'glass' | 'ghost' | 'ghost' | 쉬고 있는 페이지 버튼의 재질. 현재 페이지는 언제나 solid입니다 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 버튼 높이와 타입 스케일. PlButton과 같은 사다리라 옆에 놓으면 기준선이 맞습니다 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | 'default' | 'compact' | 'compact' | 여백만 바꿉니다. 높이와 글자 크기는 그대로 |
| elevation공통 | 0 | 1 | 2 | 3 | 0 | 그림자 깊이. 0은 그림자 없음 |
| count * | number | — | 전체 페이지 수. 2보다 작으면 컨트롤 전체가 아무것도 렌더링하지 않습니다 |
| page | number | — | 현재 페이지 (1부터). onPageChange와 함께 controlled로 씁니다 |
| defaultPage | number | 1 | uncontrolled일 때 시작 페이지 |
| onPageChange | (page: number) => void | — | 페이지가 바뀔 때 호출됩니다 |
| siblingCount | number | 1 | 현재 페이지 양옆에 항상 보이는 페이지 수 |
| boundaryCount | number | 1 | 양 끝에 항상 보이는 페이지 수. 0이면 첫 페이지와 마지막 페이지가 사라지고 창만 남습니다 |
| showEdges | boolean | false | 첫 페이지 / 마지막 페이지로 건너뛰는 버튼을 보여 줍니다 |
| showArrows | boolean | true | 이전 / 다음 버튼을 보여 줍니다 |
| disabled | boolean | false | 줄 안의 모든 버튼이 반응하지 않습니다 |
| getPageHref | (page: number) => string | — | 페이지 주소. 주면 모든 숫자가 진짜 <a href>가 되어 크롤러가 따라갈 수 있습니다 |
| renderLink | (page: number, href: string) => ReactElement | — | 각 페이지 링크를 <a> 대신 다른 것으로 그립니다 — 보통 라우터의 Link. 없으면 SPA에서 매 클릭이 전체 문서 로드가 됩니다 |
| label | string | 'Pagination' | <nav>의 접근 가능한 이름 |
| pageLabel | (page: number) => string | `Page {n}` | 페이지 버튼의 접근 가능한 이름 |
| previousLabel · nextLabel · firstLabel · lastLabel | string | — | 각 이동 버튼의 접근 가능한 이름. 화면에 그려지지 않습니다 |
| statusLabel | (page: number, count: number) => string | `Page {n} of {total}` | 페이지가 바뀔 때 스크린리더가 듣는 live region 문장 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| count * | int | — | 전체 페이지 수. 2보다 작으면 컨트롤 전체가 아무것도 렌더링하지 않습니다 |
| page * | int | — | 현재 페이지 (1부터). onPageChange와 함께 controlled로 씁니다 |
| onPageChanged | ValueChanged<int>? | — | 페이지가 바뀔 때 호출됩니다 |
| variant공통 | PlassVariant | PlassVariant.ghost | 쉬고 있는 페이지 버튼의 재질. 현재 페이지는 언제나 solid입니다 |
| size공통 | PlassSize | PlassSize.md | 버튼 높이와 타입 스케일. PlButton과 같은 사다리라 옆에 놓으면 기준선이 맞습니다 |
| color공통 | PlassColor | PlassColor.primary | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | PlassDensity | PlassDensity.compact | 여백만 바꿉니다. 높이와 글자 크기는 그대로 |
| elevation공통 | int | 0 | 그림자 깊이. 0은 그림자 없음 |
| siblingCount | int | 1 | 현재 페이지 양옆에 항상 보이는 페이지 수 |
| boundaryCount | int | 1 | 양 끝에 항상 보이는 페이지 수. 0이면 첫 페이지와 마지막 페이지가 사라지고 창만 남습니다 |
| showEdges | bool | false | 첫 페이지 / 마지막 페이지로 건너뛰는 버튼을 보여 줍니다 |
| showArrows | bool | true | 이전 / 다음 버튼을 보여 줍니다 |
| disabled | bool | false | 줄 안의 모든 버튼이 반응하지 않습니다 |
| label | String | 'Pagination' | <nav>의 접근 가능한 이름 |
| pageLabel | String Function(int) | (page) => 'Page $page' | 페이지 버튼의 접근 가능한 이름 |
| previousLabel · nextLabel · firstLabel · lastLabel | String | — | 이동 버튼들의 이름. 그려지지 않습니다 |
네이티브 <nav> 속성은 그대로 전달됩니다. color는 위 표의 color와 충돌해서, onChange는 이 줄이 onPageChange로 쓰기 때문에 제외됩니다.
줄은 controlled입니다. 현재 page를 받고, 선택된 페이지를 알립니다.
라이브러리 전체에서 공유 축(variant size color density elevation)이 뜻하는 바는 prop 규칙에 있습니다.
Examples
variant
쉬고 있는 페이지가 어떻게 보일지를 정합니다. 현재 페이지는 줄의 variant가 무엇이든 언제나 solid입니다. 읽지 않고도 알아볼 수 있어야 하는 것이 여기서는 그것 하나뿐입니다.
기본값이 PlButton 혼자일 때의 solid가 아니라 ghost인 이유는 간단합니다. 색 유리판 아홉 개가 한 줄에 놓이면 아홉 개 전부가 주 액션이라고 말하는 셈입니다.
siblingCount과 boundaryCount
boundaryCount는 양 끝에 고정으로 남는 페이지 수, siblingCount는 현재 페이지 양옆에 놓이는 페이지 수입니다. 그 사이는 전부 ellipsis가 되는데, 딱 한 페이지만 가려지는 경우에는 대신 그 페이지를 그립니다. 1 … 3 … 9는 숫자 하나를, 그 숫자보다 넓은 기호 뒤에 숨기는 일이기 때문입니다.
줄은 어느 페이지에 있든 슬롯 개수를 일정하게 유지합니다. 창이 끝에 잘리는 대신 가까운 쪽으로 미끄러집니다. 이렇게 하지 않으면 1페이지에서 2페이지로 넘어갈 때 줄 전체가 다시 배치되고, 방금 누른 버튼이 포인터 아래에서 빠져나가 버립니다.
showArrows와 showEdges
이동 버튼은 아이콘만 있는 버튼이라 정사각형이 되고, 한 자리 숫자 페이지와 정확히 같은 크기에 놓입니다. 양 끝의 폭이 가운데와 다른 줄은 컨트롤 두 개를 붙여 놓은 것처럼 읽힙니다. 범위의 양 끝에서는 해당 버튼이 disabled가 되면서 자리를 지키므로, 줄이 옆으로 밀리는 일이 없습니다.
getPageHref
모든 숫자를 진짜 <a href>로 만듭니다. 이것이 없으면 줄은 버튼이고, 크롤러는 버튼을 누를 수 없습니다. 기사나 상품의 페이지 목록이 사람에게만 존재하고 나머지에게는 1페이지에서 끝나 버립니다.
href와 onPageChange가 둘 다 있으면 핸들러가 이기고 이동은 취소됩니다. 클라이언트 라우터가 이미 가진 페이지를 그대로 쥐고 있는 경우입니다. href만 있고 핸들러가 없으면 링크가 링크답게 동작하고, 그래서 JavaScript가 로드되기 전에도 줄이 작동합니다. ⌘, Ctrl, Shift, Alt를 누른 채로 한 클릭은 절대 취소하지 않습니다. 새 탭을 열어 달라는 요청이기 때문입니다.
현재 페이지와 범위 끝의 이동 버튼은 <button>으로 남습니다. disabled는 <a>가 될 수 있는 상태가 아니기 때문입니다.
그 링크가 무엇으로 만들어질지는 renderLink가 정합니다. 맨 <a>는 SPA에서 전체 문서 로드입니다. 라우터가 클릭을 보지 못하니 숫자 하나 바꾸려고 페이지 전체를 다시 받고, 파싱하고, 부팅합니다. 쓰고 있는 라우터의 Link를 돌려주면 됩니다. 주소는 이미 만들어져서 들어오므로 그 안에 getPageHref를 한 번 더 쓸 필요가 없습니다. rel="prev"와 rel="next"는 돌려준 것 위에 그대로 얹힙니다.
<PlPagination
count={12}
page={page}
getPageHref={(to) => `/articles?page=${to}`}
renderLink={(to, href) => <Link href={href} />}
/>size
PlButton과 같은 높이 사다리를 씁니다. pagination과 button을 한 줄에 놓아도 기준선이 유지됩니다. 여기서 density의 기본값이 compact인 이유는, 숫자가 단어보다 옆에 필요한 자리가 적기 때문입니다.
Accessibility
<ul>을 감싼<nav>로 렌더링됩니다. 스크린리더가 건너뛸 수 있는 이름 붙은 landmark 안에, 길이로 페이지 범위를 말해 주는 목록이 들어 있습니다.- 현재 페이지는
aria-current="page"를 갖고, 화면에 보이지 않는aria-live문장이 전체 몇 페이지 중 몇 페이지인지 말해 줍니다. ellipsis가 끼는 순간 목록 길이만으로는 알 수 없기 때문입니다. - 모든 버튼에 접근 가능한 이름이 있습니다 (
Page 4,Next page). 전부 prop이라 다른 언어의 페이지는 자기 문구를 넣으면 되고, 여기 있는 문자열은 화면에 그려지지 않습니다. - ellipsis는 disabled 버튼이 아니라
aria-hidden인<span>입니다. 쓸 수 없는 컨트롤이 아니라 문장 부호입니다. - 페이지가 두 개 미만이면 아무것도 렌더링하지 않습니다. disabled된
1하나만 있는 줄은 할 일이 없다고 광고하는 컨트롤입니다. - 이동 버튼은 드로잉 네 개를 싣는 대신 chevron 글리프 하나를 돌려 씁니다. RTL에서는 방향이 뒤집힙니다.
- 줄은 이름이 붙은 묶음이고,
label이 그 이름입니다. - 모든 버튼에 자기 이름이 있습니다. "Page 4", "Next page". 전부 파라미터라 다른 언어의 화면은 자기 문구를 넣으면 되고, 여기 있는 문자열은 화면에 그려지지 않습니다.
- 페이지 버튼에 그려진 숫자는 읽히는 것에서 제외됩니다.
pageLabel이 이미 그 숫자를 말하고 있고, 둘을 합친 라벨은 숫자를 두 번 읽게 됩니다. - ellipsis는 semantics에서 통째로 제외됩니다. 쓸 수 없는 컨트롤이 아니라 문장 부호입니다.
- 페이지가 두 개 미만이면 아무것도 그리지 않습니다. disabled된
1하나만 있는 줄은 할 일이 없다고 광고하는 컨트롤입니다. - 범위 끝의 이동 버튼은 disabled가 되면서 자리를 지키므로, 줄이 옆으로 밀리는 일이 없습니다.
React 빌드와 다른 점
| React | Flutter | 이유 |
|---|---|---|
getPageHref, renderLink | — | Flutter에는 링크 요소가 없고 Flutter 앱을 크롤링하는 것도 없으니, 줄은 버튼이고 라우터는 onPageChanged에서 부릅니다. |
defaultPage / onPageChange | page / onPageChanged | Flutter 자신의 컨트롤이 controlled이고, 콜백 이름도 Flutter의 것입니다. |
live region인 statusLabel | — | 현재 페이지는 focus가 닿을 때 그 버튼의 이름으로 알려지고, 페이지가 바뀔 때마다 울리는 live region은 방금 갈아 끼운 목록 위에 말을 겹쳐 놓게 됩니다. |
<ul>을 감싼 <nav> | 이름이 붙은 semantics 묶음 | 건너뛸 landmark도, 리셋이 앗아 갈 목록 의미도 없습니다. |
aria-current="page" | 채워진 variant와 버튼의 이름 | Flutter의 semantics 트리에는 current가 없습니다. |