Explorar o código

Implement custom hook to handle sticky elements

https://youtrack.weseek.co.jp/issue/GW-7946
- Create custom useSticky hook to observe sticky elements in use-sticky-utils
- Get isSticky status with useSticky hook
- Remove implementation of useSticky hook form react-use-sticky
- Remove isSticky state from Fab componnet
- Remove sticky-events and react-use-sticky library
Mudana-Grune %!s(int64=3) %!d(string=hai) anos
pai
achega
5287e95862

+ 0 - 1
packages/app/package.json

@@ -247,7 +247,6 @@
     "simple-load-script": "^1.0.2",
     "simple-load-script": "^1.0.2",
     "simplebar-react": "^2.3.6",
     "simplebar-react": "^2.3.6",
     "socket.io-client": "^4.2.0",
     "socket.io-client": "^4.2.0",
-    "sticky-events": "^3.4.11",
     "swagger2openapi": "^5.3.1",
     "swagger2openapi": "^5.3.1",
     "ts-node-dev": "^2.0.0",
     "ts-node-dev": "^2.0.0",
     "tsc-alias": "^1.2.9"
     "tsc-alias": "^1.2.9"

+ 8 - 24
packages/app/src/components/Fab.tsx

@@ -4,12 +4,12 @@ import React, {
 
 
 import { animateScroll } from 'react-scroll';
 import { animateScroll } from 'react-scroll';
 import { useRipple } from 'react-use-ripple';
 import { useRipple } from 'react-use-ripple';
-import StickyEvents from 'sticky-events';
 
 
 import { usePageCreateModal } from '~/stores/modal';
 import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
 import { useCurrentPagePath } from '~/stores/page';
 import { useIsAbleToChangeEditorMode } from '~/stores/ui';
 import { useIsAbleToChangeEditorMode } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
+import { useSticky } from '~/utils/use-sticky-utils';
 
 
 import { CreatePageIcon } from './Icons/CreatePageIcon';
 import { CreatePageIcon } from './Icons/CreatePageIcon';
 import { ReturnTopIcon } from './Icons/ReturnTopIcon';
 import { ReturnTopIcon } from './Icons/ReturnTopIcon';
@@ -26,12 +26,14 @@ export const Fab = (): JSX.Element => {
 
 
   const [animateClasses, setAnimateClasses] = useState<string>('invisible');
   const [animateClasses, setAnimateClasses] = useState<string>('invisible');
   const [buttonClasses, setButtonClasses] = useState<string>('');
   const [buttonClasses, setButtonClasses] = useState<string>('');
-  const [isSticky, setIsSticky] = useState<boolean>(false);
 
 
   // ripple
   // ripple
   const createBtnRef = useRef(null);
   const createBtnRef = useRef(null);
   useRipple(createBtnRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
   useRipple(createBtnRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
 
 
+  // Get sticky status
+  const isSticky = useSticky('#grw-fav-sticky-trigger');
+
   /**
   /**
    * After the fade animation is finished, fix the button display status.
    * After the fade animation is finished, fix the button display status.
    * Prevents the fade animation occurred each time by button components rendered.
    * Prevents the fade animation occurred each time by button components rendered.
@@ -50,31 +52,13 @@ export const Fab = (): JSX.Element => {
     return () => clearTimeout(timer);
     return () => clearTimeout(timer);
   }, [isSticky]);
   }, [isSticky]);
 
 
-  const stickyChangeHandler = useCallback((event) => {
-    logger.debug('StickyEvents.CHANGE detected');
-
-    const newAnimateClasses = event.detail.isSticky ? 'animated fadeInUp faster' : 'animated fadeOut faster';
-    const newButtonClasses = event.detail.isSticky ? '' : 'disabled grw-pointer-events-none';
+  useEffect(() => {
+    const newAnimateClasses = isSticky ? 'animated fadeInUp faster' : 'animated fadeOut faster';
+    const newButtonClasses = isSticky ? '' : 'disabled grw-pointer-events-none';
 
 
     setAnimateClasses(newAnimateClasses);
     setAnimateClasses(newAnimateClasses);
     setButtonClasses(newButtonClasses);
     setButtonClasses(newButtonClasses);
-    setIsSticky(event.detail.isSticky);
-  }, []);
-
-  // setup effect by sticky event
-  useEffect(() => {
-    // sticky
-    // See: https://github.com/ryanwalters/sticky-events
-    const stickyEvents = new StickyEvents({ stickySelector: '#grw-fav-sticky-trigger' });
-    const { stickySelector } = stickyEvents;
-    const elem = document.querySelector(stickySelector);
-    elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-
-    // return clean up handler
-    return () => {
-      elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-    };
-  }, [stickyChangeHandler]);
+  }, [isSticky]);
 
 
   const PageCreateButton = useCallback(() => {
   const PageCreateButton = useCallback(() => {
     return (
     return (

+ 5 - 22
packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.tsx

@@ -2,12 +2,12 @@ import React, {
   useState, useRef, useEffect, useCallback,
   useState, useRef, useEffect, useCallback,
 } from 'react';
 } from 'react';
 
 
-import StickyEvents from 'sticky-events';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSidebarCollapsed } from '~/stores/ui';
 import { useSidebarCollapsed } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
+import { useSticky } from '~/utils/use-sticky-utils';
 
 
 import GrowiContextualSubNavigation from './GrowiContextualSubNavigation';
 import GrowiContextualSubNavigation from './GrowiContextualSubNavigation';
 
 
@@ -32,7 +32,6 @@ export const GrowiSubNavigationSwitcher = (props: GrowiSubNavigationSwitcherProp
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: isSidebarCollapsed } = useSidebarCollapsed();
   const { data: isSidebarCollapsed } = useSidebarCollapsed();
 
 
-  const [isVisible, setIsVisible] = useState<boolean>(false);
   const [width, setWidth] = useState<number>(0);
   const [width, setWidth] = useState<number>(0);
 
 
   // use more specific type HTMLDivElement for avoid assertion error.
   // use more specific type HTMLDivElement for avoid assertion error.
@@ -40,6 +39,9 @@ export const GrowiSubNavigationSwitcher = (props: GrowiSubNavigationSwitcherProp
   const fixedContainerRef = useRef<HTMLDivElement>(null);
   const fixedContainerRef = useRef<HTMLDivElement>(null);
   const clientWidth = fixedContainerRef.current?.parentElement?.clientWidth;
   const clientWidth = fixedContainerRef.current?.parentElement?.clientWidth;
 
 
+  // Get sticky status
+  const isSticky = useSticky('#grw-subnav-sticky-trigger');
+
   // Do not use clientWidth as useCallback deps, resizing events will not work in production builds.
   // Do not use clientWidth as useCallback deps, resizing events will not work in production builds.
   const initWidth = useCallback(() => {
   const initWidth = useCallback(() => {
     if (fixedContainerRef.current != null && fixedContainerRef.current.parentElement != null) {
     if (fixedContainerRef.current != null && fixedContainerRef.current.parentElement != null) {
@@ -49,25 +51,6 @@ export const GrowiSubNavigationSwitcher = (props: GrowiSubNavigationSwitcherProp
     }
     }
   }, []);
   }, []);
 
 
-  const stickyChangeHandler = useCallback((event) => {
-    logger.debug('StickyEvents.CHANGE detected');
-    setIsVisible(event.detail.isSticky);
-  }, []);
-
-  // setup effect by sticky-events
-  useEffect(() => {
-    // sticky-events
-    // See: https://github.com/ryanwalters/sticky-events
-    const { stickySelector } = new StickyEvents({ stickySelector: '#grw-subnav-sticky-trigger' });
-    const elem = document.querySelector(stickySelector);
-    elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-
-    // return clean up handler
-    return () => {
-      elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-    };
-  }, [stickyChangeHandler]);
-
   // setup effect by resizing event
   // setup effect by resizing event
   useEffect(() => {
   useEffect(() => {
     const resizeHandler = debounce(100, initWidth);
     const resizeHandler = debounce(100, initWidth);
@@ -100,7 +83,7 @@ export const GrowiSubNavigationSwitcher = (props: GrowiSubNavigationSwitcherProp
   }
   }
 
 
   return (
   return (
-    <div className={`${styles['grw-subnav-switcher']} ${isVisible ? '' : 'grw-subnav-switcher-hidden'}`}>
+    <div className={`${styles['grw-subnav-switcher']} ${isSticky ? '' : 'grw-subnav-switcher-hidden'}`}>
       <div
       <div
         id="grw-subnav-fixed-container"
         id="grw-subnav-fixed-container"
         className={`grw-subnav-fixed-container ${styles['grw-subnav-fixed-container']} position-fixed grw-subnav-append-shadow-container`}
         className={`grw-subnav-fixed-container ${styles['grw-subnav-fixed-container']} position-fixed grw-subnav-append-shadow-container`}

+ 7 - 21
packages/app/src/components/StickyStretchableScroller.tsx

@@ -1,12 +1,12 @@
 import React, {
 import React, {
-  useEffect, useCallback, FC, useRef, useState, useMemo, RefObject,
+  useEffect, useCallback, useRef, useState, useMemo, RefObject,
 } from 'react';
 } from 'react';
 
 
 import SimpleBar from 'simplebar-react';
 import SimpleBar from 'simplebar-react';
-import StickyEvents from 'sticky-events';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
+import { useSticky } from '~/utils/use-sticky-utils';
 
 
 const logger = loggerFactory('growi:cli:StickyStretchableScroller');
 const logger = loggerFactory('growi:cli:StickyStretchableScroller');
 
 
@@ -49,6 +49,9 @@ export const StickyStretchableScroller = (props: StickyStretchableScrollerProps)
   const simplebarRef = useRef<SimpleBar>(null);
   const simplebarRef = useRef<SimpleBar>(null);
   const [simplebarMaxHeight, setSimplebarMaxHeight] = useState<number|undefined>();
   const [simplebarMaxHeight, setSimplebarMaxHeight] = useState<number|undefined>();
 
 
+  // Get sticky status
+  const isSticky = useSticky(stickyElemSelector);
+
   /**
   /**
    * Reset scrollbar
    * Reset scrollbar
    */
    */
@@ -70,26 +73,9 @@ export const StickyStretchableScroller = (props: StickyStretchableScrollerProps)
 
 
   const resetScrollbarDebounced = useMemo(() => debounce(100, resetScrollbar), [resetScrollbar]);
   const resetScrollbarDebounced = useMemo(() => debounce(100, resetScrollbar), [resetScrollbar]);
 
 
-  const stickyChangeHandler = useCallback(() => {
-    logger.debug('StickyEvents.CHANGE detected');
-    resetScrollbarDebounced();
-  }, [resetScrollbarDebounced]);
-
-  // setup effect by sticky event
   useEffect(() => {
   useEffect(() => {
-    // sticky
-    // See: https://github.com/ryanwalters/sticky-events
-    const stickyEvents = new StickyEvents({ stickySelector: stickyElemSelector });
-    stickyEvents.enableEvents();
-    const { stickySelector } = stickyEvents;
-    const elem = document.querySelector(stickySelector);
-    elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-
-    // return clean up handler
-    return () => {
-      elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-    };
-  }, [stickyElemSelector, stickyChangeHandler]);
+    resetScrollbarDebounced();
+  }, [isSticky, resetScrollbarDebounced]);
 
 
   // setup effect by resizing event
   // setup effect by resizing event
   useEffect(() => {
   useEffect(() => {

+ 37 - 0
packages/app/src/utils/use-sticky-utils.ts

@@ -0,0 +1,37 @@
+import { useState, useEffect } from 'react';
+
+// Custom hook that accepts a selector string as an argument
+// and returns a boolean indicating whether the selected element is currently sticky.
+export const useSticky = (selector: string): boolean => {
+  const [isSticky, setIsSticky] = useState<boolean>(false);
+
+  useEffect(() => {
+    // Get element to observe
+    const stickyElement = document.querySelector(selector);
+    // Updates the sticky status based on the current position of the observed element.
+    const observe = () => {
+      // If the observed element is empty or not an instance of Element, return early.
+      if (stickyElement == null || stickyElement === undefined || !(stickyElement instanceof Element)) return;
+
+      // Calculate the element's offset from the top of the viewport and the value of its "top" CSS property.
+      const elemOffset = stickyElement.getBoundingClientRect().top;
+      const stickyOffset = parseInt(window.getComputedStyle(stickyElement).top);
+
+      // Update the sticky status based on whether the element's offset is less than or equal to the sticky offset.
+      setIsSticky(elemOffset <= stickyOffset);
+    };
+    // Call the observe function immediately and add it as a listener for scroll and resize events.
+    observe();
+    document.addEventListener('scroll', observe);
+    window.addEventListener('resize', observe);
+
+    // Remove the scroll and resize event listeners when the component unmounts or the selector value changes.
+    return () => {
+      document.removeEventListener('scroll', observe);
+      window.removeEventListener('resize', observe);
+    };
+  }, [selector]);
+
+  // Return the current sticky status.
+  return isSticky;
+};

+ 0 - 5
yarn.lock

@@ -20052,11 +20052,6 @@ stealthy-require@^1.1.1:
   resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
   resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
   integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
   integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
 
 
-sticky-events@^3.4.11:
-  version "3.4.11"
-  resolved "https://registry.yarnpkg.com/sticky-events/-/sticky-events-3.4.11.tgz#c44b7866648c5b2818a00fe93f709aa86e9a09d3"
-  integrity sha512-g1ex5lR7EGJv8EXJh4gdBu0m8FMgAVeqFAow3dRR9MwxAIfBNVC2GtlXI1z+oMLE+/Ot2At+gp1aO/tbUGoOnQ==
-
 stoppable@^1.1.0:
 stoppable@^1.1.0:
   version "1.1.0"
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b"
   resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b"