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