diff --git a/package-lock.json b/package-lock.json index 6f82b9b97..7f6f5a53f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20407,6 +20407,11 @@ } } }, + "simulant": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simulant/-/simulant-0.2.2.tgz", + "integrity": "sha1-8bzlJxK2p6DaON392n6DsgsdoB4=" + }, "sinon": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/sinon/-/sinon-6.1.3.tgz", diff --git a/package.json b/package.json index 40dc922bc..9047ce6a4 100644 --- a/package.json +++ b/package.json @@ -150,6 +150,7 @@ "relay-runtime": "github:coralproject/patched#relay-runtime", "relay-test-utils": "github:coralproject/patched#relay-test-utils", "sane": "^2.5.2", + "simulant": "^0.2.2", "sinon": "^6.1.3", "style-loader": "^0.21.0", "ts-jest": "^23.0.0", diff --git a/src/core/client/ui/components/ClickOutside/ClickOutside.mdx b/src/core/client/ui/components/ClickOutside/ClickOutside.mdx new file mode 100644 index 000000000..0d8126b5d --- /dev/null +++ b/src/core/client/ui/components/ClickOutside/ClickOutside.mdx @@ -0,0 +1,27 @@ +--- +name: ClickOutside +menu: UI Kit +--- + +import { Playground, PropsTable } from 'docz' +import ClickOutside from './ClickOutside' +import Button from '../Button/Button' + +# ClickOutside + +A Component to handle click events outside the children components. + +## Basic usage +Wrap a Component with `` and pass a function to `onClickOutside`. This function will trigger when the user +clicks outside the component. + +### Test +Click the blue background. It should trigger an alert. Nothing should happen if you click the button. + + +
+ alert('You clicked outside!')}> + + +
+
diff --git a/src/core/client/ui/components/ClickOutside/ClickOutside.spec.tsx b/src/core/client/ui/components/ClickOutside/ClickOutside.spec.tsx new file mode 100644 index 000000000..fd5ba04b8 --- /dev/null +++ b/src/core/client/ui/components/ClickOutside/ClickOutside.spec.tsx @@ -0,0 +1,62 @@ +import { mount } from "enzyme"; +import React from "react"; +import simulant from "simulant"; +import sinon from "sinon"; + +import ClickOutside from "./ClickOutside"; + +let container: HTMLElement; + +beforeAll(() => { + container = document.createElement("div"); + document.body.appendChild(container); +}); + +afterAll(() => { + document.body.removeChild(container); +}); + +it("should render correctly", () => { + const noop = () => null; + const wrapper = mount( + + Hello World! + + ); + expect(wrapper.html()).toMatchSnapshot(); + wrapper.unmount(); +}); + +it("should detect click outside", () => { + const onClickOutside = sinon.spy(); + const wrapper = mount( + + Hello World! + , + { + attachTo: container, + } + ); + simulant.fire(container, "click"); + + expect(onClickOutside.calledOnce).toEqual(true); + wrapper.unmount(); +}); + +it("should ignore click inside", () => { + const onClickOutside = sinon.spy(); + const wrapper = mount( + + + , + { + attachTo: container, + } + ); + + const target = document.getElementById("click-outside-test-button")!; + simulant.fire(target, "click"); + + expect(onClickOutside.calledOnce).toEqual(false); + wrapper.unmount(); +}); diff --git a/src/core/client/ui/components/ClickOutside/ClickOutside.tsx b/src/core/client/ui/components/ClickOutside/ClickOutside.tsx new file mode 100644 index 000000000..cf13762f0 --- /dev/null +++ b/src/core/client/ui/components/ClickOutside/ClickOutside.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { findDOMNode } from "react-dom"; + +interface Props { + onClickOutside: () => void; + children: React.ReactNode; +} + +class ClickOutside extends React.Component { + public domNode: Element | null = null; + + public handleClick = (e: MouseEvent) => { + const { onClickOutside } = this.props; + if (!e || !this.domNode!.contains(e.target as HTMLInputElement)) { + // tslint:disable-next-line:no-unused-expression + onClickOutside && onClickOutside(); + } + }; + + public componentDidMount() { + this.domNode = findDOMNode(this) as Element; + document.addEventListener("click", this.handleClick, true); + } + + public componentWillUnmount() { + document.removeEventListener("click", this.handleClick, true); + } + + public render() { + return this.props.children; + } +} +export default ClickOutside; diff --git a/src/core/client/ui/components/ClickOutside/__snapshots__/ClickOutside.spec.tsx.snap b/src/core/client/ui/components/ClickOutside/__snapshots__/ClickOutside.spec.tsx.snap new file mode 100644 index 000000000..883a02a51 --- /dev/null +++ b/src/core/client/ui/components/ClickOutside/__snapshots__/ClickOutside.spec.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = `"Hello World!"`; diff --git a/src/types/simulant.d.ts b/src/types/simulant.d.ts new file mode 100644 index 000000000..0537e0911 --- /dev/null +++ b/src/types/simulant.d.ts @@ -0,0 +1,16 @@ +declare module "simulant" { + type SimulantEvent = {}; + + interface Simulant { + (event: string, extendedParams: Record): SimulantEvent; + fire( + target: HTMLElement, + event: string | SimulantEvent, + extendedParams?: Record + ): void; + polyfill(): void; + } + + const simulant: Simulant; + export default simulant; +}