Wykryj kliknięcie poza komponentem React

410

Szukam sposobu na wykrycie, czy zdarzenie kliknięcia nastąpiło poza komponentem, jak opisano w tym artykule . Funkcja jQuery closest () służy do sprawdzania, czy element docelowy ze zdarzenia click ma element dom jako jedno z jego elementów nadrzędnych. Jeśli istnieje dopasowanie, zdarzenie click należy do jednego z elementów potomnych i dlatego nie jest uważane za znajdujące się poza komponentem.

Więc w moim komponencie chcę dołączyć moduł obsługi kliknięć do okna. Kiedy program obsługi uruchamia się, muszę porównać cel z elementami podrzędnymi dom mojego komponentu.

Zdarzenie kliknięcia zawiera właściwości takie jak „ścieżka”, która wydaje się zawierać ścieżkę dom, którą przeszło zdarzenie. Nie jestem pewien, co porównać ani jak najlepiej go przejść, i myślę, że ktoś musiał już włączyć to w sprytną funkcję użyteczności ... Nie?

Thijs Koerselman
źródło
Czy możesz dołączyć moduł obsługi kliknięć do elementu nadrzędnego zamiast do okna?
J. Mark Stevens,
Jeśli podłączysz moduł obsługi kliknięć do elementu nadrzędnego, wiesz, kiedy kliknięty zostanie ten element lub jedno z jego elementów podrzędnych, ale muszę wykryć wszystkie inne kliknięte miejsca, więc moduł obsługi musi zostać dołączony do okna.
Thijs Koerselman
Spojrzałem na artykuł po poprzedniej odpowiedzi. Co powiesz na ustawienie parametru clickState w górnym komponencie i przekazywanie akcji kliknięcia od dzieci. Następnie sprawdziłbyś rekwizyty u dzieci, aby zarządzać stanem otwartego zamknięcia.
J. Mark Stevens,
Najważniejszym komponentem byłaby moja aplikacja. Ale komponent odsłuchowy ma kilka poziomów głębokości i nie ma ścisłej pozycji w domu. Nie mogę dodać programów obsługi kliknięć do wszystkich składników w mojej aplikacji, tylko dlatego, że jeden z nich chce wiedzieć, czy kliknąłeś gdzieś poza nim. Inne komponenty nie powinny być świadome tej logiki, ponieważ spowodowałoby to straszne zależności i kod podstawowy.
Thijs Koerselman
5
Chciałbym polecić ci bardzo fajną bibliotekę. utworzony przez AirBnb: github.com/airbnb/react-outside-click-handler
Osoian Marcel

Odpowiedzi:

681

Poniższe rozwiązanie wykorzystuje ES6 i postępuje zgodnie z najlepszymi praktykami w zakresie wiązania, a także ustawiania odwołania za pomocą metody.

Aby zobaczyć to w akcji:

Realizacja klasy:

import React, { Component } from 'react';

/**
 * Component that alerts if you click outside of it
 */
export default class OutsideAlerter extends Component {
  constructor(props) {
    super(props);

    this.setWrapperRef = this.setWrapperRef.bind(this);
    this.handleClickOutside = this.handleClickOutside.bind(this);
  }

  componentDidMount() {
    document.addEventListener('mousedown', this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener('mousedown', this.handleClickOutside);
  }

  /**
   * Set the wrapper ref
   */
  setWrapperRef(node) {
    this.wrapperRef = node;
  }

  /**
   * Alert if clicked on outside of element
   */
  handleClickOutside(event) {
    if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
      alert('You clicked outside of me!');
    }
  }

  render() {
    return <div ref={this.setWrapperRef}>{this.props.children}</div>;
  }
}

OutsideAlerter.propTypes = {
  children: PropTypes.element.isRequired,
};

Implementacja haków:

import React, { useRef, useEffect } from "react";

/**
 * Hook that alerts clicks outside of the passed ref
 */
function useOutsideAlerter(ref) {
  useEffect(() => {
    /**
     * Alert if clicked on outside of element
     */
    function handleClickOutside(event) {
      if (ref.current && !ref.current.contains(event.target)) {
        alert("You clicked outside of me!");
      }
    }

    // Bind the event listener
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      // Unbind the event listener on clean up
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [ref]);
}

/**
 * Component that alerts if you click outside of it
 */
export default function OutsideAlerter(props) {
  const wrapperRef = useRef(null);
  useOutsideAlerter(wrapperRef);

  return <div ref={wrapperRef}>{props.children}</div>;
}
Ben Bud
źródło
3
Jak to testujesz? Jak wysłać prawidłowy event.target do funkcji handleClickOutside?
csilk
5
Jak możesz użyć syntetycznych wydarzeń React, zamiast document.addEventListenertutaj?
Ralph David Abernathy
9
@ polkovnikov.ph 1. contextjest niezbędny jako argument w konstruktorze, jeśli jest używany. W tym przypadku nie jest używany. Powodem, dla którego zespół reagujący zaleca propsjako argument w konstruktorze jest to, że użycie this.propsprzed wywołaniem super(props)będzie niezdefiniowane i może prowadzić do błędów. contextjest nadal dostępny w wewnętrznych węzłach, to jest cały cel context. W ten sposób nie musisz przekazywać go między komponentami, jak w przypadku rekwizytów. 2. Jest to tylko preferencja stylistyczna i nie uzasadnia debaty w tym przypadku.
Ben Bud,
7
Pojawia się następujący błąd: „this.wrapperRef.contains nie jest funkcją”
Bogdan
5
@Bogdan, otrzymywałem ten sam błąd podczas używania komponentu w stylu. Rozważ użycie <div> na najwyższym poziomie
user3040068
149

Oto rozwiązanie, które najlepiej dla mnie działało bez dołączania zdarzeń do kontenera:

Niektóre elementy HTML mogą mieć tak zwane „ fokus ”, na przykład elementy wejściowe. Elementy te będą również reagować na zdarzenie rozmycia , gdy stracą koncentrację.

Aby dowolny element mógł się skupić, upewnij się, że jego atrybut tabindex jest ustawiony na wartość inną niż -1. W zwykłym HTML byłoby to przez ustawienie tabindexatrybutu, ale w React musisz użyć tabIndex(zwróć uwagę na kapitał I).

Możesz to również zrobić za pomocą JavaScript za pomocą element.setAttribute('tabindex',0)

Do tego właśnie go użyłem, aby stworzyć niestandardowe menu DropDown.

var DropDownMenu = React.createClass({
    getInitialState: function(){
        return {
            expanded: false
        }
    },
    expand: function(){
        this.setState({expanded: true});
    },
    collapse: function(){
        this.setState({expanded: false});
    },
    render: function(){
        if(this.state.expanded){
            var dropdown = ...; //the dropdown content
        } else {
            var dropdown = undefined;
        }

        return (
            <div className="dropDownMenu" tabIndex="0" onBlur={ this.collapse } >
                <div className="currentValue" onClick={this.expand}>
                    {this.props.displayValue}
                </div>
                {dropdown}
            </div>
        );
    }
});
Pablo Barría Urenda
źródło
10
Czy nie spowodowałoby to problemów z dostępnością? Ponieważ tworzysz dodatkowy element, na którym można ustawić ostrość tylko po to, aby wykryć, kiedy to jest fokus wejścia / wyjścia, ale interakcja powinna dotyczyć rozwijanych wartości nie?
Thijs Koerselman,
2
To bardzo interesujące podejście. Działało idealnie w mojej sytuacji, ale chciałbym dowiedzieć się, jak to może wpłynąć na dostępność.
klinore
17
Mam to „działające” do pewnego stopnia, jest to fajny mały hack. Moim problemem jest to, że moja rozwijana zawartość jest display:absolutetaka, że ​​rozwijana lista nie wpłynie na rozmiar div rodzica. Oznacza to, że kiedy kliknę element w menu rozwijanym, uruchomi się onblur.
Dave
2
Miałem problemy z tym podejściem, żaden element w rozwijanej zawartości nie uruchamia zdarzeń takich jak onClick.
AnimaSola,
8
Możesz napotkać inne problemy, jeśli rozwijana zawartość zawiera pewne elementy, na które można się skupić, na przykład dane wejściowe formularza. Ukradną twoją koncentrację, onBlur zostaną uruchomione na twoim rozwijanym pojemniku i expanded będą ustawione na false.
Kamagatos
106

Utknąłem w tej samej sprawie. Jestem tu trochę spóźniony na imprezę, ale dla mnie to naprawdę dobre rozwiązanie. Mam nadzieję, że będzie to pomocne dla kogoś innego. Musisz zaimportować findDOMNodezreact-dom

import ReactDOM from 'react-dom';
// ... ✂

componentDidMount() {
    document.addEventListener('click', this.handleClickOutside, true);
}

componentWillUnmount() {
    document.removeEventListener('click', this.handleClickOutside, true);
}

handleClickOutside = event => {
    const domNode = ReactDOM.findDOMNode(this);

    if (!domNode || !domNode.contains(event.target)) {
        this.setState({
            visible: false
        });
    }
}

Podejście do reakcji na haki (16,8 +)

Możesz utworzyć hak wielokrotnego użytku o nazwie useComponentVisible.

import { useState, useEffect, useRef } from 'react';

export default function useComponentVisible(initialIsVisible) {
    const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible);
    const ref = useRef(null);

    const handleClickOutside = (event) => {
        if (ref.current && !ref.current.contains(event.target)) {
            setIsComponentVisible(false);
        }
    };

    useEffect(() => {
        document.addEventListener('click', handleClickOutside, true);
        return () => {
            document.removeEventListener('click', handleClickOutside, true);
        };
    });

    return { ref, isComponentVisible, setIsComponentVisible };
}

Następnie w komponencie chcesz dodać funkcjonalność, aby wykonać następujące czynności:

const DropDown = () => {
    const { ref, isComponentVisible } = useComponentVisible(true);
    return (
       <div ref={ref}>
          {isComponentVisible && (<p>Dropdown Component</p>)}
       </div>
    );

}

Znajdź przykład codeandbox tutaj.

Paul Fitzgerald
źródło
1
@LeeHanKyeol Nie do końca - ta odpowiedź wywołuje procedury obsługi zdarzeń podczas fazy przechwytywania obsługi zdarzeń, podczas gdy odpowiedź powiązana z wywołuje procedury obsługi zdarzeń podczas fazy bąbelkowej.
stevejay,
3
To powinna być zaakceptowana odpowiedź. Doskonale działał w menu rozwijanych z absolutnym pozycjonowaniem.
zmywarka do naczyń Z
1
ReactDOM.findDOMNodei jest przestarzałe, należy użyć refwywołania zwrotne: github.com/yannickcr/eslint-plugin-react/issues/...
Dain
2
świetne i czyste rozwiązanie
Karim
1
To zadziałało dla mnie, chociaż musiałem zhakować twój komponent, aby zresetować widoczny powrót do „true” po 200 ms (w przeciwnym razie moje menu nigdy więcej się nie pokaże). Dziękuję Ci!
Lee Moe
87

Po wypróbowaniu wielu metod tutaj postanowiłem użyć github.com/Pomax/react-onclickoutside ze względu na jego kompletność.

Zainstalowałem moduł przez npm i zaimportowałem go do mojego komponentu:

import onClickOutside from 'react-onclickoutside'

Następnie w mojej klasie komponentów zdefiniowałem handleClickOutsidemetodę:

handleClickOutside = () => {
  console.log('onClickOutside() method called')
}

A podczas eksportowania mojego komponentu zapakowałem go onClickOutside():

export default onClickOutside(NameOfComponent)

Otóż ​​to.

Beau Smith
źródło
2
Czy są jakieś konkretne zalety tego rozwiązania w stosunku do tabIndex/ onBlurpodejścia zaproponowanego przez Pablo ? Jak działa wdrożenie i czym różni się jego zachowanie od onBlurpodejścia?
Mark Amery
4
Lepiej jest użyć dedykowanego komponentu niż hakowania tabIndex. Poprawić to!
Atul Yadav
5
@MarkAmery - Skomentowałem to tabIndex/onBlurpodejście. Nie działa, gdy menu rozwijane jest position:absolute, na przykład menu najechane na inną treść.
Beau Smith
4
Kolejną zaletą korzystania z tego w porównaniu z tabindex jest to, że rozwiązanie tabindex uruchamia również zdarzenie rozmycia, jeśli skupisz się na elemencie
potomnym
1
@BeauSmith - Dzięki bardzo! Uratowałem mój dzień!
Mika
38

Znalazłem rozwiązanie dzięki Benowi Alpertowi na przedyskutuj.reactjs.org . Sugerowane podejście dołącza program obsługi do dokumentu, ale okazało się to problematyczne. Kliknięcie jednego ze składników w moim drzewie spowodowało ponowne wyrenderowanie, które usunęło kliknięty element podczas aktualizacji. Ponieważ ponowne wysyłanie z React zdarza się wcześniej obsługi treści dokumentu, element nie został wykryty jako „wewnątrz” drzewa.

Rozwiązaniem tego było dodanie modułu obsługi do elementu głównego aplikacji.

Główny:

window.__myapp_container = document.getElementById('app')
React.render(<App/>, window.__myapp_container)

składnik:

import { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';

export default class ClickListener extends Component {

  static propTypes = {
    children: PropTypes.node.isRequired,
    onClickOutside: PropTypes.func.isRequired
  }

  componentDidMount () {
    window.__myapp_container.addEventListener('click', this.handleDocumentClick)
  }

  componentWillUnmount () {
    window.__myapp_container.removeEventListener('click', this.handleDocumentClick)
  }

  /* using fat arrow to bind to instance */
  handleDocumentClick = (evt) => {
    const area = ReactDOM.findDOMNode(this.refs.area);

    if (!area.contains(evt.target)) {
      this.props.onClickOutside(evt)
    }
  }

  render () {
    return (
      <div ref='area'>
       {this.props.children}
      </div>
    )
  }
}
Thijs Koerselman
źródło
8
To nie działa już dla mnie z React 0.14.7 - może React coś zmienił, a może popełniłem błąd podczas dostosowywania kodu do wszystkich zmian w React. Zamiast tego używam github.com/Pomax/react-onclickoutside, który działa jak urok.
Nicole,
1
Hmm Nie widzę żadnego powodu, dla którego miałoby to działać. Dlaczego program obsługi w głównym węźle DOM aplikacji powinien gwarantować uruchomienie przed ponownym wyzwoleniem wywołanym przez inny moduł obsługi, jeśli documentnie ma go w programie?
Mark Amery
1
Czysty punkt UX: mousedownprawdopodobnie byłby lepszym programem obsługi niż clicktutaj. W większości aplikacji zachowanie zamykania menu poprzez kliknięcie na zewnątrz występuje w momencie, gdy najedziesz myszką, a nie w momencie zwolnienia. Wypróbuj na przykład flagę przepełnienia stosu lub dialogi udostępniania lub jedno z menu rozwijanych z górnego paska menu przeglądarki.
Mark Amery
ReactDOM.findDOMNodei łańcuch refsą przestarzałe, należy użyć refwywołania zwrotne: github.com/yannickcr/eslint-plugin-react/issues/...
Dain
jest to najprostsze rozwiązanie i działa dla mnie idealnie, nawet gdy document
dołączam
37

Wdrożenie haczyka na podstawie doskonałej rozmowy Tannera Linsleya na JSConf Hawaii 2020 :

useOuterClick Interfejs API hooka

const Client = () => {
  const innerRef = useOuterClick(ev => { /* what you want do on outer click */ });
  return <div ref={innerRef}> Inside </div> 
};

Realizacja

/*
  Custom Hook
*/
function useOuterClick(callback) {
  const innerRef = useRef();
  const callbackRef = useRef();

  // set current callback in ref, before second useEffect uses it
  useEffect(() => { // useEffect wrapper to be safe for concurrent mode
    callbackRef.current = callback;
  });

  useEffect(() => {
    document.addEventListener("click", handleClick);
    return () => document.removeEventListener("click", handleClick);

    // read most recent callback and innerRef dom node from refs
    function handleClick(e) {
      if (
        innerRef.current && 
        callbackRef.current &&
        !innerRef.current.contains(e.target)
      ) {
        callbackRef.current(e);
      }
    }
  }, []); // no need for callback + innerRef dep
  
  return innerRef; // return ref; client can omit `useRef`
}

/*
  Usage 
*/
const Client = () => {
  const [counter, setCounter] = useState(0);
  const innerRef = useOuterClick(e => {
    // counter state is up-to-date, when handler is called
    alert(`Clicked outside! Increment counter to ${counter + 1}`);
    setCounter(c => c + 1);
  });
  return (
    <div>
      <p>Click outside!</p>
      <div id="container" ref={innerRef}>
        Inside, counter: {counter}
      </div>
    </div>
  );
};

ReactDOM.render(<Client />, document.getElementById("root"));
#container { border: 1px solid red; padding: 20px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js" integrity="sha256-Ef0vObdWpkMAnxp39TYSLVS/vVUokDE8CDFnx7tjY6U=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js" integrity="sha256-p2yuFdE8hNZsQ31Qk+s8N+Me2fL5cc6NKXOC0U9uGww=" crossorigin="anonymous"></script>
<script> var {useRef, useEffect, useCallback, useState} = React</script>
<div id="root"></div>

useOuterClickwykorzystuje zmienne referencje, aby stworzyć lean API dla Client. Oddzwonienie można ustawić bez konieczności zapamiętywania go przez useCallback. Ciało zwrotne nadal ma dostęp do najnowszych rekwizytów i stanu (brak nieaktualnych wartości zamknięcia).


Uwaga dla użytkowników iOS

iOS traktuje tylko niektóre elementy jako klikalne. Aby obejść to zachowanie, wybierz inny odbiornik kliknięć zewnętrznych niż document- w górę nic body. Na przykład możesz dodać detektor do katalogu głównego React divw powyższym przykładzie i zwiększyć jego wysokość ( height: 100vhlub podobny), aby złapać wszystkie kliknięcia zewnętrzne. Źródła: quirksmode.org i ta odpowiedź

ford04
źródło
nie działa na urządzeniach mobilnych, więc niechlujny
Omar
@Omar właśnie przetestował Codesandbox w moim poście z Firefoksem 67.0.3 na urządzeniu z Androidem - działa dobrze. Czy mógłbyś opracować używaną przeglądarkę, jaki jest błąd itp.?
for0404
Brak błędów, ale nie działa na Chrome, iPhone 7+. Nie wykrywa kranów na zewnątrz. W narzędziach Chrome dla urządzeń mobilnych działa, ale w prawdziwym urządzeniu iOS nie działa.
Omar
1
@ ford04 dziękuję za wykopanie tego, natknąłem się na inny wątek, który sugerował następującą sztuczkę. @media (hover: none) and (pointer: coarse) { body { cursor:pointer } }Wydaje mi się, że dodanie tego do moich globalnych stylów rozwiązało problem.
Kostas Sarantopoulos
1
@ ford04 tak, btw według tego wątku istnieje jeszcze bardziej szczegółowe zapytanie medialne, @supports (-webkit-overflow-scrolling: touch)które dotyczy tylko IOS, chociaż nie wiem więcej, ile! Właśnie to przetestowałem i działa.
Kostas Sarantopoulos
30

Żadna z pozostałych odpowiedzi tutaj nie działała dla mnie. Próbowałem ukryć wyskakujące okienko przy rozmyciu, ale ponieważ zawartość była absolutnie ustawiona, onBlur strzelał nawet po kliknięciu zawartości wewnętrznej.

Oto podejście, które zadziałało dla mnie:

// Inside the component:
onBlur(event) {
    // currentTarget refers to this component.
    // relatedTarget refers to the element where the user clicked (or focused) which
    // triggered this event.
    // So in effect, this condition checks if the user clicked outside the component.
    if (!event.currentTarget.contains(event.relatedTarget)) {
        // do your thing.
    }
},

Mam nadzieję że to pomoże.

Niyaz
źródło
Bardzo dobre! Dzięki. Ale w moim przypadku problemem było złapanie „bieżącego miejsca” z pozycją bezwzględną dla div, więc używam „OnMouseLeave” dla div z wejściem i rozwijanym kalendarzem, aby po prostu wyłączyć wszystkie div, gdy mysz opuszcza div.
Alex
1
Dzięki! Bardzo mi pomóż.
Ângelo Lucas
1
Podoba mi się to lepiej niż te metody, które się do niego wiążą document. Dzięki.
kyw
Aby to zadziałało, musisz się upewnić, że klikalny element (relatedTarget) jest aktywny. Zobacz stackoverflow.com/questions/42764494/…
xabitrigo
21

[Aktualizacja] Rozwiązanie z React ^ 16,8 za pomocą haków

CodeSandbox

import React, { useEffect, useRef, useState } from 'react';

const SampleComponent = () => {
    const [clickedOutside, setClickedOutside] = useState(false);
    const myRef = useRef();

    const handleClickOutside = e => {
        if (!myRef.current.contains(e.target)) {
            setClickedOutside(true);
        }
    };

    const handleClickInside = () => setClickedOutside(false);

    useEffect(() => {
        document.addEventListener('mousedown', handleClickOutside);
        return () => document.removeEventListener('mousedown', handleClickOutside);
    });

    return (
        <button ref={myRef} onClick={handleClickInside}>
            {clickedOutside ? 'Bye!' : 'Hello!'}
        </button>
    );
};

export default SampleComponent;

Rozwiązanie z React ^ 16,3 :

CodeSandbox

import React, { Component } from "react";

class SampleComponent extends Component {
  state = {
    clickedOutside: false
  };

  componentDidMount() {
    document.addEventListener("mousedown", this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClickOutside);
  }

  myRef = React.createRef();

  handleClickOutside = e => {
    if (!this.myRef.current.contains(e.target)) {
      this.setState({ clickedOutside: true });
    }
  };

  handleClickInside = () => this.setState({ clickedOutside: false });

  render() {
    return (
      <button ref={this.myRef} onClick={this.handleClickInside}>
        {this.state.clickedOutside ? "Bye!" : "Hello!"}
      </button>
    );
  }
}

export default SampleComponent;
onoya
źródło
3
To jest teraz najważniejsze rozwiązanie. Jeśli pojawia się błąd .contains is not a function, przyczyną może być przekazanie rekwizytu referencyjnego do komponentu niestandardowego, a nie rzeczywistego elementu DOM, takiego jak<div>
willlma
Dla tych, którzy próbują przekazać refrekwizyt do niestandardowego komponentu, możesz React.forwardRef
rzucić
4

Oto moje podejście (demo - https://jsfiddle.net/agymay93/4/ ):

Stworzyłem specjalny komponent o nazwie WatchClickOutsidei można go używać w następujący sposób (zakładam JSXskładnię):

<WatchClickOutside onClickOutside={this.handleClose}>
  <SomeDropdownEtc>
</WatchClickOutside>

Oto kod WatchClickOutsidekomponentu:

import React, { Component } from 'react';

export default class WatchClickOutside extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  componentWillMount() {
    document.body.addEventListener('click', this.handleClick);
  }

  componentWillUnmount() {
    // remember to remove all events to avoid memory leaks
    document.body.removeEventListener('click', this.handleClick);
  }

  handleClick(event) {
    const {container} = this.refs; // get container that we'll wait to be clicked outside
    const {onClickOutside} = this.props; // get click outside callback
    const {target} = event; // get direct click event target

    // if there is no proper callback - no point of checking
    if (typeof onClickOutside !== 'function') {
      return;
    }

    // if target is container - container was not clicked outside
    // if container contains clicked target - click was not outside of it
    if (target !== container && !container.contains(target)) {
      onClickOutside(event); // clicked outside - fire callback
    }
  }

  render() {
    return (
      <div ref="container">
        {this.props.children}
      </div>
    );
  }
}
pie6k
źródło
Świetne rozwiązanie - na mój użytek zmieniłem, aby słuchać dokumentu zamiast treści w przypadku stron z krótką zawartością, i zmieniłem ref z łańcucha na referencję do domu: jsfiddle.net/agymay93/9
Billy Moon
i użył span zamiast div, ponieważ jest mniej prawdopodobne, że zmieni układ i dodał demo obsługi kliknięć poza ciałem: jsfiddle.net/agymay93/10
Billy Moon
String refjest przestarzała, należy użyć refwywołania zwrotne: reactjs.org/docs/refs-and-the-dom.html
Dain
4

To już ma wiele odpowiedzi, ale nie dotyczą one i nie e.stopPropagation()zapobiegają klikaniu linków reagujących poza elementem, który chcesz zamknąć.

Ze względu na fakt, że React ma własną procedurę obsługi sztucznych zdarzeń, nie można używać dokumentu jako podstawy do nasłuchiwania zdarzeń. Musisz to zrobić e.stopPropagation()wcześniej, ponieważ React używa samego dokumentu. Jeśli użyjesz na przykład document.querySelector('body')zamiast. Jesteś w stanie zapobiec kliknięciu linku React. Poniżej znajduje się przykład tego, jak wdrażam kliknięcie na zewnątrz i zamknięcie.
Używa ES6 i React 16.3 .

import React, { Component } from 'react';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      isOpen: false,
    };

    this.insideContainer = React.createRef();
  }

  componentWillMount() {
    document.querySelector('body').addEventListener("click", this.handleClick, false);
  }

  componentWillUnmount() {
    document.querySelector('body').removeEventListener("click", this.handleClick, false);
  }

  handleClick(e) {
    /* Check that we've clicked outside of the container and that it is open */
    if (!this.insideContainer.current.contains(e.target) && this.state.isOpen === true) {
      e.preventDefault();
      e.stopPropagation();
      this.setState({
        isOpen: false,
      })
    }
  };

  togggleOpenHandler(e) {
    e.preventDefault();

    this.setState({
      isOpen: !this.state.isOpen,
    })
  }

  render(){
    return(
      <div>
        <span ref={this.insideContainer}>
          <a href="#open-container" onClick={(e) => this.togggleOpenHandler(e)}>Open me</a>
        </span>
        <a href="/" onClick({/* clickHandler */})>
          Will not trigger a click when inside is open.
        </a>
      </div>
    );
  }
}

export default App;
Richard Herries
źródło
3

Dla tych, którzy potrzebują bezwzględnego pozycjonowania, prostą opcją, którą wybrałem, jest dodanie elementu opakowania, który jest zaprojektowany tak, aby pokrył całą stronę przezroczystym tłem. Następnie możesz dodać onClick na tym elemencie, aby zamknąć wewnętrzny komponent.

<div style={{
        position: 'fixed',
        top: '0', right: '0', bottom: '0', left: '0',
        zIndex: '1000',
      }} onClick={() => handleOutsideClick()} >
    <Content style={{position: 'absolute'}}/>
</div>

Tak jak jest teraz, jeśli dodasz moduł obsługi kliknięć w treści, zdarzenie zostanie również propagowane do górnego div, a zatem wyzwoli moduł handlerOutsideClick. Jeśli nie jest to pożądane zachowanie, po prostu zatrzymaj postęp zdarzenia w module obsługi.

<Content style={{position: 'absolute'}} onClick={e => {
                                          e.stopPropagation();
                                          desiredFunctionCall();
                                        }}/>

`

Anthony Garant
źródło
Problem z tym podejściem polega na tym, że nie można klikać Treści - zamiast tego div otrzyma kliknięcie.
rbonick
Ponieważ zawartość znajduje się w div, jeśli nie zatrzymasz propagacji zdarzeń, oba otrzymają kliknięcie. Jest to często pożądane zachowanie, ale jeśli nie chcesz, aby kliknięcie było propagowane do div, po prostu zatrzymaj eventPropagation w treści w module obsługiClick. Zaktualizowałem swoją odpowiedź, aby pokazać, jak to zrobić.
Anthony Garant
3

Rozszerzyć na zaakceptowaną odpowiedź udzieloną przez Bena Buda , jeśli używasz stylizowanych komponentów, przekazanie referencji w ten sposób spowoduje błąd, taki jak „this.wrapperRef.contains nie jest funkcją”.

Sugerowana poprawka w komentarzach, aby owinąć stylizowany komponent div i przekazać tam referencję, działa. Powiedziawszy to, w swoich dokumentach już wyjaśniają powód tego i właściwe użycie referencji w stylizowanych komponentach:

Przekazanie rekwizytu referencyjnego do komponentu w stylu da ci instancję opakowania StyledComponent, ale nie do bazowego węzła DOM. Wynika to z działania refs. Nie można wywoływać metod DOM, takich jak focus, bezpośrednio na naszych opakowaniach. Aby uzyskać odwołanie do rzeczywistego, owiniętego węzła DOM, przekaż wywołanie zwrotne do propa innerRef.

Tak jak:

<StyledDiv innerRef={el => { this.el = el }} />

Następnie możesz uzyskać do niego bezpośredni dostęp w ramach funkcji „handleClickOutside”:

handleClickOutside = e => {
    if (this.el && !this.el.contains(e.target)) {
        console.log('clicked outside')
    }
}

Dotyczy to również podejścia „onBlur”:

componentDidMount(){
    this.el.focus()
}
blurHandler = () => {
    console.log('clicked outside')
}
render(){
    return(
        <StyledDiv
            onBlur={this.blurHandler}
            tabIndex="0"
            innerRef={el => { this.el = el }}
        />
    )
}
Płaskowód
źródło
2

Moim największym zmartwieniem dla wszystkich innych odpowiedzi jest filtrowanie zdarzeń kliknięcia od katalogu głównego / rodzica w dół. Odkryłem, że najprostszym sposobem było po prostu ustawienie elementu rodzeństwa z pozycją: naprawiony, indeks Z 1 za listą rozwijaną i obsłużenie zdarzenia kliknięcia na stałym elemencie w tym samym komponencie. Utrzymuje wszystko scentralizowane w danym komponencie.

Przykładowy kod

#HTML
<div className="parent">
  <div className={`dropdown ${this.state.open ? open : ''}`}>
    ...content
  </div>
  <div className="outer-handler" onClick={() => this.setState({open: false})}>
  </div>
</div>

#SASS
.dropdown {
  display: none;
  position: absolute;
  top: 0px;
  left: 0px;
  z-index: 100;
  &.open {
    display: block;
  }
}
.outer-handler {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    opacity: 0;
    z-index: 99;
    display: none;
    &.open {
      display: block;
    }
}
metroninja
źródło
1
Hmm fajne. Czy działałoby to z wieloma instancjami? Czy może każdy stały element potencjalnie blokowałby kliknięcie dla innych?
Thijs Koerselman
2
componentWillMount(){

  document.addEventListener('mousedown', this.handleClickOutside)
}

handleClickOutside(event) {

  if(event.path[0].id !== 'your-button'){
     this.setState({showWhatever: false})
  }
}

Zdarzenie path[0]to ostatni kliknięty element

Ivan Sanchez
źródło
3
Upewnij się, że usunąłeś detektor zdarzeń, gdy składnik odmontowuje się, aby zapobiec problemom z zarządzaniem pamięcią itp.
agm1984
2

Korzystałem z tego modułu (nie mam skojarzeń z autorem)

npm install react-onclickout --save

const ClickOutHandler = require('react-onclickout');
 
class ExampleComponent extends React.Component {
 
  onClickOut(e) {
    if (hasClass(e.target, 'ignore-me')) return;
    alert('user clicked outside of the component!');
  }
 
  render() {
    return (
      <ClickOutHandler onClickOut={this.onClickOut}>
        <div>Click outside of me!</div>
      </ClickOutHandler>
    );
  }
}

Dobrze się spisał.

ErichBSchulz
źródło
Działa to fantastycznie! O wiele prostsze niż wiele innych odpowiedzi tutaj i w przeciwieństwie do co najmniej 3 innych rozwiązań tutaj, gdzie dla wszystkich z nich otrzymałem "Support for the experimental syntax 'classProperties' isn't currently enabled"to po prostu wyszło z pudełka. Każdemu, kto wypróbuje to rozwiązanie, pamiętaj o usunięciu kodu, który mógł zostać skopiowany podczas wypróbowywania innych rozwiązań. np. dodanie detektorów zdarzeń wydaje się kolidować z tym.
Frikster
2

Zrobiłem to częściowo, śledząc to i postępując zgodnie z oficjalnymi dokumentami React dotyczącymi obsługi referencji, które wymagają reakcji ^ 16.3. To jedyna rzecz, która zadziałała dla mnie po wypróbowaniu innych sugestii tutaj ...

class App extends Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  componentWillMount() {
    document.addEventListener("mousedown", this.handleClick, false);
  }
  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClick, false);
  }
  handleClick = e => {
    if (this.inputRef.current === e.target) {
      return;
    }
    this.handleclickOutside();
  };
handleClickOutside(){
...***code to handle what to do when clicked outside***...
}
render(){
return(
<div>
...***code for what's outside***...
<span ref={this.inputRef}>
...***code for what's "inside"***...
</span>
...***code for what's outside***
)}}
RawCode
źródło
1

Przykład ze strategią

Podobają mi się dostarczone rozwiązania, które wykorzystują to samo, tworząc opakowanie wokół komponentu.

Ponieważ jest to bardziej zachowanie, pomyślałem o strategii i wymyśliłem następujące.

Jestem nowy z React i potrzebuję trochę pomocy, aby zaoszczędzić trochę płyty grzewczej w przypadkach użycia

Przejrzyj i powiedz mi, co myślisz.

ClickOutsideBehavior

import ReactDOM from 'react-dom';

export default class ClickOutsideBehavior {

  constructor({component, appContainer, onClickOutside}) {

    // Can I extend the passed component's lifecycle events from here?
    this.component = component;
    this.appContainer = appContainer;
    this.onClickOutside = onClickOutside;
  }

  enable() {

    this.appContainer.addEventListener('click', this.handleDocumentClick);
  }

  disable() {

    this.appContainer.removeEventListener('click', this.handleDocumentClick);
  }

  handleDocumentClick = (event) => {

    const area = ReactDOM.findDOMNode(this.component);

    if (!area.contains(event.target)) {
        this.onClickOutside(event)
    }
  }
}

Przykładowe użycie

import React, {Component} from 'react';
import {APP_CONTAINER} from '../const';
import ClickOutsideBehavior from '../ClickOutsideBehavior';

export default class AddCardControl extends Component {

  constructor() {
    super();

    this.state = {
      toggledOn: false,
      text: ''
    };

    this.clickOutsideStrategy = new ClickOutsideBehavior({
      component: this,
      appContainer: APP_CONTAINER,
      onClickOutside: () => this.toggleState(false)
    });
  }

  componentDidMount () {

    this.setState({toggledOn: !!this.props.toggledOn});
    this.clickOutsideStrategy.enable();
  }

  componentWillUnmount () {
    this.clickOutsideStrategy.disable();
  }

  toggleState(isOn) {

    this.setState({toggledOn: isOn});
  }

  render() {...}
}

Notatki

Pomyślałem o przechowywaniu componentprzechwyconych haków cyklu życia i zastąpiłem je metodami podobnymi do tego:

const baseDidMount = component.componentDidMount;

component.componentDidMount = () => {
  this.enable();
  baseDidMount.call(component)
}

componentjest komponentem przekazanym do konstruktora ClickOutsideBehavior.
Spowoduje to usunięcie włączania / wyłączania elementu kotłowego od użytkownika tego zachowania, ale nie wygląda to zbyt ładnie

kidroca
źródło
1

W moim przypadku DROPDOWN rozwiązanie Bena Buda działało dobrze, ale miałem osobny przycisk przełączania z funkcją obsługi onClick. Tak więc logika klikania na zewnątrz była w konflikcie z przyciskiem na przełączniku Kliknij. Oto jak to rozwiązałem, przekazując również odnośnik przycisku:

import React, { useRef, useEffect, useState } from "react";

/**
 * Hook that triggers onClose when clicked outside of ref and buttonRef elements
 */
function useOutsideClicker(ref, buttonRef, onOutsideClick) {
  useEffect(() => {

    function handleClickOutside(event) {
      /* clicked on the element itself */
      if (ref.current && !ref.current.contains(event.target)) {
        return;
      }

      /* clicked on the toggle button */
      if (buttonRef.current && !buttonRef.current.contains(event.target)) {
        return;
      }

      /* If it's something else, trigger onClose */
      onOutsideClick();
    }

    // Bind the event listener
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      // Unbind the event listener on clean up
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [ref]);
}

/**
 * Component that alerts if you click outside of it
 */
export default function DropdownMenu(props) {
  const wrapperRef = useRef(null);
  const buttonRef = useRef(null);
  const [dropdownVisible, setDropdownVisible] = useState(false);

  useOutsideClicker(wrapperRef, buttonRef, closeDropdown);

  const toggleDropdown = () => setDropdownVisible(visible => !visible);

  const closeDropdown = () => setDropdownVisible(false);

  return (
    <div>
      <button onClick={toggleDropdown} ref={buttonRef}>Dropdown Toggler</button>
      {dropdownVisible && <div ref={wrapperRef}>{props.children}</div>}
    </div>
  );
}
Ilyas Assainov
źródło
0

Jeśli chcesz użyć małego komponentu (466 bajtów spakowanych gzip), który już istnieje dla tej funkcjonalności, możesz sprawdzić tę bibliotekę - reakcja-kliknięcie .

Zaletą biblioteki jest to, że pozwala ona również wykrywać kliknięcia poza komponentem i wewnątrz innego. Obsługuje również wykrywanie innych rodzajów zdarzeń.

Tushar Sharma
źródło
0

Jeśli chcesz użyć małego komponentu (466 bajtów spakowanych gzipem), który już istnieje dla tej funkcjonalności, możesz sprawdzić bibliotekę reagującą outclick . Przechwytuje zdarzenia poza komponentem reagującym lub elementem jsx.

Zaletą biblioteki jest to, że pozwala ona również wykrywać kliknięcia poza komponentem i wewnątrz innego. Obsługuje również wykrywanie innych rodzajów zdarzeń.

Tushar Sharma
źródło
0

Wiem, że to stare pytanie, ale ciągle się z tym spotykam i miałem problemy z rozgryzieniem tego w prostym formacie. Więc jeśli to ułatwiłoby komuś życie, użyj OutsideClickHandler przez airbnb. Jest to najprostsza wtyczka do wykonania tego zadania bez pisania własnego kodu.

Przykład:

hideresults(){
   this.setState({show:false})
}
render(){
 return(
 <div><div onClick={() => this.setState({show:true})}>SHOW</div> {(this.state.show)? <OutsideClickHandler onOutsideClick={() => 
  {this.hideresults()}} > <div className="insideclick"></div> </OutsideClickHandler> :null}</div>
 )
}
Sam Nikaein
źródło
0
import { useClickAway } from "react-use";

useClickAway(ref, () => console.log('OUTSIDE CLICKED'));
Melounek
źródło
0

możesz w prosty sposób rozwiązać problem, pokażę ci:

const [dropDwonStatus , setDropDownStatus] = useState(false)

const openCloseDropDown = () =>{
 setDropDownStatus(prev => !prev)
}

const closeDropDown = ()=> {
 if(dropDwonStatus){
   setDropDownStatus(false)
 }
}
.
.
.
<parent onClick={closeDropDown}>
 <child onClick={openCloseDropDown} />
</parent>

to działa dla mnie, powodzenia;)

Moshtaba Darzi
źródło
-1

Znalazłem to w poniższym artykule:

render () {return ({this.node = node;}}> Przełącz Popover {this.state.popupVisible && (Jestem popover!)}); }}

Oto świetny artykuł na ten temat: „Obsługa kliknięć poza komponentami React” https://larsgraubner.com/handle-outside-clicks-react/

Amanda Koster
źródło
Witamy w Stack Overflow! Odpowiedzi, które są po prostu linkiem, są zasadniczo odrzucone : meta.stackoverflow.com/questions/251006/… . W przyszłości dołącz cytat lub przykład kodu wraz z informacjami dotyczącymi pytania. Twoje zdrowie!
Fred Stark,
-1

Dodaj onClickmoduł obsługi do kontenera najwyższego poziomu i zwiększaj wartość stanu za każdym razem, gdy użytkownik kliknie. Przekaż tę wartość do odpowiedniego komponentu i za każdym razem, gdy zmieni się wartość, możesz wykonać pracę.

W tym przypadku dzwonimy this.closeDropdown()za każdym razem, gdy clickCountzmienia się wartość.

Te incrementClickCountpożary metoda wewnątrz .apppojemnika, ale nie .dropdowndlatego, że używamyevent.stopPropagation() , aby zapobiec pęcherzyków zdarzeń.

Twój kod może wyglądać mniej więcej tak:

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            clickCount: 0
        };
    }
    incrementClickCount = () => {
        this.setState({
            clickCount: this.state.clickCount + 1
        });
    }
    render() {
        return (
            <div className="app" onClick={this.incrementClickCount}>
                <Dropdown clickCount={this.state.clickCount}/>
            </div>
        );
    }
}
class Dropdown extends Component {
    constructor(props) {
        super(props);
        this.state = {
            open: false
        };
    }
    componentDidUpdate(prevProps) {
        if (this.props.clickCount !== prevProps.clickCount) {
            this.closeDropdown();
        }
    }
    toggleDropdown = event => {
        event.stopPropagation();
        return (this.state.open) ? this.closeDropdown() : this.openDropdown();
    }
    render() {
        return (
            <div className="dropdown" onClick={this.toggleDropdown}>
                ...
            </div>
        );
    }
}
wdm
źródło
Nie jestem pewien, kto przegłosowuje, ale to rozwiązanie jest dość czyste w porównaniu do detektorów zdarzeń wiązania / rozłączania. Nie miałem problemów z tą implementacją.
wdm
-1

Aby rozwiązanie „fokusowe” działało na liście rozwijanej za pomocą detektorów zdarzeń, możesz dodać je za pomocą zdarzenia onMouseDown zamiast onClick . W ten sposób zdarzenie zostanie uruchomione, a następnie wyskakujące okienko zostanie zamknięte w następujący sposób:

<TogglePopupButton
                    onClick = { this.toggleDropup }
                    tabIndex = '0'
                    onBlur = { this.closeDropup }
                />
                { this.state.isOpenedDropup &&
                <ul className = { dropupList }>
                    { this.props.listItems.map((item, i) => (
                        <li
                            key = { i }
                            onMouseDown = { item.eventHandler }
                        >
                            { item.itemName}
                        </li>
                    ))}
                </ul>
                }
idtmc
źródło
-1
import ReactDOM from 'react-dom' ;

class SomeComponent {

  constructor(props) {
    // First, add this to your constructor
    this.handleClickOutside = this.handleClickOutside.bind(this);
  }

  componentWillMount() {
    document.addEventListener('mousedown', this.handleClickOutside, false); 
  }

  // Unbind event on unmount to prevent leaks
  componentWillUnmount() {
    window.removeEventListener('mousedown', this.handleClickOutside, false);
  }

  handleClickOutside(event) {
    if(!ReactDOM.findDOMNode(this).contains(event.path[0])){
       console.log("OUTSIDE");
    }
  }
}
moment dipolowy
źródło
I nie zapomnijimport ReactDOM from 'react-dom';
dipole_moment
-1

Zrobiłem rozwiązanie na każdą okazję.

Powinieneś użyć komponentu High Order, aby owinąć komponent, którego chcesz słuchać poza kliknięciami.

Ten przykład komponentu ma tylko jedną prop: „onClickedOutside”, która odbiera funkcję.

ClickedOutside.js
import React, { Component } from "react";

export default class ClickedOutside extends Component {
  componentDidMount() {
    document.addEventListener("mousedown", this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClickOutside);
  }

  handleClickOutside = event => {
    // IF exists the Ref of the wrapped component AND his dom children doesnt have the clicked component 
    if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
      // A props callback for the ClikedClickedOutside
      this.props.onClickedOutside();
    }
  };

  render() {
    // In this piece of code I'm trying to get to the first not functional component
    // Because it wouldn't work if use a functional component (like <Fade/> from react-reveal)
    let firstNotFunctionalComponent = this.props.children;
    while (typeof firstNotFunctionalComponent.type === "function") {
      firstNotFunctionalComponent = firstNotFunctionalComponent.props.children;
    }

    // Here I'm cloning the element because I have to pass a new prop, the "reference" 
    const children = React.cloneElement(firstNotFunctionalComponent, {
      ref: node => {
        this.wrapperRef = node;
      },
      // Keeping all the old props with the new element
      ...firstNotFunctionalComponent.props
    });

    return <React.Fragment>{children}</React.Fragment>;
  }
}
Higor Lorenzon
źródło
-1

UseOnClickOutside Hook - React 16,8 +

Utwórz ogólną funkcję useOnOutsideClick

export const useOnOutsideClick = handleOutsideClick => {
  const innerBorderRef = useRef();

  const onClick = event => {
    if (
      innerBorderRef.current &&
      !innerBorderRef.current.contains(event.target)
    ) {
      handleOutsideClick();
    }
  };

  useMountEffect(() => {
    document.addEventListener("click", onClick, true);
    return () => {
      document.removeEventListener("click", onClick, true);
    };
  });

  return { innerBorderRef };
};

const useMountEffect = fun => useEffect(fun, []);

Następnie użyj haka w dowolnym funkcjonalnym elemencie.

const OutsideClickDemo = ({ currentMode, changeContactAppMode }) => {

  const [open, setOpen] = useState(false);
  const { innerBorderRef } = useOnOutsideClick(() => setOpen(false));

  return (
    <div>
      <button onClick={() => setOpen(true)}>open</button>
      {open && (
        <div ref={innerBorderRef}>
           <SomeChild/>
        </div>
      )}
    </div>
  );

};

Link do wersji demo

Częściowo zainspirowany odpowiedzią @ pau1fitzgerald.

Ben Carp
źródło