diff --git a/packages/.DS_Store b/packages/.DS_Store
new file mode 100644
index 0000000..acaa2a2
Binary files /dev/null and b/packages/.DS_Store differ
diff --git a/packages/behaviors/.DS_Store b/packages/behaviors/.DS_Store
new file mode 100644
index 0000000..150dc98
Binary files /dev/null and b/packages/behaviors/.DS_Store differ
diff --git a/packages/behaviors/hzcore-hook-click-outside-callback/.DS_Store b/packages/behaviors/hzcore-hook-click-outside-callback/.DS_Store
new file mode 100644
index 0000000..87a7f7f
Binary files /dev/null and b/packages/behaviors/hzcore-hook-click-outside-callback/.DS_Store differ
diff --git a/packages/behaviors/hzcore-hook-click-outside-callback/CHANGELOG.md b/packages/behaviors/hzcore-hook-click-outside-callback/CHANGELOG.md
new file mode 100644
index 0000000..e4d87c4
--- /dev/null
+++ b/packages/behaviors/hzcore-hook-click-outside-callback/CHANGELOG.md
@@ -0,0 +1,4 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
diff --git a/packages/behaviors/hzcore-hook-click-outside-callback/README.mdx b/packages/behaviors/hzcore-hook-click-outside-callback/README.mdx
new file mode 100644
index 0000000..eb6cbfd
--- /dev/null
+++ b/packages/behaviors/hzcore-hook-click-outside-callback/README.mdx
@@ -0,0 +1,214 @@
+---
+name: useClickOutsideCallback
+menu: Hooks
+route: /use-click-outside-callback
+---
+
+import {Playground, Props} from 'docz';
+import useClickOutsideCallback from './src';
+import {useState} from 'react';
+import Styles from './ReadmeStyles.tsx';
+
+# useClickOutsideCallback
+
+## Installation
+
+```shell
+yarn add @hzcore/hook-click-outside-callback
+```
+
+## Usage
+
+```js
+import useClickOutsideCallback from '@hzcore/hook-click-outside-callback';
+```
+
+### To use the hook:
+
+1. Create a callback function that accepts a `MouseEvent`
+2. Pass that callback to `useClickOutsideCallback` to get a React `ref`
+3. Add the ref to the target element/component
+
+```typescript
+ const onClickOutside = (event: MouseEvent) => {/* ...do something */};
+ const clickRef = useClickOutsideCallback(onClickOutside);
+ const el =
This div will respond to outside clicks
+```
+
+## Simple Example
+
+Check out the example's code to see the above steps in context.
+
+Here two event listeners are set up: one for clicks inside the element, and one
+for outside clicks. The text in the box will update to show the source of the
+last click (anywhere on this page) as 'Inside' or 'Outside'
+
+
+ {() => {
+ const Demo = () => {
+ // const [clickCount, setClickCount] = useState(0);
+ const [lastClick, setLastClick] = useState('(none)');
+
+ const clickInside = (e) => {
+ setLastClick('Inside');
+ e.preventDefault();
+ };
+
+ const clickOutside = (e) => {
+ setLastClick('Outside');
+ e.preventDefault();
+ };
+
+ // Pass in callback (clickOutside) and get reference to affix to an element
+ const clickRef = useClickOutsideCallback(clickOutside)
+
+ return (
+
+
This box is listening for clicks, inside and out. Click stuff.
+
Last clicked: {lastClick}
+
+
+ );
+ };
+ return ;
+ }}
+
+
+## Composing Components
+
+We can use this hook to compose higher level components like a pop-up info
+dialog. In the following example the `InfoPopup` component is built to accept
+`isOpen` and `onClose` props, delegating its state management.
+
+The component encapsulates the usage of the hook, and presents a popup dialog
+that will call the passed callback when the user clicks either outside of the
+dialog or on the close button.
+
+
+ {() => {
+ const Demo = () => {
+
+ // example component that accepts isOpen and onClose props
+ const InfoPopup = (props) => {
+ // bind passed callback to outside click
+ const clickRef = useClickOutsideCallback(props.onClose)
+ // only return body when isOpen == true
+ return !props.isOpen ? null : (
+
+
+
info
+ {/* also bind passed callback to classic close button */}
+
+
+
+ {props.children}
+
+
)
+ }
+
+ // setup events to hide and show an instance of the popup component
+ const [showingPopup, setShowingPopup] = useState(false)
+ const showPopup = (e) => {
+ setShowingPopup(true)
+ e.preventDefault()
+ }
+ const closePopup = (e) => {
+ setShowingPopup(false)
+ e.preventDefault()
+ }
+
+ // render a button to trigger the popup, and the popup itself,
+ // passing in content and the onClose callback
+ return (
+
+
+
+ This is an info popup, a helpful interuption, but not
+ so critical that we can't click out of it.
+
+
+ To close it:
+
+
+
click the close button
+
OR just click outside the dialog
+
+
+
)
+
+ }
+ return ;
+ }}
+
+
+## Nullable callback parameter
+
+When composing a component it may be useful to toggle outside click handling
+via some prop or state. One way to do this is to pass `null` as the callback
+parameter to the `useClickOutsideCallback` hook.
+
+When `null` is passed as the callback, no event listeners are registerd, but a
+ref is still returned and can be safely attached without further logic.
+
+As an example, if our component had a prop called `shouldCloseOnClick` we could
+toggle the behavior with the following code. In this case, the event listener is
+only registered if `shouldCloseOnClick` is truthy
+
+```js
+useClickOutsideCallback(shouldCloseOnClick ? handler : null)
+```
+
+## Typescript Considerations
+
+When using typescript react elements expect properly typed props. Here's how to
+keep React and Typescript happy:
+
+### the ref element
+
+If we are attaching the returned `ref` to a div element, react will expect that
+ref to be of the type `React.RefObject`. We can meet this
+requirement by specifying the element type `` when calling the
+hook.
+
+```typescript
+// receives a ref of type React.RefObject
+const clickRef = useClickOutsideCallback(clickOutside);
+
+// then it's ok to feed that ref to a react component
+const el =
...
+```
+
+### The callback
+
+React generally expects to work with React event objects, but the
+`useClickOutsideCallback` requires a standard (non-react) MouseEvent. This is
+because the outside click monitoring relies on a document level click handler
+that is propogated back down to the target element.
+
+As a result, the callback should be of type `MouseEvent`. However if the
+callback might also be used as a standard react callback (see above modal example)
+then we also need it to work with `React.MouseEvent`. In that case, we can implement
+our call back like this:
+
+```typescript
+const clickOutsideOrClose = (e: MouseEvent | React.MouseEvent) => {
+ closeTheDialog();
+ e.stopPropagation();
+};
+```
+
+This will work for both cases. If the handler is only passed to the hook we can
+omit the union with the latter type:
+
+```typescript
+const clickOutsideOrClose = (e: MouseEvent | React.MouseEvent) => {/*...*/}
+```
+
+## Event weirdness.
+
+React consumes standard browser events at the document level before repackaging
+them as React events. As a result, when using outside clicks to close a dialog,
+it is possible for the dialog to close, but still handle latent react click
+events that may have happened inside that dialog. Do not rely on the closing
+the dialog to cancel all pending listeners; use `stopPropogation` to keep your
+events in check.
diff --git a/packages/behaviors/hzcore-hook-click-outside-callback/ReadmeStyles.tsx b/packages/behaviors/hzcore-hook-click-outside-callback/ReadmeStyles.tsx
new file mode 100644
index 0000000..d9c18d7
--- /dev/null
+++ b/packages/behaviors/hzcore-hook-click-outside-callback/ReadmeStyles.tsx
@@ -0,0 +1,117 @@
+import styled from 'styled-components';
+import miniSvg from 'mini-svg-data-uri';
+
+const colors = {
+ primary: '#f38230',
+ bg_alt1: '#eee',
+};
+
+const infoIconSvg = ``;
+const closeIconSvg = ``;
+
+const svgUrl = (svg: string, fill: string = '#000'): string => {
+ const coloredSvg = svg.replace('fill=""', `fill="${fill}"`);
+ const url = miniSvg(coloredSvg);
+ console.log(url);
+ return `url("${url}")`;
+};
+
+export default styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .clickBox {
+ margin: 1.2em;
+ background: ${colors.bg_alt1};
+ padding: 0.8em 1.2em;
+ border: dashed 2px ${colors.primary};
+ }
+
+ .stage {
+ min-height: 20em;
+ width: 100%;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1.2em;
+ }
+
+ .info-popup {
+ position: absolute;
+ top: 20%;
+ right: 33%;
+ left: 33%;
+ min-height: 33%;
+
+ display: flex;
+ flex-direction: column;
+ justify-items: flex-start;
+ align-items: stretch;
+
+ z-index: 10;
+ }
+
+ .info-popup-top {
+ background-color: ${colors.primary};
+ border-radius: 0.5em 0.5em 0 0;
+ width: 100%;
+ height: 1.5em;
+ position: relative;
+ margin-bottom: -2px;
+ }
+
+ .info-popup-title {
+ color: #fff;
+ text-align: center;
+ font-weight: bold;
+ }
+
+ .info-popup-close {
+ background-color: #fff;
+ border-radius: 3em;
+ position: absolute;
+ right: 0.3em;
+ top: 0.3em;
+ height: 1em;
+ width: 1em;
+ &:hover {
+ transform: scale(1.2);
+ }
+ &::after {
+ content: 'close';
+ color: transparent;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-image: ${svgUrl(closeIconSvg, colors.primary)};
+ background-repeat: no-repeat;
+ background-position: center;
+ overflow: hidden;
+ }
+ }
+
+ .off {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='16' viewBox='0 0 12 16'%3E%3Cpath fill='%23999' fill-rule='evenodd' d='M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48L7.48 8z'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: center;
+ }
+
+ .info-popup-content {
+ padding: 0.5em;
+ border: 2px solid ${colors.primary};
+ background-color: ${colors.bg_alt1};
+ background-image: ${svgUrl(infoIconSvg, '#fff')};
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+ border-radius: 0 0 0.5em 0.5em;
+ p,
+ ul {
+ margin: 0.5em 0;
+ }
+ }
+`;
diff --git a/packages/behaviors/hzcore-hook-click-outside-callback/__tests__/clickOutsideCallback_test.tsx b/packages/behaviors/hzcore-hook-click-outside-callback/__tests__/clickOutsideCallback_test.tsx
new file mode 100644
index 0000000..2da23ea
--- /dev/null
+++ b/packages/behaviors/hzcore-hook-click-outside-callback/__tests__/clickOutsideCallback_test.tsx
@@ -0,0 +1,7 @@
+/* eslint-env jest, browser */
+import clickOutsideCallback from '../src';
+
+test('clickOutsideCallback is implemented', () => {
+ expect(() => clickOutsideCallback()).not.toThrow();
+ throw new Error('implement clickOutsideCallback and write some tests!');
+});
diff --git a/packages/behaviors/hzcore-hook-click-outside-callback/package.json b/packages/behaviors/hzcore-hook-click-outside-callback/package.json
new file mode 100644
index 0000000..5172492
--- /dev/null
+++ b/packages/behaviors/hzcore-hook-click-outside-callback/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@hzcore/hook-click-outside-callback",
+ "version": "0.0.1",
+ "main": "cjs/index.js",
+ "module": "es/index.js",
+ "typings": "src/index.tsx",
+ "license": "MIT",
+ "private": true,
+ "publishConfig": {
+ "registry": "http://npmregistry.hzdg.com"
+ },
+ "files": [
+ "cjs",
+ "es",
+ "src",
+ "types",
+ "!**/examples",
+ "!**/__test*"
+ ],
+ "dependencies": {
+ "mini-svg-data-uri": "^1.1.3"
+ }
+}
diff --git a/packages/behaviors/hzcore-hook-click-outside-callback/src/index.tsx b/packages/behaviors/hzcore-hook-click-outside-callback/src/index.tsx
new file mode 100644
index 0000000..4c54e09
--- /dev/null
+++ b/packages/behaviors/hzcore-hook-click-outside-callback/src/index.tsx
@@ -0,0 +1,54 @@
+import React, {useRef, useEffect} from 'react';
+
+function isSameOrDescendantOf(
+ maybeAncestor: Node | null,
+ node: Node | null,
+): boolean {
+ if (!maybeAncestor || !node) return false;
+ if (maybeAncestor === node) return true;
+ let parent = node.parentElement;
+ while (parent) {
+ if (parent === maybeAncestor) return true;
+ parent = parent.parentElement;
+ }
+ return false;
+}
+
+/**
+ * `useClickOutsideCallback` will call the given callback function
+ * whenever a click event is detected 'outside' of the element currently
+ * referenced by the returned ref object.
+ *
+ * This is useful for behaviors like closing a popover or modal
+ * by clicking 'behind' or 'around' it.
+ */
+export default function useClickOutsideCallback(
+ callback?: ((event: MouseEvent) => void) | null,
+): React.RefObject {
+ const clickOutsideRef = useRef(null);
+ useEffect(() => {
+ if (typeof callback === 'function' && typeof document !== 'undefined') {
+ const handleClickOutside = (event: MouseEvent): void => {
+ if (
+ clickOutsideRef.current &&
+ !isSameOrDescendantOf(
+ clickOutsideRef.current,
+ event.target as Node | null,
+ )
+ ) {
+ callback(event);
+ }
+ };
+ // options param specifies capture only ignoring events from within
+ // this prevents some corner cases that arise when inner elements are
+ // manipulated or replaced by react
+ document.addEventListener('click', handleClickOutside, {capture: true});
+ return () => {
+ document.removeEventListener('click', handleClickOutside, {
+ capture: true,
+ });
+ };
+ }
+ }, [callback]);
+ return clickOutsideRef;
+}
diff --git a/yarn.lock b/yarn.lock
index 5272f90..c99e111 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11769,6 +11769,11 @@ mini-html-webpack-plugin@^0.2.3:
dependencies:
webpack-sources "^1.1.0"
+mini-svg-data-uri@^1.1.3:
+ version "1.1.3"
+ resolved "http://npmregistry.hzdg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.1.3.tgz#9759ee5f4d89a4b724d089ce52eab4b623bfbc88"
+ integrity sha512-EeKOmdzekjdPe53/GdxmUpNgDQFkNeSte6XkJmOBt4BfWL6FQ9G9RtLNh+JMjFS3LhdpSICMIkZdznjiecASHQ==
+
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1"
resolved "http://npmregistry.hzdg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"