ScrollArea#
์๋ฌด child ๋ ์คํฌ๋กค ๊ฐ๋ฅํ ๋ทฐํฌํธ๋ก ๊ฐ์ธ๊ณ ์ฌ๋ฆผํ ๋์์ธ ์์คํ
์คํฌ๋กค๋ฐ๋ฅผ ์
ํ๋ ๋ํผ์
๋๋ค. ์ฃ์ง๋ฅผ ๊ทธ๋ผ๋ฐ์ด์
์ผ๋ก ๊ฐ๋ฆฌ๋
FadeScroll ๊ณผ ๋ฌ๋ฆฌ ์ฝํ
์ธ ๋ฅผ ๊ทธ๋๋ก ๋ณด์ฌ์ฃผ๊ณ , ์ค๋ฒํ๋ก๋ ์คํฌ๋กค๋ฐ๋ก๋ง ์๋ฆฝ๋๋ค. Flutter / Web ๋์ผ API ์
๋๋ค.
Live Preview#
class ScrollAreaDefaultExample extends StatelessComponent {
const ScrollAreaDefaultExample({super.key});
@override
Component build(BuildContext context) {
return div(
styles: Styles(raw: {'width': '260px'}),
[
ScrollArea(
scrollAreaStyle: const CoreScrollAreaStyle(maxHeight: 180),
child: div(
classes: 'flex flex-col gap-${CoreSpace.scale.space8}',
[
for (var i = 1; i <= 20; i += 1) Text('์คํฌ๋กค ํญ๋ชฉ $i').bodyMedium,
],
),
),
],
);
}
}
class ScrollAreaDefaultExample extends StatelessWidget {
const ScrollAreaDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 260,
child: ScrollArea(
scrollAreaStyle: const CoreScrollAreaStyle(maxHeight: 180),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: CoreSpace.space8,
children: [
for (var i = 1; i <= 20; i += 1) Text('์คํฌ๋กค ํญ๋ชฉ $i').bodyMedium,
],
),
),
);
}
}
class ScrollAreaChainExample extends StatelessComponent {
const ScrollAreaChainExample({super.key});
@override
Component build(BuildContext context) {
return div(
styles: Styles(raw: {'width': '260px'}),
[
ScrollArea(
child: div(
classes: 'flex flex-col gap-${CoreSpace.scale.space8}',
[
for (var i = 1; i <= 20; i += 1) Text('์คํฌ๋กค ํญ๋ชฉ $i').bodyMedium,
],
),
).withStyle(
const CoreScrollAreaStyle(
height: CoreSpace.space192,
maxHeight: CoreSpace.space192,
scrollbarStyle: CoreScrollbarStyle(
thumbColor: CoreColor.token(CoreColors.primary),
thumbRadius: CoreBorderRadius.all(CoreRadius.radius9999),
thickness: CoreStrokeWidth.stroke8,
),
),
),
],
);
}
}
class ScrollAreaChainExample extends StatelessWidget {
const ScrollAreaChainExample({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 260,
child:
ScrollArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: CoreSpace.space8,
children: [
for (var i = 1; i <= 20; i += 1) Text('์คํฌ๋กค ํญ๋ชฉ $i').bodyMedium,
],
),
).withStyle(
const CoreScrollAreaStyle(
height: CoreSpace.space192,
maxHeight: CoreSpace.space192,
scrollbarStyle: CoreScrollbarStyle(
thumbColor: CoreColor.token(CoreColors.primary),
thumbRadius: CoreBorderRadius.all(CoreRadius.radius9999),
thickness: CoreStrokeWidth.stroke8,
),
),
),
);
}
}
์ฌ์ฉ๋ฒ#
// Flutter / Web ๋์ผ
ScrollArea(
scrollAreaStyle: const CoreScrollAreaStyle(maxHeight: 180),
child: Column(
children: [for (var i = 0; i < 20; i += 1) Text('Item $i')],
),
)
๋น ๋ฅธ ์ค๋ฒ๋ผ์ด๋ (Chain)#
์ด๋ฏธ ๋ง๋ ScrollArea ์ธ์คํด์ค์ ์คํ์ผ์ ๋น ๋ฅด๊ฒ ๋ง๋ถ์ด๊ณ ์ถ๋ค๋ฉด withStyle ์ฒด์ธ์ ์ธ ์ ์์ต๋๋ค. ์์ฑ์์ style ์ฌ๋กฏ ์ธ์์ ๋์ผํ๊ฒ ๋์ํ์ง๋ง, ์ด๋ฏธ ๊ตฌ์ฑ๋ ์์ ฏ ์์์ ๋ฐ๋ก ์ด์ด ์ธ ์ ์์ต๋๋ค.
class ScrollAreaChainExample extends StatelessWidget {
const ScrollAreaChainExample({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 260,
child:
ScrollArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: CoreSpace.space8,
children: [
for (var i = 1; i <= 20; i += 1) Text('์คํฌ๋กค ํญ๋ชฉ $i').bodyMedium,
],
),
).withStyle(
const CoreScrollAreaStyle(
height: CoreSpace.space192,
maxHeight: CoreSpace.space192,
scrollbarStyle: CoreScrollbarStyle(
thumbColor: CoreColor.token(CoreColors.primary),
thumbRadius: CoreBorderRadius.all(CoreRadius.radius9999),
thickness: CoreStrokeWidth.stroke8,
),
),
),
);
}
}
class ScrollAreaChainExample extends StatelessComponent {
const ScrollAreaChainExample({super.key});
@override
Component build(BuildContext context) {
return div(
styles: Styles(raw: {'width': '260px'}),
[
ScrollArea(
child: div(
classes: 'flex flex-col gap-${CoreSpace.scale.space8}',
[
for (var i = 1; i <= 20; i += 1) Text('์คํฌ๋กค ํญ๋ชฉ $i').bodyMedium,
],
),
).withStyle(
const CoreScrollAreaStyle(
height: CoreSpace.space192,
maxHeight: CoreSpace.space192,
scrollbarStyle: CoreScrollbarStyle(
thumbColor: CoreColor.token(CoreColors.primary),
thumbRadius: CoreBorderRadius.all(CoreRadius.radius9999),
thickness: CoreStrokeWidth.stroke8,
),
),
),
],
);
}
}
Props#
| ํ๋ผ๋ฏธํฐ | ํ์ | ๊ธฐ๋ณธ๊ฐ | ์ค๋ช |
|---|---|---|---|
child |
Widget / Component |
required | ๋ทฐํฌํธ ์์์ ์คํฌ๋กค๋๋ ์ฝํ ์ธ |
direction |
CoreScrollAreaDirection |
vertical |
์คํฌ๋กค ์ถ (vertical / horizontal) |
showScrollbar |
bool |
true |
๋์์ธ ์์คํ
์คํฌ๋กค๋ฐ ํ์ ์ฌ๋ถ (false ๋ฉด ์คํฌ๋กค์ ๋๋ ๋ฐ๋ ์จ๊น) |
scrollAreaStyle |
CoreScrollAreaStyle? |
null |
chrome ๋จ์ผ ์ง์ ์ (์๋ ํ๋ ํ) |
controller |
ScrollController? |
null (Flutter ์ ์ฉ ๋ฐํ์ ์ธํ๋ผ) |
์ธ๋ถ์์ ์คํฌ๋กค ์์น๋ฅผ ๊ตฌ๋. Web ์ ๋ธ๋ผ์ฐ์ native overflow ๋ก ๋์ |
CoreScrollAreaStyle ํ๋#
| ํ๋ | ํ์ | ์ค๋ช |
|---|---|---|
height |
double? |
Fixed viewport height in logical pixels. When null the scroll area adopts its parent's height (or its child's intrinsic height when no parent constraint exists).
No default, and must not have one.
Both fields together are a switch, not a measurement: Flutter builds a
ConstrainedBox
only under
if (resolved.height != null || resolved.maxHeight != null)
and returns the unwrapped scroll view otherwise, and Web writes the
height
declaration only inside
if (height != null)
. A constant would make that branch permanently taken, so every scroll area in the tree would be clamped to one height regardless of the space its parent gave it โ and a scroll area that ignores its parent is the one thing this component must not do.
|
maxHeight |
double? |
Maximum viewport height in logical pixels. When set, the scroll area sizes to the child's intrinsic height up to this maximum and becomes scrollable when overflowed. Mutually composes with [height] (host platforms apply both when both are set). No default, and must not have one โ it is the other half of the switch described on [height], and a constant would additionally introduce a scroll boundary where the caller asked for none. |
scrollbarStyle |
CoreScrollbarStyle? |
Nested chrome for the composed
Scrollbar
(thumb colour / thickness / corner radius / minimum length / track colour). When null falls back to [defaultScrollbarStyle]; partial overrides merge on top of it.
|
์ฌ์ฉ ๊ฐ์ด๋๋ผ์ธ (Usage Guidelines)#
โ Do#
๊ฐ๋ณ ๊ธธ์ด ์ฝํ
์ธ ์๋ maxHeight ๋ฅผ ๋ช
์
ScrollArea(
scrollAreaStyle: const CoreScrollAreaStyle(maxHeight: 240),
child: Column(children: [for (final item in items) Text(item)]),
)
maxHeight ๋ฅผ ์ฃผ์ง ์์ผ๋ฉด ๋ถ๋ชจ ์ ์ฝ์ด ์๋ ํ ์์์ intrinsic ๋์ด๋ฅผ ๊ทธ๋๋ก ์ฑํํฉ๋๋ค โ ์ด ๊ฒฝ์ฐ ๋์นจ ์์ฒด๊ฐ ์๊ธฐ์ง ์์ ์คํฌ๋กค๋ฐ๋ ๋ฑ์ฅํ์ง ์์ต๋๋ค.
โ Don't#
๋์ฒด ์ดํฌ๋์ค ์์ด showScrollbar: false ์ฌ์ฉ ๊ธ์ง
// โ ๋์นจ์ ์๋ฆด ๋ค๋ฅธ ์๋จ ์์ด ์คํฌ๋กค๋ฐ๋ง ์จ๊น
ScrollArea(
showScrollbar: false,
scrollAreaStyle: const CoreScrollAreaStyle(maxHeight: 180),
child: longContent,
)
์ฝํ ์ธ ๊ฐ ๋ ์๋ค๋ ์ฌ์ค์ ์๋ฆฌ๋ ์ ์ผํ ์๊ฐ ๋จ์๊ฐ ์ฌ๋ผ์ง๋๋ค โ ๊ทธ๋ผ๋ฐ์ด์ ์ด๋ ํ์ด์ง๋ค์ด์ ์ฒ๋ผ ์ด๋ฅผ ๋์ฒดํ ์ดํฌ๋์ค๊ฐ ์์ผ๋ฉด ์ฌ์ฉ์๋ ์คํฌ๋กค์ด ๊ฐ๋ฅํ๋ค๋ ๊ฒ ์์ฒด๋ฅผ ์ ์ ์์ต๋๋ค.
์ ๊ทผ์ฑ (Accessibility)#
์ญํ (Semantics)#
์ ํ๋ซํผ ๋ชจ๋ ์ญํ ์ ๋ด๋ณด๋ด์ง ์์ต๋๋ค. Flutter ๋ SingleChildScrollView ์
Scrollbar ๋ฅผ ํฉ์ฑํ๊ณ ConstrainedBox ๋ก ๊ฐ์ ๋ฟ Semantics
๋
ธ๋๋ฅผ ๋ง๋ค์ง ์์ผ๋ฉฐ,
Web ์ ํฉ์ฑ๋ Scrollbar ๊ฐ ๊ทธ๋ฆฌ๋ overflow-* ํด๋์ค๊ฐ ๋ถ์ <div>
ํ๋๋ง ๋ด๋ณด๋ด๊ณ
role / aria-* ๋ ํ๋๋ ๋ถ์ด์ง ์์ต๋๋ค(ํธ์ถ์๊ฐ ๋๊ธด attributes
๋ ๊ทธ๋๋ก ํต๊ณผ).
์ฆ "์ฌ๊ธฐ๊ฐ ์คํฌ๋กค๋๋ ์์ญ"์ด๋ผ๋ ์ฌ์ค ์์ฒด๊ฐ ๋ณด์กฐ๊ธฐ์ ์ ์ ๋ฌ๋์ง ์์ต๋๋ค.
์์ญ ์ด๋ฆ์ด ํ์ํ๋ฉด ์๋น์๊ฐ ์ง์ ๋ถ์ฌ์ผ ํฉ๋๋ค โ Web ์ attributes ๋ก
role="region" ยท aria-label ์, Flutter ๋ ๋ฐ๊นฅ์ ์๊ธฐ
Semantics ๋ฅผ ๊ฐ์ธ๋ ๋ฐฉ์์
๋๋ค.
ํค๋ณด๋#
์ด ์ปดํฌ๋ํธ๊ฐ ํด์ํ๋ ํค๋ ์์ต๋๋ค. Flutter ์์ ๋ฐฉํฅํค ยท Page ยท Home ยท End ๋ก
์คํฌ๋กค๋๋ ๊ฒ์ SingleChildScrollView ์์ ํ๋ ์์ํฌ Scrollable
์ด ์ฃผ๋ ๋์์ด๊ณ ,
Web ์์๋ ๋ธ๋ผ์ฐ์ ๊ฐ ์ฃผ๋ ๋์์
๋๋ค โ ๊ทธ๋ฆฌ๊ณ Web ์ชฝ์ ์คํฌ๋กค ์ปจํ
์ด๋๊ฐ ํฌ์ปค์ค๋ฅผ
๋ฐ์์ ๋๋ง ๋์ํฉ๋๋ค(์๋ ์ ์ฝ ์ฐธ์กฐ). Web ์ onKeyDown / onKeyUp
์ ํธ์ถ์๊ฐ
๋๊ธด ํธ๋ค๋ฌ๋ฅผ ๋ฃจํธ์ ์ฐ๊ฒฐํ๋ ํต๊ณผ ์ฌ๋กฏ์ผ ๋ฟ, ์ปดํฌ๋ํธ๊ฐ ์ฑ์ฐ๋ ๊ฐ์ด ์๋๋๋ค.
ํฌ์ปค์ค#
ํฌ์ปค์ค๋ฅผ ๋ฐ์ง ์์ต๋๋ค. ์ ํ๋ซํผ ๋ชจ๋ FocusNode / Focus / tabindex ๊ฐ ์๊ณ
ํฌ์ปค์ค ๋ง๋ ๊ทธ๋ฆฌ์ง ์์ต๋๋ค. ํญ ์์์ ๋ฑ์ฅํ์ง ์์ผ๋ฉฐ, ํฌ์ปค์ค๋ฅผ ๊ฐ๋๊ฑฐ๋ ๋๋๋ฆฌ์ง๋
์์ต๋๋ค โ ์ฝํ
์ธ ์์ ํฌ์ปค์ค ๊ฐ๋ฅํ ์์๋ ๊ฐ์์ ๋์์ ๊ทธ๋๋ก ์ ์งํฉ๋๋ค.
์คํฌ๋ฆฐ ๋ฆฌ๋#
์์ญ ์์ฒด์๋ ์ด๋ฆ์ด ์์ต๋๋ค โ Flutter ๋ ํ๋ ์์ํฌ Scrollable ์ด ๊ธฐ์ฌํ๋ ์คํฌ๋กค
์ก์
์ด, Web ์ ์ด๋ฆ ์๋ <div> ์์ชฝ ์ฝํ
์ธ ๊ฐ ์ฝํ๋๋ค.
showScrollbar ๊ฐ ์ผ์ ธ ์์ผ๋ฉด ํฉ์ฑ๋ Scrollbar ๊ฐ ์คํฌ๋กค
์์น๋ฅผ 0โ100 ํผ์ผํธ๋ก ์๋ดํฉ๋๋ค (Web role="scrollbar" + aria-valuenow, Flutter
slider Semantics). ๋จ์ ๋ถ๋ ์์ฒด๋ ์ด๋ ์ชฝ์์๋ ์๋ด๋์ง ์์ต๋๋ค.
์๋ ค์ง ์ ์ฝ#
-
Web ์คํฌ๋กค ์ปจํ
์ด๋์
tabindex๊ฐ ์์ด ํค๋ณด๋๋ง ์ฐ๋ ์ฌ์ฉ์๊ฐ ์ด ์์ญ์ ์คํฌ๋กคํ ์ ์๋์ง๊ฐ ์ ์ ์ผ๋ก ๋ธ๋ผ์ฐ์ ๋์์ ๋ฌ๋ ค ์์ต๋๋ค. ํค๋ณด๋๋ก ํ์คํ ์คํฌ๋กค์ํค๋ ค๋ฉด ์๋น์๊ฐattributes๋กtabindex="0"๊ณผrole="region"ยทaria-label์ ์ง์ ๋ฃ์ด์ผ ํฉ๋๋ค. -
showScrollbar: false๋ ๋์นจ์ ์๋ฆฌ๋ ์ ์ผํ ์๊ฐ ๋จ์๋ฅผ ์์ ๋ฉด์ ๋์ฒด ์ดํฌ๋์ค๋ฅผ ์ ๊ณตํ์ง ์์ต๋๋ค โ ์ฝํ ์ธ ๊ฐ ๋ ์๋ค๋ ์ฌ์ค์ด ์๊ฐ์ ์ผ๋ก๋ ์์ฑ์ผ๋ก๋ ๋๋ฌ๋์ง ์์ต๋๋ค. -
controller๋ Flutter ์ ์ฉ์ด๋ผ ํฌ๋ก์ค ํ๋ซํผ ํ๋ก๊ทธ๋๋งคํฑ ์คํฌ๋กค ๊ฒฝ๋ก๊ฐ ์์ต๋๋ค. "ํน์ ํญ๋ชฉ์ผ๋ก ์คํฌ๋กค" ๊ฐ์ ๋์์ ์ ํ๋ซํผ์์ ๋์ผํ๊ฒ ์ ๊ณตํ๋ ค๋ฉด ์๋น์ ์ชฝ์์ ํ๋ซํผ๋ณ๋ก ๊ตฌํํด์ผ ํฉ๋๋ค.
reduced motion ยท ๊ณ ๋๋น ยท ์ต์ ํฐ์น ํ๊ฒ ๋ฑ ์ปดํฌ๋ํธ๋ฅผ ๊ฐ๋ก์ง๋ฅด๋ ์ถ์ ์ ์ญ ์ ๊ทผ์ฑ ์ถ์์ ๋ค๋ฃน๋๋ค.