Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to disallow keyboard shortcut within <input> #17

Open
rockwotj opened this issue Aug 13, 2020 · 9 comments
Open

Option to disallow keyboard shortcut within <input> #17

rockwotj opened this issue Aug 13, 2020 · 9 comments

Comments

@rockwotj
Copy link

Right now tinykeys runs while I have focus in inputs, and I think that should be able to be disabled.

@jamiebuilds
Copy link
Owner

Could you just event.stopPropagation() on the input? I'm not sure what most people would expect as the default behavior

@rockwotj
Copy link
Author

I ended up wrapping my handlers in an extra function like:

(evt: KeyboardEvent) => {
            const inTextInput = document.activeElement && document.activeElement !== document.body;
            if (!inTextInput) return handler(evt);
          }

@jamiebuilds
Copy link
Owner

That's actually not a good way to wrap it, you should be testing the element type directly. Otherwise you'll break other keyboard navigation for accessibility

@Harrisonl
Copy link

@jamiebuilds love the package - do you have an example of what your recommending above? (or somewhere in the docs)?

@rockwotj
Copy link
Author

rockwotj commented Aug 15, 2020

@Harrisonl something like this is what I believe @jamiebuilds means:

FWIW adding event.stopPropagation() on every input within an app does not really scale well and has issues with React bc listeners are also attached to window.

(evt: KeyboardEvent) => {
  const active = document.activeElement;
  const enteringText = active instanceof HTMLElement &&
                      (active.isContentEditable || 
                       active.tagName === 'INPUT' || 
                       active.tagName === 'TEXTAREA');
  if (!enteringText) return handler(evt);
}

@jamiebuilds
Copy link
Owner

jamiebuilds commented Aug 18, 2020

That code is right except I don't know if the check on activeElement is necessary over just event.target, a full solution here should also support shadow dom

@sasivarnan
Copy link

@jamiebuilds Having this feature out of the box or adding a recommended documentation/example to handle this scenario would be great.

For applications having a lot of forms, adding event.stopPropagation() to all the input elements is not really scalable. Especially, in React we won't be binding keyUp or keyDown handlers to input elements in most cases.

If you are building it within the library, here is something for inspiration.

@mattiloh
Copy link

We wrapped tinykeys in a hook anyways, so it is easy to wrap all key-bindings in there:

import { useEffect } from 'react';
import tinykeys, { KeyBindingMap } from 'tinykeys';

function isEventTargetInputOrTextArea(eventTarget: EventTarget | null) {
  if (eventTarget === null) return false;

  const eventTargetTagName = (eventTarget as HTMLElement).tagName.toLowerCase();
  return ['input', 'textarea'].includes(eventTargetTagName);
}

export default function useTinyKeys(
  target: Document | Window | HTMLElement,
  bindings: KeyBindingMap,
  deps: unknown[],
  disableOnInputs: boolean,
) {
  useEffect(() => {
    const wrappedBindings = disableOnInputs
      ? Object.fromEntries(
          Object.entries(bindings).map(([key, handler]) => [
            key,
            (event: KeyboardEvent) => {
              if (!isEventTargetInputOrTextArea(event.target)) {
                handler(event);
              }
            },
          ]),
        )
      : bindings;
    const unsubscribe = tinykeys(target as HTMLElement, wrappedBindings);

    return () => {
      unsubscribe();
    };
  }, deps);
}

Then you can use it like this:

useTinyKeys(
  window,
  {
    ArrowLeft: (e) => previous(),
    ArrowRight: (e) => next(),
  },
  [previous, next],
  true, // <-- blockOnInputs
);

This could be extended to allow custom filters as well, but we only needed to disable key-bindings in text-inputs so far.

@rairulyle
Copy link

I also made a js version of this @mattiloh react code, for anyone interested. This one disables all keybindings on inputs by default.

Code

import tinykeys from 'tinykeys';

function isEventTargetInputOrTextArea(target) {
    if (target === null) return false;

    const targetElementName = target.tagName.toLowerCase();
    return ['input', 'textarea'].includes(targetElementName);
}

export default function hotkeys(target, bindings, disableOnInputs = true) {
    const wrappedBindings = disableOnInputs
        ? Object.fromEntries(
              Object.entries(bindings).map(([key, handler]) => [
                  key,
                  event => {
                      if (!isEventTargetInputOrTextArea(event.target)) {
                          handler(event);
                      }
                  },
              ])
          )
        : bindings;
    tinykeys(target, wrappedBindings);
}

Usage

hotkeys(window, {
            'g t p': () => {
                document.getElementById('nav-projects').click();
            },
        });
    },
    false, //disable blockOnInputs
    );

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants