Fab.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import React, {
  2. useState, useCallback, useRef, useEffect,
  3. } from 'react';
  4. import { animateScroll } from 'react-scroll';
  5. import { useRipple } from 'react-use-ripple';
  6. import StickyEvents from 'sticky-events';
  7. import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
  8. import { usePageCreateModal } from '~/stores/modal';
  9. import { useCurrentPagePath } from '~/stores/page';
  10. import { useIsAbleToChangeEditorMode } from '~/stores/ui';
  11. import loggerFactory from '~/utils/logger';
  12. import { CreatePageIcon } from './Icons/CreatePageIcon';
  13. import { ReturnTopIcon } from './Icons/ReturnTopIcon';
  14. import styles from './Fab.module.scss';
  15. const logger = loggerFactory('growi:cli:Fab');
  16. export const Fab = (): JSX.Element => {
  17. const { data: isAbleToChangeEditorMode } = useIsAbleToChangeEditorMode();
  18. const { data: currentPath = '' } = useCurrentPagePath();
  19. const { open: openCreateModal } = usePageCreateModal();
  20. const [animateClasses, setAnimateClasses] = useState<string>('invisible');
  21. const [buttonClasses, setButtonClasses] = useState<string>('');
  22. const [isSticky, setIsSticky] = useState<boolean>(false);
  23. // ripple
  24. const createBtnRef = useRef(null);
  25. useRipple(createBtnRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
  26. /**
  27. * After the fade animation is finished, fix the button display status.
  28. * Prevents the fade animation occurred each time by button components rendered.
  29. * Check Fab.module.scss for fade animation time.
  30. */
  31. useEffect(() => {
  32. const timer = setTimeout(() => {
  33. if (isSticky) {
  34. setAnimateClasses('visible');
  35. setButtonClasses('');
  36. }
  37. else {
  38. setAnimateClasses('invisible');
  39. }
  40. }, 500);
  41. return () => clearTimeout(timer);
  42. }, [isSticky]);
  43. const stickyChangeHandler = useCallback((event) => {
  44. logger.debug('StickyEvents.CHANGE detected');
  45. const newAnimateClasses = event.detail.isSticky ? 'animated fadeInUp faster' : 'animated fadeOut faster';
  46. const newButtonClasses = event.detail.isSticky ? '' : 'disabled grw-pointer-events-none';
  47. setAnimateClasses(newAnimateClasses);
  48. setButtonClasses(newButtonClasses);
  49. setIsSticky(event.detail.isSticky);
  50. }, []);
  51. // setup effect by sticky event
  52. useEffect(() => {
  53. // sticky
  54. // See: https://github.com/ryanwalters/sticky-events
  55. const stickyEvents = new StickyEvents({ stickySelector: '#grw-fav-sticky-trigger' });
  56. const { stickySelector } = stickyEvents;
  57. const elem = document.querySelector(stickySelector);
  58. elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
  59. // return clean up handler
  60. return () => {
  61. elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
  62. };
  63. }, [stickyChangeHandler]);
  64. const PageCreateButton = useCallback(() => {
  65. return (
  66. <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: '2.3rem', right: '4rem' }}>
  67. <button
  68. type="button"
  69. className={`btn btn-lg btn-create-page btn-primary rounded-circle p-0 ${buttonClasses}`}
  70. ref={createBtnRef}
  71. onClick={currentPath != null
  72. ? () => openCreateModal(currentPath)
  73. : undefined}
  74. >
  75. <CreatePageIcon />
  76. </button>
  77. </div>
  78. );
  79. }, [animateClasses, buttonClasses, currentPath, openCreateModal]);
  80. const ScrollToTopButton = useCallback(() => {
  81. const clickHandler = () => {
  82. animateScroll.scrollToTop(DEFAULT_AUTO_SCROLL_OPTS);
  83. };
  84. return (
  85. <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }} data-testid="grw-fab-return-to-top">
  86. <button
  87. type="button"
  88. className={`btn btn-light btn-scroll-to-top rounded-circle p-0 ${buttonClasses}`}
  89. onClick={clickHandler}
  90. >
  91. <ReturnTopIcon />
  92. </button>
  93. </div>
  94. );
  95. }, [animateClasses, buttonClasses]);
  96. if (currentPath == null) {
  97. return <></>;
  98. }
  99. return (
  100. <div className={`${styles['grw-fab']} grw-fab d-none d-md-block d-edit-none`} data-testid="grw-fab-container">
  101. {isAbleToChangeEditorMode && <PageCreateButton />}
  102. <ScrollToTopButton />
  103. </div>
  104. );
  105. };