feat(EventListener): add `capture` option (#1062)
This commit is contained in:
Родитель
d0604bea60
Коммит
872dccfe58
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Загрузка…
Ссылка в новой задаче