PlStepper
사용자가 지나가고 있는 절차와 그 안에서의 현재 위치를 보여 줍니다. 각 step은 버튼이고, 현재 step이 패널을 쥐며, 누르면 사용자가 그리로 옮겨 갑니다.
import { PlStep, PlStepper } from 'plass-ui';
<PlStepper active={step} onActiveChange={setStep}>
<PlStep label="Account">…</PlStep>
<PlStep label="Verify">…</PlStep>
<PlStep label="Profile" optional>
…
</PlStep>
</PlStepper>;import 'package:plass_ui/plass_ui.dart';
PlStepper(
active: step,
onActiveChanged: (int next) => setState(() => step = next),
steps: const <PlStep>[
PlStep(label: Text('Account'), child: Text('…')),
PlStep(label: Text('Verify'), child: Text('…')),
PlStep(label: Text('Profile'), optional: Text('Optional'), child: Text('…')),
],
);Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| active | number | — | 사용자가 있는 step의 인덱스. 제어하려면 onActiveChange와 함께 |
| defaultActive | number | 0 | 제어하지 않을 때 시작하는 step |
| onActiveChange | (active: number) => void | — | step을 눌렀을 때 |
| linear | boolean | true | 현재보다 앞선 step으로 건너뛸 수 있는지. 뒤의 step은 언제나 닿을 수 있습니다 |
| orientation공통 | PlassResponsive<'horizontal' | 'vertical'> | 'horizontal' | 가로는 패널을 레일 아래에, 세로는 각 step 안에 놓습니다 |
| connector | 'solid' | 'dashed' | 'dotted' | 'none' | 'solid' | step 사이의 선. none은 간격을 비웁니다 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | bullet과 타입 스케일 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 레일의 색 역할 |
| density공통 | 'default' | 'compact' | 'default' | step 사이의 간격만 바꿉니다 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| steps * | List<PlStep> | — | 순서대로의 step들 |
| active * | int | — | 사용자가 있는 step의 인덱스. 제어하려면 onActiveChange와 함께 |
| onActiveChanged | ValueChanged<int>? | — | 눌린 step과 함께 호출됩니다. null이면 모든 step이 비활성입니다 — 구동하지 않고 보여 주기만 하는 방법입니다 |
| linear | bool | true | 현재보다 앞선 step으로 건너뛸 수 있는지. 뒤의 step은 언제나 닿을 수 있습니다 |
| orientation공통 | PlassResponsive<PlassOrientation> | PlassOrientation.horizontal | 가로는 패널을 레일 아래에, 세로는 각 step 안에 놓습니다 |
| size공통 | PlassSize | PlassSize.md | bullet과 타입 스케일 |
| color공통 | PlassColor | PlassColor.primary | 레일의 색 역할 |
| density공통 | PlassDensity | PlassDensity.standard | step 사이의 간격만 바꿉니다 |
PlStep
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| label | ReactNode | — | step의 이름 |
| description | ReactNode | — | 아래 한 줄 — 이 step이 무엇을 묻는지 |
| bullet | ReactNode | — | bullet 안에 그릴 것. 기본은 step의 번호이고, 지나가면 체크입니다 |
| status | 'complete' | 'current' | 'upcoming' | — | 시퀀스가 말하는 위치를 덮어씁니다. 사용자가 더 간 사이 유효성 검사에 걸린 step을 위한 것 |
| optional | boolean | ReactNode | — | 건너뛸 수 있음을 표시합니다. true는 "Optional"을, node는 그 node를 그립니다 |
| disabled | boolean | false | linear가 무엇이든 닿을 수 없습니다 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | — | 이 step만 stepper의 family를 덮어씁니다 |
| children | ReactNode | — | 현재 step일 때 보여 줄 패널 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| label | Widget? | — | step의 이름 |
| description | Widget? | — | 아래 한 줄 — 이 step이 무엇을 묻는지 |
| bullet | Widget? | — | bullet 안에 그릴 것. 기본은 step의 번호이고, 지나가면 체크입니다 |
| status | PlStepStatus? | — | 시퀀스가 말하는 위치를 덮어씁니다. 사용자가 더 간 사이 유효성 검사에 걸린 step을 위한 것 |
| optional | Widget? | — | 건너뛸 수 있음을 표시하고, 그 말로 말합니다. 기본 문자열이 없습니다 — 패키지는 번역을 싣지 않고, 지어낸 단어는 어느 한 언어의 것이기 때문입니다 |
| disabled | bool | false | linear가 무엇이든 닿을 수 없습니다 |
| color공통 | PlassColor | — | 이 step만 stepper의 family를 덮어씁니다 |
| connector | PlassStepConnector | PlassStepConnector.solid | 다음 step으로 가는 선. none은 간격을 비웁니다 |
| child | Widget? | — | 현재 step일 때 보여 줄 패널 |
네이티브 <div> 속성은 stepper로, <li> 속성은 step으로 그대로 통과합니다. 공유 축이 라이브러리 전체에서 무엇을 뜻하는지는 prop 규약에 있습니다.
step은 children이 아니라 리스트입니다. PlTimeline의 것이 그런 이유와 같습니다(stepper가 그것들에 대해 추론해야 하고(어느 것이 complete인지는 인덱스 산수이고, 어느 것에 닿을 수 있는지도 같은 인덱스 산수입니다), 두 질문 모두 불투명한 Widget에게는 물을 수 없습니다. React 빌드가 경고해야 하는 날카로운 모서리도 그것으로 사라집니다). step 셋을 품은 wrapper를 건넬 방법이 아예 없습니다.
optional은 bool이 아니라 Widget을 받습니다. 물러설 기본 문자열이 없기 때문입니다. 패키지는 번역을 싣지 않고, 지어낸 단어는 어느 한 언어의 것입니다.
stepper인가 timeline인가
둘은 같은 레일을 그립니다(같은 세 가지 bullet 상태, 같은 connector). 그리고 소스에서도 그것을 공유합니다. 후광이 진 bullet이 두 가지를 뜻해서는 안 되기 때문입니다. 차이는 각각이 무엇을 위한 것이냐입니다.
PlTimeline | 보고합니다. 이미 일어난 시퀀스를 텍스트로. 아무것도 누를 수 없습니다 |
PlStepper | 시퀀스 그 자체입니다. step이 버튼이고, 현재 step이 패널을 갖고, 사용자가 그 안에 있습니다 |
아무것도 클릭할 수 없어야 한다면 그것은 timeline입니다.
Examples
active
timeline의 것과 똑같이 값이 아니라 인덱스입니다. stepper에는 선택이 없습니다. 그 앞은 전부 complete, 그 자리가 current, 그 뒤는 전부 upcoming입니다.
defaultActive로 제어하지 않거나, active와 onActiveChange로 제어합니다. 폼 마법사가 원하는 것이 후자입니다. Next 버튼은 호출자의 것이고, 그것이 움직일지 정하는 유효성 검사도 마찬가지입니다.
linear
기본이 켜짐이고, 이것이 이 컴포넌트를 탭 한 줄이 아니라 절차로 만듭니다. 가입 세 번째 단계를 두 번째보다 먼저 채울 수는 없습니다. 사용자 뒤에 있는 step은 언제나 닿을 수 있습니다. 답을 고치러 돌아갈 수 있다는 것이 stepper가 문이 하나뿐인 마법사가 아닌 이유 전부입니다.
모든 step이 이미 답해졌고 사용자가 하나를 확인하러 돌아가는 검토 화면에서는 끄세요.
<PlStepper active={3} linear={false}>
…
</PlStepper>orientation
가로는 패널을 레일 전체 아래에 놓습니다. 세로는 각 step의 패널을 그 step 안에 놓습니다. 애초에 세로로 두는 이유가 그것입니다. 답이 레일 아래가 아니라 질문 아래에 놓입니다.
반응형입니다. 그래서 한 집합이 폰에서는 이쪽으로, 노트북에서는 저쪽으로 갈 수 있습니다. 서버는 xs 항목을 렌더링하고 브라우저가 hydration에서 고칩니다.build에서 창 너비를 기준으로 풀리므로 첫 프레임부터 정확합니다. 브레이크포인트 참고.
import { useState } from 'react';
import { PlButton, PlStep, PlStepper, PlTypography } from 'plass-ui';
export default function StepperVertical() {
const [active, setActive] = useState(0);
return (
<div className="w-full max-w-md">
<PlStepper orientation="vertical" active={active} onActiveChange={setActive}>
{[
['Pick a plan', 'Ten seats on the team plan.'],
['Add a card', 'Charged on the first of the month.'],
['Invite the team', 'You can do this later.']
].map(([label, body], index) => (
<PlStep key={label} label={label}>
<div className="flex flex-col items-start gap-2">
<PlTypography level="body">{body}</PlTypography>
<PlButton size="sm" onClick={() => setActive(index + 1)} disabled={index === 2}>
Continue
</PlButton>
</div>
</PlStep>
))}
</PlStepper>
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class StepperVertical extends StatefulWidget {
const StepperVertical({super.key});
@override
State<StepperVertical> createState() => _StepperVerticalState();
}
class _StepperVerticalState extends State<StepperVertical> {
int _active = 0;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 380,
child: PlStepper(
orientation: const PlassResponsive<PlassOrientation>(PlassOrientation.vertical),
active: _active,
onActiveChanged: (int next) => setState(() => _active = next),
steps: <PlStep>[
for (final (int index, (String label, String body)) in <(String, String)>[
('Pick a plan', 'Ten seats on the team plan.'),
('Add a card', 'Charged on the first of the month.'),
('Invite the team', 'You can do this later.'),
].indexed)
PlStep(
label: Text(label),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(body),
const SizedBox(height: 8),
PlButton(
size: PlassSize.sm,
onPressed: index == 2 ? null : () => setState(() => _active = index + 1),
child: const Text('Continue'),
),
],
),
),
],
),
);
}
}status와 color
세 상태 전부를 active가 정하고, status가 그중 하나를 덮어씁니다. 사용자가 세 단계 더 간 사이에 유효성 검사에 걸린 step을 위한 것입니다. stepper를 움직이지 않고 그 step만 다시 current가 되고, color="danger"가 이유를 말합니다.
import { PlStep, PlStepper } from 'plass-ui';
export default function StepperStatus() {
return (
<div className="w-full max-w-2xl">
{/* The reader has moved on, and the second step failed validation behind
them. `status` and `color` say so without moving `active`. */}
<PlStepper active={2} linear={false}>
<PlStep label="Account" />
<PlStep label="Verify" description="Code expired" status="current" color="danger" />
<PlStep label="Profile" />
</PlStepper>
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class StepperStatus extends StatelessWidget {
const StepperStatus({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 560,
// The reader has moved on, and the second step failed validation behind
// them. `status` and `color` say so without moving `active`.
child: PlStepper(
active: 2,
linear: false,
onActiveChanged: (int _) {},
steps: const <PlStep>[
PlStep(label: Text('Account')),
PlStep(
label: Text('Verify'),
description: Text('Code expired'),
status: PlStepStatus.current,
color: PlassColor.danger,
),
PlStep(label: Text('Profile')),
],
),
);
}
}optional
true는 "Optional"이라는 단어를 그립니다. node를 주면 그 node를 대신 그리고, 그것이 이 단어를 번역하는 방법입니다. optionalLabel prop이 없는 이유는, 둘 다 받는 prop 하나면 prop이 하나이기 때문입니다.
<PlStep label="Profile" optional="건너뛸 수 있음" />Notes
step은 stepper의 직계 자식이어야 합니다. stepper는 자식을 걸어가며 번호를 매기므로, step 세 개를 돌려주는 여러분의 컴포넌트는 셋을 품은 자식 하나이고 그 안의 모든 step이 1번이 됩니다. 감싸는 컴포넌트 대신
.map()이나 배열로 목록을 만드세요. 둘 다 평탄화됩니다.
- 아무것도 렌더링하지 않은 조건부 step은 뒤 step들의 번호를 밀지 않습니다.
- stepper 밖의 step도 렌더링됩니다. 앞뒤에 아무것도 없는 step 하나입니다.
step이 리스트이므로 잘못 감쌀 wrapper 자체가 없습니다. props 표 위의 설명을 보세요.
Accessibility
- 진짜
<ol>과<li>이고, 현재 step이aria-current="step"을 답니다. - 일부러
role="tablist"가 아닙니다. tab list는 키보드 사용자에게 tab stop 하나와 화살표 키를, 스크린 리더에게 탭마다 패널 하나를 빚집니다. stepper는 서로 다른 컨트롤의 시퀀스이고, 동작 없이 role만 선언하는 것은 아예 주장하지 않는 것보다 나쁩니다. 닿을 수 있는 각 step이 각자의 tab stop이며, 그것이 stepper의 step이 하는 일입니다. - 닿을 수 없는 step은 disabled 버튼이 아니라 아예 버튼이 아닙니다. 아직 누를 것이 거기 없습니다.
- 패널은 그것이 속한 step으로 이름 붙습니다. 그래서 패널에 도착한 스크린 리더가 어느 step의 패널인지 듣습니다.
현재 step은 selected로 표시합니다. 프레임워크가 가진 것 중 aria-current="step"에 가장 가까운 것입니다. 닿을 수 없는 step은 disabled 버튼이 아니라 그냥 상자입니다.