CSS position:sticky에 대한 이벤트

요약

다음 앱에서는 scroll 이벤트가 필요하지 않을 수도 있습니다. IntersectionObserver를 사용하여 position:sticky 요소가 고정되거나 고정이 중지될 때 맞춤 이벤트를 실행하는 방법을 보여줍니다. 모두 스크롤 리스너의 사용입니다. 이를 입증할 수 있는 멋진 데모도 있습니다.

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder"></ph>
데모 보기 | 소스

sticky-change 이벤트 소개

CSS 고정 위치를 사용할 때의 실질적인 제한사항 중 하나는 속성이 활성 상태인지 알 수 있는 플랫폼 신호를 제공하지 않습니다. 즉, 요소가 고정되는 시점이나 요소가 변경되는 시점을 알 수 있는 이벤트가 없습니다. 끈적끈적거리지 않습니다.

다음 예를 살펴보세요. 이 예에서는 <div class="sticky">를 상위 컨테이너 상단에서 10픽셀 고정합니다.

.sticky {
  position: sticky;
  top: 10px;
}

요소가 해당 지점에 도달하면 브라우저에서 알려주면 얼마나 좋을까요? 저만 그렇게 생각하는 것은 아닌 것 같습니다. position:sticky 신호를 사용하면 다음과 같은 다양한 사용 사례를 활용할 수 있습니다.

  1. 배너가 고정될 때 배너에 그림자를 적용합니다.
  2. 사용자가 콘텐츠를 읽을 때 분석 조회수를 기록하여 있습니다.
  3. 사용자가 페이지를 스크롤할 때 플로팅 목차 위젯을 현재 섹션으로 업데이트합니다.

이러한 사용 사례를 염두에 두고 position:sticky 요소가 고정될 때 실행되는 이벤트를 만드는 최종 목표를 세웠습니다. 이 이벤트를 sticky-change 이벤트라고 하겠습니다.

document.addEventListener('sticky-change', e => {
  const header = e.detail.target;  // header became sticky or stopped sticking.
  const sticking = e.detail.stuck; // true when header is sticky.
  header.classList.toggle('shadow', sticking); // add drop shadow when sticking.

  document.querySelector('.who-is-sticking').textContent = header.textContent;
});

데모에서는 수정될 때 그림자를 헤더에 표시합니다. 또한 포드의 상태를 새 제목을 클릭합니다.

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder"></ph>
데모에서는 스크롤 이벤트 없이 효과가 적용됩니다.

스크롤 이벤트 없이 스크롤 효과 여부

페이지의 구조입니다.
페이지 구조

이 이름들을 부르기 위해 일부 용어를 정리해 봅시다. 이 게시물의 나머지 부분에서는

  1. 스크롤 컨테이너 - '블로그 게시물' 목록입니다.
  2. 헤더 - position:sticky가 있는 각 섹션의 파란색 제목입니다.
  3. 고정 섹션: 각 콘텐츠 섹션 고정 헤더 아래에서 스크롤되는 텍스트입니다.
  4. '고정 모드' - position:sticky가 요소에 적용되는 경우

어떤 헤더가 '고정 모드'로 전환되는지 파악하려면 스크롤 컨테이너의 스크롤 오프셋입니다. 그것은 우리가 현재 표시되는 헤더를 계산합니다. 하지만 scroll 이벤트 없이 하기가 까다롭습니다. :) 또 다른 문제는 position:sticky는 요소가 수정되면 레이아웃에서 요소를 삭제합니다.

따라서 스크롤 이벤트가 없으면 레이아웃 관련 기능을 수행할 수 없습니다. 계산을 수행합니다.

스크롤 위치 확인을 위해 dumby DOM 추가

scroll 이벤트 대신 IntersectionObserver를 사용하여 헤더가 고정 모드를 시작하고 종료하는 시점을 결정합니다. 2개의 노드 추가 각 고정 섹션에 하나씩(센티널이라고도 함) 스크롤 위치를 파악하기 위한 경유지 역할을 합니다. 이러한 마커가 컨테이너에 들어오고 나갈 때 표시 상태가 변경되고 교차 관찰자가 콜백을 실행합니다.

<ph type="x-smartling-placeholder">
</ph> 표시되는 센티널 요소 없음
숨겨진 센티널 요소.

위아래로 스크롤하는 4가지 사례를 처리하려면 센티넬이 2개 필요합니다.

  1. 아래로 스크롤 - 헤더가 상단 센티널을 가로지르면 고정됨 컨테이너 상단에 위치합니다
  2. 아래로 스크롤 - header가 섹션 하단에 도달하고 하단 센티널이 컨테이너 상단을 지나면 고정 모드를 종료합니다.
  3. 위로 스크롤 - header의 상단 감시자가 위에서 다시 스크롤되어 표시되면 고정 모드를 종료합니다.
  4. 위로 스크롤 - 하단 감시자가 위에서 다시 뷰로 다시 지나가면서 헤더가 고정됩니다.

1~4의 스크린캐스트를 발생 순서대로 보는 것이 좋습니다.

<ph type="x-smartling-placeholder">
</ph>
센티널이 센티널을 수신했을 때 들어가거나 닫습니다.

CSS

전초 기지는 각 섹션의 상단과 하단에 배치됩니다. .sticky_sentinel--top는 헤더 상단에 있고 .sticky_sentinel--bottom는 섹션 하단에 있습니다.

하위 센티널이 한계점에 도달함.
상단 및 하단 센티널 요소의 위치입니다.
:root {
  --default-padding: 16px;
  --header-height: 80px;
}
.sticky {
  position: sticky;
  top: 10px; /* adjust sentinel height/positioning based on this position. */
  height: var(--header-height);
  padding: 0 var(--default-padding);
}
.sticky_sentinel {
  position: absolute;
  left: 0;
  right: 0; /* needs dimensions */
  visibility: hidden;
}
.sticky_sentinel--top {
  /* Adjust the height and top values based on your on your sticky top position.
  e.g. make the height bigger and adjust the top so observeHeaders()'s
  IntersectionObserver fires as soon as the bottom of the sentinel crosses the
  top of the intersection container. */
  height: 40px;
  top: -24px;
}
.sticky_sentinel--bottom {
  /* Height should match the top of the header when it's at the bottom of the
  intersection container. */
  height: calc(var(--header-height) + var(--default-padding));
  bottom: 0;
}

Intersection Observers 설정

교차점 관찰자는 교차점의 변경사항을 비동기식으로 관찰합니다. 문서 표시 영역 또는 상위 컨테이너일 수 있습니다. 이 경우에는 상위 컨테이너와의 교차점을 관찰합니다.

IntersectionObserver가 비법입니다. 각 센티널은 IntersectionObserver: 스크롤 컨테이너를 사용합니다. 센티널이 표시된 뷰포트로 스크롤하면 헤더가 고정되거나 고정되지 않음을 알 수 있습니다. 마찬가지로, 센티널이 표시 영역을 종료할 때도 마찬가지입니다.

먼저 머리글과 바닥글 센티널에 대한 관찰자를 설정합니다.

/**
 * Notifies when elements w/ the `sticky` class begin to stick or stop sticking.
 * Note: the elements should be children of `container`.
 * @param {!Element} container
 */
function observeStickyHeaderChanges(container) {
  observeHeaders(container);
  observeFooters(container);
}

observeStickyHeaderChanges(document.querySelector('#scroll-container'));

그런 다음 .sticky_sentinel--top 요소가 전달되면 실행할 관찰자를 추가했습니다. 스크롤 컨테이너 상단을 통해 스크롤할 수 있습니다 (양방향). observeHeaders 함수는 최상위 센티널을 만들어 확인할 수 있습니다 관찰자는 센티널과 임베딩의 교집합을 계산하며 표시 영역에 들어올지 벗어날지를 결정합니다 이 정보에 따라 섹션 헤더가 고정되는지 여부가 결정됩니다.

/**
 * Sets up an intersection observer to notify when elements with the class
 * `.sticky_sentinel--top` become visible/invisible at the top of the container.
 * @param {!Element} container
 */
function observeHeaders(container) {
  const observer = new IntersectionObserver((records, observer) => {
    for (const record of records) {
      const targetInfo = record.boundingClientRect;
      const stickyTarget = record.target.parentElement.querySelector('.sticky');
      const rootBoundsInfo = record.rootBounds;

      // Started sticking.
      if (targetInfo.bottom < rootBoundsInfo.top) {
        fireEvent(true, stickyTarget);
      }

      // Stopped sticking.
      if (targetInfo.bottom >= rootBoundsInfo.top &&
          targetInfo.bottom < rootBoundsInfo.bottom) {
       fireEvent(false, stickyTarget);
      }
    }
  }, {threshold: [0], root: container});

  // Add the top sentinels to each section and attach an observer.
  const sentinels = addSentinels(container, 'sticky_sentinel--top');
  sentinels.forEach(el => observer.observe(el));
}

관찰자는 콜백이 즉시 실행되도록 threshold: [0]로 구성되어 있습니다. 감시가 보이기 시작하면 됩니다.

이 프로세스는 하위 센티널 (.sticky_sentinel--bottom)과 유사합니다. 바닥글이 하단을 통과할 때 실행되는 두 번째 관찰자를 만듭니다. (스크롤 컨테이너)에 있습니다. observeFooters 함수는 각 섹션에 연결합니다 관찰자는 센티널이 컨테이너 하단과 교차하는 지점에서 들어가거나 나갑니다. 이 정보는 섹션 헤더가 잘 보이게 할 수 있죠.

/**
 * Sets up an intersection observer to notify when elements with the class
 * `.sticky_sentinel--bottom` become visible/invisible at the bottom of the
 * container.
 * @param {!Element} container
 */
function observeFooters(container) {
  const observer = new IntersectionObserver((records, observer) => {
    for (const record of records) {
      const targetInfo = record.boundingClientRect;
      const stickyTarget = record.target.parentElement.querySelector('.sticky');
      const rootBoundsInfo = record.rootBounds;
      const ratio = record.intersectionRatio;

      // Started sticking.
      if (targetInfo.bottom > rootBoundsInfo.top && ratio === 1) {
        fireEvent(true, stickyTarget);
      }

      // Stopped sticking.
      if (targetInfo.top < rootBoundsInfo.top &&
          targetInfo.bottom < rootBoundsInfo.bottom) {
        fireEvent(false, stickyTarget);
      }
    }
  }, {threshold: [1], root: container});

  // Add the bottom sentinels to each section and attach an observer.
  const sentinels = addSentinels(container, 'sticky_sentinel--bottom');
  sentinels.forEach(el => observer.observe(el));
}

관찰자는 threshold: [1]로 구성되므로 전체 노드가 뷰 내에 있습니다

마지막으로 sticky-change 맞춤 이벤트를 실행하고 센티널을 생성하는 두 가지 유틸리티가 있습니다.

/**
 * @param {!Element} container
 * @param {string} className
 */
function addSentinels(container, className) {
  return Array.from(container.querySelectorAll('.sticky')).map(el => {
    const sentinel = document.createElement('div');
    sentinel.classList.add('sticky_sentinel', className);
    return el.parentElement.appendChild(sentinel);
  });
}

/**
 * Dispatches the `sticky-event` custom event on the target element.
 * @param {boolean} stuck True if `target` is sticky.
 * @param {!Element} target Element to fire the event on.
 */
function fireEvent(stuck, target) {
  const e = new CustomEvent('sticky-change', {detail: {stuck, target}});
  document.dispatchEvent(e);
}

작업이 끝났습니다.

최종 데모

position:sticky가 있는 요소가 다음과 같이 될 때 맞춤 이벤트를 만들었습니다. scroll 이벤트를 사용하지 않는 스크롤 효과를 수정하고 추가했습니다.

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder"></ph>
데모 보기 | 소스

결론

IntersectionObserver를 사용하면 이전에 발생했던 scroll 이벤트 기반 UI 패턴을 발전했습니다 답은 '예'와 '아니요'입니다. 시맨틱스 IntersectionObserver API의 일부 때문에 모든 경우에 사용하기가 어렵습니다. 하지만 여기에서 보여드린 것처럼 흥미로운 기법에 사용할 수 있습니다.

스타일 변경을 감지하는 또 다른 방법은 무엇일까요?

아니요 DOM 요소의 스타일 변경을 관찰하는 방법이 필요했습니다. 안타깝게도 웹 플랫폼 API에는 확인할 수 있습니다.

MutationObserver가 논리적으로 첫 번째 선택이지만 대부분의 경우에는 작동하지 않습니다. 예를 들어 데모에서는 sticky 클래스가 요소에 추가될 때 콜백을 수신하지만 요소의 계산된 스타일이 변경될 때는 콜백을 수신하지 않습니다. sticky 클래스는 페이지 로드 시 이미 선언되었습니다.

향후 Mutation Observer의 'Style Mutation Observer' 확장 프로그램은 요소의 계산된 스타일 변경사항을 관찰하는 데 유용할 수 있습니다. position: sticky