feat(EventListener): add `capture` option (#1062)

This commit is contained in:
Oleksandr Fediashov 2019-03-15 15:03:02 +02:00 коммит произвёл GitHub
Родитель d0604bea60
Коммит 872dccfe58
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 80 добавлений и 39 удалений

Просмотреть файл

@ -4,6 +4,9 @@
"version": "0.23.1", "version": "0.23.1",
"author": "Oleksandr Fediashov <a@fedyashov.com>", "author": "Oleksandr Fediashov <a@fedyashov.com>",
"bugs": "https://github.com/stardust-ui/react/issues", "bugs": "https://github.com/stardust-ui/react/issues",
"dependencies": {
"prop-types": "^15.6.1"
},
"files": [ "files": [
"dist" "dist"
], ],

Просмотреть файл

@ -7,18 +7,21 @@ import removeEventListener from './lib/removeEventListener'
class EventListener extends React.PureComponent<EventListenerProps> { class EventListener extends React.PureComponent<EventListenerProps> {
static displayName = 'EventListener' static displayName = 'EventListener'
static propTypes = listenerPropTypes static propTypes = listenerPropTypes
static defaultProps = {
capture: false,
}
componentDidMount() { componentDidMount() {
addEventListener(this.props.targetRef, this.props.type, this.handleEvent) addEventListener(this.handleEvent, this.props as Required<EventListenerProps>)
} }
componentDidUpdate(prevProps: EventListenerProps) { componentDidUpdate(prevProps: EventListenerProps) {
removeEventListener(prevProps.targetRef, prevProps.type, this.handleEvent) removeEventListener(this.handleEvent, prevProps as Required<EventListenerProps>)
addEventListener(this.props.targetRef, this.props.type, this.handleEvent) addEventListener(this.handleEvent, this.props as Required<EventListenerProps>)
} }
componentWillUnmount() { componentWillUnmount() {
removeEventListener(this.props.targetRef, this.props.type, this.handleEvent) removeEventListener(this.handleEvent, this.props as Required<EventListenerProps>)
} }
handleEvent = (e: Event) => this.props.listener(e) handleEvent = (e: Event) => this.props.listener(e)

Просмотреть файл

@ -8,23 +8,26 @@ import * as listenerRegistries from './lib/listenerRegistries'
class StackableEventListener extends React.PureComponent<EventListenerProps> { class StackableEventListener extends React.PureComponent<EventListenerProps> {
static displayName = 'StackableEventListener' static displayName = 'StackableEventListener'
static propTypes = listenerPropTypes static propTypes = listenerPropTypes
static defaultProps = {
capture: false,
}
componentDidMount() { componentDidMount() {
listenerRegistries.add(this.props.type, this.handleEvent) listenerRegistries.add(this.props.type, this.handleEvent)
addEventListener(this.props.targetRef, this.props.type, this.handleEvent) addEventListener(this.handleEvent, this.props as Required<EventListenerProps>)
} }
componentDidUpdate(prevProps: EventListenerProps) { componentDidUpdate(prevProps: EventListenerProps) {
listenerRegistries.remove(this.props.type, this.handleEvent) listenerRegistries.remove(this.props.type, this.handleEvent)
removeEventListener(prevProps.targetRef, prevProps.type, this.handleEvent) removeEventListener(this.handleEvent, prevProps as Required<EventListenerProps>)
listenerRegistries.add(this.props.type, this.handleEvent) listenerRegistries.add(this.props.type, this.handleEvent)
addEventListener(this.props.targetRef, this.props.type, this.handleEvent) addEventListener(this.handleEvent, this.props as Required<EventListenerProps>)
} }
componentWillUnmount() { componentWillUnmount() {
listenerRegistries.remove(this.props.type, this.handleEvent) listenerRegistries.remove(this.props.type, this.handleEvent)
removeEventListener(this.props.targetRef, this.props.type, this.handleEvent) removeEventListener(this.handleEvent, this.props as Required<EventListenerProps>)
} }
handleEvent = (e: Event) => { handleEvent = (e: Event) => {

Просмотреть файл

@ -2,6 +2,7 @@ import * as React from 'react'
import { EventHandler, EventTypes } from '../types' import { EventHandler, EventTypes } from '../types'
export type UseListenerHookOptions<N, T extends EventTypes> = { export type UseListenerHookOptions<N, T extends EventTypes> = {
capture: boolean
listener: EventHandler<T> listener: EventHandler<T>
targetRef: React.RefObject<N> targetRef: React.RefObject<N>
type: T type: T

Просмотреть файл

@ -8,15 +8,15 @@ import { UseListenerHookOptions } from './types'
const useEventListener = <N extends Node, T extends EventTypes>( const useEventListener = <N extends Node, T extends EventTypes>(
options: UseListenerHookOptions<N, T>, options: UseListenerHookOptions<N, T>,
): void => { ): void => {
const { listener, targetRef, type } = options const { listener, type } = options
const handler = React.useCallback((event: DocumentEventMap[T]) => { const handler = React.useCallback((event: DocumentEventMap[T]) => {
return listener(event) return listener(event)
}, []) }, [])
React.useEffect( React.useEffect(
() => { () => {
addEventListener(targetRef, type, handler) addEventListener(handler, options)
return () => removeEventListener(targetRef, type, handler) return () => removeEventListener(handler, options)
}, },
[type], [type],
) )

Просмотреть файл

@ -9,7 +9,7 @@ import { UseListenerHookOptions } from './types'
const useStackableEventListener = <N extends Node, T extends EventTypes>( const useStackableEventListener = <N extends Node, T extends EventTypes>(
options: UseListenerHookOptions<N, T>, options: UseListenerHookOptions<N, T>,
): void => { ): void => {
const { listener, targetRef, type } = options const { listener, type } = options
const handler = React.useCallback((event: DocumentEventMap[T]) => { const handler = React.useCallback((event: DocumentEventMap[T]) => {
if (listenerRegistries.isDispatchable(type, handler)) { if (listenerRegistries.isDispatchable(type, handler)) {
return listener(event) return listener(event)
@ -18,11 +18,11 @@ const useStackableEventListener = <N extends Node, T extends EventTypes>(
React.useEffect(() => { React.useEffect(() => {
listenerRegistries.add(type, handler) listenerRegistries.add(type, handler)
addEventListener(targetRef, type, handler) addEventListener(handler, options)
return () => { return () => {
listenerRegistries.remove(type, handler) listenerRegistries.remove(type, handler)
removeEventListener(targetRef, type, handler) removeEventListener(handler, options)
} }
}, []) }, [])
} }

Просмотреть файл

@ -1,12 +1,12 @@
import * as React from 'react' import { TargetRef } from './types'
export const documentRef: React.RefObject<HTMLDocument> = { export const documentRef: TargetRef = {
current: typeof document === 'undefined' ? null : document, current: typeof document === 'undefined' ? null : document,
} }
export const windowRef: React.RefObject<Window> = { export const windowRef: TargetRef = {
current: typeof window === 'undefined' ? null : window, current: typeof window === 'undefined' ? null : window,
} }
export { default as EventListener } from './EventListener' export { default as EventListener } from './EventListener'
export { default as StackableEventListener } from './StackableEventListener' export { default as StackableEventListener } from './StackableEventListener'
export { EventHandler, EventListenerProps, EventTypes } from './types' export { EventHandler, EventListenerProps, EventTypes, TargetRef } from './types'

Просмотреть файл

@ -1,21 +1,16 @@
import * as React from 'react' import { EventHandler, EventTypes, ListenerActionOptions } from '../types'
import { EventHandler, EventTypes } from '../types'
const addEventListener = ( const addEventListener = (listener: EventHandler<EventTypes>, options: ListenerActionOptions) => {
targetRef: React.RefObject<Node>, const { targetRef, type, capture } = options
type: EventTypes, const isSupported = targetRef && !!targetRef.current && !!targetRef.current.addEventListener
listener: EventHandler<EventTypes>,
) => {
const isSupported: boolean =
targetRef && !!targetRef.current && !!targetRef.current.addEventListener
if (isSupported) { if (isSupported) {
;(targetRef.current as NonNullable<Node>).addEventListener(type, listener) ;(targetRef.current as NonNullable<Node>).addEventListener(type, listener, capture)
} }
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
if (!isSupported) { if (!isSupported) {
console.error( throw new Error(
'@stardust-ui/react-component-event-listener: Passed `targetRef` is not valid or does not support `addEventListener()` method.', '@stardust-ui/react-component-event-listener: Passed `targetRef` is not valid or does not support `addEventListener()` method.',
) )
} }

Просмотреть файл

@ -1,21 +1,19 @@
import * as React from 'react' import { EventHandler, EventTypes, ListenerActionOptions } from '../types'
import { EventHandler, EventTypes } from '../types'
const removeEventListener = ( const removeEventListener = (
targetRef: React.RefObject<Node>,
type: EventTypes,
listener: EventHandler<EventTypes>, listener: EventHandler<EventTypes>,
options: ListenerActionOptions,
) => { ) => {
const isSupported: boolean = const { targetRef, type, capture } = options
targetRef && !!targetRef.current && !!targetRef.current.removeEventListener const isSupported = targetRef && !!targetRef.current && !!targetRef.current.removeEventListener
if (isSupported) { if (isSupported) {
;(targetRef.current as NonNullable<Node>).removeEventListener(type, listener) ;(targetRef.current as NonNullable<Node>).removeEventListener(type, listener, capture)
} }
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
if (!isSupported) { if (!isSupported) {
console.error( throw new Error(
'@stardust-ui/react-component-event-listener: Passed `targetRef` is not valid or does not support `removeEventListener()` method.', '@stardust-ui/react-component-event-listener: Passed `targetRef` is not valid or does not support `removeEventListener()` method.',
) )
} }

Просмотреть файл

@ -2,21 +2,31 @@ import * as PropTypes from 'prop-types'
import * as React from 'react' import * as React from 'react'
export interface EventListenerProps { export interface EventListenerProps {
/** Idicating that events of this type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree. */
capture?: boolean
/** A function which receives a notification when an event of the specified type occurs. */ /** A function which receives a notification when an event of the specified type occurs. */
listener: EventHandler<EventTypes> listener: EventHandler<EventTypes>
/** A ref object with a target node. */ /** A ref object with a target node. */
targetRef: React.RefObject<Node> targetRef: TargetRef
/** A case-sensitive string representing the event type to listen for. */ /** A case-sensitive string representing the event type to listen for. */
type: EventTypes type: EventTypes
} }
export type EventHandler<T extends EventTypes> = (e: DocumentEventMap[T]) => void export type EventHandler<T extends EventTypes> = (e: DocumentEventMap[T]) => void
export type EventTypes = keyof DocumentEventMap export type EventTypes = keyof DocumentEventMap
export type ListenerActionOptions = {
capture: boolean
targetRef: TargetRef
type: EventTypes
}
export type TargetRef = React.RefObject<Node | Window>
export const listenerPropTypes = { export const listenerPropTypes = {
capture: PropTypes.bool,
listener: PropTypes.func.isRequired, listener: PropTypes.func.isRequired,
targetRef: PropTypes.shape({ targetRef: PropTypes.shape({
current: PropTypes.object, current: PropTypes.object,

Просмотреть файл

@ -34,4 +34,32 @@ describe('EventListener', () => {
simulant.fire(document, 'click') simulant.fire(document, 'click')
expect(onClick).not.toHaveBeenCalled() expect(onClick).not.toHaveBeenCalled()
}) })
describe('capture', () => {
it('passes "false" by default', () => {
const addEventListener = jest.spyOn(document, 'addEventListener')
const removeEventListener = jest.spyOn(document, 'removeEventListener')
const wrapper = mount(
<EventListener listener={() => {}} targetRef={documentRef} type="click" />,
)
wrapper.unmount()
expect(addEventListener).toHaveBeenCalledWith('click', expect.any(Function), false)
expect(removeEventListener).toHaveBeenCalledWith('click', expect.any(Function), false)
})
it('passes `capture` prop when it is defined', () => {
const addEventListener = jest.spyOn(document, 'addEventListener')
const removeEventListener = jest.spyOn(document, 'removeEventListener')
const wrapper = mount(
<EventListener capture listener={() => {}} targetRef={documentRef} type="click" />,
)
wrapper.unmount()
expect(addEventListener).toHaveBeenCalledWith('click', expect.any(Function), true)
expect(removeEventListener).toHaveBeenCalledWith('click', expect.any(Function), true)
})
})
}) })