Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Restricted prop set in the `Button`, `Avatar`, `Box` and `Image` styles; changed `avatarBorderWidth` and `statusBorderWidth` avatar variables types from number to string and updated styles in Teams theme @mnajdova ([#2238](https://github.com/microsoft/fluent-ui-react/pull/2238))
- Restricted prop set in the `List` & `ListItem` @layershifter ([#2238](https://github.com/microsoft/fluent-ui-react/pull/2238))
- Remove `mountDocument` prop in `Popup` & `MenuButton` components @layershifter ([#2286](https://github.com/microsoft/fluent-ui-react/pull/2286))
- Remove `toRefObject` function @layershifter ([#2287](https://github.com/microsoft/fluent-ui-react/pull/2287))

### Fixes
- Fix styleParam to always be required in the styles functions @layershifter, @mnajdova ([#2235](https://github.com/microsoft/fluent-ui-react/pull/2235))
Expand All @@ -42,6 +43,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add `useStyles()` hook to use theming capabilities in custom components @layershifter, @mnajdova ([#2217](https://github.com/microsoft/fluent-ui-react/pull/2217))
- Add optional wrapper function to `List` which can be used to inject custom scrollbars to `Dropdown` @jurokapsiar ([#2092](https://github.com/microsoft/fluent-ui-react/pull/2092))
- Add `useTelemetry()` hook for adding telemetry information for the Fluent components and improve return types for the `useStyles` and `useStateManager` hooks @mnajdova ([#2257](https://github.com/microsoft/fluent-ui-react/pull/2257))
- Add `target` prop to `EventListener` component and `useEventListener()` hook @layershifter ([#2287](https://github.com/microsoft/fluent-ui-react/pull/2287))

### Documentation
- Add per-component performance charts @miroslavstastny ([#2240](https://github.com/microsoft/fluent-ui-react/pull/2240))
Expand Down
5 changes: 2 additions & 3 deletions docs/src/prototypes/EditorToolbar/EditorToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
ToolbarMenuItemShorthandKinds,
} from '@fluentui/react'
import { useEventListener } from '@fluentui/react-component-event-listener'
import { toRefObject } from '@fluentui/react-component-ref'
import * as keyboardKey from 'keyboard-key'
import * as _ from 'lodash'
import * as React from 'react'
Expand Down Expand Up @@ -243,7 +242,7 @@ const EditorToolbar: React.FC<EditorToolbarProps> = props => {
}
},
type: 'keydown',
targetRef: toRefObject(props.target),
target: props.target,
})
useEventListener({
listener: () => {
Expand All @@ -258,7 +257,7 @@ const EditorToolbar: React.FC<EditorToolbarProps> = props => {
}
},
type: 'resize',
targetRef: toRefObject(props.target.defaultView),
target: props.target.defaultView,
})

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as PropTypes from 'prop-types'

import useEventListener from './useEventListener'
import { EventListenerOptions, EventTypes, TargetRef } from './types'
import { EventListenerOptions, EventTypes, Target, TargetRef } from './types'

function EventListener<T extends EventTypes>(props: EventListenerOptions<T>) {
useEventListener(props)
Expand All @@ -16,9 +16,10 @@ EventListener.propTypes =
? {
capture: PropTypes.bool,
listener: PropTypes.func.isRequired,
target: PropTypes.object as PropTypes.Validator<Target>,
targetRef: PropTypes.shape({
current: PropTypes.object,
}).isRequired as PropTypes.Validator<TargetRef>,
}) as PropTypes.Validator<TargetRef>,
type: PropTypes.string.isRequired as PropTypes.Validator<EventTypes>,
}
: {}
Expand Down
8 changes: 6 additions & 2 deletions packages/react-component-event-listener/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ export interface EventListenerOptions<T extends EventTypes = 'click'> {
/** A function which receives a notification when an event of the specified type occurs. */
listener: EventHandler<T>

/** A target node. Use `target` or `targetRef` prop. */
target?: Target

/** A ref object with a target node. */
targetRef: TargetRef
targetRef?: TargetRef

/** A case-sensitive string representing the event type to listen for. */
type: T
Expand All @@ -17,4 +20,5 @@ export interface EventListenerOptions<T extends EventTypes = 'click'> {
export type EventHandler<T extends EventTypes> = (e: DocumentEventMap[T]) => void
export type EventTypes = keyof DocumentEventMap

export type TargetRef = React.RefObject<Node | Window>
export type Target = Node | Window
export type TargetRef = React.RefObject<Target>
41 changes: 30 additions & 11 deletions packages/react-component-event-listener/src/useEventListener.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import * as React from 'react'

import { EventHandler, EventListenerOptions, EventTypes, TargetRef } from './types'
import { EventHandler, EventListenerOptions, EventTypes, Target } from './types'

const isActionSupported = (
targetRef: TargetRef,
element: Target | null | undefined,
method: 'addEventListener' | 'removeEventListener',
) => targetRef && !!targetRef.current && !!targetRef.current[method]
): element is Target => (element ? !!element[method] : false)

const useEventListener = <T extends EventTypes>(options: EventListenerOptions<T>): void => {
const { capture, listener, type, targetRef } = options
const { capture, listener, type, target, targetRef } = options

const latestListener = React.useRef<EventHandler<T>>(listener)
latestListener.current = listener
Expand All @@ -17,25 +17,44 @@ const useEventListener = <T extends EventTypes>(options: EventListenerOptions<T>
return latestListener.current(event)
}, [])

if (process.env.NODE_ENV !== 'production') {
React.useEffect(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a check for both being set but no propTypes/check for none. In that case you will receive a cryptic error

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a check 👍

if (typeof target !== 'undefined' && typeof targetRef !== 'undefined') {
throw new Error(
'`target` and `targetRef` props are mutually exclusive, please use one of them.',
)
}

if (typeof target === 'undefined' && typeof targetRef === 'undefined') {
throw new Error(
"`target` and `targetRef` props are `undefined`, it' required to use one of them.",
)
}
}, [target, targetRef])
}

React.useEffect(() => {
if (isActionSupported(targetRef, 'addEventListener')) {
;(targetRef.current as NonNullable<Node>).addEventListener(type, eventHandler, capture)
const element: Target | null | undefined =
typeof targetRef === 'undefined' ? target : targetRef.current

if (isActionSupported(element, 'addEventListener')) {
element.addEventListener(type, eventHandler, capture)
} else if (process.env.NODE_ENV !== 'production') {
throw new Error(
'@fluentui/react-component-event-listener: Passed `targetRef` is not valid or does not support `addEventListener()` method.',
'@fluentui/react-component-event-listener: Passed `element` is not valid or does not support `addEventListener()` method.',
)
}

return () => {
if (isActionSupported(targetRef, 'removeEventListener')) {
;(targetRef.current as NonNullable<Node>).removeEventListener(type, eventHandler, capture)
if (isActionSupported(element, 'removeEventListener')) {
element.removeEventListener(type, eventHandler, capture)
} else if (process.env.NODE_ENV !== 'production') {
throw new Error(
'@fluentui/react-component-event-listener: Passed `targetRef` is not valid or does not support `removeEventListener()` method.',
'@fluentui/react-component-event-listener: Passed `element` is not valid or does not support `removeEventListener()` method.',
)
}
}
}, [capture, targetRef, type])
}, [capture, target, targetRef, type])
}

export default useEventListener
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ import * as React from 'react'
// @ts-ignore
import * as simulant from 'simulant'

class TestBoundary extends React.Component<{ onError: (e: Error) => void }, { hasError: boolean }> {
state = { hasError: false }

componentDidCatch(error: Error) {
this.props.onError(error)
this.setState({ hasError: true })
}

render() {
if (this.state.hasError) {
return null
}

return this.props.children
}
}

describe('EventListener', () => {
describe('listener', () => {
it('handles events on `target`', () => {
Expand Down Expand Up @@ -83,4 +100,70 @@ describe('EventListener', () => {
expect(listener).toHaveBeenCalledTimes(2)
})
})

describe('target', () => {
it('handles events', () => {
const listener = jest.fn()
mount(<EventListener listener={listener} target={document} type="click" />)

simulant.fire(document, 'click')
expect(listener).toHaveBeenCalledTimes(1)
})

it('throws an error when is used with `targetRef`', () => {
jest.spyOn(console, 'error').mockImplementation(() => {})
const onError = jest.fn()

mount(
<TestBoundary onError={onError}>
<EventListener
listener={jest.fn()}
target={document}
targetRef={documentRef}
type="click"
/>
</TestBoundary>,
)

expect(onError).toBeCalledWith(
expect.objectContaining({
message: '`target` and `targetRef` props are mutually exclusive, please use one of them.',
}),
)

// We need to clean up mocks to avoid errors reported by React
;(console.error as any).mockClear()
})

it('throws an error when not defined', () => {
jest.spyOn(console, 'error').mockImplementation(() => {})
const onError = jest.fn()

mount(
<TestBoundary onError={onError}>
<EventListener listener={jest.fn()} type="click" />
</TestBoundary>,
)

expect(onError).toBeCalledWith(
expect.objectContaining({
message:
"`target` and `targetRef` props are `undefined`, it' required to use one of them.",
}),
)

// We need to clean up mocks to avoid errors reported by React
;(console.error as any).mockClear()
})
})

describe('targetRef', () => {
it('handles events', () => {
const listener = jest.fn()
mount(<EventListener listener={listener} targetRef={documentRef} type="click" />)

simulant.fire(document, 'click')
expect(listener).toHaveBeenCalledTimes(1)
})
})
})
1 change: 0 additions & 1 deletion packages/react-component-ref/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export { default as handleRef } from './handleRef'
export { default as isRefObject } from './isRefObject'
export { default as toRefObject } from './toRefObject'

export { default as Ref } from './Ref'
export { default as RefFindNode } from './RefFindNode'
Expand Down
25 changes: 0 additions & 25 deletions packages/react-component-ref/src/toRefObject.ts

This file was deleted.

19 changes: 0 additions & 19 deletions packages/react-component-ref/test/toRefObject-test.ts

This file was deleted.

11 changes: 3 additions & 8 deletions packages/react/src/components/Debug/Debug.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import keyboardKey from 'keyboard-key'
import * as PropTypes from 'prop-types'
import * as React from 'react'
import { toRefObject } from '@fluentui/react-component-ref'
import { EventListener } from '@fluentui/react-component-event-listener'

import { isBrowser } from '../../utils'
Expand Down Expand Up @@ -146,22 +145,18 @@ class Debug extends React.Component<DebugProps, DebugState> {
if (process.env.NODE_ENV !== 'production' && isDebugEnabled) {
return (
<>
<EventListener
targetRef={toRefObject(mountDocument.body)}
listener={this.handleKeyDown}
type="keydown"
/>
<EventListener listener={this.handleKeyDown} target={mountDocument.body} type="keydown" />
{isSelecting && (
<EventListener
targetRef={toRefObject(mountDocument.body)}
listener={this.handleMouseMove}
target={mountDocument.body}
type="mousemove"
/>
)}
{isSelecting && fiberNav && fiberNav.domNode && (
<EventListener
targetRef={toRefObject(fiberNav.domNode)}
listener={this.handleDOMNodeClick}
target={fiberNav.domNode}
type="click"
/>
)}
Expand Down
7 changes: 3 additions & 4 deletions packages/react/src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Accessibility, dialogBehavior } from '@fluentui/accessibility'
import { FocusTrapZoneProps } from '@fluentui/react-bindings'
import { Unstable_NestingAuto } from '@fluentui/react-component-nesting-registry'
import { EventListener } from '@fluentui/react-component-event-listener'
import { Ref, toRefObject } from '@fluentui/react-component-ref'
import { Ref } from '@fluentui/react-component-ref'
import * as customPropTypes from '@fluentui/react-proptypes'
import * as _ from 'lodash'
import * as PropTypes from 'prop-types'
Expand Down Expand Up @@ -330,7 +330,6 @@ class Dialog extends AutoControlledComponent<WithAsProp<DialogProps>, DialogStat
</Ref>
)

const targetRef = toRefObject(this.context.target)
const triggerAccessibility: TriggerAccessibility = {
attributes: accessibility.attributes.trigger,
keyHandlers: accessibility.keyHandlers.trigger,
Expand Down Expand Up @@ -366,14 +365,14 @@ class Dialog extends AutoControlledComponent<WithAsProp<DialogProps>, DialogStat
{closeOnOutsideClick && (
<EventListener
listener={this.handleOverlayClick}
targetRef={targetRef}
target={this.context.target}
type="click"
capture
/>
)}
<EventListener
listener={this.handleDocumentKeydown(getRefs)}
targetRef={targetRef}
target={this.context.target}
type="keydown"
capture
/>
Expand Down
9 changes: 6 additions & 3 deletions packages/react/src/components/Menu/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Accessibility, menuItemBehavior, submenuBehavior } from '@fluentui/accessibility'
import { focusAsync } from '@fluentui/react-bindings'
import { EventListener } from '@fluentui/react-component-event-listener'
import { Ref, toRefObject } from '@fluentui/react-component-ref'
import { Ref } from '@fluentui/react-component-ref'
import * as customPropTypes from '@fluentui/react-proptypes'
import * as _ from 'lodash'
import cx from 'classnames'
Expand Down Expand Up @@ -215,7 +215,6 @@ class MenuItem extends AutoControlledComponent<WithAsProp<MenuItemProps>, MenuIt

const defaultIndicator = { name: vertical ? 'icon-arrow-end' : 'icon-arrow-down' }
const indicatorWithDefaults = indicator === undefined ? defaultIndicator : indicator
const targetRef = toRefObject(this.context.target)

const menuItemInner = childrenExist(children) ? (
children
Expand Down Expand Up @@ -274,7 +273,11 @@ class MenuItem extends AutoControlledComponent<WithAsProp<MenuItemProps>, MenuIt
})}
</Popper>
</Ref>
<EventListener listener={this.outsideClickHandler} targetRef={targetRef} type="click" />
<EventListener
listener={this.outsideClickHandler}
target={this.context.target}
type="click"
/>
</>
) : null

Expand Down
Loading