Jak nasłuchiwać zdarzeń kliknięcia spoza komponentu

89

Chcę zamknąć menu rozwijane, gdy nastąpi kliknięcie poza elementem rozwijanym.

Jak mogę to zrobić?

Allan Hortle
źródło

Odpowiedzi:

59

W elemencie dodałem mousedowni mouseuptak:

onMouseDown={this.props.onMouseDown} onMouseUp={this.props.onMouseUp}

Następnie u rodzica robię to:

componentDidMount: function () {
    window.addEventListener('mousedown', this.pageClick, false);
},

pageClick: function (e) {
  if (this.mouseIsDownOnCalendar) {
      return;
  }

  this.setState({
      showCal: false
  });
},

mouseDownHandler: function () {
    this.mouseIsDownOnCalendar = true;
},

mouseUpHandler: function () {
    this.mouseIsDownOnCalendar = false;
}

Jest showCalto wartość logiczna, która kiedy truepokazuje w moim przypadku kalendarz i falseukrywa go.

Robbert van Elk
źródło
to jednak wiąże kliknięcie konkretnie z myszą. Zdarzenia kliknięcia mogą być generowane przez zdarzenia dotykowe i wprowadzanie kluczy, na które to rozwiązanie nie będzie w stanie zareagować, przez co jest nieodpowiednie dla celów mobilnych i dostępności = (
Mike 'Pomax' Kamermans
@ Mike'Pomax'Kamermans Możesz teraz używać onTouchStart i onTouchEnd dla telefonów komórkowych. facebook.github.io/react/docs/events.html#touch-events
naoufal
3
Te istniały od dawna, ale nie będą dobrze działać preventDefault()z Androidem , ponieważ musisz natychmiast wywoływać zdarzenia lub włącza się natywne zachowanie Androida, z którym przeszkadza przetwarzanie wstępne Reacta. Od tego czasu napisałem npmjs.com/package/react-onclickoutside .
Mike 'Pomax' Kamermans
Kocham to! Pochwalony. Pomocne będzie usunięcie nasłuchiwania wydarzeń po naciśnięciu myszki. componentWillUnmount = () => window.removeEventListener ('mousedown', this.pageClick, false);
Juni Brosas
73

Korzystając z metod cyklu życia, dodaj i usuń detektory zdarzeń w dokumencie.

React.createClass({
    handleClick: function (e) {
        if (this.getDOMNode().contains(e.target)) {
            return;
        }
    },

    componentWillMount: function () {
        document.addEventListener('click', this.handleClick, false);
    },

    componentWillUnmount: function () {
        document.removeEventListener('click', this.handleClick, false);
    }
});

Sprawdź wiersze 48-54 tego komponentu: https://github.com/i-like-robots/react-tube-tracker/blob/91dc0129a1f6077bef57ea4ad9a860be0c600e9d/app/component/tube-tracker.jsx#L48-54

i_like_robots
źródło
Spowoduje to dodanie jednego do dokumentu, ale oznacza to, że wszelkie zdarzenia kliknięcia komponentu również wyzwalają zdarzenie dokumentu. Powoduje to własne problemy, ponieważ celem nasłuchiwania dokumentów jest zawsze jakiś element div znajdujący się nisko na końcu komponentu, a nie sam komponent.
Allan Hortle
Nie jestem do końca pewien, czy podążam za twoim komentarzem, ale jeśli powiążesz detektory zdarzeń z dokumentem, możesz odfiltrować wszelkie zdarzenia kliknięć, które się do niego pojawiają, albo przez dopasowanie selektora (standardowa delegacja zdarzenia), albo przez inne arbitralne wymagania (np. element docelowy nie znajduje się w innym elemencie).
i_like_robots
3
To powoduje problemy, jak wskazuje @AllanHortle. stopPropagation w zdarzeniu react nie uniemożliwi obsłudze zdarzeń dokumentów odebranie zdarzenia.
Matt Crinklaw-Vogt,
4
Dla wszystkich zainteresowanych miałem problemy z stopPropagation podczas korzystania z tego dokumentu. Jeśli dołączysz odbiornik zdarzeń do okna, wydaje się, że rozwiązuje to problem?
Titus
10
Jak wspomniano, this.getDOMNode () jest przestarzałe. użyj ReactDOM zamiast tego w ten sposób: ReactDOM.findDOMNode (this) .contains (e.target)
Arne H. Bitubekk
17

Spójrz na cel zdarzenia, jeśli zdarzenie było bezpośrednio w komponencie lub elementach podrzędnych tego komponentu, to kliknięcie było wewnątrz. W przeciwnym razie był na zewnątrz.

React.createClass({
    clickDocument: function(e) {
        var component = React.findDOMNode(this.refs.component);
        if (e.target == component || $(component).has(e.target).length) {
            // Inside of the component.
        } else {
            // Outside of the component.
        }

    },
    componentDidMount: function() {
        $(document).bind('click', this.clickDocument);
    },
    componentWillUnmount: function() {
        $(document).unbind('click', this.clickDocument);
    },
    render: function() {
        return (
            <div ref='component'>
                ...
            </div> 
        )
    }
});

Jeśli ma być zastosowany w wielu komponentach, to ładniej jest z mieszanką:

var ClickMixin = {
    _clickDocument: function (e) {
        var component = React.findDOMNode(this.refs.component);
        if (e.target == component || $(component).has(e.target).length) {
            this.clickInside(e);
        } else {
            this.clickOutside(e);
        }
    },
    componentDidMount: function () {
        $(document).bind('click', this._clickDocument);
    },
    componentWillUnmount: function () {
        $(document).unbind('click', this._clickDocument);
    },
}

Zobacz przykład tutaj: https://jsfiddle.net/0Lshs7mg/1/

ja
źródło
@ Mike'Pomax'Kamermans, który został naprawiony, myślę, że ta odpowiedź dodaje przydatnych informacji, być może Twój komentarz może zostać teraz usunięty.
ja
cofnąłeś moje zmiany z niewłaściwego powodu. this.refs.componentodnosi się do elementu DOM z 0.14 facebook.github.io/react/blog/2015/07/03/…
Gajus
@GajusKuizinas - dobrze, aby wprowadzić tę zmianę, gdy 0.14 będzie najnowszą wersją (teraz jest beta).
ja
Co to jest dolar?
pronebird
2
Nienawidzę wbijać jQuery w React, ponieważ są to ramy dwóch widoków.
Abdennour TOUMI
11

Dla twojego konkretnego przypadku użycia, obecnie akceptowana odpowiedź jest odrobinę przesadzona. Jeśli chcesz nasłuchiwać, kiedy użytkownik opuszcza listę rozwijaną, po prostu użyj <select>komponentu jako elementu nadrzędnego i dołącz onBlurdo niego procedurę obsługi.

Jedyną wadą tego podejścia jest to, że zakłada ono, że użytkownik zachował już fokus na elemencie i opiera się na kontrolce formularza (która może być tym, czego chcesz, ale nie musi, jeśli weźmiesz pod uwagę, że tabklucz również skupia się i rozmywa elementy ) - ale te wady są tak naprawdę ograniczeniem tylko dla bardziej skomplikowanych przypadków użycia, w którym to przypadku może być konieczne bardziej skomplikowane rozwiązanie.

 var Dropdown = React.createClass({

   handleBlur: function(e) {
     // do something when user clicks outside of this element
   },

   render: function() {
     return (
       <select onBlur={this.handleBlur}>
         ...
       </select>
     );
   }
 });
brzytwa
źródło
10
onBlurdiv działa również z div, jeśli ma tabIndexatrybut
gorpacrate.
Dla mnie było to rozwiązanie, które uznałem za najprostsze i najłatwiejsze w użyciu, używam go na elemencie guzikowym i działa jak urok. Dzięki!
Amir5000
5

Napisałem ogólny program obsługi zdarzeń dla zdarzeń, które pochodzą spoza komponentu, reakcja poza zdarzeniem .

Sama implementacja jest prosta:

  • Gdy komponent jest zamontowany, do windowobiektu dołączana jest procedura obsługi zdarzeń .
  • Gdy wystąpi zdarzenie, komponent sprawdza, czy zdarzenie pochodzi z wnętrza komponentu. Jeśli tak się nie stanie, wyzwala onOutsideEventkomponent docelowy.
  • Gdy komponent jest odmontowany, procedura obsługi zdarzeń jest usuwana.
import React from 'react';
import ReactDOM from 'react-dom';

/**
 * @param {ReactClass} Target The component that defines `onOutsideEvent` handler.
 * @param {String[]} supportedEvents A list of valid DOM event names. Default: ['mousedown'].
 * @return {ReactClass}
 */
export default (Target, supportedEvents = ['mousedown']) => {
    return class ReactOutsideEvent extends React.Component {
        componentDidMount = () => {
            if (!this.refs.target.onOutsideEvent) {
                throw new Error('Component does not defined "onOutsideEvent" method.');
            }

            supportedEvents.forEach((eventName) => {
                window.addEventListener(eventName, this.handleEvent, false);
            });
        };

        componentWillUnmount = () => {
            supportedEvents.forEach((eventName) => {
                window.removeEventListener(eventName, this.handleEvent, false);
            });
        };

        handleEvent = (event) => {
            let target,
                targetElement,
                isInside,
                isOutside;

            target = this.refs.target;
            targetElement = ReactDOM.findDOMNode(target);
            isInside = targetElement.contains(event.target) || targetElement === event.target;
            isOutside = !isInside;



            if (isOutside) {
                target.onOutsideEvent(event);
            }
        };

        render() {
            return <Target ref='target' {... this.props} />;
        }
    }
};

Aby użyć komponentu, musisz zawinąć deklarację klasy komponentu docelowego za pomocą komponentu wyższego rzędu i zdefiniować zdarzenia, które chcesz obsłużyć:

import React from 'react';
import ReactDOM from 'react-dom';
import ReactOutsideEvent from 'react-outside-event';

class Player extends React.Component {
    onOutsideEvent = (event) => {
        if (event.type === 'mousedown') {

        } else if (event.type === 'mouseup') {

        }
    }

    render () {
        return <div>Hello, World!</div>;
    }
}

export default ReactOutsideEvent(Player, ['mousedown', 'mouseup']);
Gajus
źródło
4

Głosowałem na jedną z odpowiedzi, mimo że nie zadziałała. To doprowadziło mnie do tego rozwiązania. Nieznacznie zmieniłem kolejność operacji. Nasłuchuję mouseDown na celu i mouseUp na celu. Jeśli którykolwiek z nich zwraca TRUE, nie zamykamy modalu. Gdy tylko kliknięcie zostanie zarejestrowane, w dowolnym miejscu te dwa parametry logiczne {mouseDownOnModal, mouseUpOnModal} są ustawiane z powrotem na false.

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

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

_handlePageClick(e) {
    var wasDown = this.mouseDownOnModal;
    var wasUp = this.mouseUpOnModal;
    this.mouseDownOnModal = false;
    this.mouseUpOnModal = false;
    if (!wasDown && !wasUp)
        this.close();
},

_handleMouseDown() {
    this.mouseDownOnModal = true;
},

_handleMouseUp() {
    this.mouseUpOnModal = true;
},

render() {
    return (
        <Modal onMouseDown={this._handleMouseDown} >
               onMouseUp={this._handleMouseUp}
            {/* other_content_here */}
        </Modal>
    );
}

Ma to tę zaletę, że cały kod spoczywa na komponencie potomnym, a nie nadrzędnym. Oznacza to, że nie ma standardowego kodu do skopiowania podczas ponownego użycia tego komponentu.

Steve Zelaznik
źródło
3
  1. Utwórz stałą warstwę obejmującą cały ekran ( .backdrop).
  2. Miej element docelowy ( .target) poza .backdropelementem i z większym indeksem stosu ( z-index).

Wtedy każde kliknięcie na .backdropelement będzie traktowane jako „poza .targetelementem”.

.click-overlay {
    position: fixed;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    z-index: 1;
}

.target {
    position: relative;
    z-index: 2;
}
Federico
źródło
2

Możesz użyć refs, aby to osiągnąć, coś takiego powinno działać.

Dodaj refdo swojego elementu:

<div ref={(element) => { this.myElement = element; }}></div>

Następnie możesz dodać funkcję do obsługi kliknięcia poza elementem w następujący sposób:

handleClickOutside(e) {
  if (!this.myElement.contains(e)) {
    this.setState({ myElementVisibility: false });
  }
}

Na koniec dodaj i usuń nasłuchiwania zdarzeń, które zostaną zamontowane i odmontowane.

componentWillMount() {
  document.addEventListener('click', this.handleClickOutside, false);  // assuming that you already did .bind(this) in constructor
}

componentWillUnmount() {
  document.removeEventListener('click', this.handleClickOutside, false);  // assuming that you already did .bind(this) in constructor
}
Chris Borrowdale
źródło
Zmodyfikowany swoją odpowiedź, miał możliwego błędu w odniesieniu do wywoływania handleClickOutsidew document.addEventListener()dodając thisodniesienie. W przeciwnym razie zwraca błąd Uncaught ReferenceError: handleClickOutside nie jest zdefiniowany wcomponentWillMount()
Muhammad Hannan
1

Bardzo późno na imprezę, ale udało mi się ustawić zdarzenie rozmycia w elemencie nadrzędnym listy rozwijanej z powiązanym kodem, aby zamknąć listę rozwijaną, a także dołączyć nasłuchiwanie myszy do elementu nadrzędnego, który sprawdza, czy lista rozwijana jest otwarta lub nie, i zatrzyma propagację zdarzenia, jeśli jest otwarte, aby zdarzenie rozmycia nie zostało uruchomione.

Ponieważ zdarzenie przesunięcia kursora się pojawi, zapobiegnie to zamazaniu myszy u dzieci.

/* Some react component */
...

showFoo = () => this.setState({ showFoo: true });

hideFoo = () => this.setState({ showFoo: false });

clicked = e => {
    if (!this.state.showFoo) {
        this.showFoo();
        return;
    }
    e.preventDefault()
    e.stopPropagation()
}

render() {
    return (
        <div 
            onFocus={this.showFoo}
            onBlur={this.hideFoo}
            onMouseDown={this.clicked}
        >
            {this.state.showFoo ? <FooComponent /> : null}
        </div>
    )
}

...

e.preventDefault () nie powinno być wywoływane tak daleko, jak potrafię, ale Firefox z jakiegoś powodu nie działa dobrze bez niego. Działa w Chrome, Firefox i Safari.

Tanner Stults
źródło
0

Znalazłem prostszy sposób.

Musisz tylko dodać onHide(this.closeFunction)modal

<Modal onHide={this.closeFunction}>
...
</Modal>

Zakładając, że masz funkcję zamykania modalu.

MR Murazza
źródło
-1

Skorzystaj z doskonałej mieszanki reakcyjnej na zewnątrz :

npm install --save react-onclickoutside

I wtedy

var Component = React.createClass({
  mixins: [
    require('react-onclickoutside')
  ],
  handleClickOutside: function(evt) {
    // ...handling code goes here... 
  }
});
e18r
źródło
4
Miksery FYI są teraz przestarzałe w React.
machineghost