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"> 10px 상위 컨테이너 위에 놓입니다.

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

요소가 해당 표시에 도달하면 브라우저에서 알려주면 좋지 않을까요? 나만 그런 게 아니야 그렇게 생각하는 것입니다. position:sticky의 신호로 다양한 사용 사례를 활용할 수 있습니다.

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

Google에서는 이러한 사용 사례를 염두에 두고 사용자가 쉽게 참여할 수 있는 이벤트를 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>
데모에서는 스크롤 이벤트 없이 효과가 적용됩니다.

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

<ph type="x-smartling-placeholder">
</ph> 페이지의 구조입니다.
페이지의 구조

이 이름들을 가리킬 수 있도록 용어를 정리해 봅시다. 이 게시물의 나머지 부분에서는

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

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

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

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

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

<ph type="x-smartling-placeholder">
</ph> 센티널 요소 표시 안함
숨겨진 센티널 요소.

위아래로 스크롤하는 네 가지 경우를 다루려면 두 개의 센티널이 필요합니다.

  1. 아래로 스크롤 - 헤더가 상단 센티널을 가로지르면 고정됨 컨테이너 상단에 위치합니다
  2. 아래로 스크롤 - header는 섹션과 하단의 센티널이 컨테이너 상단을 가로지릅니다.
  3. 위로 스크롤 - 헤더는 상단 센티널을 스크롤하면 고정 모드를 종료합니다. 위로 다시 돌아가 보겠습니다.
  4. 위로 스크롤 - 헤더가 하단의 센티널을 가로지르면서 고정됨 볼 수 있습니다.

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

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

CSS

센티널은 각 섹션의 상단과 하단에 위치합니다. .sticky_sentinel--top는 헤더 상단에 있으며 .sticky_sentinel--bottom는 섹션 하단에 있습니다.

<ph type="x-smartling-placeholder">
</ph> 하위 센티널이 한계점에 도달함.
상단 및 하단 센티널 요소의 위치입니다.
: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 Observer 설정

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

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 클래스는 페이지 로드 시 이미 선언되었습니다.

앞으로는 '스타일 변형 관찰자' 변형 관찰자에 대한 확장은 특정 변수의 변경사항을 관찰하는 데 유용할 수 있고 요소의 계산된 스타일입니다. position: sticky