react-dom#flushSync JavaScript Examples

The following examples show how to use react-dom#flushSync. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: standalone.js    From ReactSourceCodeAnalyze with MIT License 5 votes vote down vote up
function safeUnmount() {
  flushSync(() => {
    if (root !== null) {
      root.unmount();
    }
  });
  root = null;
}
Example #2
Source File: Job.js    From FSE-Planner with MIT License 5 votes vote down vote up
// Generate all components to render leg
function Job(props) {

  // Add line
  return new JobSegment(props.positions, {
    weight: props.weight,
    color: props.color,
    highlight: props.highlight,
    bothWays: props.rleg
  })
    .bindTooltip(() => {
      var div = document.createElement('div');
      const root = createRoot(div);
      flushSync(() => {
        root.render((
          <ThemeProvider theme={Theme}>
            <Tooltip {...props} />
          </ThemeProvider>
        ));
      });
      return div;
    }, {sticky: true})
    .on('contextmenu', (evt) => {
      L.DomEvent.stopPropagation(evt);
      const actions = [
        {
          name: <span>Open {props.fromIcao} in FSE</span>,
          onClick: () => {
            let w = window.open('https://server.fseconomy.net/airport.jsp?icao='+props.fromIcao, 'fse');
            w.focus();
          }
        },
        {
          name: <span>Open {props.toIcao} in FSE</span>,
          onClick: () => {
            let w = window.open('https://server.fseconomy.net/airport.jsp?icao='+props.toIcao, 'fse');
            w.focus();
          }
        },
        {
          name: <span>View jobs <NavigationIcon fontSize="inherit" sx={{transform: 'rotate('+props.leg.direction+'deg)', verticalAlign: 'text-top'}} /></span>,
          onClick: () => {
            props.actions.current.openTable();
            props.actions.current.goTo(props.toIcao, props.fromIcao);
          }
        }
      ];
      if (props.rleg) {
        actions.push({
          name: <span>View jobs <NavigationIcon fontSize="inherit" sx={{transform: 'rotate('+(props.leg.direction+180)+'deg)', verticalAlign: 'text-top'}} /></span>,
          onClick: () => {
            props.actions.current.openTable();
            props.actions.current.goTo(props.fromIcao, props.toIcao);
          }
        })
      }
      props.actions.current.contextMenu({
        mouseX: evt.originalEvent.clientX,
        mouseY: evt.originalEvent.clientY,
        title: <span>{props.fromIcao} - {props.toIcao} <NavigationIcon fontSize="inherit" sx={{transform: 'rotate('+props.leg.direction+'deg)', verticalAlign: 'text-top'}} /></span>,
        actions: actions
      });
    });

}
Example #3
Source File: GPS.js    From FSE-Planner with MIT License 5 votes vote down vote up
function GPSLayer(props) {

  const s = props.settings;
  const group = L.featureGroup();
  const wrap = num => num+iWrap(num, s.display.map.center);

  // Create lines if needed
  if (props.connections) {
    let legs = {};
    for (const c of props.connections) {
      const [frID, toID] = c;

      const fr = { latitude: props.points[frID][0], longitude: props.points[frID][1] };
      const to = { latitude: props.points[toID][0], longitude: props.points[toID][1] };

      let key = frID+"-"+toID;
      if (!legs.hasOwnProperty(key)) {
        legs[key] = {
          direction: Math.round(getRhumbLineBearing(fr, to)),
          distance: Math.round(convertDistance(getDistance(fr, to), 'sm')),
        }
      }
    }

    const legsKeys = Object.keys(legs);

    for (var i = 0; i < legsKeys.length; i++) {
      const [fr, to] = legsKeys[i].split('-');
      const leg = legs[legsKeys[i]];
      const rleg = legs[to+'-'+fr]

      // Ensure only one line for both way legs
      if (rleg && fr > to) { continue; }

      new JobSegment([[props.points[fr][0], wrap(props.points[fr][1])], [props.points[to][0], wrap(props.points[to][1])]], {
        weight: props.weight,
        color: props.color,
        highlight: props.highlight,
        bothWays: rleg
      })
        .bindTooltip(() => {
          var div = document.createElement('div');
          const root = createRoot(div);
          flushSync(() => {
            root.render((
              <ThemeProvider theme={Theme}>
                <Typography variant="body1"><b>{leg.distance} NM</b></Typography>
              </ThemeProvider>
            ));
          });
          return div;
        }, {sticky: true})
        .addTo(group);
    }
  }

  // Create markers
  for (const [latitude, longitude, label] of props.points) {
    Marker({
      position: [latitude, wrap(longitude)],
      size: props.size,
      color: props.color,
      icao: label,
      icaodata: props.fseicaodata,
      actions: props.actions,
      sim: 'gps'
    })
      .addTo(group);
  }

  return group;

}
Example #4
Source File: main.js    From ReactSourceCodeAnalyze with MIT License 4 votes vote down vote up
function createPanelIfReactLoaded() {
  if (panelCreated) {
    return;
  }

  chrome.devtools.inspectedWindow.eval(
    'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
    function(pageHasReact, error) {
      if (!pageHasReact || panelCreated) {
        return;
      }

      panelCreated = true;

      clearInterval(loadCheckInterval);

      let bridge = null;
      let store = null;

      let profilingData = null;

      let componentsPortalContainer = null;
      let profilerPortalContainer = null;

      let cloneStyleTags = null;
      let mostRecentOverrideTab = null;
      let render = null;
      let root = null;

      const tabId = chrome.devtools.inspectedWindow.tabId;

      function initBridgeAndStore() {
        const port = chrome.runtime.connect({
          name: '' + tabId,
        });
        // Looks like `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation,
        // so it makes no sense to handle it here.

        bridge = new Bridge({
          listen(fn) {
            const listener = message => fn(message);
            // Store the reference so that we unsubscribe from the same object.
            const portOnMessage = port.onMessage;
            portOnMessage.addListener(listener);
            return () => {
              portOnMessage.removeListener(listener);
            };
          },
          send(event: string, payload: any, transferable?: Array<any>) {
            port.postMessage({event, payload}, transferable);
          },
        });
        bridge.addListener('reloadAppForProfiling', () => {
          localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true');
          chrome.devtools.inspectedWindow.eval('window.location.reload();');
        });
        bridge.addListener('syncSelectionToNativeElementsPanel', () => {
          setBrowserSelectionFromReact();
        });

        // This flag lets us tip the Store off early that we expect to be profiling.
        // This avoids flashing a temporary "Profiling not supported" message in the Profiler tab,
        // after a user has clicked the "reload and profile" button.
        let isProfiling = false;
        let supportsProfiling = false;
        if (
          localStorageGetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY) === 'true'
        ) {
          supportsProfiling = true;
          isProfiling = true;
          localStorageRemoveItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY);
        }

        if (store !== null) {
          profilingData = store.profilerStore.profilingData;
        }

        store = new Store(bridge, {
          isProfiling,
          supportsReloadAndProfile: isChrome,
          supportsProfiling,
        });
        store.profilerStore.profilingData = profilingData;

        // Initialize the backend only once the Store has been initialized.
        // Otherwise the Store may miss important initial tree op codes.
        inject(chrome.runtime.getURL('build/backend.js'));

        const viewElementSourceFunction = createViewElementSource(
          bridge,
          store,
        );

        root = createRoot(document.createElement('div'));

        render = (overrideTab = mostRecentOverrideTab) => {
          mostRecentOverrideTab = overrideTab;

          root.render(
            createElement(DevTools, {
              bridge,
              browserTheme: getBrowserTheme(),
              componentsPortalContainer,
              overrideTab,
              profilerPortalContainer,
              showTabBar: false,
              showWelcomeToTheNewDevToolsDialog: true,
              store,
              viewElementSourceFunction,
            }),
          );
        };

        render();
      }

      cloneStyleTags = () => {
        const linkTags = [];
        // eslint-disable-next-line no-for-of-loops/no-for-of-loops
        for (let linkTag of document.getElementsByTagName('link')) {
          if (linkTag.rel === 'stylesheet') {
            const newLinkTag = document.createElement('link');
            // eslint-disable-next-line no-for-of-loops/no-for-of-loops
            for (let attribute of linkTag.attributes) {
              newLinkTag.setAttribute(attribute.nodeName, attribute.nodeValue);
            }
            linkTags.push(newLinkTag);
          }
        }
        return linkTags;
      };

      initBridgeAndStore();

      function ensureInitialHTMLIsCleared(container) {
        if (container._hasInitialHTMLBeenCleared) {
          return;
        }
        container.innerHTML = '';
        container._hasInitialHTMLBeenCleared = true;
      }

      function setBrowserSelectionFromReact() {
        // This is currently only called on demand when you press "view DOM".
        // In the future, if Chrome adds an inspect() that doesn't switch tabs,
        // we could make this happen automatically when you select another component.
        chrome.devtools.inspectedWindow.eval(
          '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
            '(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' +
            'false',
          (didSelectionChange, evalError) => {
            if (evalError) {
              console.error(evalError);
            }
          },
        );
      }

      function setReactSelectionFromBrowser() {
        // When the user chooses a different node in the browser Elements tab,
        // copy it over to the hook object so that we can sync the selection.
        chrome.devtools.inspectedWindow.eval(
          '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
            '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' +
            'false',
          (didSelectionChange, evalError) => {
            if (evalError) {
              console.error(evalError);
            } else if (didSelectionChange) {
              // Remember to sync the selection next time we show Components tab.
              needsToSyncElementSelection = true;
            }
          },
        );
      }

      setReactSelectionFromBrowser();
      chrome.devtools.panels.elements.onSelectionChanged.addListener(() => {
        setReactSelectionFromBrowser();
      });

      let currentPanel = null;
      let needsToSyncElementSelection = false;

      chrome.devtools.panels.create(
        isChrome ? '⚛ Components' : 'Components',
        '',
        'panel.html',
        extensionPanel => {
          extensionPanel.onShown.addListener(panel => {
            if (needsToSyncElementSelection) {
              needsToSyncElementSelection = false;
              bridge.send('syncSelectionFromNativeElementsPanel');
            }

            if (currentPanel === panel) {
              return;
            }

            currentPanel = panel;
            componentsPortalContainer = panel.container;

            if (componentsPortalContainer != null) {
              ensureInitialHTMLIsCleared(componentsPortalContainer);
              render('components');
              panel.injectStyles(cloneStyleTags);
            }
          });
          extensionPanel.onHidden.addListener(panel => {
            // TODO: Stop highlighting and stuff.
          });
        },
      );

      chrome.devtools.panels.create(
        isChrome ? '⚛ Profiler' : 'Profiler',
        '',
        'panel.html',
        extensionPanel => {
          extensionPanel.onShown.addListener(panel => {
            if (currentPanel === panel) {
              return;
            }

            currentPanel = panel;
            profilerPortalContainer = panel.container;

            if (profilerPortalContainer != null) {
              ensureInitialHTMLIsCleared(profilerPortalContainer);
              render('profiler');
              panel.injectStyles(cloneStyleTags);
            }
          });
        },
      );

      chrome.devtools.network.onNavigated.removeListener(checkPageForReact);

      // Re-initialize DevTools panel when a new page is loaded.
      chrome.devtools.network.onNavigated.addListener(function onNavigated() {
        // Re-initialize saved filters on navigation,
        // since global values stored on window get reset in this case.
        syncSavedPreferences();

        // It's easiest to recreate the DevTools panel (to clean up potential stale state).
        // We can revisit this in the future as a small optimization.
        flushSync(() => {
          root.unmount(() => {
            initBridgeAndStore();
          });
        });
      });
    },
  );
}
Example #5
Source File: main.js    From protostar-relay with MIT License 4 votes vote down vote up
function createPanelIfReactLoaded() {
  if (panelCreated) {
    return;
  }

  chrome.devtools.inspectedWindow.eval(
    'window.__RELAY_DEVTOOLS_HOOK__ && window.__RELAY_DEVTOOLS_HOOK__.environments.size > 0',
    (pageHasRelay, error) => {
      if (!pageHasRelay || panelCreated) {
        return;
      }

      panelCreated = true;

      clearInterval(loadCheckInterval);

      let bridge = null;
      let store = null;

      let cloneStyleTags = null;
      let render = null;
      let root = null;
      let currentPanel = null;

      const tabId = chrome.devtools.inspectedWindow.tabId;

      function initBridgeAndStore() {
        const port = chrome.runtime.connect({
          name: '' + tabId
        });
        // Looks like `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation,
        // so it makes no sense to handle it here.

        bridge = new Bridge({
          listen(fn) {
            const listener = message => fn(message);
            // Store the reference so that we unsubscribe from the same object.
            const portOnMessage = port.onMessage;
            portOnMessage.addListener(listener);
            return () => {
              portOnMessage.removeListener(listener);
            };
          },
          send(event: string, payload: any, transferable?: Array<any>) {
            port.postMessage({ event, payload }, transferable);
          }
        });

        store = new Store(bridge);

        // Initialize the backend only once the Store has been initialized.
        // Otherwise the Store may miss important initial tree op codes.
        inject(chrome.runtime.getURL('build/backend.js'));

        const viewElementSourceFunction = createViewElementSource(bridge, store);

        render = () => {
          console.log('Rendering...');
          if (root) {
            root.render(
              createElement(DevTools, {
                bridge,
                // showTabBar: true,
                store,
                // viewElementSourceFunction,
                rootContainer: currentPanel.container
              })
            );
          }
        };

        render();
      }

      cloneStyleTags = () => {
        const linkTags = [];
        for (const linkTag of document.getElementsByTagName('link')) {
          if (linkTag.rel === 'stylesheet') {
            const newLinkTag = document.createElement('link');
            for (const attribute of linkTag.attributes) {
              newLinkTag.setAttribute(attribute.nodeName, attribute.nodeValue);
            }
            linkTags.push(newLinkTag);
          }
        }
        return linkTags;
      };

      initBridgeAndStore();

      function ensureInitialHTMLIsCleared(container) {
        if (container._hasInitialHTMLBeenCleared) {
          return;
        }
        container.innerHTML = '';
        container._hasInitialHTMLBeenCleared = true;
      }

      chrome.devtools.panels.create('proto*', '', 'index.html', panel => {
        panel.onShown.addListener(listenPanel => {
          if (currentPanel === listenPanel) {
            return;
          }
          currentPanel = listenPanel;

          if (listenPanel.container != null) {
            listenPanel.injectStyles(cloneStyleTags);
            ensureInitialHTMLIsCleared(listenPanel.container);
            root = createRoot(listenPanel.container);
            render();
          }
        });
        panel.onHidden.addListener(() => {
          // TODO: Stop highlighting and stuff.
        });
      });

      chrome.devtools.network.onNavigated.removeListener(checkPageForReact);

      // Shutdown bridge before a new page is loaded.
      chrome.webNavigation.onBeforeNavigate.addListener(function onBeforeNavigate(details) {
        // Ignore navigation events from other tabs (or from within frames).
        if (details.tabId !== tabId || details.frameId !== 0) {
          return;
        }

        // `bridge.shutdown()` will remove all listeners we added, so we don't have to.
        bridge.shutdown();
      });

      // Re-initialize DevTools panel when a new page is loaded.
      chrome.devtools.network.onNavigated.addListener(function onNavigated() {
        // It's easiest to recreate the DevTools panel (to clean up potential stale state).
        // We can revisit this in the future as a small optimization.
        flushSync(() => {
          root.unmount(() => {
            initBridgeAndStore();
          });
        });
      });
    }
  );
}
Example #6
Source File: Marker.js    From FSE-Planner with MIT License 4 votes vote down vote up
function Marker({position, size, color, sim, allJobs, ...props}) {
  let type = 'default';
  if (!sim || (props.icaodata[props.icao] && props.icaodata[props.icao][sim][0] === props.icao)) {
    const a = props.icaodata[props.icao];
    type = a.type + (a.size >= 3500 ? 3 : a.size >= 1000 ? 2 : 1);
  }
  return new AirportIcon(
    position,
    {
      radius: parseInt(size)/2,
      color: '#fff',
      fillColor: color,
      type: type,
      allJobs: allJobs,
    }
  )
    .bindPopup(() => {
      var div = document.createElement('div');
      const root = createRoot(div);
      if (sim) {
        flushSync(() => {
          root.render((
            <ThemeProvider theme={Theme}>
              <Typography variant="h5" style={{padding:'3px 24px 3px 8px'}}>{props.icao}</Typography>
            </ThemeProvider>
          ));
        });
      }
      else {
        flushSync(() => {
          root.render((
            <ThemeProvider theme={Theme}>
              <Popup {...props} />
            </ThemeProvider>
          ));
        });
      }
      return div;
    }, {
      autoPanPadding: new L.Point(30, 30),
      minWidth: sim ? 50 : Math.min(250, window.innerWidth-10),
      maxWidth: Math.max(600, window.innerWidth-10)
    })
    .on('contextmenu', (evt) => {
      L.DomEvent.stopPropagation(evt);
      const actions = [];
      if (!sim) {
        actions.push({
          name: 'Open in FSE',
          onClick: () => {
            let w = window.open('https://server.fseconomy.net/airport.jsp?icao='+props.icao, 'fse');
            w.focus();
          }
        });
        actions.push({
          name: 'View jobs',
          onClick: () => {
            props.actions.current.openTable();
            props.actions.current.goTo(props.icao);
          }
        });
        const isFromIcao = props.actions.current.isFromIcao(props.icao);
        const isToIcao = props.actions.current.isToIcao(props.icao);
        if (isFromIcao) {
          actions.push({
            name: 'Cancel jobs radiating FROM',
            onClick: () => props.actions.current.fromIcao('')
          });
        }
        else {
          actions.push({
            name: 'Jobs radiating FROM',
            onClick: () => {
              if (isToIcao) {
                props.actions.current.toIcao('');
              }
              props.actions.current.fromIcao(props.icao);
            }
          });
        }
        if (isToIcao) {
          actions.push({
            name: 'Cancel jobs radiating TO',
            onClick: () => props.actions.current.toIcao('')
          });
        }
        else {
          actions.push({
            name: 'Jobs radiating TO',
            onClick: () => {
              if (isFromIcao) {
                props.actions.current.fromIcao('');
              }
              props.actions.current.toIcao(props.icao);
            }
          });
        }
        actions.push({
          name: 'Mesure distance from this point',
          onClick: () => props.actions.current.measureDistance(evt.latlng)
        });
        // Custom layers action
        const layers = props.actions.current.getCustomLayers(props.icao);
        if (layers.length) {
          actions.push({
            divider: true
          });
          for (const [id, name, exist] of layers) {
            if (!exist) {
              actions.push({
                name: 'Add to layer "'+name+'"',
                onClick: () => props.actions.current.addToCustomLayer(id, props.icao)
              });
            }
            else {
              actions.push({
                name: 'Remove from layer "'+name+'"',
                onClick: () => props.actions.current.removeFromCustomLayer(id, props.icao)
              });
            }
          }
        }
        // Chart links
        actions.push({
          divider: true
        });
        actions.push({
          name: 'Charts on ChartFox',
          onClick: () => window.open(`https://chartfox.org/${props.icao}`, '_blank')
        });
        actions.push({
          name: 'Airport on SkyVector',
          onClick: () => window.open(`https://skyvector.com/airport/${props.icao}`, '_blank')
        });
      }
      props.actions.current.contextMenu({
        mouseX: evt.originalEvent.clientX,
        mouseY: evt.originalEvent.clientY,
        title: props.icao,
        actions: actions
      });
    });
}
Example #7
Source File: MenuList.js    From react-menu with MIT License 4 votes vote down vote up
MenuList = function MenuList(_ref) {
  var ariaLabel = _ref.ariaLabel,
      menuClassName = _ref.menuClassName,
      menuStyle = _ref.menuStyle,
      arrowClassName = _ref.arrowClassName,
      arrowStyle = _ref.arrowStyle,
      anchorPoint = _ref.anchorPoint,
      anchorRef = _ref.anchorRef,
      containerRef = _ref.containerRef,
      externalRef = _ref.externalRef,
      parentScrollingRef = _ref.parentScrollingRef,
      arrow = _ref.arrow,
      _ref$align = _ref.align,
      align = _ref$align === void 0 ? 'start' : _ref$align,
      _ref$direction = _ref.direction,
      direction = _ref$direction === void 0 ? 'bottom' : _ref$direction,
      _ref$position = _ref.position,
      position = _ref$position === void 0 ? 'auto' : _ref$position,
      _ref$overflow = _ref.overflow,
      overflow = _ref$overflow === void 0 ? 'visible' : _ref$overflow,
      setDownOverflow = _ref.setDownOverflow,
      repositionFlag = _ref.repositionFlag,
      _ref$captureFocus = _ref.captureFocus,
      captureFocus = _ref$captureFocus === void 0 ? true : _ref$captureFocus,
      state = _ref.state,
      endTransition = _ref.endTransition,
      isDisabled = _ref.isDisabled,
      menuItemFocus = _ref.menuItemFocus,
      _ref$offsetX = _ref.offsetX,
      offsetX = _ref$offsetX === void 0 ? 0 : _ref$offsetX,
      _ref$offsetY = _ref.offsetY,
      offsetY = _ref$offsetY === void 0 ? 0 : _ref$offsetY,
      children = _ref.children,
      onClose = _ref.onClose,
      restProps = _objectWithoutPropertiesLoose(_ref, _excluded);

  var _useState = useState({
    x: 0,
    y: 0
  }),
      menuPosition = _useState[0],
      setMenuPosition = _useState[1];

  var _useState2 = useState({}),
      arrowPosition = _useState2[0],
      setArrowPosition = _useState2[1];

  var _useState3 = useState(),
      overflowData = _useState3[0],
      setOverflowData = _useState3[1];

  var _useState4 = useState(direction),
      expandedDirection = _useState4[0],
      setExpandedDirection = _useState4[1];

  var _useState5 = useState(0),
      openSubmenuCount = _useState5[0],
      setOpenSubmenuCount = _useState5[1];

  var _useReducer = useReducer(function (c) {
    return c + 1;
  }, 1),
      reposSubmenu = _useReducer[0],
      forceReposSubmenu = _useReducer[1];

  var _useContext = useContext(SettingsContext),
      transition = _useContext.transition,
      boundingBoxRef = _useContext.boundingBoxRef,
      boundingBoxPadding = _useContext.boundingBoxPadding,
      rootMenuRef = _useContext.rootMenuRef,
      rootAnchorRef = _useContext.rootAnchorRef,
      scrollNodesRef = _useContext.scrollNodesRef,
      reposition = _useContext.reposition,
      viewScroll = _useContext.viewScroll;

  var reposFlag = useContext(MenuListContext).reposSubmenu || repositionFlag;
  var menuRef = useRef(null);
  var focusRef = useRef();
  var arrowRef = useRef();
  var prevOpen = useRef(false);
  var latestMenuSize = useRef({
    width: 0,
    height: 0
  });
  var latestHandlePosition = useRef(function () {});

  var _useItems = useItems(menuRef),
      hoverItem = _useItems.hoverItem,
      dispatch = _useItems.dispatch,
      updateItems = _useItems.updateItems;

  var isOpen = isMenuOpen(state);
  var openTransition = getTransition(transition, 'open');
  var closeTransition = getTransition(transition, 'close');
  var scrollNodes = scrollNodesRef.current;

  var handleKeyDown = function handleKeyDown(e) {
    var handled = false;

    switch (e.key) {
      case Keys.HOME:
        dispatch(HoverActionTypes.FIRST);
        handled = true;
        break;

      case Keys.END:
        dispatch(HoverActionTypes.LAST);
        handled = true;
        break;

      case Keys.UP:
        dispatch(HoverActionTypes.DECREASE, hoverItem);
        handled = true;
        break;

      case Keys.DOWN:
        dispatch(HoverActionTypes.INCREASE, hoverItem);
        handled = true;
        break;

      case Keys.SPACE:
        if (e.target && e.target.className.indexOf(menuClass) !== -1) {
          e.preventDefault();
        }

        break;
    }

    if (handled) {
      e.preventDefault();
      e.stopPropagation();
    }
  };

  var handleAnimationEnd = function handleAnimationEnd() {
    if (state === 'closing') {
      setOverflowData();
    }

    safeCall(endTransition);
  };

  var handlePosition = useCallback(function (noOverflowCheck) {
    if (!containerRef.current) {
      if (process.env.NODE_ENV !== 'production') {
        console.error('[React-Menu] Menu cannot be positioned properly as container ref is null. If you need to initialise `state` prop to "open" for ControlledMenu, please see this solution: https://codesandbox.io/s/initial-open-sp10wn');
      }

      return;
    }

    if (!scrollNodes.menu) {
      scrollNodes.menu = (boundingBoxRef ? boundingBoxRef.current : getScrollAncestor(rootMenuRef.current)) || window;
    }

    var positionHelpers = getPositionHelpers(containerRef, menuRef, scrollNodes.menu, boundingBoxPadding);
    var menuRect = positionHelpers.menuRect;
    var results = {
      computedDirection: 'bottom'
    };

    if (anchorPoint) {
      results = positionContextMenu({
        positionHelpers: positionHelpers,
        anchorPoint: anchorPoint
      });
    } else if (anchorRef) {
      results = positionMenu({
        arrow: arrow,
        align: align,
        direction: direction,
        offsetX: offsetX,
        offsetY: offsetY,
        position: position,
        anchorRef: anchorRef,
        arrowRef: arrowRef,
        positionHelpers: positionHelpers
      });
    }

    var _results = results,
        arrowX = _results.arrowX,
        arrowY = _results.arrowY,
        x = _results.x,
        y = _results.y,
        computedDirection = _results.computedDirection;
    var menuHeight = menuRect.height;

    if (!noOverflowCheck && overflow !== 'visible') {
      var getTopOverflow = positionHelpers.getTopOverflow,
          getBottomOverflow = positionHelpers.getBottomOverflow;

      var height, _overflowAmt;

      var prevHeight = latestMenuSize.current.height;
      var bottomOverflow = getBottomOverflow(y);

      if (bottomOverflow > 0 || floatEqual(bottomOverflow, 0) && floatEqual(menuHeight, prevHeight)) {
        height = menuHeight - bottomOverflow;
        _overflowAmt = bottomOverflow;
      } else {
        var topOverflow = getTopOverflow(y);

        if (topOverflow < 0 || floatEqual(topOverflow, 0) && floatEqual(menuHeight, prevHeight)) {
          height = menuHeight + topOverflow;
          _overflowAmt = 0 - topOverflow;
          if (height >= 0) y -= topOverflow;
        }
      }

      if (height >= 0) {
        menuHeight = height;
        setOverflowData({
          height: height,
          overflowAmt: _overflowAmt
        });
      } else {
        setOverflowData();
      }
    }

    if (arrow) setArrowPosition({
      x: arrowX,
      y: arrowY
    });
    setMenuPosition({
      x: x,
      y: y
    });
    setExpandedDirection(computedDirection);
    latestMenuSize.current = {
      width: menuRect.width,
      height: menuHeight
    };
  }, [arrow, align, boundingBoxPadding, direction, offsetX, offsetY, position, overflow, anchorPoint, anchorRef, containerRef, boundingBoxRef, rootMenuRef, scrollNodes]);
  useIsomorphicLayoutEffect(function () {
    if (isOpen) {
      handlePosition();
      if (prevOpen.current) forceReposSubmenu();
    }

    prevOpen.current = isOpen;
    latestHandlePosition.current = handlePosition;
  }, [isOpen, handlePosition, reposFlag]);
  useIsomorphicLayoutEffect(function () {
    if (overflowData && !setDownOverflow) menuRef.current.scrollTop = 0;
  }, [overflowData, setDownOverflow]);
  useEffect(function () {
    return updateItems;
  }, [updateItems]);
  useEffect(function () {
    var menuScroll = scrollNodes.menu;
    if (!isOpen || !menuScroll) return;
    menuScroll = menuScroll.addEventListener ? menuScroll : window;

    if (!scrollNodes.anchors) {
      scrollNodes.anchors = [];
      var anchorScroll = getScrollAncestor(rootAnchorRef && rootAnchorRef.current);

      while (anchorScroll && anchorScroll !== menuScroll) {
        scrollNodes.anchors.push(anchorScroll);
        anchorScroll = getScrollAncestor(anchorScroll);
      }
    }

    var scroll = viewScroll;
    if (scrollNodes.anchors.length && scroll === 'initial') scroll = 'auto';
    if (scroll === 'initial') return;

    var handleScroll = function handleScroll() {
      if (scroll === 'auto') {
        batchedUpdates(function () {
          return handlePosition(true);
        });
      } else {
        safeCall(onClose, {
          reason: CloseReason.SCROLL
        });
      }
    };

    var scrollObservers = scrollNodes.anchors.concat(viewScroll !== 'initial' ? menuScroll : []);
    scrollObservers.forEach(function (o) {
      return o.addEventListener('scroll', handleScroll);
    });
    return function () {
      return scrollObservers.forEach(function (o) {
        return o.removeEventListener('scroll', handleScroll);
      });
    };
  }, [rootAnchorRef, scrollNodes, isOpen, onClose, viewScroll, handlePosition]);
  var hasOverflow = !!overflowData && overflowData.overflowAmt > 0;
  useEffect(function () {
    if (hasOverflow || !isOpen || !parentScrollingRef) return;

    var handleScroll = function handleScroll() {
      return batchedUpdates(handlePosition);
    };

    var parentScroll = parentScrollingRef.current;
    parentScroll.addEventListener('scroll', handleScroll);
    return function () {
      return parentScroll.removeEventListener('scroll', handleScroll);
    };
  }, [isOpen, hasOverflow, parentScrollingRef, handlePosition]);
  useEffect(function () {
    if (typeof ResizeObserver !== 'function' || reposition === 'initial') return;
    var resizeObserver = new ResizeObserver(function (_ref2) {
      var entry = _ref2[0];
      var borderBoxSize = entry.borderBoxSize,
          target = entry.target;
      var width, height;

      if (borderBoxSize) {
        var _ref3 = borderBoxSize[0] || borderBoxSize,
            inlineSize = _ref3.inlineSize,
            blockSize = _ref3.blockSize;

        width = inlineSize;
        height = blockSize;
      } else {
        var borderRect = target.getBoundingClientRect();
        width = borderRect.width;
        height = borderRect.height;
      }

      if (width === 0 || height === 0) return;
      if (floatEqual(width, latestMenuSize.current.width, 1) && floatEqual(height, latestMenuSize.current.height, 1)) return;
      flushSync(function () {
        latestHandlePosition.current();
        forceReposSubmenu();
      });
    });
    var observeTarget = menuRef.current;
    resizeObserver.observe(observeTarget, {
      box: 'border-box'
    });
    return function () {
      return resizeObserver.unobserve(observeTarget);
    };
  }, [reposition]);
  useEffect(function () {
    if (!isOpen) {
      dispatch(HoverActionTypes.RESET);
      if (!closeTransition) setOverflowData();
      return;
    }

    var _ref4 = menuItemFocus || {},
        position = _ref4.position,
        alwaysUpdate = _ref4.alwaysUpdate;

    var setItemFocus = function setItemFocus() {
      if (position === FocusPositions.FIRST) {
        dispatch(HoverActionTypes.FIRST);
      } else if (position === FocusPositions.LAST) {
        dispatch(HoverActionTypes.LAST);
      } else if (position >= -1) {
        dispatch(HoverActionTypes.SET_INDEX, undefined, position);
      }
    };

    if (alwaysUpdate) {
      setItemFocus();
    } else if (captureFocus) {
      var id = setTimeout(function () {
        if (!menuRef.current.contains(document.activeElement)) {
          focusRef.current.focus();
          setItemFocus();
        }
      }, openTransition ? 170 : 100);
      return function () {
        return clearTimeout(id);
      };
    }
  }, [isOpen, openTransition, closeTransition, captureFocus, menuItemFocus, dispatch]);
  var isSubmenuOpen = openSubmenuCount > 0;
  var itemContext = useMemo(function () {
    return {
      isParentOpen: isOpen,
      isSubmenuOpen: isSubmenuOpen,
      setOpenSubmenuCount: setOpenSubmenuCount,
      dispatch: dispatch,
      updateItems: updateItems
    };
  }, [isOpen, isSubmenuOpen, dispatch, updateItems]);
  var maxHeight, overflowAmt;

  if (overflowData) {
    setDownOverflow ? overflowAmt = overflowData.overflowAmt : maxHeight = overflowData.height;
  }

  var listContext = useMemo(function () {
    return {
      reposSubmenu: reposSubmenu,
      overflow: overflow,
      overflowAmt: overflowAmt,
      parentMenuRef: menuRef,
      parentDir: expandedDirection
    };
  }, [reposSubmenu, overflow, overflowAmt, expandedDirection]);
  var overflowStyle = maxHeight >= 0 ? {
    maxHeight: maxHeight,
    overflow: overflow
  } : undefined;
  var modifiers = useMemo(function () {
    return {
      state: state,
      dir: expandedDirection
    };
  }, [state, expandedDirection]);
  var arrowModifiers = useMemo(function () {
    return {
      dir: expandedDirection
    };
  }, [expandedDirection]);

  var _arrowClass = useBEM({
    block: menuClass,
    element: menuArrowClass,
    modifiers: arrowModifiers,
    className: arrowClassName
  });

  var handlers = attachHandlerProps({
    onKeyDown: handleKeyDown,
    onAnimationEnd: handleAnimationEnd
  }, restProps);
  return /*#__PURE__*/jsxs("ul", _extends({
    role: "menu",
    "aria-label": ariaLabel
  }, restProps, handlers, commonProps(isDisabled), {
    ref: useCombinedRef(externalRef, menuRef),
    className: useBEM({
      block: menuClass,
      modifiers: modifiers,
      className: menuClassName
    }),
    style: _extends({}, menuStyle, overflowStyle, {
      margin: 0,
      display: state === 'closed' ? 'none' : undefined,
      position: 'absolute',
      left: menuPosition.x,
      top: menuPosition.y
    }),
    children: [/*#__PURE__*/jsx("div", {
      ref: focusRef,
      tabIndex: -1,
      style: {
        position: 'absolute',
        left: 0,
        top: 0
      }
    }), arrow && /*#__PURE__*/jsx("div", {
      className: _arrowClass,
      style: _extends({}, arrowStyle, {
        position: 'absolute',
        left: arrowPosition.x,
        top: arrowPosition.y
      }),
      ref: arrowRef
    }), /*#__PURE__*/jsx(MenuListContext.Provider, {
      value: listContext,
      children: /*#__PURE__*/jsx(MenuListItemContext.Provider, {
        value: itemContext,
        children: /*#__PURE__*/jsx(HoverItemContext.Provider, {
          value: hoverItem,
          children: children
        })
      })
    })]
  }));
}
Example #8
Source File: MenuList.js    From react-menu with MIT License 4 votes vote down vote up
MenuList = ({
  ariaLabel,
  menuClassName,
  menuStyle,
  arrowClassName,
  arrowStyle,
  anchorPoint,
  anchorRef,
  containerRef,
  externalRef,
  parentScrollingRef,
  arrow,
  align = 'start',
  direction = 'bottom',
  position = 'auto',
  overflow = 'visible',
  setDownOverflow,
  repositionFlag,
  captureFocus = true,
  state,
  endTransition,
  isDisabled,
  menuItemFocus,
  offsetX = 0,
  offsetY = 0,
  children,
  onClose,
  ...restProps
}) => {
  const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
  const [arrowPosition, setArrowPosition] = useState({});
  const [overflowData, setOverflowData] = useState();
  const [expandedDirection, setExpandedDirection] = useState(direction);
  const [openSubmenuCount, setOpenSubmenuCount] = useState(0);
  const [reposSubmenu, forceReposSubmenu] = useReducer((c) => c + 1, 1);
  const {
    transition,
    boundingBoxRef,
    boundingBoxPadding,
    rootMenuRef,
    rootAnchorRef,
    scrollNodesRef,
    reposition,
    viewScroll
  } = useContext(SettingsContext);
  const reposFlag = useContext(MenuListContext).reposSubmenu || repositionFlag;
  const menuRef = useRef(null);
  const focusRef = useRef();
  const arrowRef = useRef();
  const prevOpen = useRef(false);
  const latestMenuSize = useRef({ width: 0, height: 0 });
  const latestHandlePosition = useRef(() => {});
  const { hoverItem, dispatch, updateItems } = useItems(menuRef);

  const isOpen = isMenuOpen(state);
  const openTransition = getTransition(transition, 'open');
  const closeTransition = getTransition(transition, 'close');
  const scrollNodes = scrollNodesRef.current;

  const handleKeyDown = (e) => {
    let handled = false;

    switch (e.key) {
      case Keys.HOME:
        dispatch(HoverActionTypes.FIRST);
        handled = true;
        break;

      case Keys.END:
        dispatch(HoverActionTypes.LAST);
        handled = true;
        break;

      case Keys.UP:
        dispatch(HoverActionTypes.DECREASE, hoverItem);
        handled = true;
        break;

      case Keys.DOWN:
        dispatch(HoverActionTypes.INCREASE, hoverItem);
        handled = true;
        break;

      // prevent browser from scrolling the page when SPACE is pressed
      case Keys.SPACE:
        // Don't preventDefault on children of FocusableItem
        if (e.target && e.target.className.indexOf(menuClass) !== -1) {
          e.preventDefault();
        }
        break;
    }

    if (handled) {
      e.preventDefault();
      e.stopPropagation();
    }
  };

  const handleAnimationEnd = () => {
    if (state === 'closing') {
      setOverflowData(); // reset overflowData after closing
    }

    safeCall(endTransition);
  };

  const handlePosition = useCallback(
    (noOverflowCheck) => {
      if (!containerRef.current) {
        if (process.env.NODE_ENV !== 'production') {
          console.error(
            '[React-Menu] Menu cannot be positioned properly as container ref is null. If you need to initialise `state` prop to "open" for ControlledMenu, please see this solution: https://codesandbox.io/s/initial-open-sp10wn'
          );
        }
        return;
      }

      if (!scrollNodes.menu) {
        scrollNodes.menu =
          (boundingBoxRef
            ? boundingBoxRef.current // user explicitly sets boundingBoxRef
            : getScrollAncestor(rootMenuRef.current)) || window; // try to discover bounding box automatically
      }

      const positionHelpers = getPositionHelpers(
        containerRef,
        menuRef,
        scrollNodes.menu,
        boundingBoxPadding
      );
      const { menuRect } = positionHelpers;
      let results = { computedDirection: 'bottom' };
      if (anchorPoint) {
        results = positionContextMenu({ positionHelpers, anchorPoint });
      } else if (anchorRef) {
        results = positionMenu({
          arrow,
          align,
          direction,
          offsetX,
          offsetY,
          position,
          anchorRef,
          arrowRef,
          positionHelpers
        });
      }
      let { arrowX, arrowY, x, y, computedDirection } = results;
      let menuHeight = menuRect.height;

      if (!noOverflowCheck && overflow !== 'visible') {
        const { getTopOverflow, getBottomOverflow } = positionHelpers;

        let height, overflowAmt;
        const prevHeight = latestMenuSize.current.height;
        const bottomOverflow = getBottomOverflow(y);
        // When bottomOverflow is 0, menu is on the bottom edge of viewport
        // This might be the result of a previous maxHeight set on the menu.
        // In this situation, we need to still apply a new maxHeight.
        // Same reason for the top side
        if (
          bottomOverflow > 0 ||
          (floatEqual(bottomOverflow, 0) && floatEqual(menuHeight, prevHeight))
        ) {
          height = menuHeight - bottomOverflow;
          overflowAmt = bottomOverflow;
        } else {
          const topOverflow = getTopOverflow(y);
          if (
            topOverflow < 0 ||
            (floatEqual(topOverflow, 0) && floatEqual(menuHeight, prevHeight))
          ) {
            height = menuHeight + topOverflow;
            overflowAmt = 0 - topOverflow; // avoid getting -0
            if (height >= 0) y -= topOverflow;
          }
        }

        if (height >= 0) {
          // To avoid triggering reposition in the next ResizeObserver callback
          menuHeight = height;
          setOverflowData({ height, overflowAmt });
        } else {
          setOverflowData();
        }
      }

      if (arrow) setArrowPosition({ x: arrowX, y: arrowY });
      setMenuPosition({ x, y });
      setExpandedDirection(computedDirection);
      latestMenuSize.current = { width: menuRect.width, height: menuHeight };
    },
    [
      arrow,
      align,
      boundingBoxPadding,
      direction,
      offsetX,
      offsetY,
      position,
      overflow,
      anchorPoint,
      anchorRef,
      containerRef,
      boundingBoxRef,
      rootMenuRef,
      scrollNodes
    ]
  );

  useLayoutEffect(() => {
    if (isOpen) {
      handlePosition();
      // Reposition submenu whenever deps(except isOpen) have changed
      if (prevOpen.current) forceReposSubmenu();
    }
    prevOpen.current = isOpen;
    latestHandlePosition.current = handlePosition;
  }, [isOpen, handlePosition, /* effect dep */ reposFlag]);

  useLayoutEffect(() => {
    if (overflowData && !setDownOverflow) menuRef.current.scrollTop = 0;
  }, [overflowData, setDownOverflow]);

  useEffect(() => updateItems, [updateItems]);

  useEffect(() => {
    let { menu: menuScroll } = scrollNodes;
    if (!isOpen || !menuScroll) return;

    menuScroll = menuScroll.addEventListener ? menuScroll : window;
    if (!scrollNodes.anchors) {
      scrollNodes.anchors = [];
      let anchorScroll = getScrollAncestor(rootAnchorRef && rootAnchorRef.current);
      while (anchorScroll && anchorScroll !== menuScroll) {
        scrollNodes.anchors.push(anchorScroll);
        anchorScroll = getScrollAncestor(anchorScroll);
      }
    }

    let scroll = viewScroll;
    if (scrollNodes.anchors.length && scroll === 'initial') scroll = 'auto';
    if (scroll === 'initial') return;

    const handleScroll = () => {
      if (scroll === 'auto') {
        batchedUpdates(() => handlePosition(true));
      } else {
        safeCall(onClose, { reason: CloseReason.SCROLL });
      }
    };

    const scrollObservers = scrollNodes.anchors.concat(viewScroll !== 'initial' ? menuScroll : []);
    scrollObservers.forEach((o) => o.addEventListener('scroll', handleScroll));
    return () => scrollObservers.forEach((o) => o.removeEventListener('scroll', handleScroll));
  }, [rootAnchorRef, scrollNodes, isOpen, onClose, viewScroll, handlePosition]);

  const hasOverflow = !!overflowData && overflowData.overflowAmt > 0;
  useEffect(() => {
    if (hasOverflow || !isOpen || !parentScrollingRef) return;

    const handleScroll = () => batchedUpdates(handlePosition);
    const parentScroll = parentScrollingRef.current;
    parentScroll.addEventListener('scroll', handleScroll);
    return () => parentScroll.removeEventListener('scroll', handleScroll);
  }, [isOpen, hasOverflow, parentScrollingRef, handlePosition]);

  useEffect(() => {
    if (typeof ResizeObserver !== 'function' || reposition === 'initial') return;

    const resizeObserver = new ResizeObserver(([entry]) => {
      const { borderBoxSize, target } = entry;
      let width, height;
      if (borderBoxSize) {
        const { inlineSize, blockSize } = borderBoxSize[0] || borderBoxSize;
        width = inlineSize;
        height = blockSize;
      } else {
        const borderRect = target.getBoundingClientRect();
        width = borderRect.width;
        height = borderRect.height;
      }

      if (width === 0 || height === 0) return;
      if (
        floatEqual(width, latestMenuSize.current.width, 1) &&
        floatEqual(height, latestMenuSize.current.height, 1)
      )
        return;
      flushSync(() => {
        latestHandlePosition.current();
        forceReposSubmenu();
      });
    });

    const observeTarget = menuRef.current;
    resizeObserver.observe(observeTarget, { box: 'border-box' });
    return () => resizeObserver.unobserve(observeTarget);
  }, [reposition]);

  useEffect(() => {
    if (!isOpen) {
      dispatch(HoverActionTypes.RESET);
      if (!closeTransition) setOverflowData();
      return;
    }

    const { position, alwaysUpdate } = menuItemFocus || {};
    const setItemFocus = () => {
      if (position === FocusPositions.FIRST) {
        dispatch(HoverActionTypes.FIRST);
      } else if (position === FocusPositions.LAST) {
        dispatch(HoverActionTypes.LAST);
      } else if (position >= -1) {
        dispatch(HoverActionTypes.SET_INDEX, undefined, position);
      }
    };

    if (alwaysUpdate) {
      setItemFocus();
    } else if (captureFocus) {
      // Use a timeout here because if set focus immediately, page might scroll unexpectedly.
      const id = setTimeout(
        () => {
          // If focus has already been set to a children element, don't set focus on menu or item
          if (!menuRef.current.contains(document.activeElement)) {
            focusRef.current.focus();
            setItemFocus();
          }
        },
        openTransition ? 170 : 100
      );

      return () => clearTimeout(id);
    }
  }, [isOpen, openTransition, closeTransition, captureFocus, menuItemFocus, dispatch]);

  const isSubmenuOpen = openSubmenuCount > 0;
  const itemContext = useMemo(
    () => ({
      isParentOpen: isOpen,
      isSubmenuOpen,
      setOpenSubmenuCount,
      dispatch,
      updateItems
    }),
    [isOpen, isSubmenuOpen, dispatch, updateItems]
  );

  let maxHeight, overflowAmt;
  if (overflowData) {
    setDownOverflow ? (overflowAmt = overflowData.overflowAmt) : (maxHeight = overflowData.height);
  }

  const listContext = useMemo(
    () => ({
      reposSubmenu,
      overflow,
      overflowAmt,
      parentMenuRef: menuRef,
      parentDir: expandedDirection
    }),
    [reposSubmenu, overflow, overflowAmt, expandedDirection]
  );
  const overflowStyle = maxHeight >= 0 ? { maxHeight, overflow } : undefined;

  const modifiers = useMemo(
    () => ({
      state,
      dir: expandedDirection
    }),
    [state, expandedDirection]
  );
  const arrowModifiers = useMemo(() => ({ dir: expandedDirection }), [expandedDirection]);
  const _arrowClass = useBEM({
    block: menuClass,
    element: menuArrowClass,
    modifiers: arrowModifiers,
    className: arrowClassName
  });

  const handlers = attachHandlerProps(
    {
      onKeyDown: handleKeyDown,
      onAnimationEnd: handleAnimationEnd
    },
    restProps
  );

  return (
    <ul
      role="menu"
      aria-label={ariaLabel}
      {...restProps}
      {...handlers}
      {...commonProps(isDisabled)}
      ref={useCombinedRef(externalRef, menuRef)}
      className={useBEM({ block: menuClass, modifiers, className: menuClassName })}
      style={{
        ...menuStyle,
        ...overflowStyle,
        margin: 0,
        display: state === 'closed' ? 'none' : undefined,
        position: 'absolute',
        left: menuPosition.x,
        top: menuPosition.y
      }}
    >
      <div ref={focusRef} tabIndex={-1} style={{ position: 'absolute', left: 0, top: 0 }} />
      {arrow && (
        <div
          className={_arrowClass}
          style={{
            ...arrowStyle,
            position: 'absolute',
            left: arrowPosition.x,
            top: arrowPosition.y
          }}
          ref={arrowRef}
        />
      )}

      <MenuListContext.Provider value={listContext}>
        <MenuListItemContext.Provider value={itemContext}>
          <HoverItemContext.Provider value={hoverItem}>{children}</HoverItemContext.Provider>
        </MenuListItemContext.Provider>
      </MenuListContext.Provider>
    </ul>
  );
}