Accordion
์์ฝ๋์ธ/์ ๊ธฐ ์ปดํฌ๋ํธ
๋ชฉ์ฐจ
์ฝํ ์ธ ๋ฅผ ์ ์๋ค ํผ ์ ์๋ ์์ฝ๋์ธ ์ปดํฌ๋ํธ์ ๋๋ค.
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ (Basic Usage)#
// ๊ธฐ๋ณธ ์์ฝ๋์ธ
Accordion(
items: [
AccordionItem(
title: '์น์
1',
content: Text('์น์
1์ ๋ด์ฉ์
๋๋ค.'),
),
AccordionItem(
title: '์น์
2',
content: Text('์น์
2์ ๋ด์ฉ์
๋๋ค.'),
),
AccordionItem(
title: '์น์
3',
content: Text('์น์
3์ ๋ด์ฉ์
๋๋ค.'),
),
],
)
// ๋ค์ค ์ด๊ธฐ ํ์ฉ
Accordion(
allowMultiple: true,
items: accordionItems,
)
// ์ด๊ธฐ ์ด๋ฆผ ์ํ โ ํญ๋ชฉ๋ณ expanded
Accordion(
items: [
AccordionItem(
title: '๊ธฐ๋ณธ ์ ๋ณด',
content: Text('๊ธฐ๋ณธ ์ ๋ณด ๋ด์ฉ์
๋๋ค.'),
expanded: true,
),
AccordionItem(
title: '์ถ๊ฐ ์ ๋ณด',
content: Text('์ถ๊ฐ ์ ๋ณด ๋ด์ฉ์
๋๋ค.'),
),
],
)
// ๊ธฐ๋ณธ ์์ฝ๋์ธ
Accordion(
items: [
AccordionItem(
title: '์น์
1',
content: Text('์น์
1์ ๋ด์ฉ์
๋๋ค.'),
),
AccordionItem(
title: '์น์
2',
content: Text('์น์
2์ ๋ด์ฉ์
๋๋ค.'),
),
AccordionItem(
title: '์น์
3',
content: Text('์น์
3์ ๋ด์ฉ์
๋๋ค.'),
),
],
)
// ๋ค์ค ์ด๊ธฐ ํ์ฉ
Accordion(
allowMultiple: true,
items: accordionItems,
)
// ์ด๊ธฐ ์ด๋ฆผ ์ํ โ ํญ๋ชฉ๋ณ expanded
Accordion(
items: [
AccordionItem(
title: '๊ธฐ๋ณธ ์ ๋ณด',
content: Text('๊ธฐ๋ณธ ์ ๋ณด ๋ด์ฉ์
๋๋ค.'),
expanded: true,
),
AccordionItem(
title: '์ถ๊ฐ ์ ๋ณด',
content: Text('์ถ๊ฐ ์ ๋ณด ๋ด์ฉ์
๋๋ค.'),
),
],
)
Live Preview#
์ฝ๋ ๋ณด๊ธฐ
class AccordionDefaultExample extends StatelessComponent {
const AccordionDefaultExample({super.key});
@override
Component build(BuildContext context) {
return Accordion(
items: [
AccordionItem(
title: 'Is it accessible?',
content: Text('Yes. It adheres to the WAI-ARIA design pattern.'),
expanded: true,
),
AccordionItem(
title: 'Is it styled?',
content: Text('Yes. It comes with default styles that matches the other components.'),
),
AccordionItem(
title: 'Is it animated?',
content: Text('Yes. It\'s animated by default, but you can disable it if you prefer.'),
),
],
);
}
}
class AccordionDefaultExample extends StatelessWidget {
const AccordionDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return const Accordion(
items: [
AccordionItem(
title: 'Is it accessible?',
content: Text('Yes. It adheres to the WAI-ARIA design pattern.'),
expanded: true,
),
AccordionItem(
title: 'Is it styled?',
content: Text(
'Yes. It comes with default styles that matches the other components.',
),
),
AccordionItem(
title: 'Is it animated?',
content: Text(
"Yes. It's animated by default, but you can disable it if you prefer.",
),
),
],
);
}
}
์ฌ์ฉ ์๊ธฐ (When to Use)#
์ด ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ์ธ์:
- FAQ, ๋์๋ง ๋ฑ ์ง๋ฌธ-๋ต๋ณ ํํ์ ์ฝํ ์ธ ๋ฅผ ํ์ํ ๋
- ๊ธด ๋ด์ฉ์ ์น์ ๋ณ๋ก ์ ์ด์ ๊ณต๊ฐ์ ์ ์ฝํ ๋
- ์ค์ ํจ๋์ ์นดํ ๊ณ ๋ฆฌ๋ณ ์ต์ ์ ๊ทธ๋ฃนํํ ๋
๋์ ๋ค๋ฅธ ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ์ธ์:
- Tree: ๊ณ ์ ์ฝํ ์ธ ์น์ ์ด ์๋๋ผ ๋ฐ์ดํฐ์ ๋ถ๋ชจยท์์ ๊ฐ์ง๋ฅผ ํ์ํด์ผ ํ ๋
-
Collapsible: ํญ๋ชฉ ๋ชฉ๋ก์ ์ํธ๋ฐฐํ ์ด๋ฆผ ์์ด, ํ ์์ญ์
expanded๋ฅผ ํธ์ถ๋ถ์์ ์ ์ดํด์ผ ํ ๋ - Disclosure: ํธ๋ฆฌ๊ฑฐ ยท ์์ฝ ยท ์ถ๊ฐ ๋ด์ฉ ์ฌ๋กฏ์ ๊ฐ๋ ๋ ๋ฆฝ์ ์ธ ์ ๋ณด ๊ณต๊ฐ๊ฐ ํ์ํ ๋
- Tabs: ํผ์นจ ๋ชฉ๋ก์ด ์๋๋ผ ์ ํ๋ ํจ๋ ํ๋๋ง ๊ต์ฒดํด์ผ ํ ๋
- Dialog / Drawer: ๋ณธ๋ฌธ ํ๋ฆ ์์ ์ ํ ์์ญ์ด ์๋๋ผ ์ค๋ฒ๋ ์ด๊ฐ ํ์ํ ๋
Props / Parameters#
Accordion ํ๋ผ๋ฏธํฐ#
| ์์ฑ | ํ์ | ๊ธฐ๋ณธ๊ฐ | ์ค๋ช |
|---|---|---|---|
items |
List<W> |
ํ์ | ์์ฝ๋์ธ ํญ๋ชฉ ๋ชฉ๋ก (AccordionItem) |
allowMultiple |
bool |
false |
๋์์ ์ฌ๋ฌ ํญ๋ชฉ ์ด๊ธฐ ํ์ฉ |
accordionStyle |
CoreAccordionStyle? |
null |
์ธ์คํด์ค ์คํ์ผ (Style ์์คํ ์ฐธ์กฐ) |
AccordionItem#
W ๋ Flutter์ Widget, Web์ Jaspr Component ์
๋๋ค. ์ฌ๋กฏ์ ์๋ฏธ์ ํ์ ์ฌ๋ถ๋ ๊ณตํต์
๋๋ค.
| ์์ฑ | ํ์ | ๊ธฐ๋ณธ๊ฐ | ์ค๋ช |
|---|---|---|---|
content | W | ํ์ | ํผ์นจ ๋ด์ฉ |
title | String? | null | ๊ธฐ๋ณธ ํค๋ ์ ๋ชฉ |
trigger |
W? |
null |
์ปค์คํ
ํค๋. title ๋๋ trigger ์ค ํ๋๋ ํ์ |
expanded |
bool |
false |
์ต์ด ํผ์นจ ์ํ. ์ธ๋ถ ์ ์ด ๊ฐ์ด๋ ๋ณ๊ฒฝ ์ฝ๋ฐฑ์ด ์๋ |
AccordionTrigger (Flutter)#
| ์์ฑ | ํ์ | ๊ธฐ๋ณธ๊ฐ | ์ค๋ช |
|---|---|---|---|
child | Widget | ํ์ | ํ ๊ธ ๋ฐฐ์ ์ ์์ํ๋ ์ปค์คํ ํค๋ ๋ด์ฉ |
์คํ์ผ ์์คํ (Style System)#
Accordion ์ ๋ชจ๋ chrome / dimensional / nested-slot ์ค๋ฒ๋ผ์ด๋๋ CoreAccordionStyle ๋จ์ผ ์ฌ๋กฏ์ผ๋ก ํ๋ฆ
๋๋ค. ์๋งจํฑ enum / behaviour (items
/ allowMultiple) ๋ ์์ ฏ ํ๋ผ๋ฏธํฐ๋ก ์ง์ ์ ๋ฌํฉ๋๋ค.
์๋งจํฑ vs ์คํ์ผ#
-
์๋งจํฑ enum / behaviour: ์์ ฏ/์ปดํฌ๋ํธ ํ๋ผ๋ฏธํฐ๋ก ์ง์ (
items,allowMultiple) -
chrome / dimensional / ์ฌ๋กฏ ์คํ์ผ:
CoreAccordionStyleํ ๊ณณ์ผ๋ก (backgroundColor/borderColor/borderWidth/borderRadius/contentPadding/itemSpacing/dividerColor/dividerThickness/duration/arrowIconStyle/triggerStyle/contentTextStyle/chevronIconStyle/iconGapStyle/clickableStyle)
Resolve chain#
design system default for accordion
โ CoreAccordionTheme.style // ํ๋ก์ ํธ ๊ณตํต
โ parent component slot override
โ widget.accordionStyle // ์ธ์คํด์ค๋ณ
๊ฐ nested ์ฌ๋กฏ ์คํ์ผ (triggerStyle / contentTextStyle / chevronIconStyle) ์ ์๊ธฐ ์ปดํฌ๋ํธ์ ์์ฒด resolve chain ์ผ๋ก ๋ค์ ํ ๋ฒ ๋จธ์ง๋ฉ๋๋ค.
CoreAccordionStyle ํ๋#
| ํ๋ | ํ์ | ์ค๋ช |
|---|---|---|
duration |
Duration? |
Expand / collapse animation duration. Visual transition timing โ chrome category, lives on the style not the widget. |
arrowIconStyle |
CoreIconStyle? |
Chevron / disclosure icon style override. size defers to [defaultArrowIconStyle]. |
backgroundColor |
CoreColor? |
Container background colour for an expanded item.
Deliberately has no default* โ absence is the design.
The Web resolver adds the
bg-*
class and its inline fragment only inside
if (merged.backgroundColor != null)
, so a null here is what leaves the accordion transparent over whichever surface hosts it. A constant would turn that branch permanently on and give every accordion in every tree an opaque panel of its own โ and only on one platform, because the Flutter widget never reads the resolved value (it paints no container fill at all, so seat (4) of the 1:1:1:1:1:1 mapping is empty for this field).
|
borderColor |
CoreColor? |
Outer border stroke colour.
Deliberately has no default* โ absence is the design.
The Web resolver emits the border colour only inside
if (merged.borderColor != null)
, and that emit sets
border-color
alone โ width comes from the separate [borderWidth] override, whose own default is
stroke0
([defaultBorderWidth]). A colour constant would therefore put a
border-*
class on every accordion to describe a stroke that has no width to draw it. The outline this component ships is the between-items rule, not a box around the whole list.
|
borderWidth |
double? |
Outer border stroke width (logical px). |
borderRadius |
CoreBorderRadius? |
Outer border radius.
Deliberately has no default* โ absence is the design.
The Web resolver writes
border-radius
only inside
if (merged.borderRadius != null)
. Square is correct for the shipped accordion: it has no outer fill and no outer stroke ([borderColor] / [defaultBorderWidth] above), so there is no edge for a radius to round. A constant would turn that branch permanently on, so the rule would ship on every accordion and start rounding the moment a caller sets [backgroundColor] or [borderColor] โ a corner nobody asked for, arriving with an unrelated override.
|
contentPadding |
CoreEdgeInsets? |
Padding applied to expanded content. |
itemSpacing |
double? |
Vertical spacing between accordion items (logical px) โ native
Column.spacing
(Flutter) /
row-gap
(Web). When
null
items share a divider line instead of a gap.
Deliberately has no
default*
โ absence is the design, and it is load-bearing on both platforms.
Being null is what selects the divider-separated layout: the Flutter widget branches on
itemSpacing != null
to decide between
Column.spacing
and interleaved
Divider
s, and the Web resolver only switches the container to
display:flex; flex-direction:column; row-gap
inside
if (merged.itemSpacing != null)
. A constant would delete the separator from every accordion that never asked for gaps โ the two layouts are alternatives, so a default here is a default for the choice, not for a measurement.
|
dividerColor |
CoreColor? |
Divider line colour between items.
Deliberately has no default* โ absence delegates to the child.
Both platforms treat null as "let the composed
Divider
supply its own colour": Flutter renders a bare
const Divider
unless [dividerColor] or [dividerThickness] is set, and the Web resolver returns a null
divider
chrome for the same condition so the widget falls back to the
Divider
component instead of a raw
<div>
. A constant would flip that pair of branches and, being equal to
CoreDividerStyle.defaultColor
, would restate the child's own default here โ the flat-duplication half of "ํฉ์ฑ = ์ฌ๊ตฌํ ๊ธ์ง".
|
dividerThickness |
double? |
Divider line thickness (logical px).
Deliberately has no default* โ same delegation as [dividerColor].
It is the other half of the one condition both platforms test (
dividerColor == null && dividerThickness == null
), so a constant on either field alone is enough to swap the composed
Divider
for a hand-built rule on every accordion, and its value would restate
CoreDividerStyle.defaultThickness
.
|
triggerStyle |
CoreButtonStyle? |
Trigger row chrome. Each rendered trigger uses
Button(variant: ghost or accordion)
internally; this slot tweaks chrome (paddingH / labelStyle / leadingIconStyle).
|
contentTextStyle |
CoreTextStyle? |
Expanded content text style (applied when the content is a plain string). |
chevronIconStyle |
CoreIconStyle? |
Chevron / disclosure icon style (the rotating indicator). |
iconGapStyle |
CoreGapStyle? |
1-off spacer between the trigger label and the chevron icon โ rendered by
Gap
(Flutter) / inline
gap
CSS (Web). Defers to [defaultIconGapStyle].
|
clickableStyle |
CoreClickableStyle? |
Nested [CoreClickableStyle] slot for the composed trigger
Clickable
(press scale / focus ring / cursor / keyboard activation). Merged on top of [defaultClickableStyle] and raw-forwarded โ the Clickable's own resolver fills the rest.
|
์ฌ์ฉ ์ (Flutter)#
Accordion(
items: [
AccordionItem(
title: 'FAQ Question',
content: Text('Answer'),
),
],
accordionStyle: CoreAccordionStyle(
contentPadding: CoreEdgeInsets.symmetric(vertical: CoreSpace.space24),
dividerColor: CoreColor.token(CoreColors.outlineVariant),
dividerThickness: CoreStrokeWidth.stroke1,
triggerStyle: CoreButtonStyle(
padding: CoreEdgeInsets.symmetric(vertical: CoreSpace.space16),
labelStyle: CoreTextStyle.token(CoreTextStyles.bodyLarge),
),
contentTextStyle: CoreTextStyle.token(CoreTextStyles.bodySmall),
chevronIconStyle: CoreIconStyle(size: CoreIconSize.size20),
),
)
์ฌ์ฉ ์ (Web)#
Accordion(
items: [
AccordionItem(
title: 'FAQ Question',
content: Text('Answer'),
),
],
accordionStyle: CoreAccordionStyle(
contentPadding: CoreEdgeInsets.symmetric(vertical: CoreSpace.space24),
dividerThickness: CoreStrokeWidth.stroke1,
triggerStyle: CoreButtonStyle(
padding: CoreEdgeInsets.symmetric(vertical: CoreSpace.space16),
),
),
)
๋น ๋ฅธ ์ค๋ฒ๋ผ์ด๋ (Chain)#
์ด๋ฏธ ๋ง๋ Accordion ์ธ์คํด์ค์ ์คํ์ผ์ ๋น ๋ฅด๊ฒ ๋ง๋ถ์ด๊ณ ์ถ๋ค๋ฉด withStyle ์ฒด์ธ์ ์ธ ์ ์์ต๋๋ค. ์์ฑ์์ style ์ฌ๋กฏ ์ธ์์ ๋์ผํ๊ฒ ๋์ํ์ง๋ง, ์ด๋ฏธ ๊ตฌ์ฑ๋ ์์ ฏ ์์์ ๋ฐ๋ก ์ด์ด ์ธ ์ ์์ต๋๋ค.
.radius4์ฒ๋ผ Core ํ ํฐ ์์ ์ด๋ฆ๊ณผ ๋๊ฐ์ ์ด๋ฆ์ getter๋ ์์ต๋๋ค โ withStyle์ ํ ๋ฒ ๋ ์ค์ธ sugar๋ก, ์ด๋ฆ์ด ๊ณง ๊ฐ์ด๋ผ(radius4 ==
CoreRadius.radius4) ์ด๋ ์ปดํฌ๋ํธ์์ ์จ๋ ๋ป์ด ๊ฐ๋ฆฌ์ง ์์ต๋๋ค.
class AccordionChainExample extends StatelessWidget {
const AccordionChainExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ํ ํฐ-์ ํ getter combo โ ์ด๋ฆ์ด ๊ณง ๊ฐ (getter ์ด๋ฆ = Core ํ ํฐ ์์ 1:1).
const Accordion(
items: [
AccordionItem(
title: 'Is it accessible?',
content: Text('Yes. It adheres to the WAI-ARIA design pattern.'),
expanded: true,
),
AccordionItem(
title: 'Is it styled?',
content: Text(
'Yes. It comes with default styles that matches the other components.',
),
),
AccordionItem(
title: 'Is it animated?',
content: Text(
"Yes. It's animated by default, but you can disable it if you prefer.",
),
),
],
).radius16.primary,
const Gap.space12(),
// withStyle full-control โ getter ๊ฐ ์๋ ํ๋(borderColor/borderWidth)๊น์ง.
const Accordion(
items: [
AccordionItem(
title: 'Is it accessible?',
content: Text('Yes. It adheres to the WAI-ARIA design pattern.'),
expanded: true,
),
AccordionItem(
title: 'Is it styled?',
content: Text(
'Yes. It comes with default styles that matches the other components.',
),
),
AccordionItem(
title: 'Is it animated?',
content: Text(
"Yes. It's animated by default, but you can disable it if you prefer.",
),
),
],
).withStyle(
const CoreAccordionStyle(
backgroundColor: CoreColor.token(
CoreColors.surfaceContainerHighest,
),
borderColor: CoreColor.token(CoreColors.tertiary),
borderWidth: CoreStrokeWidth.stroke2,
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
),
),
],
);
}
}
class AccordionChainExample extends StatelessComponent {
const AccordionChainExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
// ํ ํฐ-์ ํ getter combo โ ์ด๋ฆ์ด ๊ณง ๊ฐ (getter ์ด๋ฆ = Core ํ ํฐ ์์ 1:1).
Accordion(
items: [
AccordionItem(
title: 'Is it accessible?',
content: Text('Yes. It adheres to the WAI-ARIA design pattern.'),
expanded: true,
),
AccordionItem(
title: 'Is it styled?',
content: Text(
'Yes. It comes with default styles that matches the other components.',
),
),
AccordionItem(
title: 'Is it animated?',
content: Text(
'Yes. It\'s animated by default, but you can disable it if you prefer.',
),
),
],
).radius16.primary,
const Gap.space12(),
// withStyle full-control โ getter ๊ฐ ์๋ ํ๋(borderColor/borderWidth)๊น์ง.
Accordion(
items: [
AccordionItem(
title: 'Is it accessible?',
content: Text('Yes. It adheres to the WAI-ARIA design pattern.'),
expanded: true,
),
AccordionItem(
title: 'Is it styled?',
content: Text(
'Yes. It comes with default styles that matches the other components.',
),
),
AccordionItem(
title: 'Is it animated?',
content: Text(
'Yes. It\'s animated by default, but you can disable it if you prefer.',
),
),
],
).withStyle(
const CoreAccordionStyle(
backgroundColor: CoreColor.token(
CoreColors.surfaceContainerHighest,
),
borderColor: CoreColor.token(CoreColors.tertiary),
borderWidth: CoreStrokeWidth.stroke2,
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
),
),
],
classes: 'flex flex-col items-start',
);
}
}
๋ณํ (Variants)#
์์ด์ฝ ํฌํจ (์ปค์คํ ํธ๋ฆฌ๊ฑฐ)#
title ์ ๋ฌธ์์ด ์ ์ฉ์ด๋ผ, ํค๋์ ์์ด์ฝ์ ๋ฃ์ผ๋ ค๋ฉด trigger ์ฌ๋กฏ์ ์ง์ ์กฐ๋ฆฝํฉ๋๋ค. Flutter ๋ AccordionTrigger
๋ก ๊ฐ์ธ๋ฉด ํ ๊ธยทํฌ์ปค์คยทchevron ์ด ๊ทธ๋๋ก ๋ฐ๋ผ์ค๊ณ , Web ์ trigger ๋ ์์ Component ๋ผ ํ์ํ ์ธํฐ๋์
์ ์ง์ ๋ถ์
๋๋ค.
// Flutter
AccordionItem(
trigger: AccordionTrigger(
child: Row(
children: [
Icon(LucideIcons.user),
Gap(gapStyle: CoreGapStyle(size: CoreSpace.space8)),
Text('๊ฐ์ธ ์ ๋ณด').bodyMedium.onSurface,
],
),
),
content: PersonalInfoForm(),
)
FAQ ์คํ์ผ#
Accordion(
items: faqList.map((faq) => AccordionItem(
title: faq.question,
content: Text(faq.answer),
)).toList(),
)
๋จ์ผ ํญ๋ชฉ (์ ๊ธฐ/ํผ์น๊ธฐ)#
AccordionItem(
title: '๊ณ ๊ธ ์ค์ ',
expanded: false,
content: AdvancedSettingsPanel(),
)
๋์ ์คํ (Behavior)#
์ด๊ธฐ/๋ซ๊ธฐ#
- ํค๋ ํด๋ฆญ ์ ํด๋น ์น์ ์ด ์ฌ๋ผ์ด๋ ์ ๋๋ฉ์ด์ ์ผ๋ก ํผ์ณ์ง/์ ํ
allowMultiple: false(๊ธฐ๋ณธ): ํ๋๋ฅผ ์ด๋ฉด ์ด์ ์ ์ด๋ฆฐ ํญ๋ชฉ์ด ์๋์ผ๋ก ๋ซํallowMultiple: true: ์ฌ๋ฌ ํญ๋ชฉ์ ๋์์ ์ด ์ ์์-
expanded๋ ํญ๋ชฉ์ ๋ง๋ค ๋์ ์ด๊ธฐ๊ฐ์ ๋๋ค. ์ด๋ฆผ์ ์ปดํฌ๋ํธ ๋ด๋ถ์์ ๊ด๋ฆฌํ๋ฉฐ ์ธ๋ถ ์ ์ด ๊ฐ ยท ๋ณ๊ฒฝ ์ฝ๋ฐฑ ยท ๋นํ์ฑ ํ๋๊ทธ๋ ์์ต๋๋ค. ์ด๊ธฐ๊ฐ์ผ๋ก ์ฌ๋ฌ ํญ๋ชฉ์ ์ด ๋๋ ๋จ์ผ ์ด๋ฆผ ๋ชจ๋๊ฐ ์ด๊ธฐ ์ํ๋ฅผ ์ ๊ทํํ๋ค๊ณ ๊ธฐ๋ํ์ง ๋ง์ธ์.
์ ๋๋ฉ์ด์ #
- ๊ธฐ๋ณธ
CoreDuration.normal(200ms),easeInOut์ปค๋ธ -
์๊ฐ์
accordionStyle.duration์ผ๋ก ์กฐ์ ํฉ๋๋ค (ํ๋ก์ ํธ ๊ณตํต์CoreAccordionTheme.style.duration)
Accordion(
items: accordionItems,
accordionStyle: CoreAccordionStyle(
duration: Duration(milliseconds: CoreDuration.moderate),
),
)
ํ์ดํ ์์ด์ฝ#
- ํค๋ ์ฐ์ธก์ chevron ์์ด์ฝ์ด ์ด๋ฆผ/๋ซํ ๋ฐฉํฅ์ ํ์ (180ยฐ ํ์ ํธ๋์ง์ )
- ํฌ๊ธฐยท์์
accordionStyle.chevronIconStyle(๊ธฐ๋ณธ๊ฐ์arrowIconStyle) ๋ก ์กฐ์ ํฉ๋๋ค - ์์ด์ฝ ๊ธ๋ฆฌํ ์์ฒด๋ฅผ ๋ฐ๊พธ๋ ค๋ฉด
trigger์ฌ๋กฏ์ ํค๋๋ฅผ ์ง์ ์กฐ๋ฆฝํฉ๋๋ค
์ฌ์ฉ ๊ฐ์ด๋๋ผ์ธ (Usage Guidelines)#
โ Do#
์์ฃผ ๋ฌป๋ ์ง๋ฌธ์ ์์ฝ๋์ธ์ ํ์ฉํ์ธ์.
Accordion(
items: faqList.map((faq) => AccordionItem(
title: faq.question,
content: Text(faq.answer),
)).toList(),
)
์ง๋ฌธ์ ํ์ด๋ณด๊ณ ์ํ๋ ๋ต๋ณ๋ง ํผ์ณ ๋ณผ ์ ์์ต๋๋ค.
โ Don't#
ํ์๋ก ๋ด์ผ ํ๋ ๋ด์ฉ์ ์์ฝ๋์ธ์ ์จ๊ธฐ์ง ๋ง์ธ์.
Accordion(items: [
AccordionItem(
title: '๊ฒฐ์ ์ ๋ณด',
content: PaymentForm(), // ๋ฐ๋์ ์
๋ ฅํด์ผ ํ๋ ํผ
),
])
์ค์ํ ๋ด์ฉ์ด ์ ํ ์์ผ๋ฉด ์ฌ์ฉ์๊ฐ ๋์น ์ ์์ต๋๋ค.
โ Do#
์น์ ์๊ฐ ๋ง์ผ๋ฉด ์์ฝ๋์ธ์ ์ฌ์ฉํ์ธ์.
Accordion(
items: categories.map((cat) => AccordionItem(
title: cat.name,
content: cat.settingsPanel,
)).toList(),
)
์ค์ ํ์ด์ง์ฒ๋ผ ๋ง์ ์น์ ์ ํ ํ์ด์ง์ ๋ด์ ์ ์์ต๋๋ค.
โ Don't#
์ ์ ํ์๊ฐ ์๋ ๋ด์ฉ์ ์์ฝ๋์ธ์ ์ฌ์ฉํ์ง ๋ง์ธ์.
Accordion(items: [
AccordionItem(title: '์ ์ผํ ์น์
', content: onlySection),
])
ํญ๋ชฉ ์ ์ ํ์ ์์ต๋๋ค. ์ฌ์ฉ์๊ฐ ํญ์ ์ฝ์ด์ผ ํ๊ณ ์ ์ด์ ์ป๋ ๊ณต๊ฐ๋ ์๋ค๋ฉด ๊ทธ๋๋ก ํ์ํ์ธ์.
์ ๊ทผ์ฑ (Accessibility)#
์ญํ #
-
Flutter โ ํค๋๋
Clickableํฉ์ฑ์ด๋ผSemantics(button: true, enabled: โฆ)๋ฅผ ๋ฐ๊ณ (clickable.dart:505), ๊ทธ ์๋ฅผCoUISemantics(expanded: โฆ)๊ฐ ๊ฐ์ธ ํผ์นจ ์ํ๋ฅผ ์น์ต๋๋ค(accordion.dart:488).Accordion๋ฃจํธ์ ํญ๋ชฉ ๋ํผ์๋ role ์ด ์์ต๋๋ค. -
Web โ ํค๋๋
Clickable์<div>๋ผrole="button"+tabindex="0"์ด ๋ถ๊ณ (clickable.dart:436), ๊ทธ ์์aria-expanded๊ฐ caller attributes ๋ก ์ค๋ฆฝ๋๋ค(accordion.dart:363). ๋ฃจํธ<div>๋configureAttributes๋ฅผ override ํ์ง ์์ ํธ์ถ์๊ฐ ๋๊ธด attributes ์ธ์๋ ์๋ฌด role ๋ ๋ถ์ง ์์ต๋๋ค.
์ ํ๋ซํผ ๋ชจ๋ ํค๋์ ํจ๋์ ์๋ aria-controls ๋, ํจ๋์ region role ๋, ํค๋ฉ ๋ ๋ฒจ๋ ๋ด๋ณด๋ด์ง ์์ต๋๋ค
โ ๋ฆฌ๋์ heading ๋ชฉ๋ก์ ์์ฝ๋์ธ ํค๋๋ ์กํ์ง ์์ต๋๋ค.
ํค๋ณด๋#
| ํค | Flutter | Web |
|---|---|---|
Enter / Space | ํฌ์ปค์ค๋ ํค๋ ์ด๊ธฐ/๋ซ๊ธฐ | ํฌ์ปค์ค๋ ํค๋ ์ด๊ธฐ/๋ซ๊ธฐ |
Tab / Shift+Tab | ํค๋๋ง๋ค ๊ฐ๋ณ tab stop | ํค๋๋ง๋ค ๊ฐ๋ณ tab stop |
ArrowUp / ArrowDown ๋ก ํค๋ ๊ฐ ์ด๋ |
์์ | ์์ |
Home / End ๋ก ์ฒซ/๋ง์ง๋ง ํค๋ |
์์ | ์์ |
์ปดํฌ๋ํธ๊ฐ ๋ฐ์ธ๋ฉํ๋ ํค๋ Enter / Space ๋ฟ์
๋๋ค(clickable.dart:14). WAI-ARIA ์์ฝ๋์ธ ํจํด์ด ์ ํ ์ฌํญ์ผ๋ก ๋๋ ํค๋ ๊ฐ ํ์ดํยทHome/End
์ด๋์ ๊ตฌํ๋ผ ์์ง ์์ต๋๋ค.
Flutter ์์ ํ์ดํ๋ก ํฌ์ปค์ค๊ฐ ์์ง์ด๋ ๊ฒ์ ํ๋ ์์ํฌ์ ๋ฐฉํฅ์ฑ ์ํ์ด์ง ์์ฝ๋์ธ์ ๋์์ด ์๋๋๋ค โ ์์ฝ๋์ธ์ ์ค์ฝํ๋์ง ์๊ณ , Flutter Web ์์๋ ๊ฐ์ ํค๊ฐ ์คํฌ๋กค๋ก ๊ฐ๋๋ค.
์คํฌ๋ฆฐ ๋ฆฌ๋#
์ ๊ทผ ๊ฐ๋ฅํ ์ด๋ฆ์ title ๋ฌธ์์ด์ด ๊ทธ๋๋ก ํค๋ ํ
์คํธ๊ฐ ๋์ด ๋์ต๋๋ค(Flutter accordion.dart:324, Web accordion.dart:402).
trigger ์ฌ๋กฏ์ ์ฑ์ฐ๋ฉด ์ด๋ฆ์ ๊ทธ ์์ ๋ฃ์ ์ฝํ
์ธ ์์ ์ต๋๋ค.
ํผ์นจ ์ํ๋ ํค๋์ ์ค๋ ค ํจ๊ป ์ฝํ์ง๋ง, ์ฌ๋ซ์ ๊ฒฐ๊ณผ๋ฅผ ์๋ฆฌ๋ live region ์ ์์ชฝ ๋ค ์์ต๋๋ค โ ์ฌ์ฉ์๋ ํค๋๋ก ๋๋์์ ์ํ๋ฅผ ๋ค์ ๋ค์ด์ผ ํฉ๋๋ค.
์ ํ ํจ๋์ ๋ฆฌ๋์ ๋
ธ์ถ๋์ง ์์ต๋๋ค. Flutter ๋ ๋์ด 0 ์ผ๋ก ํด๋ฆฝ๋์ด ์๋งจํฑ ํธ๋ฆฌ์ ์ฌ๋ผ๊ฐ์ง ์๊ณ , Web ์ inert ๋ก ์ ๊ทผ์ฑ ํธ๋ฆฌยทํญ ์์ยทhit-test ์์ ํจ๊ป ๋น ์ง๋๋ค(accordion.dart:439).
ํญ๋ชฉ ์ฌ์ด ๊ตฌ๋ถ์ ์ Divider ๊ธฐ๋ณธ๊ฐ(semantic: false)์ด๋ผ ๋ฆฌ๋์ ์๋ฌด๊ฒ๋ ์ฝํ์ง ์์ต๋๋ค(Flutter divider.dart:51, Web
divider.dart:76).
ํฌ์ปค์ค ๊ด๋ฆฌ#
- ์ง์ / ์ดํ โ ์์. ์ค๋ฒ๋ ์ด๊ฐ ์๋๋ผ ์ธ๋ผ์ธ ์น์ ์ด๋ผ ์ด๊ณ ๋ซ์ ๋ ํฌ์ปค์ค๋ฅผ ์ฎ๊ธฐ์ง ์์ต๋๋ค.
- ํธ๋ฉ โ ์์.
Tab์ ์์ฝ๋์ธ ๋ฐ์ผ๋ก ๊ทธ๋๋ก ๋น ์ ธ๋๊ฐ๋๋ค. -
ํผ์น ํจ๋ ์์ ํฌ์ปค์ค ๊ฐ๋ฅ ์์๋ ํญ ์์์ ๋ค์ด์ค๊ณ , ์ ์ผ๋ฉด ๋น ์ง๋๋ค. Flutter ๋ ๋ซํ ์ ๋๋ฉ์ด์
์ด ๋๋ ์์ (
_controller.isDismissed)์ExcludeFocus๋ก ์ ์ธํ๊ณ (accordion.dart:351), Web ์ ์ํ๊ฐ ๋ค์งํ๋ ์ฆ์inert๋ฅผ ๋ถ์ ๋๋ค(accordion.dart:439). - ํจ๋ ์์ ํฌ์ปค์ค๊ฐ ์๋ ์ฑ๋ก ์ ์ผ๋ฉด ํฌ์ปค์ค๋ฅผ ํค๋๋ก ๋๋๋ฆฌ๋ ์ฝ๋๊ฐ ์์ต๋๋ค.
์๋ ค์ง ์ ์ฝ#
-
Web ์์
trigger์ฌ๋กฏ์ ์ฑ์ฐ๋ฉด ๊ทธ ํค๋๋ ๋์ํ์ง ์์ต๋๋ค โ ์ปค์คํ ํธ๋ฆฌ๊ฑฐ๋ ๊ทธ๋๋ก ๋ ๋๋ ๋ฟ ํ ๊ธ ๋ฐฐ์ ๋aria-expanded๋ ๋ถ์ง ์๊ณ (accordion.dart:402), ํผ์นจ ์ํ๋ ๋ถ๋ชจAccordion์ด ์ธ๋ฑ์ค๋ก ๋ค๊ณ ์์ด ํธ์ถ์๊ฐ ๋ฐ์์ ๋ฐ๊ฟ ์๋ ์์ต๋๋ค. Web ์์๋title์ ์ฐ์ธ์. Flutter ๋AccordionTrigger๋ก ๊ฐ์ธ๋ฉด ํ ๊ธยทํฌ์ปค์คยทํผ์นจ ์ํ๊ฐ ๊ทธ๋๋ก ๋ฐ๋ผ์ต๋๋ค(accordion.dart:324). -
ํค๋์ ํจ๋์ ์์ฑ์ ์ค์ ์ง์
์ ์ด ์์ต๋๋ค โ Web
AccordionItem(attributes: โฆ)๋ ํญ๋ชฉ ๋ํผ<div>์๋ง ๋ฟ๊ณ (accordion.dart:445) ํค๋(Clickable)์ ํจ๋์๋ caller ํ๋ฉด์ด ์์ผ๋ฉฐ, FlutterAccordionItem์๋ ๊ทธ๋ฐ ํ๋ฉด ์์ฒด๊ฐ ์์ต๋๋ค. ์์์ ๋งํaria-controls/ ํค๋ฉ ๋ ๋ฒจ์ ์๋น์๊ฐ ๋ฐ์์ ๋ณด๊ฐํ ์ ์๋ค๋ ๋ป์ ๋๋ค.
์ ์ญ ์ถ(๋์ ์ค์ด๊ธฐ ยท ๊ณ ๋๋น ยท ์ ๊ฐ์ ๋ชจ๋ ยท ์๋งจํฑ ์ญํ ยท ์ต์ ํฐ์น ํ๊ฒ)์ ์ ๊ทผ์ฑ ํ์ด์ง๊ฐ ํ ๊ณณ์์ ๋ค๋ฃน๋๋ค.
ํฌ๋ก์ค ํ๋ซํผ ์ฐจ์ด์ (Platform Differences)#
| ํญ๋ชฉ | Flutter | Web |
|---|---|---|
| ์ฌ๋กฏ ํ์ | items: List<Widget> / content: Widget |
items: List<Component> / content: Component |
| ์ปค์คํ ํค๋ | AccordionTrigger ๋ก ๊ฐ์ธ๋ฉด ๊ธฐ๋ณธ ํ ๊ธ ๋ฐฐ์ ์์ |
trigger ๋ฅผ ๊ทธ๋๋ก ๋ ๋ํด ์๋ ํ ๊ธ ๋ฐฐ์ ์์ |
| ํผ์นจ ์ ๋๋ฉ์ด์ | SizeTransition + AnimationController |
CSS grid-rows ํธ๋์ง์ |
| ์ ํ ํจ๋ ๊ฒฉ๋ฆฌ | ExcludeFocus | inert ์์ฑ |
๊ด๋ จ ์ปดํฌ๋ํธ (Related Components)#
- Tabs: ์ฝํ ์ธ ๋ฅผ ํญ์ผ๋ก ์ ํ. ์์ฝ๋์ธ๊ณผ ๋ฌ๋ฆฌ ํ ๋ฒ์ ํ๋์ ํจ๋๋ง ํ์
- Card: ์ฝํ ์ธ ์ปจํ ์ด๋. ์์ฝ๋์ธ ํญ๋ชฉ ๋ด๋ถ์ ์นด๋๋ฅผ ๋ฐฐ์น ๊ฐ๋ฅ
์กฐํฉ ์์ #
// ์ค์ ํ์ด์ง ํจํด
Accordion(
allowMultiple: true,
items: [
AccordionItem(
title: 'ํ๋กํ ์ค์ ',
content: ProfileSettingsForm(),
),
AccordionItem(
title: '์๋ฆผ ์ค์ ',
content: NotificationSettingsForm(),
),
AccordionItem(
title: '๋ณด์ ์ค์ ',
content: SecuritySettingsForm(),
),
],
)