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",
"author": "Oleksandr Fediashov <a@fedyashov.com>",
"bugs": "https://github.com/stardust-ui/react/issues",
"dependencies": {
"prop-types": "^15.6.1"
},
"files": [
"dist"
],

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

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

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

@ -8,23 +8,26 @@ import * as listenerRegistries from './lib/listenerRegistries'
class StackableEventListener extends React.PureComponent<EventListenerProps> {
static displayName = 'StackableEventListener'
static propTypes = listenerPropTypes
static defaultProps = {
capture: false,
}
componentDidMount() {
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) {
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)
addEventListener(this.props.targetRef, this.props.type, this.handleEvent)
addEventListener(this.handleEvent, this.props as Required<EventListenerProps>)
}
componentWillUnmount() {
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) => {

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

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

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

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

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

@ -9,7 +9,7 @@ import { UseListenerHookOptions } from './types'
const useStackableEventListener = <N extends Node, T extends EventTypes>(
options: UseListenerHookOptions<N, T>,
): void => {
const { listener, targetRef, type } = options
const { listener, type } = options
const handler = React.useCallback((event: DocumentEventMap[T]) => {
if (listenerRegistries.isDispatchable(type, handler)) {
return listener(event)
@ -18,11 +18,11 @@ const useStackableEventListener = <N extends Node, T extends EventTypes>(
React.useEffect(() => {
listenerRegistries.add(type, handler)
addEventListener(targetRef, type, handler)
addEventListener(handler, options)
return () => {
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,
}
export const windowRef: React.RefObject<Window> = {
export const windowRef: TargetRef = {
current: typeof window === 'undefined' ? null : window,
}
export { default as EventListener } from './EventListener'
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 } from '../types'
import { EventHandler, EventTypes, ListenerActionOptions } from '../types'
const addEventListener = (
targetRef: React.RefObject<Node>,
type: EventTypes,
listener: EventHandler<EventTypes>,
) => {
const isSupported: boolean =
targetRef && !!targetRef.current && !!targetRef.current.addEventListener
const addEventListener = (listener: EventHandler<EventTypes>, options: ListenerActionOptions) => {
const { targetRef, type, capture } = options
const isSupported = targetRef && !!targetRef.current && !!targetRef.current.addEventListener
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 (!isSupported) {
console.error(
throw new Error(
'@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 } from '../types'
import { EventHandler, EventTypes, ListenerActionOptions } from '../types'
const removeEventListener = (
targetRef: React.RefObject<Node>,
type: EventTypes,
listener: EventHandler<EventTypes>,
options: ListenerActionOptions,
) => {
const isSupported: boolean =
targetRef && !!targetRef.current && !!targetRef.current.removeEventListener
const { targetRef, type, capture } = options
const isSupported = targetRef && !!targetRef.current && !!targetRef.current.removeEventListener
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 (!isSupported) {
console.error(
throw new Error(
'@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'
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. */
listener: EventHandler<EventTypes>
/** A ref object with a target node. */
targetRef: React.RefObject<Node>
targetRef: TargetRef
/** A case-sensitive string representing the event type to listen for. */
type: EventTypes
}
export type EventHandler<T extends EventTypes> = (e: DocumentEventMap[T]) => void
export type EventTypes = keyof DocumentEventMap
export type ListenerActionOptions = {
capture: boolean
targetRef: TargetRef
type: EventTypes
}
export type TargetRef = React.RefObject<Node | Window>
export const listenerPropTypes = {
capture: PropTypes.bool,
listener: PropTypes.func.isRequired,
targetRef: PropTypes.shape({
current: PropTypes.object,

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

@ -34,4 +34,32 @@ describe('EventListener', () => {
simulant.fire(document, 'click')
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)
})
})
})