npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

use-key-state

v0.2.2

Published

Keyboard events as values for React (hook)

Downloads

1,589

Readme

useKeyState

Keyboard events as values for React

Introduction

useKeyState monitors key presses and when a rule matches, your component re-renders.

Read this first: https://use-key-state.mihaicernusca.com

Example: https://codesandbox.io/s/n4o5z6yk3l

Install

npm install use-key-state

Usage

Pass it a map of hotkey rules as strings and it hands back one of the same shape:

import {useKeyState} from 'use-key-state'

const {asd} = useKeyState({asd: 'a+s+d'})

Or pass it an array of rules per key:

const {asd, copy} = useKeyState({asd: 'a+s+d', copy: ['meta+c', 'ctrl+c']})

The values are state objects with three boolean properties: pressed, down and up.

Use pressed if you want to know if the keys are currently down. This is always true while the rule associated with it matches.

Use down or up if you want to know when the keydown and keyup events that caused the rule to match trigger. These values will be false after you read the value so be sure to capture it if you need it in multiple places! This is the equivalent of an event callback - you read it, consider yourself notified.

This behavior is also what makes it safe to use because if it returns true in one render it is guaranteed to return false at the next:

React.useEffect(() => {
  if (asd.down) {
    dispatch({type: 'do-the-down-thing'})
  } else if (asd.up) {
    dispatch({type: 'do-the-up-thing'})
  }
}, [asd])

The pressed property is appropriate to use if you need to base your render logic on the pressed state:

<div className={asd.pressed ? 'is-active' : 'is-not-active'} />

or inside an event handler or other form of render loop:

handleDrag = (e) => {
  if (asd.pressed) {
    // do things differently while key is pressed
  }
}

Document Events

While useKeyState hooks maintain their own internal state, they share one singleton document event listener making them relatively cheap. Events are called in a first-in-last-out order giving your deeper components a chance to handle the event first. If you use multiple instances of the useKeyState hook in one component the same rule applies.

Late mounted children however get added to the end of the priority list despite being deeper in the tree. In complex apps where you rely on deterministic event order you can enforce a depth priority by wrapping your app layers in <KeyStateLayer> components which keep track of depth relative to parent <KeyStateLayer>. Key callbacks will be sorted first by depth and then by insertion order.

import {KeyStateLayer} from 'use-key-state'

function Page() {
  return (
    <KeyStateLayer>
      <Content />
    </KeyStateLayer>
  )
}

Alternatively set priority config option (see Configuration below). This will override other default or inferred priorities.

Configuration

useKeyState accepts a second parameter for configuration which will be merged in with the default:

const defaultConfig = {
  captureEvents: false, // call event.preventDefault()
  ignoreRepeatEvents: true, // filter out repeat key events (whos event.repeat property is true)
  ignoreCapturedEvents: true, // respect the defaultPrevented event flag
  ignoreInputAcceptingElements: true, // filter out events from all forms of inputs
  priority: undefined, // see Document Events above
  debug: false, // enabled debug logging
}

Configuration is at the hook level - feel free to use multiple hooks in the same component where needed.

Dynamic Rules and Configuration

Both the rules map and the configuration objects can be updated dynamically. For example, only capture if we're in editing mode:

const {asd} = useKeyState({asd: 'a+s+d'}, {captureEvents: isEditing})

or, don't bind at all unless we're editing:

const {asd} = useKeyState({asd: isEditing ? 'a+s+d' : ''})

Query

If you just need a way to query the pressed keys and not re-render your component you can instantiate the hook with no parameters and get a query object with a few helper methods on it:

const query = useKeyState().keyStateQuery

if (query.pressed('space') {
  // true while space key is pressed
}

// also comes with some helper methods. Equivalent to above:

if (query.space() {
  // true while space key is pressed
}

This object gets merged into all returns under the key keyStateQuery in case you need access to the query object but don't want to create another instance:

const { asd, keyStateQuery } = useKeyState({ "a+s+d"});

Rule syntax

useKeyState keeps track of keyboard event.code values between key up and down events. A valid rule is a plus sign separated string of keyboard event codes.

Codes map to the physical key not the key value. To test the key code of a particular key I recommend this tool: keycode.info. This is a valid rule:

const { shiftA } = useKeyState({ ["ShiftLeft + KeyA", "ShiftRight + KeyA"]});

For convenience we map a few common codes to more sensible alternatives. This is also an equivalent rule:

const { shiftA } = useKeyState({"shift + a"});

Much better!

We map a-Z, 0-9, f1-f12, [, ], shift,meta|cmd|command|win,ctrl|cntrl|control,tab,esc|escape,plus|equal|equals|=,minus,delete|backspace,space,alt|opt,period|.,up,down,right,left,enter|return,slash|/,backslash|\. This list may fall out of sync, check the source (toCodes function) if not sure!

Overlapping rules

Consider the example:

const {forward, backward, backspace, tab, undo, redo} = useKeyState({
  undo: ['meta+z', 'ctrl+z'],
  redo: ['shift+meta+z', 'shift+ctrl+z'],
})

If you have rules that are a subset of another rule they will both match when the more specific rule fires (although they'll both only match once - it won't reset). Because of this you have to be careful to check the specific rule first:

// If undo (meta+z) matches, make sure it isn't a redo (shift+meta+z)
if (undo.down) {
  if (redo.down) {
    return void onRedo()
  }
  return void onUndo()
}

Avoid separate instances of the useKeyState hook that contain overlapping rules as your component will re-render twice. A good reason to use separate instances of this hook in one component is because you want a to pass a different configuration object in the 2nd parameter. Here is a real-life example:

// We want to capture and support key repeat for arrow keys while editing:
const {upArrow, downArrow, leftArrow, rightArrow} = useKeyState(
  {
    upArrow: isEditing ? 'up' : '',
    downArrow: isEditing ? 'down' : '',
    leftArrow: isEditing ? 'left' : '',
    rightArrow: isEditing ? 'right' : '',
  },
  {
    ignoreRepeatEvents: false,
    captureEvents: isEdit && focusKey,
  }
)
// But we don't want to support key repeat for the undo and redo key bindings
const {forward, backward, backspace, tab, undo, redo} = useKeyState(
  {
    undo: isEdit ? ['meta+z', 'ctrl+z'] : '',
    redo: isEdit ? ['shift+meta+z', 'shift+ctrl+z'] : '',
  },
  {
    captureEvents: isEditing,
  }
)

That's it!

Goals

  • enable a different way to program with key events

Non-Goals

  • legacy browsers support

  • support multiple rule syntaxes

  • key sequences, although that could be a specific form of the keyState hook at some point

Think carefully about what you need!

Quirks

Meta key clears the map when it goes up as we don't get key up events after the meta key is pressed. That means while meta is down all further key presses will return pressed until meta goes up.

These are implementation details which might change but this is the current behavior.

Notes

If you're still confused, this is essentially hook sugar over a callback API like:

// not real code
KeyState.on('a+s+d', (down) => {
  this.setState({asdPressed: down}, () => {
    if (down) {
      // do the down thing
    } else {
      // do the up thing
    }
  })
})