useCurrentItemScroller.tsx 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. import type {RefObject} from 'react';
  2. import {useEffect, useState} from 'react';
  3. const defer = (fn: () => void) => setTimeout(fn, 0);
  4. export function useCurrentItemScroller(containerRef: RefObject<HTMLElement>) {
  5. const [isAutoScrollDisabled, setIsAutoScrollDisabled] = useState(false);
  6. useEffect(() => {
  7. const containerEl = containerRef.current;
  8. let observer: MutationObserver | undefined;
  9. if (containerEl) {
  10. const isContainerScrollable = () =>
  11. containerEl.scrollHeight > containerEl.offsetHeight;
  12. observer = new MutationObserver(mutationList => {
  13. for (const mutation of mutationList) {
  14. if (
  15. mutation.type === 'attributes' &&
  16. mutation.attributeName === 'aria-current' &&
  17. mutation.target.nodeType === 1 // Element nodeType
  18. ) {
  19. const element = mutation.target as HTMLElement;
  20. const isCurrent = element?.ariaCurrent === 'true';
  21. if (isCurrent && isContainerScrollable() && !isAutoScrollDisabled) {
  22. let offset: number;
  23. // If possible scroll to the middle of the container instead of to the top
  24. if (element.clientHeight < containerEl.clientHeight) {
  25. offset =
  26. element.offsetTop -
  27. (containerEl.clientHeight / 2 - element.clientHeight / 2);
  28. } else {
  29. // Align it to the top as per default if the element is higher than the container
  30. offset = element.offsetTop;
  31. }
  32. // Deferring the scroll helps prevent it from not being executed
  33. // in certain situations. (jumping to a time with the scrubber)
  34. defer(() => {
  35. containerEl?.scrollTo({
  36. behavior: 'smooth',
  37. top: offset,
  38. });
  39. });
  40. }
  41. }
  42. }
  43. });
  44. observer.observe(containerRef.current, {
  45. attributes: true,
  46. childList: false,
  47. subtree: true,
  48. });
  49. }
  50. const handleMouseEnter = () => {
  51. setIsAutoScrollDisabled(true);
  52. };
  53. const handleMouseLeave = () => {
  54. setIsAutoScrollDisabled(false);
  55. };
  56. containerEl?.addEventListener('mouseenter', handleMouseEnter);
  57. containerEl?.addEventListener('mouseleave', handleMouseLeave);
  58. return () => {
  59. observer?.disconnect();
  60. containerEl?.removeEventListener('mouseenter', handleMouseEnter);
  61. containerEl?.removeEventListener('mouseleave', handleMouseLeave);
  62. };
  63. }, [containerRef, isAutoScrollDisabled]);
  64. }