ScrollableClient | CoUI

ScrollableClient

오프셋을 builder에 전달하는 2D 스크롤 뷰포트

ScrollableClient#

고정 크기 뷰포트 안에서 2D 스크롤을 제공하고, 매 스크롤 프레임마다 현재 offset / viewportSizebuilder 콜백에 전달하는 컴포넌트입니다. CoTable의 가로/세로 고정 셀이나 가상화 레이아웃처럼 스크롤 위치에 따라 렌더링을 조정해야 하는 서브트리에 최적화되어 있습니다.

Live Preview#

Web
Offset: 0.0, 0.0
Viewport: 0 × 0
Drag or scroll to move around this 1200 × 560 content.
Flutter
Loading Flutter...
class ScrollableClientDefaultExample extends StatelessComponent {
  const ScrollableClientDefaultExample({super.key});

  @override
  Component build(BuildContext context) {
    final cs = context.theme.colorScheme;
    final ts = context.theme.typography;
    return div(
      [
        CoScrollableClient(
          diagonalDragBehavior: CoreDiagonalDragBehavior.free,
          builder: (context, offset, viewportSize, child) {
            return div(
              [
                Component.text(
                  'Offset: ${offset.dx.toStringAsFixed(1)}, '
                  '${offset.dy.toStringAsFixed(1)}',
                ),
                CoGap(size: CoreSpace.space8),
                Component.text(
                  'Viewport: ${viewportSize.width.toStringAsFixed(0)} × '
                  '${viewportSize.height.toStringAsFixed(0)}',
                ),
                CoGap(size: CoreSpace.space16),
                Component.text(
                  'Drag or scroll to move around this 1200 × 560 content.',
                ),
              ],
              classes:
                  'flex flex-col bg-${cs.surfaceContainer} text-${ts.bodyMedium} text-${cs.onSurface} rounded-${CoreRadius.scale.radius16} p-${CoreSpace.scale.space16}',
              styles: Styles(
                raw: const {
                  'width': '${1200 / 16}rem',
                  'height': '${560 / 16}rem',
                },
              ),
            );
          },
        ),
      ],
      styles: Styles(
        raw: const {
          'height': '${320 / 16}rem',
          'width': '100%',
          'min-width': '0',
        },
      ),
    );
  }
}
class ScrollableClientDefaultExample extends StatelessWidget {
  const ScrollableClientDefaultExample({super.key});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return SizedBox(
      height: 320,
      child: CoScrollableClient(
        diagonalDragBehavior: CoreDiagonalDragBehavior.free,
        builder: (context, offset, viewportSize, child) {
          return Container(
            width: 1200,
            height: 560,
            padding: const EdgeInsets.all(CoreSpace.space16),
            decoration: BoxDecoration(
              color: theme.colorScheme.surfaceContainer,
              borderRadius: BorderRadius.circular(CoreRadius.radius16),
            ),
            child: DefaultTextStyle.merge(
              style: theme.typography.bodyMedium.copyWith(
                color: theme.colorScheme.onSurface,
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(
                    'Offset: ${offset.dx.toStringAsFixed(1)}, '
                    '${offset.dy.toStringAsFixed(1)}',
                  ),
                  const CoGap(size: CoreSpace.space8),
                  Text(
                    'Viewport: ${viewportSize.width.toStringAsFixed(0)} × '
                    '${viewportSize.height.toStringAsFixed(0)}',
                  ),
                  const CoGap(size: CoreSpace.space16),
                  const Text(
                    'Drag or scroll to move around this 1200 × 560 content.',
                  ),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 가로/세로 동시 스크롤이 필요한 고정 크기 영역을 만들 때
  • 현재 스크롤 오프셋을 기준으로 자식을 다르게 렌더링해야 할 때 (예: 테이블의 frozen 헤더/컬럼, 가상화된 리스트)
  • Web/Flutter 양쪽에 동일한 builder 인터페이스로 스크롤 영역을 재사용하고 싶을 때

대신 다른 컴포넌트를 사용하세요:

  • SingleChildScrollView / <div overflow:auto>로 충분한 단방향 스크롤: 그냥 네이티브 스크롤 컨테이너를 쓰는 게 더 가볍습니다
  • 리사이저블 레이아웃이 필요: CoResizable

기본 사용법 (Basic Usage)#

SizedBox(
  height: 320,
  child: CoScrollableClient(
    diagonalDragBehavior: CoreDiagonalDragBehavior.free,
    builder: (context, offset, viewportSize, child) {
      return CoCard(
        cardStyle: const CoreCardStyle(
          sizing: CoreCardSizing.fixed,
          width: 720,
          height: 560,
        ),
        child: /* content */,
      );
    },
  ),
)

Web에서도 동일한 생성자로 사용합니다 — 단 builderoffset({double dx, double dy}) 레코드, viewportSize({double width, double height}) 레코드입니다.

Props / Parameters#

이름타입기본값설명
builder CoreScrollableBuilder<W, Off, Sz> required 매 스크롤 프레임마다 호출되는 빌더 (context, offset, viewportSize, child)
child W? null builder에 그대로 전달되는 정적 자식 (리빌드 비용 절감용)
mainAxis CoreAxis vertical 주 스크롤 축 (diagonalDragBehavior 결정에도 사용)
verticalDetails CoreScrollableDetails .vertical() 세로 축의 direction + initialOffset
horizontalDetails CoreScrollableDetails .horizontal() 가로 축의 direction + initialOffset
diagonalDragBehavior CoreDiagonalDragBehavior? theme or none 대각선 드래그 처리 방식
overscroll bool? theme or false 끝에서 계속 스크롤 허용 여부 (네이티브 rubber-band)
clipBehavior Clip / CoreClip hardEdge 오버플로우 클리핑 (Web: CoreClip.none=overflow:visible)
hitTestBehavior CoreHitTestBehavior? theme or opaque 포인터 이벤트 수신 방식
keyboardDismissBehavior CoreKeyboardDismissBehavior? theme or manual 드래그 스크롤 시 포커스/키보드 해제 여부

Flutter 전용#

이름타입설명
flutterVerticalDetails ScrollableDetails? controller / physics / decorationClipBehavior 등 프레임워크 전용 파라미터
flutterHorizontalDetails ScrollableDetails? 위와 동일 (가로 축)
primary bool? PrimaryScrollController 사용 여부
dragStartBehavior DragStartBehavior? Flutter gesture arena 전용

접근성 (Accessibility)#

  • 스크롤바는 네이티브 OS 스크롤바 + CoUI onSurface 틴트로 렌더링되어 사용자 설정(감추기/포커스 색)이 반영됩니다.
  • keyboardDismissBehavior: CoreKeyboardDismissBehavior.onDrag로 설정하면 드래그 스크롤이 시작될 때 document.activeElement(Web) / 현재 포커스된 FocusScope(Flutter)가 해제되어 모바일에서 소프트 키보드가 닫힙니다.

관련 컴포넌트#

  • CoTable — ScrollableClient와 함께 가상화/frozen 셀 구현
  • CoResizable — 사용자 지정 분할 패널