Skip to content

Commit

Permalink
Port use-listener package to aacc (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronccasanova committed Sep 26, 2022
1 parent a011584 commit c6c61b0
Show file tree
Hide file tree
Showing 13 changed files with 475 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/smart-suns-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'use-listener': minor
---

Port to aacc
11 changes: 11 additions & 0 deletions hooks/use-listener/.babelrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"presets": [
[
"@aacc/babel-preset",
{
"typescript": true,
"react": true
}
]
]
}
12 changes: 12 additions & 0 deletions hooks/use-listener/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* @type {import('eslint').Linter.Config}
*/
module.exports = {
root: true,
extends: ['@aacc/eslint-config/typescript'],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.eslint.json',
},
ignorePatterns: ['node_modules', 'dist'],
}
101 changes: 101 additions & 0 deletions hooks/use-listener/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# use-listener

The **most** type safe React hook for adding and removing event listeners.

Supports the following event targets:

- `Window`
- `Document`
- `HTMLElement`
- `React.RefObject<HTMLElement>`

## Installation

```sh
npm install use-listener
```

## Usage

### With default target (`Window`)

```tsx
import { useListener } from 'use-listener'

function App() {
useListener('click', (e) => console.log(e))

return <div>Use listener</div>
}
```

> The `click` event and default `Window` target are used to resolve the event
> object type (`MouseEvent`).
### With explicit target (`Document`)

```tsx
import { useListener } from 'use-listener'

function App() {
useListener('copy', (e) => console.log(e), window.document)

return <div>Use listener</div>
}
```

> Note: The `copy` event and explicit `Document` target are used to resolve the
> event object type (`ClipboardEvent`).
### With React `ref` (`HTMLDivElement`)

```tsx
import { useListener } from 'use-listener'

function App() {
const ref = React.useRef<HTMLDivElement>(null)

useListener('mouseover', (e) => console.log(e), ref)

return <div ref={ref}>Use listener</div>
}
```

> Note: The `mouseover` event and inferred `HTMLDivElement` target are used to
> resolve the event object type (`MouseEvent`)
### Dynamic React `ref` (`HTMLParagraphElement`)

```jsx
import React from 'react'
import { useListener } from 'use-listener'

function App() {
const [element, ref] = useElementRef<HTMLParagraphElement>()
const [on, setOn] = React.useState(false)

useListener('mouseover', (e) => console.log(e), element)

return (
<div>
{on && <p ref={ref}>Hover me</p>}
<button onClick={() => setOn((prevOn) => !prevOn)}>
Toggle element
</button>
</div>
)
}

function useElementRef<T extends HTMLElement>() {
const [node, setNode] = React.useState<T | null>(null)

const ref: React.RefCallback<T> = React.useCallback((node) => {
if (node !== null) setNode(node)
}, [])

return [node, ref] as const
}
```

> Note: The `mouseover` event and dynamically set `HTMLParagraphElement` target
> are used to resolve the event object type (`MouseEvent`)
64 changes: 64 additions & 0 deletions hooks/use-listener/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"name": "use-listener",
"version": "0.1.4",
"description": "",
"author": "Aaron Casanova <aaronccasanova@gmail.com>",
"license": "MIT",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.mjs",
"browser": "dist/browser/index.js",
"types": "dist/types/index.d.ts",
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.js"
}
},
"scripts": {
"dev": "npm-run-all --parallel 'build:* -- --watch'",
"build": "npm-run-all --parallel build:*",
"build:js": "rollup -c",
"build:types": "tsc --emitDeclarationOnly",
"type-check": "tsc --noEmit",
"type-check:watch": "npm run type-check -- --watch",
"lint": "TIMING=1 eslint . --ext .js,.ts --cache",
"prepublishOnly": "npm run build"
},
"files": [
"dist"
],
"peerDependencies": {
"react": "^17.0.0"
},
"dependencies": {},
"devDependencies": {
"@aacc/babel-preset": "*",
"@aacc/browserslist-config": "*",
"@aacc/eslint-config": "*",
"@aacc/tsconfigs": "*",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^21.1.0",
"@rollup/plugin-node-resolve": "^13.2.1",
"@types/react": "^18.0.21",
"react": "^17.0.0",
"rollup": "^2.70.2",
"typescript": "^4.7.3"
},
"browserslist": [
"extends @aacc/browserslist-config"
],
"publishConfig": {
"access": "public",
"@aacc:registry": "https://registry.npmjs.org"
},
"repository": {
"type": "git",
"url": "git+https://github.com/aaronccasanova/aacc.git",
"directory": "hooks/use-listener"
},
"bugs": {
"url": "https://github.com/aaronccasanova/aacc/issues"
},
"homepage": "https://github.com/aaronccasanova/aacc/blob/main/hooks/use-listener/README.md"
}
57 changes: 57 additions & 0 deletions hooks/use-listener/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import path from 'path'

import nodeResolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import babel from '@rollup/plugin-babel'

import pkg from './package.json'

const name = 'useListener'

const extensions = ['.js', '.jsx', '.ts', '.tsx']

/**
* @type {import('rollup').RollupOptions}
*/
export default {
input: 'src/index.ts',
output: [
{
format: /** @type {const} */ ('cjs'),
entryFileNames: '[name][assetExtname].js',
dir: path.dirname(pkg.main),
preserveModules: true,
},
{
format: /** @type {const} */ ('es'),
entryFileNames: '[name][assetExtname].mjs',
dir: path.dirname(pkg.module),
preserveModules: true,
},
{
format: /** @type {const} */ ('iife'),
file: pkg.browser,
name,

// https://rollupjs.org/guide/en/#outputglobals
// globals: {},
},
],
plugins: [
// Allows node_modules resolution
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
nodeResolve({ extensions }),
// Allow bundling cjs modules. Rollup doesn't understand cjs
commonjs(),
// Compile TypeScript/JavaScript files
babel({
extensions,
babelHelpers: 'bundled',
include: ['src/**/*'],
}),
],
external: [
...Object.keys(pkg.dependencies ?? {}),
...Object.keys(pkg.peerDependencies ?? {}),
],
}
1 change: 1 addition & 0 deletions hooks/use-listener/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './use-listener'
14 changes: 14 additions & 0 deletions hooks/use-listener/src/use-isomorphic-layout-effect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react'

function isServer() {
const isDOM =
typeof window !== 'undefined' &&
window.document &&
window.document.documentElement

return !isDOM
}

export const useIsomorphicLayoutEffect = isServer()
? React.useEffect
: React.useLayoutEffect
98 changes: 98 additions & 0 deletions hooks/use-listener/src/use-listener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from 'react'

import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect'

/**
* Acceptable target elements for `useListener`.
*/
type UseListenerTarget =
| Window
| Document
| HTMLElement
| React.RefObject<HTMLElement>

/**
* Extracts the target element from a React `RefObject` or returns the input element.
*/
type ExtractTargetElement<Target> = Target extends React.RefObject<
infer Element
>
? Element
: Target

/**
* Extracts a (lib.dom.ts) EventMap for a given target element.
*/
type ExtractEventMap<Target> = ExtractTargetElement<Target> extends Window
? WindowEventMap
: ExtractTargetElement<Target> extends Document
? DocumentEventMap
: HTMLElementEventMap

/**
* Extracts all event names for a given target element.
*/
type ExtractEventName<Target> = keyof ExtractEventMap<
ExtractTargetElement<Target>
>

/**
* Extracts the `event` object for a given event type.
*/
type ExtractEvent<
Target,
EventName extends ExtractEventName<Target>,
> = ExtractEventMap<ExtractTargetElement<Target>>[EventName]

/**
* React hook encapsulating the boilerplate logic for adding and removing event listeners.
*/
export function useListener<
TargetEventName extends ExtractEventName<Target>,
TargetEvent extends ExtractEvent<Target, TargetEventName>,
Target extends UseListenerTarget = Window,
>(
eventName: TargetEventName,
handler: (event: TargetEvent) => void,
target?: undefined | null | Target,
options?: undefined | AddEventListenerOptions,
): void {
const handlerRef = React.useRef(handler)
const optionsRef = React.useRef(options)

useIsomorphicLayoutEffect(() => {
handlerRef.current = handler
}, [handler])

useIsomorphicLayoutEffect(() => {
optionsRef.current = options
}, [options])

React.useEffect(() => {
if (!(typeof eventName === 'string' && target !== null)) return

let targetElement: Exclude<UseListenerTarget, React.RefObject<HTMLElement>>

if (typeof target === 'undefined') {
targetElement = window
} else if ('current' in target) {
if (target.current === null) return

targetElement = target.current
} else {
targetElement = target
}

const eventOptions = optionsRef.current

const eventListener = (event: Event) =>
handlerRef.current(event as unknown as TargetEvent)

targetElement.addEventListener(eventName, eventListener, eventOptions)

// eslint-disable-next-line consistent-return
return () => {
targetElement.removeEventListener(eventName, eventListener, eventOptions)
}
}, [eventName, target])
}
14 changes: 14 additions & 0 deletions hooks/use-listener/tsconfig.eslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
// https://typescript-eslint.io/docs/linting/monorepo#one-root-tsconfigjson
"extends": "./tsconfig.json",
"compilerOptions": {
// Ensures this config is not used for a build
"noEmit": true
},
"include": [
// Paths to lint
"src",
".eslintrc.js",
"rollup.config.js"
]
}
10 changes: 10 additions & 0 deletions hooks/use-listener/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "@aacc/tsconfigs/node-library.json",
"compilerOptions": {
"outDir": "dist",
"declarationDir": "dist/types",
"lib": ["DOM"]
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

3 comments on commit c6c61b0

@vercel
Copy link

@vercel vercel bot commented on c6c61b0 Sep 26, 2022

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

aacc-next-ts – ./recipes/next-ts

aacc-next-ts-aaronccasanova-gmailcom.vercel.app
aacc-next-ts.vercel.app
aacc-next-ts-git-main-aaronccasanova-gmailcom.vercel.app

@vercel
Copy link

@vercel vercel bot commented on c6c61b0 Sep 26, 2022

Choose a reason for hiding this comment

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

@vercel
Copy link

@vercel vercel bot commented on c6c61b0 Sep 26, 2022

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

aacc-next-ts-styled-comps – ./recipes/next-ts-styled-comps

aacc-next-ts-styled-comps.vercel.app
aacc-next-ts-styled-comps-git-main-aaronccasanova-gmailcom.vercel.app
aacc-next-ts-styled-comps-aaronccasanova-gmailcom.vercel.app

Please sign in to comment.