UI/ClickOutsideWrapper: Convert to functional component and add test (#108765)

This commit is contained in:
kay delaney
2025-07-31 16:09:30 +01:00
committed by GitHub
parent 2d81d03ba5
commit aa0f9bb1a0
2 changed files with 62 additions and 47 deletions
@@ -0,0 +1,26 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ClickOutsideWrapper } from './ClickOutsideWrapper';
describe('ClickOutsideWrapper', () => {
it('should call callback when clicked outside', async () => {
let clickedOutside = false;
render(
<div>
<ClickOutsideWrapper
onClick={() => {
clickedOutside = true;
}}
>
Click Outside
</ClickOutsideWrapper>
<button>Click me</button>
</div>
);
const button = screen.getByText('Click me');
await userEvent.click(button);
expect(clickedOutside).toBe(true);
});
});
@@ -1,62 +1,51 @@
import { PureComponent, createRef } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import * as React from 'react';
export interface Props {
/**
* Callback to trigger when clicking outside of current element occurs.
*/
/** Callback to trigger when clicking outside of current element occurs. */
onClick: () => void;
/**
* Runs the 'onClick' function when pressing a key outside of the current element. Defaults to true.
*/
includeButtonPress: boolean;
/** Runs the 'onClick' function when pressing a key outside of the current element. Defaults to true. */
includeButtonPress?: boolean;
/** Object to attach the click event listener to. */
parent: Window | Document;
/**
* https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener. Defaults to false.
*/
parent?: Window | Document;
/** https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener. Defaults to false. */
useCapture?: boolean;
children: React.ReactNode;
}
interface State {
hasEventListener: boolean;
}
export function ClickOutsideWrapper({
includeButtonPress = true,
parent = window,
useCapture = false,
onClick,
children,
}: Props) {
const wrapperRef = useRef<HTMLDivElement>(null);
const onOutsideClick = useCallback(
(event: Event) => {
const domNode = wrapperRef.current;
export class ClickOutsideWrapper extends PureComponent<React.PropsWithChildren<Props>, State> {
static defaultProps = {
includeButtonPress: true,
parent: typeof window !== 'undefined' ? window : undefined,
useCapture: false,
};
myRef = createRef<HTMLDivElement>();
state = {
hasEventListener: false,
};
if (!domNode || (event.target instanceof Node && !domNode.contains(event.target))) {
onClick();
}
},
[onClick]
);
componentDidMount() {
this.props.parent.addEventListener('click', this.onOutsideClick, this.props.useCapture);
if (this.props.includeButtonPress) {
useEffect(() => {
parent.addEventListener('click', onOutsideClick, useCapture);
if (includeButtonPress) {
// Use keyup since keydown already has an event listener on window
this.props.parent.addEventListener('keyup', this.onOutsideClick, this.props.useCapture);
parent.addEventListener('keyup', onOutsideClick, useCapture);
}
}
componentWillUnmount() {
this.props.parent.removeEventListener('click', this.onOutsideClick, this.props.useCapture);
if (this.props.includeButtonPress) {
this.props.parent.removeEventListener('keyup', this.onOutsideClick, this.props.useCapture);
}
}
return () => {
parent.removeEventListener('click', onOutsideClick, useCapture);
if (includeButtonPress) {
parent.removeEventListener('keyup', onOutsideClick, useCapture);
}
};
}, [includeButtonPress, onOutsideClick, parent, useCapture]);
onOutsideClick: EventListener = (event) => {
const domNode = this.myRef.current;
if (!domNode || (event.target instanceof Node && !domNode.contains(event.target))) {
this.props.onClick();
}
};
render() {
return <div ref={this.myRef}>{this.props.children}</div>;
}
return <div ref={wrapperRef}>{children}</div>;
}